can work with anime extensions successfully

This commit is contained in:
Aria Moradi 2021-05-27 05:13:01 +04:30
parent 994ae97256
commit c17e3bd04f
33 changed files with 546 additions and 206 deletions

View File

@ -8,62 +8,67 @@ package suwayomi.anime
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin import io.javalin.Javalin
import suwayomi.server.JavalinSetup import suwayomi.anime.impl.extension.Extension.getExtensionIcon
import suwayomi.anime.impl.extension.Extension.installExtension
import suwayomi.anime.impl.extension.Extension.uninstallExtension
import suwayomi.anime.impl.extension.Extension.updateExtension
import suwayomi.anime.impl.extension.ExtensionsList.getExtensionList import suwayomi.anime.impl.extension.ExtensionsList.getExtensionList
import suwayomi.server.JavalinSetup
import suwayomi.server.JavalinSetup.future
object AnimeAPI { object AnimeAPI {
fun defineEndpoints(app: Javalin) { fun defineEndpoints(app: Javalin) {
// list all extensions // list all extensions
app.get("/api/v1/extension/list") { ctx -> app.get("/api/v1/anime/extension/list") { ctx ->
ctx.json( ctx.json(
JavalinSetup.future { future {
getExtensionList() getExtensionList()
} }
) )
} }
// // install extension identified with "pkgName" // install extension identified with "pkgName"
// app.get("/api/v1/extension/install/:pkgName") { ctx -> app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
// val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
//
// ctx.json( ctx.json(
// JavalinSetup.future { JavalinSetup.future {
// installExtension(pkgName) installExtension(pkgName)
// } }
// ) )
// } }
//
// // update extension identified with "pkgName" // update extension identified with "pkgName"
// app.get("/api/v1/extension/update/:pkgName") { ctx -> app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
// val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
//
// ctx.json( ctx.json(
// JavalinSetup.future { JavalinSetup.future {
// updateExtension(pkgName) updateExtension(pkgName)
// } }
// ) )
// } }
//
// // uninstall extension identified with "pkgName" // uninstall extension identified with "pkgName"
// app.get("/api/v1/extension/uninstall/:pkgName") { ctx -> app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
// val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
//
// uninstallExtension(pkgName) uninstallExtension(pkgName)
// ctx.status(200) ctx.status(200)
// } }
//
// // icon for extension named `apkName` // icon for extension named `apkName`
// app.get("/api/v1/extension/icon/:apkName") { ctx -> // TODO: move to pkgName app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
// val apkName = ctx.pathParam("apkName") val apkName = ctx.pathParam("apkName")
//
// ctx.result( ctx.result(
// JavalinSetup.future { getExtensionIcon(apkName) } JavalinSetup.future { getExtensionIcon(apkName) }
// .thenApply { .thenApply {
// ctx.header("content-type", it.second) ctx.header("content-type", it.second)
// it.first it.first
// } }
// ) )
// } }
// // list of sources // // list of sources
// app.get("/api/v1/source/list") { ctx -> // app.get("/api/v1/source/list") { ctx ->

View File

@ -10,9 +10,9 @@ package suwayomi.anime.impl.extension
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.AnimeCatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.AnimeSource
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.AnimeSourceFactory
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
@ -125,11 +125,11 @@ object Extension {
File(dexFilePath).delete() File(dexFilePath).delete()
// collect sources from the extension // collect sources from the extension
val sources: List<CatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) { val sources: List<AnimeCatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
is Source -> listOf(instance) is AnimeSource -> listOf(instance)
is SourceFactory -> instance.createSources() is AnimeSourceFactory -> instance.createSources()
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}") else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
}.map { it as CatalogueSource } }.map { it as AnimeCatalogueSource }
val langs = sources.map { it.lang }.toSet() val langs = sources.map { it.lang }.toSet()
val extensionLang = when (langs.size) { val extensionLang = when (langs.size) {
@ -246,6 +246,6 @@ object Extension {
} }
fun getExtensionIconUrl(apkName: String): String { fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/extension/icon/$apkName" return "/api/v1/anime/extension/icon/$apkName"
} }
} }

View File

