diff --git a/multisrc/build.gradle.kts b/multisrc/build.gradle.kts index 2f1bc16ee..7f1b536cb 100644 --- a/multisrc/build.gradle.kts +++ b/multisrc/build.gradle.kts @@ -31,6 +31,10 @@ configurations { dependencies { compileOnly(libs.bundles.common) + // Only PeachScan sources uses the image-decoder dependency. + //noinspection UseTomlInstead + compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290") + // Implements all :lib libraries on the multisrc generator // Note that this does not mean that generated sources are going to // implement them too; this is just to be able to compile and generate sources. diff --git a/multisrc/overrides/madara/wickedwitchscan/src/WickedWitchScan.kt b/multisrc/overrides/madara/wickedwitchscan/src/WickedWitchScan.kt deleted file mode 100644 index 8123e7f47..000000000 --- a/multisrc/overrides/madara/wickedwitchscan/src/WickedWitchScan.kt +++ /dev/null @@ -1,20 +0,0 @@ -package eu.kanade.tachiyomi.extension.pt.wickedwitchscan - -import eu.kanade.tachiyomi.multisrc.madara.Madara -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import okhttp3.OkHttpClient -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.concurrent.TimeUnit - -class WickedWitchScan : Madara( - "Wicked Witch Scan", - "https://wickedwitchscan.com", - "pt-BR", - SimpleDateFormat("MMMMM dd, yyyy", Locale("pt", "BR")), -) { - - override val client: OkHttpClient = super.client.newBuilder() - .rateLimit(1, 2, TimeUnit.SECONDS) - .build() -} diff --git a/multisrc/overrides/peachscan/dangoscan/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/peachscan/dangoscan/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..cd2fab49f Binary files /dev/null and b/multisrc/overrides/peachscan/dangoscan/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/dangoscan/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/peachscan/dangoscan/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..9acb8b3a9 Binary files /dev/null and b/multisrc/overrides/peachscan/dangoscan/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/dangoscan/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/peachscan/dangoscan/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..875588cec Binary files /dev/null and b/multisrc/overrides/peachscan/dangoscan/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/dangoscan/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/dangoscan/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..dbdeb56e6 Binary files /dev/null and b/multisrc/overrides/peachscan/dangoscan/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/dangoscan/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/dangoscan/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..288cb9a61 Binary files /dev/null and b/multisrc/overrides/peachscan/dangoscan/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/default/additional.gradle b/multisrc/overrides/peachscan/default/additional.gradle new file mode 100644 index 000000000..da7c190c3 --- /dev/null +++ b/multisrc/overrides/peachscan/default/additional.gradle @@ -0,0 +1,5 @@ +dependencies { + // Only PeachScan sources uses the image-decoder dependency. + //noinspection UseTomlInstead + compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290") +} diff --git a/multisrc/overrides/peachscan/modescanlator/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/peachscan/modescanlator/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..9d56b523e Binary files /dev/null and b/multisrc/overrides/peachscan/modescanlator/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/modescanlator/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/peachscan/modescanlator/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..47ad39f46 Binary files /dev/null and b/multisrc/overrides/peachscan/modescanlator/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/modescanlator/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/peachscan/modescanlator/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..81080fb94 Binary files /dev/null and b/multisrc/overrides/peachscan/modescanlator/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/modescanlator/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/modescanlator/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..8c76dd549 Binary files /dev/null and b/multisrc/overrides/peachscan/modescanlator/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/modescanlator/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/modescanlator/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..d5734f484 Binary files /dev/null and b/multisrc/overrides/peachscan/modescanlator/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/nazarickscan/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..c8f93029b Binary files /dev/null and b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/nazarickscan/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..7e9be20f7 Binary files /dev/null and b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..9f9099b70 Binary files /dev/null and b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..abd680b44 Binary files /dev/null and b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..c53b4012c Binary files /dev/null and b/multisrc/overrides/peachscan/nazarickscan/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..72e5b1c08 Binary files /dev/null and b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..efe26a5b2 Binary files /dev/null and b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..728f4726c Binary files /dev/null and b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d8b3a3f15 Binary files /dev/null and b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..60a7d16f4 Binary files /dev/null and b/multisrc/overrides/peachscan/rfdragonscan/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..124bb4637 Binary files /dev/null and b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..b00cdb197 Binary files /dev/null and b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e7cf48d6c Binary files /dev/null and b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..02fbf16b8 Binary files /dev/null and b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f536697f6 Binary files /dev/null and b/multisrc/overrides/peachscan/wickedwitchscannovo/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/peachscan/wickedwitchscannovo/src/WickedWitchScan.kt b/multisrc/overrides/peachscan/wickedwitchscannovo/src/WickedWitchScan.kt new file mode 100644 index 000000000..076a2ed1f --- /dev/null +++ b/multisrc/overrides/peachscan/wickedwitchscannovo/src/WickedWitchScan.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.extension.pt.wickedwitchscannovo + +import eu.kanade.tachiyomi.multisrc.peachscan.PeachScan +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import java.util.concurrent.TimeUnit + +class WickedWitchScan : PeachScan("Wicked Witch Scan", "https://wicked-witch-scan.com", "pt-BR") { + // Source changed from Madara to PeachScan + override val versionId = 2 + + override val client = super.client.newBuilder() + .rateLimit(1, 2, TimeUnit.SECONDS) + .build() +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt index c4a8523ef..c996f0a0c 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt @@ -495,7 +495,6 @@ class MadaraGenerator : ThemeSourceGenerator { SingleLang("WebtoonUK", "https://webtoon.uk", "en", overrideVersionCode = 2), SingleLang("WebtoonXYZ", "https://www.webtoon.xyz", "en", isNsfw = true, overrideVersionCode = 3), SingleLang("Whale Manga", "https://whalemanga.com", "en", isNsfw = true), - SingleLang("Wicked Witch Scan", "https://wickedwitchscan.com", "pt-BR"), SingleLang("Winter Scan", "https://winterscan.com", "pt-BR", overrideVersionCode = 4), SingleLang("Wonderland Scan", "https://wonderlandscan.com", "pt-BR", overrideVersionCode = 3), SingleLang("WoopRead", "https://woopread.com", "en", overrideVersionCode = 1), diff --git a/src/pt/wickedwitchscannovo/src/eu/kanade/tachiyomi/extension/pt/wickedwitchscannovo/WickedWitchScan.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/peachscan/PeachScan.kt similarity index 90% rename from src/pt/wickedwitchscannovo/src/eu/kanade/tachiyomi/extension/pt/wickedwitchscannovo/WickedWitchScan.kt rename to multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/peachscan/PeachScan.kt index 9f9f61bf9..4ee66646b 100644 --- a/src/pt/wickedwitchscannovo/src/eu/kanade/tachiyomi/extension/pt/wickedwitchscannovo/WickedWitchScan.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/peachscan/PeachScan.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.extension.pt.wickedwitchscannovo +package eu.kanade.tachiyomi.multisrc.peachscan import android.annotation.SuppressLint import android.app.ActivityManager @@ -8,7 +8,6 @@ import android.graphics.Canvas import android.graphics.Rect import android.util.Base64 import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page @@ -39,37 +38,27 @@ import java.io.IOException import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone -import java.util.concurrent.TimeUnit import java.util.zip.ZipInputStream @SuppressLint("WrongConstant") -class WickedWitchScan : ParsedHttpSource() { - - override val name = "Wicked Witch Scan" - - override val lang = "pt-BR" - - override val baseUrl = "https://wicked-witch-scan.com" - - // Source changed from Madara to homegrown website - override val versionId = 2 +abstract class PeachScan( + override val name: String, + override val baseUrl: String, + override val lang: String, + private val dateFormat: SimpleDateFormat = SimpleDateFormat("d 'de' MMMM 'de' yyyy 'às' HH:mm", Locale("pt", "BR")).apply { + timeZone = TimeZone.getTimeZone("America/Sao_Paulo") + }, +) : ParsedHttpSource() { override val supportsLatest = true override val client = network.cloudflareClient .newBuilder() - .rateLimit(1, 2, TimeUnit.SECONDS) .addInterceptor(::zipImageInterceptor) .build() private val json: Json by injectLazy() - private val simpleDateFormat by lazy { - SimpleDateFormat("d 'de' MMMM 'de' yyyy 'às' HH:mm", Locale("pt", "BR")).apply { - timeZone = TimeZone.getTimeZone("America/Sao_Paulo") - } - } - override fun popularMangaRequest(page: Int) = GET("$baseUrl/todas-as-obras/", headers) override fun popularMangaSelector() = ".comics__all__box" @@ -86,7 +75,7 @@ class WickedWitchScan : ParsedHttpSource() { override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers) - override fun latestUpdatesSelector() = "div.comic" + override fun latestUpdatesSelector() = "div.comic:not(:has(a.box-image > p:contains(Novel)))" override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href")) @@ -106,10 +95,10 @@ class WickedWitchScan : ParsedHttpSource() { } override fun searchMangaParse(response: Response): MangasPage { - val manga = json.parseToJsonElement(response.body.string()).jsonArray.map { + val manga = json.parseToJsonElement(response.body.string()).jsonArray.mapNotNull { val element = Jsoup.parseBodyFragment(it.jsonObject["html"]!!.jsonPrimitive.content) - searchMangaFromElement(element) + runCatching { searchMangaFromElement(element) }.getOrNull() } return MangasPage(manga, false) @@ -142,19 +131,6 @@ class WickedWitchScan : ParsedHttpSource() { description = "Tipo: $category\n\n$synopsis" } - override fun chapterListSelector() = ".link__capitulos" - - override fun chapterFromElement(element: Element) = SChapter.create().apply { - setUrlWithoutDomain(element.attr("href")) - - name = element.selectFirst(".numero__capitulo")!!.text() - date_upload = runCatching { - val date = element.selectFirst(".data__lançamento")!!.text() - - simpleDateFormat.parse(date)?.time - }.getOrNull() ?: 0L - } - override fun chapterListParse(response: Response): List { val document = response.asJsoup() val mediaType = document.selectFirst(".categoria__comic")?.text() @@ -167,9 +143,24 @@ class WickedWitchScan : ParsedHttpSource() { return document.select(chapterListSelector()).map { chapterFromElement(it) } } + override fun chapterListSelector() = ".link__capitulos" + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + setUrlWithoutDomain(element.attr("href")) + + name = element.selectFirst(".numero__capitulo")!!.text() + date_upload = runCatching { + val date = element.selectFirst(".data__lançamento")!!.text() + + dateFormat.parse(date)!!.time + }.getOrDefault(0L) + } + override fun pageListParse(document: Document): List { val scriptElement = document.selectFirst("script:containsData(const urls =[)") - ?: throw Exception("Não foi possível encontrar o script com dados de imagem.") + ?: return document.select("#imageContainer img").mapIndexed { i, it -> + Page(i, imageUrl = it.attr("abs:src")) + } val urls = scriptElement.html().substringAfter("const urls =[").substringBefore("];") diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/peachscan/PeachScanGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/peachscan/PeachScanGenerator.kt new file mode 100644 index 000000000..47bfb2e14 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/peachscan/PeachScanGenerator.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.multisrc.peachscan + +import generator.ThemeSourceData.SingleLang +import generator.ThemeSourceGenerator + +class PeachScanGenerator : ThemeSourceGenerator { + + override val themePkg = "peachscan" + + override val themeClass = "PeachScan" + + override val baseVersionCode = 1 + + override val sources = listOf( + SingleLang("Dango Scan", "https://dangoscan.com.br", "pt-BR"), + SingleLang("Mode Scanlator", "https://modescanlator.com", "pt-BR"), + SingleLang("Nazarick Scan", "https://nazarickscan.com.br", "pt-BR"), + SingleLang("RF Dragon Scan", "https://rfdragonscan.com", "pt-BR"), + SingleLang("Wicked Witch Scan", "https://wicked-witch-scan.com", "pt-BR", pkgName = "wickedwitchscannovo", overrideVersionCode = 1), + ) + + companion object { + @JvmStatic + fun main(args: Array) { + PeachScanGenerator().createAll() + } + } +} diff --git a/src/pt/wickedwitchscannovo/AndroidManifest.xml b/src/pt/wickedwitchscannovo/AndroidManifest.xml deleted file mode 100644 index 8072ee00d..000000000 --- a/src/pt/wickedwitchscannovo/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/pt/wickedwitchscannovo/build.gradle b/src/pt/wickedwitchscannovo/build.gradle deleted file mode 100644 index a7f8de5c2..000000000 --- a/src/pt/wickedwitchscannovo/build.gradle +++ /dev/null @@ -1,11 +0,0 @@ -ext { - extName = 'Wicked Witch Scan (Novo)' - extClass = '.WickedWitchScan' - extVersionCode = 1 -} - -apply from: "$rootDir/common.gradle" - -dependencies { - compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290") -} diff --git a/src/pt/wickedwitchscannovo/res/mipmap-hdpi/ic_launcher.png b/src/pt/wickedwitchscannovo/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index ca2db7ce1..000000000 Binary files a/src/pt/wickedwitchscannovo/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/pt/wickedwitchscannovo/res/mipmap-mdpi/ic_launcher.png b/src/pt/wickedwitchscannovo/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 640f448c2..000000000 Binary files a/src/pt/wickedwitchscannovo/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/pt/wickedwitchscannovo/res/mipmap-xhdpi/ic_launcher.png b/src/pt/wickedwitchscannovo/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index d7a188216..000000000 Binary files a/src/pt/wickedwitchscannovo/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/pt/wickedwitchscannovo/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/wickedwitchscannovo/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index b350eede6..000000000 Binary files a/src/pt/wickedwitchscannovo/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/pt/wickedwitchscannovo/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/wickedwitchscannovo/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 51b083f63..000000000 Binary files a/src/pt/wickedwitchscannovo/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