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/. */
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.server.JavalinSetup
import suwayomi.server.JavalinSetup.future
object AnimeAPI {
fun defineEndpoints(app: Javalin) {
// list all extensions
app.get("/api/v1/extension/list") { ctx ->
app.get("/api/v1/anime/extension/list") { ctx ->
ctx.json(
JavalinSetup.future {
future {
getExtensionList()
}
)
}
// // install extension identified with "pkgName"
// app.get("/api/v1/extension/install/:pkgName") { ctx ->
// val pkgName = ctx.pathParam("pkgName")
//
// ctx.json(
// JavalinSetup.future {
// installExtension(pkgName)
// }
// )
// }
//
// // update extension identified with "pkgName"
// app.get("/api/v1/extension/update/:pkgName") { ctx ->
// val pkgName = ctx.pathParam("pkgName")
//
// ctx.json(
// JavalinSetup.future {
// updateExtension(pkgName)
// }
// )
// }
//
// // uninstall extension identified with "pkgName"
// app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
// val pkgName = ctx.pathParam("pkgName")
//
// uninstallExtension(pkgName)
// ctx.status(200)
// }
//
// // icon for extension named `apkName`
// app.get("/api/v1/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
// val apkName = ctx.pathParam("apkName")
//
// ctx.result(
// JavalinSetup.future { getExtensionIcon(apkName) }
// .thenApply {
// ctx.header("content-type", it.second)
// it.first
// }
// )
// }
// install extension identified with "pkgName"
app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
JavalinSetup.future {
installExtension(pkgName)
}
)
}
// update extension identified with "pkgName"
app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
JavalinSetup.future {
updateExtension(pkgName)
}
)
}
// uninstall extension identified with "pkgName"
app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
uninstallExtension(pkgName)
ctx.status(200)
}
// icon for extension named `apkName`
app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
val apkName = ctx.pathParam("apkName")
ctx.result(
JavalinSetup.future { getExtensionIcon(apkName) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
}
// // list of sources
// app.get("/api/v1/source/list") { ctx ->

View File

@ -10,9 +10,9 @@ package suwayomi.anime.impl.extension
import android.net.Uri
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.AnimeCatalogueSource
import eu.kanade.tachiyomi.source.AnimeSource
import eu.kanade.tachiyomi.source.AnimeSourceFactory
import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
@ -125,11 +125,11 @@ object Extension {
File(dexFilePath).delete()
// collect sources from the extension
val sources: List<CatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
is Source -> listOf(instance)
is SourceFactory -> instance.createSources()
val sources: List<AnimeCatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
is AnimeSource -> listOf(instance)
is AnimeSourceFactory -> instance.createSources()
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 extensionLang = when (langs.size) {
@ -246,6 +246,6 @@ object Extension {
}
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 javax.xml.parsers.DocumentBuilderFactory
object PackageTools {
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
const val EXTENSION_FEATURE = "tachiyomi.extension"
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val EXTENSION_FEATURE = "tachiyomi.animeextension"
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
const val LIB_VERSION_MIN = 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 handler = BaksmaliBaseDexExceptionHandler()
Dex2jar
.from(reader)
.withExceptionHandler(handler)
.reUseReg(false)
.topoLogicalSort()
.skipDebug(true)
.optimizeSynchronized(false)
.printIR(false)
.noCode(false)
.skipExceptions(false)
.to(jarFilePath)
.from(reader)
.withExceptionHandler(handler)
.reUseReg(false)
.topoLogicalSort()
.skipDebug(true)
.optimizeSynchronized(false)
.printIR(false)
.noCode(false)
.skipExceptions(false)
.to(jarFilePath)
if (handler.hasException()) {
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
logger.error(
"""
"""
Detail Error Information in File $errorFile
Please report this file to one of following link if possible (any one).
https://sourceforge.net/p/dex2jar/tickets/
@ -101,27 +100,27 @@ object PackageTools {
val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()
.orEmpty()
.asSequence()
.filter {
it.nodeType == Node.ELEMENT_NODE
}.map {
it as Element
}.filter {
it.tagName == "meta-data"
}.forEach {
putString(
it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
}
.orEmpty()
.asSequence()
.filter {
it.nodeType == Node.ELEMENT_NODE
}.map {
it as Element
}.filter {
it.tagName == "meta-data"
}.forEach {
putString(
it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
}
}
signatures = (
parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
}
}

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.future.future
import mu.KotlinLogging
import suwayomi.anime.AnimeAPI
import suwayomi.server.util.Browser
import suwayomi.tachidesk.TachideskAPI
import java.io.IOException
@ -75,5 +76,6 @@ object JavalinSetup {
}
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/. */
import io.javalin.Javalin
import suwayomi.server.JavalinSetup
import suwayomi.server.JavalinSetup.future
import suwayomi.server.impl.About
import suwayomi.tachidesk.impl.Category
@ -47,7 +46,7 @@ object TachideskAPI {
// list all extensions
app.get("/api/v1/extension/list") { ctx ->
ctx.json(
JavalinSetup.future {
future {
getExtensionList()
}
)
@ -58,7 +57,7 @@ object TachideskAPI {
val pkgName = ctx.pathParam("pkgName")
ctx.json(
JavalinSetup.future {
future {
installExtension(pkgName)
}
)
@ -69,7 +68,7 @@ object TachideskAPI {
val pkgName = ctx.pathParam("pkgName")
ctx.json(
JavalinSetup.future {
future {
updateExtension(pkgName)
}
)
@ -88,7 +87,7 @@ object TachideskAPI {
val apkName = ctx.pathParam("apkName")
ctx.result(
JavalinSetup.future { getExtensionIcon(apkName) }
future { getExtensionIcon(apkName) }
.thenApply {
ctx.header("content-type", it.second)
it.first
@ -112,7 +111,7 @@ object TachideskAPI {
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
JavalinSetup.future {
future {
getMangaList(sourceId, pageNum, popular = true)
}
)
@ -123,7 +122,7 @@ object TachideskAPI {
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
JavalinSetup.future {
future {
getMangaList(sourceId, pageNum, popular = false)
}
)
@ -135,7 +134,7 @@ object TachideskAPI {
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json(
JavalinSetup.future {
future {
getManga(mangaId, onlineFetch)
}
)
@ -146,7 +145,7 @@ object TachideskAPI {
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
JavalinSetup.future { getMangaThumbnail(mangaId) }
future { getMangaThumbnail(mangaId) }
.thenApply {
ctx.header("content-type", it.second)
it.first
@ -182,14 +181,14 @@ object TachideskAPI {
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
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").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
@ -214,7 +213,7 @@ object TachideskAPI {
val index = ctx.pathParam("index").toInt()
ctx.result(
JavalinSetup.future { getPageImage(mangaId, chapterIndex, index) }
future { getPageImage(mangaId, chapterIndex, index) }
.thenApply {
ctx.header("content-type", it.second)
it.first
@ -243,7 +242,7 @@ object TachideskAPI {
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) })
ctx.json(future { sourceSearch(sourceId, searchTerm, pageNum) })
}
// source filter list
@ -257,7 +256,7 @@ object TachideskAPI {
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
JavalinSetup.future { addMangaToLibrary(mangaId) }
future { addMangaToLibrary(mangaId) }
)
}
@ -266,7 +265,7 @@ object TachideskAPI {
val mangaId = ctx.pathParam("mangaId").toInt()
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"
app.post("/api/v1/backup/legacy/import/file") { ctx ->
ctx.result(
JavalinSetup.future {
future {
restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
}
)
@ -345,7 +344,7 @@ object TachideskAPI {
app.get("/api/v1/backup/legacy/export") { ctx ->
ctx.contentType("application/json")
ctx.result(
JavalinSetup.future {
future {
createLegacyBackup(
BackupFlags(
includeManga = true,
@ -367,7 +366,7 @@ object TachideskAPI {
ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
ctx.result(
JavalinSetup.future {
future {
createLegacyBackup(
BackupFlags(
includeManga = true,

View File

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

View File

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

View File

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

View File

@ -55,12 +55,20 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Library" />
</ListItem>
</Link>
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<Link to="/manga/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<ExtensionIcon />
</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>
</Link>
<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 FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import client from '../util/client';
import client from 'util/client';
const useStyles = makeStyles(() => createStyles({
paper: {

View File

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

View File

@ -12,8 +12,8 @@ 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';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
const useStyles = makeStyles((theme) => ({
root: {

View File

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

View File

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

View File

@ -13,9 +13,9 @@ import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FilterListIcon from '@material-ui/icons/FilterList';
import PublicIcon from '@material-ui/icons/Public';
import React, { useContext, useEffect, useState } from 'react';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import CategorySelect from './CategorySelect';
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 Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import useLocalStorage from '../util/useLocalStorage';
import { langCodeToName } from '../util/language';
import useLocalStorage from 'util/useLocalStorage';
import { langCodeToName } from 'util/language';
const useStyles = makeStyles((theme) => ({
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 React, { useContext, useEffect, useState } from 'react';
import MangaGrid from '../components/MangaGrid';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import cloneObject from '../util/cloneObject';
import MangaGrid from 'components/manga/MangaGrid';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import cloneObject from 'util/cloneObject';
interface IMangaCategory {
category: ICategory

View File

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

View File

@ -6,12 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react';
import ExtensionCard from '../components/ExtensionCard';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import ExtensionLangSelect from '../components/ExtensionLangSelect';
import { defualtLangs, langCodeToName, langSortCmp } from '../util/language';
import ExtensionCard from 'components/manga/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[] = [];
@ -46,7 +46,7 @@ function extensionDefaultLangs() {
return [...defualtLangs(), 'all'];
}
export default function Extensions() {
export default function MangaExtensions() {
const { setTitle, setAction } = useContext(NavbarContext);
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 React, { useContext, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import HorizontalPager from '../components/reader/pager/HorizontalPager';
import PageNumber from '../components/reader/PageNumber';
import WebtoonPager from '../components/reader/pager/PagedPager';
import VerticalPager from '../components/reader/pager/VerticalPager';
import ReaderNavBar, { defaultReaderSettings } from '../components/navbar/ReaderNavBar';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import cloneObject from '../util/cloneObject';
import HorizontalPager from 'components/manga/reader/pager/HorizontalPager';
import PageNumber from 'components/manga/reader/PageNumber';
import WebtoonPager from 'components/manga/reader/pager/PagedPager';
import VerticalPager from 'components/manga/reader/pager/VerticalPager';
import ReaderNavBar, { defaultReaderSettings } from 'components/navbar/ReaderNavBar';
import NavbarContext from 'context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import cloneObject from 'util/cloneObject';
const useStyles = (settings: IReaderSettings) => makeStyles({
root: {

View File

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

View File

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

View File

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

View File

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