@ -32,15 +32,14 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
object PackageTools { object PackageTools {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
const val EXTENSION_FEATURE = "tachiyomi.extension" const val EXTENSION_FEATURE = "tachiyomi.animeextension"
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
const val METADATA_NSFW = "tachiyomi.extension.nsfw" const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
const val LIB_VERSION_MIN = 1.3 const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MAX = 1.3 const val LIB_VERSION_MAX = 1.3
@ -58,20 +57,20 @@ object PackageTools {
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath())) val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
val handler = BaksmaliBaseDexExceptionHandler() val handler = BaksmaliBaseDexExceptionHandler()
Dex2jar Dex2jar
.from(reader) .from(reader)
.withExceptionHandler(handler) .withExceptionHandler(handler)
.reUseReg(false) .reUseReg(false)
.topoLogicalSort() .topoLogicalSort()
.skipDebug(true) .skipDebug(true)
.optimizeSynchronized(false) .optimizeSynchronized(false)
.printIR(false) .printIR(false)
.noCode(false) .noCode(false)
.skipExceptions(false) .skipExceptions(false)
.to(jarFilePath) .to(jarFilePath)
if (handler.hasException()) { if (handler.hasException()) {
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt") val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
logger.error( logger.error(
""" """
Detail Error Information in File $errorFile Detail Error Information in File $errorFile
Please report this file to one of following link if possible (any one). Please report this file to one of following link if possible (any one).
https://sourceforge.net/p/dex2jar/tickets/ https://sourceforge.net/p/dex2jar/tickets/
@ -101,27 +100,27 @@ object PackageTools {
val appTag = doc.getElementsByTagName("application").item(0) val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList() appTag?.childNodes?.toList()
.orEmpty() .orEmpty()
.asSequence() .asSequence()
.filter { .filter {
it.nodeType == Node.ELEMENT_NODE it.nodeType == Node.ELEMENT_NODE
}.map { }.map {
it as Element it as Element
}.filter { }.filter {
it.tagName == "meta-data" it.tagName == "meta-data"
}.forEach { }.forEach {
putString( putString(
it.attributes.getNamedItem("android:name").nodeValue, it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue it.attributes.getNamedItem("android:value").nodeValue
) )
} }
} }
signatures = ( signatures = (
parsed.apkSingers.flatMap { it.certificateMetas } parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/ /*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72 ) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray() .map { Signature(it.data) }.toTypedArray()
} }
} }

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.future.future import kotlinx.coroutines.future.future
import mu.KotlinLogging import mu.KotlinLogging
import suwayomi.anime.AnimeAPI
import suwayomi.server.util.Browser import suwayomi.server.util.Browser
import suwayomi.tachidesk.TachideskAPI import suwayomi.tachidesk.TachideskAPI
import java.io.IOException import java.io.IOException
@ -75,5 +76,6 @@ object JavalinSetup {
} }
TachideskAPI.defineEndpoints(app) TachideskAPI.defineEndpoints(app)
AnimeAPI.defineEndpoints(app)
} }
} }

View File

@ -0,0 +1,54 @@
package suwayomi.server.database.migration
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.server.database.migration.lib.Migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
class M0004_AnimeTablesBatch1 : Migration() {
private object AnimeExtensionTable : IntIdTable() {
val apkName = varchar("apk_name", 1024)
// default is the local source icon from tachiyomi
val iconUrl = varchar("icon_url", 2048)
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16)
val versionCode = integer("version_code")
val lang = varchar("lang", 10)
val isNsfw = bool("is_nsfw")
val isInstalled = bool("is_installed").default(false)
val hasUpdate = bool("has_update").default(false)
val isObsolete = bool("is_obsolete").default(false)
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
}
private object AnimeSourceTable : IdTable<Long>() {
override val id = long("id").entityId()
val name = varchar("name", 128)
val lang = varchar("lang", 10)
val extension = reference("extension", suwayomi.anime.model.table.AnimeExtensionTable)
val partOfFactorySource = bool("part_of_factory_source").default(false)
}
override fun run() {
transaction {
SchemaUtils.create(
AnimeExtensionTable,
AnimeSourceTable
)
}
}
}

View File

@ -8,7 +8,6 @@ package suwayomi.tachidesk
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin import io.javalin.Javalin
import suwayomi.server.JavalinSetup
import suwayomi.server.JavalinSetup.future import suwayomi.server.JavalinSetup.future
import suwayomi.server.impl.About import suwayomi.server.impl.About
import suwayomi.tachidesk.impl.Category import suwayomi.tachidesk.impl.Category
@ -47,7 +46,7 @@ object TachideskAPI {
// list all extensions // list all extensions
app.get("/api/v1/extension/list") { ctx -> app.get("/api/v1/extension/list") { ctx ->
ctx.json( ctx.json(
JavalinSetup.future { future {
getExtensionList() getExtensionList()
} }
) )
@ -58,7 +57,7 @@ object TachideskAPI {
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
ctx.json( ctx.json(
JavalinSetup.future { future {
installExtension(pkgName) installExtension(pkgName)
} }
) )
@ -69,7 +68,7 @@ object TachideskAPI {
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
ctx.json( ctx.json(
JavalinSetup.future { future {
updateExtension(pkgName) updateExtension(pkgName)
} }
) )
@ -88,7 +87,7 @@ object TachideskAPI {
val apkName = ctx.pathParam("apkName") val apkName = ctx.pathParam("apkName")
ctx.result( ctx.result(
JavalinSetup.future { getExtensionIcon(apkName) } future { getExtensionIcon(apkName) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
it.first it.first
@ -112,7 +111,7 @@ object TachideskAPI {
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json( ctx.json(
JavalinSetup.future { future {
getMangaList(sourceId, pageNum, popular = true) getMangaList(sourceId, pageNum, popular = true)
} }
) )
@ -123,7 +122,7 @@ object TachideskAPI {
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json( ctx.json(
JavalinSetup.future { future {
getMangaList(sourceId, pageNum, popular = false) getMangaList(sourceId, pageNum, popular = false)
} }
) )
@ -135,7 +134,7 @@ object TachideskAPI {
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean() val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json( ctx.json(
JavalinSetup.future { future {
getManga(mangaId, onlineFetch) getManga(mangaId, onlineFetch)
} }
) )
@ -146,7 +145,7 @@ object TachideskAPI {
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result( ctx.result(
JavalinSetup.future { getMangaThumbnail(mangaId) } future { getMangaThumbnail(mangaId) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
it.first it.first
@ -182,14 +181,14 @@ object TachideskAPI {
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
ctx.json(JavalinSetup.future { getChapterList(mangaId, onlineFetch) }) ctx.json(future { getChapterList(mangaId, onlineFetch) })
} }
// used to display a chapter, get a chapter in order to show it's pages // used to display a chapter, get a chapter in order to show it's pages
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(JavalinSetup.future { getChapter(chapterIndex, mangaId) }) ctx.json(future { getChapter(chapterIndex, mangaId) })
} }
// used to modify a chapter's parameters // used to modify a chapter's parameters
@ -214,7 +213,7 @@ object TachideskAPI {
val index = ctx.pathParam("index").toInt() val index = ctx.pathParam("index").toInt()
ctx.result( ctx.result(
JavalinSetup.future { getPageImage(mangaId, chapterIndex, index) } future { getPageImage(mangaId, chapterIndex, index) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
it.first it.first
@ -243,7 +242,7 @@ object TachideskAPI {
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) }) ctx.json(future { sourceSearch(sourceId, searchTerm, pageNum) })
} }
// source filter list // source filter list
@ -257,7 +256,7 @@ object TachideskAPI {
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result( ctx.result(
JavalinSetup.future { addMangaToLibrary(mangaId) } future { addMangaToLibrary(mangaId) }
) )
} }
@ -266,7 +265,7 @@ object TachideskAPI {
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result( ctx.result(
JavalinSetup.future { removeMangaFromLibrary(mangaId) } future { removeMangaFromLibrary(mangaId) }
) )
} }
@ -335,7 +334,7 @@ object TachideskAPI {
// expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json" // expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json"
app.post("/api/v1/backup/legacy/import/file") { ctx -> app.post("/api/v1/backup/legacy/import/file") { ctx ->
ctx.result( ctx.result(
JavalinSetup.future { future {
restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content) restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
} }
) )
@ -345,7 +344,7 @@ object TachideskAPI {
app.get("/api/v1/backup/legacy/export") { ctx -> app.get("/api/v1/backup/legacy/export") { ctx ->
ctx.contentType("application/json") ctx.contentType("application/json")
ctx.result( ctx.result(
JavalinSetup.future { future {
createLegacyBackup( createLegacyBackup(
BackupFlags( BackupFlags(
includeManga = true, includeManga = true,
@ -367,7 +366,7 @@ object TachideskAPI {
ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"") ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
ctx.result( ctx.result(
JavalinSetup.future { future {
createLegacyBackup( createLegacyBackup(
BackupFlags( BackupFlags(
includeManga = true, includeManga = true,

View File

@ -15,8 +15,8 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.impl.util.PackageTools.loadExtensionSources
import suwayomi.server.ApplicationDirs import suwayomi.server.ApplicationDirs
import suwayomi.tachidesk.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.model.table.ExtensionTable import suwayomi.tachidesk.model.table.ExtensionTable
import suwayomi.tachidesk.model.table.SourceTable import suwayomi.tachidesk.model.table.SourceTable
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap

View File

@ -32,7 +32,6 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
object PackageTools { object PackageTools {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()

View File

@ -7,27 +7,29 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
BrowserRouter as Router, Redirect, Route, Switch, BrowserRouter as Router, Switch,
Route,
Redirect,
} from 'react-router-dom'; } from 'react-router-dom';
import { Container } from '@material-ui/core'; import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from 'components/navbar/NavBar';
import NavBar from './components/navbar/NavBar'; import NavbarContext from 'context/NavbarContext';
import Sources from './screens/Sources'; import DarkTheme from 'context/DarkTheme';
import Extensions from './screens/Extensions'; import useLocalStorage from 'util/useLocalStorage';
import SourceMangas from './screens/SourceMangas'; import Sources from 'screens/manga/Sources';
import Manga from './screens/Manga'; import Settings from 'screens/Settings';
import Reader from './screens/Reader'; import About from 'screens/settings/About';
import Search from './screens/SearchSingle'; import Categories from 'screens/settings/Categories';
import NavbarContext from './context/NavbarContext'; import Backup from 'screens/settings/Backup';
import DarkTheme from './context/DarkTheme'; import Library from 'screens/manga/Library';
import Library from './screens/Library'; import SearchSingle from 'screens/manga/SearchSingle';
import Settings from './screens/Settings'; import Manga from 'screens/manga/Manga';
import Categories from './screens/settings/Categories'; import MangaExtensions from 'screens/manga/MangaExtensions';
import Backup from './screens/settings/Backup'; import SourceMangas from 'screens/manga/SourceMangas';
import useLocalStorage from './util/useLocalStorage'; import Reader from 'screens/manga/Reader';
import About from './screens/settings/About'; import AnimeExtensions from 'screens/anime/AnimeExtensions';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk'); const [title, setTitle] = useState<string>('Tachidesk');
@ -78,11 +80,37 @@ export default function App() {
style={{ paddingTop: '64px' }} style={{ paddingTop: '64px' }}
> >
<Switch> <Switch>
<Route path="/sources/:sourceId/search/"> {/* general routes */}
<Search /> <Route
exact
path="/"
render={() => (
<Redirect to="/library" />
)}
/>
<Route path="/settings/about">
<About />
</Route> </Route>
<Route path="/extensions"> <Route path="/settings/categories">
<Extensions /> <Categories />
</Route>
<Route path="/settings/backup">
<Backup />
</Route>
<Route path="/settings">
<DarkTheme.Provider value={darkThemeContext}>
<Settings />
</DarkTheme.Provider>
</Route>
{/* Manga Routes */}
<Route path="/sources/:sourceId/search/">
<SearchSingle />
</Route>
<Route path="/manga/extensions">
<MangaExtensions />
</Route> </Route>
<Route path="/sources/:sourceId/popular/"> <Route path="/sources/:sourceId/popular/">
<SourceMangas popular /> <SourceMangas popular />
@ -102,36 +130,20 @@ export default function App() {
<Route path="/library"> <Route path="/library">
<Library /> <Library />
</Route> </Route>
<Route path="/settings/about">
<About />
</Route>
<Route path="/settings/categories">
<Categories />
</Route>
<Route path="/settings/backup">
<Backup />
</Route>
<Route path="/settings">
<DarkTheme.Provider value={darkThemeContext}>
<Settings />
</DarkTheme.Provider>
</Route>
<Route <Route
exact path="/manga/:mangaId/chapter/:chapterIndex"
path="/" // passing a key re-mounts the reader when changing chapters
render={() => ( render={
<Redirect to="/library" /> (props:any) => <Reader key={props.match.params.chapterIndex} />
)} }
/> />
{/* Anime Routes */}
<Route path="/anime/extensions">
<AnimeExtensions />
</Route>
</Switch> </Switch>
</Container> </Container>
<Switch>
<Route
path="/manga/:mangaId/chapter/:chapterIndex"
// passing a key re-mounts the reader when changing chapters
render={(props:any) => <Reader key={props.match.params.chapterIndex} />}
/>
</Switch>
</NavbarContext.Provider> </NavbarContext.Provider>
</ThemeProvider> </ThemeProvider>
</Router> </Router>

View File

@ -55,12 +55,20 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Library" /> <ListItemText primary="Library" />
</ListItem> </ListItem>
</Link> </Link>
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/manga/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions"> <ListItem button key="Extensions">
<ListItemIcon> <ListItemIcon>
<ExtensionIcon /> <ExtensionIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Extensions" /> <ListItemText primary="Manga Extensions" />
</ListItem>
</Link>
<Link to="/anime/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<ExtensionIcon />
</ListItemIcon>
<ListItemText primary="Anime Extensions" />
</ListItem> </ListItem>
</Link> </Link>
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>

View File

@ -0,0 +1,149 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)',
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
icon: {
width: theme.spacing(7),
height: theme.spacing(7),
flex: '0 0 auto',
marginRight: 16,
},
}));
interface IProps {
extension: IExtension
notifyInstall: () => void
}
export default function ExtensionCard(props: IProps) {
const {
extension: {
name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl,
},
notifyInstall,
} = props;
const [installedState, setInstalledState] = useState<string>(
() => {
if (obsolete) { return 'obsolete'; }
if (hasUpdate) { return 'update'; }
return (installed ? 'uninstall' : 'install');
},
);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
function install() {
setInstalledState('installing');
client.get(`/api/v1/anime/extension/install/${pkgName}`)
.then(() => {
setInstalledState('uninstall');
notifyInstall();
});
}
function update() {
setInstalledState('updating');
client.get(`/api/v1/anime/extension/update/${pkgName}`)
.then(() => {
setInstalledState('uninstall');
notifyInstall();
});
}
function uninstall() {
setInstalledState('uninstalling');
client.get(`/api/v1/anime/extension/uninstall/${pkgName}`)
.then(() => {
// setInstalledState('install');
notifyInstall();
});
}
function handleButtonClick() {
switch (installedState) {
case 'install':
install();
break;
case 'update':
update();
break;
case 'obsolete':
uninstall();
setTimeout(() => window.location.reload(), 3000);
break;
case 'uninstall':
uninstall();
break;
default:
break;
}
}
return (
<Card>
<CardContent className={classes.root}>
<div style={{ display: 'flex' }}>
<Avatar
variant="rounded"
className={classes.icon}
alt={name}
src={serverAddress + iconUrl}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{name}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{langPress}
{' '}
{versionName}
</Typography>
</div>
</div>
<Button
variant="outlined"
style={{ color: installedState === 'obsolete' ? 'red' : 'inherit' }}
onClick={() => handleButtonClick()}
>
{installedState}
</Button>
</CardContent>
</Card>
);
}

View File

@ -15,7 +15,7 @@ import Dialog from '@material-ui/core/Dialog';
import Checkbox from '@material-ui/core/Checkbox'; import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup'; import FormGroup from '@material-ui/core/FormGroup';
import client from '../util/client'; import client from 'util/client';
const useStyles = makeStyles(() => createStyles({ const useStyles = makeStyles(() => createStyles({
paper: { paper: {

View File

@ -16,7 +16,7 @@ import { Link } from 'react-router-dom';
import Menu from '@material-ui/core/Menu'; import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem'; import MenuItem from '@material-ui/core/MenuItem';
import BookmarkIcon from '@material-ui/icons/Bookmark'; import BookmarkIcon from '@material-ui/icons/Bookmark';
import client from '../util/client'; import client from 'util/client';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {

View File

@ -12,8 +12,8 @@ import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import client from '../util/client'; import client from 'util/client';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {

View File

@ -17,8 +17,8 @@ import IconButton from '@material-ui/core/IconButton';
import FilterListIcon from '@material-ui/icons/FilterList'; import FilterListIcon from '@material-ui/icons/FilterList';
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core'; import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import { langCodeToName } from '../util/language'; import { langCodeToName } from 'util/language';
import cloneObject from '../util/cloneObject'; import cloneObject from 'util/cloneObject';
const useStyles = makeStyles(() => createStyles({ const useStyles = makeStyles(() => createStyles({
paper: { paper: {

View File

@ -13,7 +13,7 @@ import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core'; import { Grid } from '@material-ui/core';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {

View File

@ -13,9 +13,9 @@ import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FilterListIcon from '@material-ui/icons/FilterList'; import FilterListIcon from '@material-ui/icons/FilterList';
import PublicIcon from '@material-ui/icons/Public'; import PublicIcon from '@material-ui/icons/Public';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
import CategorySelect from './CategorySelect'; import CategorySelect from './CategorySelect';
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({ const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({

View File

@ -12,8 +12,8 @@ import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
import { langCodeToName } from '../util/language'; import { langCodeToName } from 'util/language';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {

View File

@ -0,0 +1,112 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react';
import ExtensionCard from 'components/anime/ExtensionCard';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import ExtensionLangSelect from 'components/manga/ExtensionLangSelect';
import { defualtLangs, langCodeToName, langSortCmp } from 'util/language';
const allLangs: string[] = [];
function groupExtensions(extensions: IExtension[]) {
allLangs.length = 0; // empty the array
const result = { installed: [], 'updates pending': [] } as any;
extensions.sort((a, b) => ((a.apkName > b.apkName) ? 1 : -1));
extensions.forEach((extension) => {
if (result[extension.lang] === undefined) {
result[extension.lang] = [];
if (extension.lang !== 'all') { allLangs.push(extension.lang); }
}
if (extension.installed) {
if (extension.hasUpdate) {
result['updates pending'].push(extension);
} else {
result.installed.push(extension);
}
} else {
result[extension.lang].push(extension);
}
});
// put english first for convience
allLangs.sort(langSortCmp);
return result;
}
function extensionDefaultLangs() {
return [...defualtLangs(), 'all'];
}
export default function AnimeExtensions() {
const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
useEffect(() => {
setTitle('Extensions');
setAction(
<ExtensionLangSelect
shownLangs={shownLangs}
setShownLangs={setShownLangs}
allLangs={allLangs}
/>,
);
}, [shownLangs]);
const [extensionsRaw, setExtensionsRaw] = useState<IExtension[]>([]);
const [extensions, setExtensions] = useState<any>({});
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
client.get('/api/v1/anime/extension/list')
.then((response) => response.data)
.then((data) => setExtensionsRaw(data));
}, [updateTriggerHolder]);
useEffect(() => {
if (extensionsRaw.length > 0) {
const groupedExtension = groupExtensions(extensionsRaw);
setExtensions(groupedExtension);
}
}, [extensionsRaw]);
if (Object.entries(extensions).length === 0) {
return <h3>loading...</h3>;
}
const groupsToShow = ['updates pending', 'installed', ...shownLangs];
return (
<>
{
Object.entries(extensions).map(([lang, list]) => (
((groupsToShow.indexOf(lang) !== -1 && (list as []).length > 0)
&& (
<React.Fragment key={lang}>
<h1 key={lang} style={{ marginLeft: 25 }}>
{langCodeToName(lang)}
</h1>
{(list as IExtension[]).map((it) => (
<ExtensionCard
key={it.apkName}
extension={it}
notifyInstall={() => {
triggerUpdate();
}}
/>
))}
</React.Fragment>
))
))
}
</>
);
}

View File

@ -7,10 +7,10 @@
import { Tab, Tabs } from '@material-ui/core'; import { Tab, Tabs } from '@material-ui/core';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import MangaGrid from '../components/MangaGrid'; import MangaGrid from 'components/manga/MangaGrid';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
import cloneObject from '../util/cloneObject'; import cloneObject from 'util/cloneObject';
interface IMangaCategory { interface IMangaCategory {
category: ICategory category: ICategory

View File

@ -9,12 +9,12 @@ import React, { useEffect, useState, useContext } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles'; import { makeStyles, Theme } from '@material-ui/core/styles';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import ChapterCard from '../components/ChapterCard'; import ChapterCard from 'components/manga/ChapterCard';
import MangaDetails from '../components/MangaDetails'; import MangaDetails from 'components/manga/MangaDetails';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
import LoadingPlaceholder from '../components/LoadingPlaceholder'; import LoadingPlaceholder from 'components/LoadingPlaceholder';
import makeToast from '../components/Toast'; import makeToast from 'components/Toast';
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
root: { root: {

View File

@ -6,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import ExtensionCard from '../components/ExtensionCard'; import ExtensionCard from 'components/manga/ExtensionCard';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
import ExtensionLangSelect from '../components/ExtensionLangSelect'; import ExtensionLangSelect from 'components/manga/ExtensionLangSelect';
import { defualtLangs, langCodeToName, langSortCmp } from '../util/language'; import { defualtLangs, langCodeToName, langSortCmp } from 'util/language';
const allLangs: string[] = []; const allLangs: string[] = [];
@ -46,7 +46,7 @@ function extensionDefaultLangs() {
return [...defualtLangs(), 'all']; return [...defualtLangs(), 'all'];
} }
export default function Extensions() { export default function MangaExtensions() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs()); const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());

View File

@ -9,15 +9,15 @@ import CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import HorizontalPager from '../components/reader/pager/HorizontalPager'; import HorizontalPager from 'components/manga/reader/pager/HorizontalPager';
import PageNumber from '../components/reader/PageNumber'; import PageNumber from 'components/manga/reader/PageNumber';
import WebtoonPager from '../components/reader/pager/PagedPager'; import WebtoonPager from 'components/manga/reader/pager/PagedPager';
import VerticalPager from '../components/reader/pager/VerticalPager'; import VerticalPager from 'components/manga/reader/pager/VerticalPager';
import ReaderNavBar, { defaultReaderSettings } from '../components/navbar/ReaderNavBar'; import ReaderNavBar, { defaultReaderSettings } from 'components/navbar/ReaderNavBar';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
import cloneObject from '../util/cloneObject'; import cloneObject from 'util/cloneObject';
const useStyles = (settings: IReaderSettings) => makeStyles({ const useStyles = (settings: IReaderSettings) => makeStyles({
root: { root: {

View File

@ -10,9 +10,9 @@ import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid'; import MangaGrid from 'components/manga/MangaGrid';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {

View File

@ -7,9 +7,9 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid'; import MangaGrid from 'components/manga/MangaGrid';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
export default function SourceMangas(props: { popular: boolean }) { export default function SourceMangas(props: { popular: boolean }) {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);

View File

@ -6,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import ExtensionLangSelect from '../components/ExtensionLangSelect'; import ExtensionLangSelect from 'components/manga/ExtensionLangSelect';
import SourceCard from '../components/SourceCard'; import SourceCard from 'components/manga/SourceCard';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../util/client'; import client from 'util/client';
import { defualtLangs, langCodeToName, langSortCmp } from '../util/language'; import { defualtLangs, langCodeToName, langSortCmp } from 'util/language';
import useLocalStorage from '../util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
function sourceToLangList(sources: ISource[]) { function sourceToLangList(sources: ISource[]) {
const result: string[] = []; const result: string[] = [];

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "./src",
"target": "es5", "target": "es5",
"lib": [ "lib": [
"dom", "dom",