/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.pitchedapps.frost.injectors
import android.content.Context
import android.graphics.Color
import ca.allanwang.kau.utils.adjustAlpha
import ca.allanwang.kau.utils.colorToBackground
import ca.allanwang.kau.utils.colorToForeground
import ca.allanwang.kau.utils.isColorVisibleOn
import ca.allanwang.kau.utils.toRgbaString
import ca.allanwang.kau.utils.use
import ca.allanwang.kau.utils.withAlpha
import com.pitchedapps.frost.enums.FACEBOOK_BLUE
import com.pitchedapps.frost.enums.Theme
import com.pitchedapps.frost.enums.ThemeCategory
import com.pitchedapps.frost.prefs.Prefs
import com.pitchedapps.frost.utils.L
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.FileNotFoundException
import javax.inject.Inject
import javax.inject.Singleton
interface ThemeProvider {
val textColor: Int
val accentColor: Int
val accentColorForWhite: Int
val nativeBgColor: Int
fun nativeBgColor(unread: Boolean): Int
val bgColor: Int
val headerColor: Int
val iconColor: Int
val isCustomTheme: Boolean
/**
* Note that while this can be loaded from any thread, it is typically done through [preload]]
*/
fun injector(category: ThemeCategory): InjectorContract
fun setTheme(id: Int)
fun reset()
suspend fun preload()
}
/**
* Provides [InjectorContract] for each [ThemeCategory].
* Can be reloaded to take in changes from [Prefs]
*/
class ThemeProviderImpl @Inject internal constructor(
@ApplicationContext private val context: Context,
private val prefs: Prefs
) : ThemeProvider {
private var theme: Theme = Theme.values[prefs.theme]
set(value) {
field = value
prefs.theme = value.ordinal
}
private val injectors: MutableMap = mutableMapOf()
override val textColor: Int
get() = theme.textColorGetter(prefs)
override val accentColor: Int
get() = theme.accentColorGetter(prefs)
override val accentColorForWhite: Int
get() = when {
accentColor.isColorVisibleOn(Color.WHITE) -> accentColor
textColor.isColorVisibleOn(Color.WHITE) -> textColor
else -> FACEBOOK_BLUE
}
override val nativeBgColor: Int
get() = bgColor.withAlpha(30)
override fun nativeBgColor(unread: Boolean) = bgColor
.colorToForeground(if (unread) 0.7f else 0.0f)
.withAlpha(30)
override val bgColor: Int
get() = theme.backgroundColorGetter(prefs)
override val headerColor: Int
get() = theme.headerColorGetter(prefs)
override val iconColor: Int
get() = theme.iconColorGetter(prefs)
override val isCustomTheme: Boolean
get() = theme == Theme.CUSTOM
override fun injector(category: ThemeCategory): InjectorContract =
injectors.getOrPut(category) { createInjector(category) }
/**
* Note that while this can be loaded from any thread, it is typically done through [preload]
*/
private fun createInjector(category: ThemeCategory): InjectorContract {
val file = theme.file ?: return JsActions.EMPTY
try {
var content =
context.assets.open("css/${category.folder}/themes/$file").bufferedReader()
.use(BufferedReader::readText)
if (theme == Theme.CUSTOM) {
val bt = if (Color.alpha(bgColor) == 255)
bgColor.toRgbaString()
else
"transparent"
val bb = bgColor.colorToForeground(0.35f)
content = content
.replace("\$T\$", textColor.toRgbaString())
.replace("\$TT\$", textColor.colorToBackground(0.05f).toRgbaString())
.replace("\$TD\$", textColor.adjustAlpha(0.6f).toRgbaString())
.replace("\$A\$", accentColor.toRgbaString())
.replace("\$AT\$", iconColor.toRgbaString())
.replace("\$B\$", bgColor.toRgbaString())
.replace("\$BT\$", bt)
.replace("\$BBT\$", bb.withAlpha(51).toRgbaString())
.replace("\$O\$", bgColor.withAlpha(255).toRgbaString())
.replace("\$OO\$", bb.withAlpha(255).toRgbaString())
.replace("\$D\$", textColor.adjustAlpha(0.3f).toRgbaString())
.replace("\$TI\$", bb.withAlpha(60).toRgbaString())
.replace("\$C\$", bt)
}
return JsBuilder().css(content).build()
} catch (e: FileNotFoundException) {
L.e(e) { "CssAssets file not found" }
return JsActions.EMPTY
}
}
override fun setTheme(id: Int) {
if (theme.ordinal == id) return
theme = Theme.values[id]
reset()
}
override fun reset() {
injectors.clear()
}
override suspend fun preload() {
withContext(Dispatchers.IO) {
reset()
ThemeCategory.values().forEach { injector(it) }
}
}
}
@Module
@InstallIn(SingletonComponent::class)
interface ThemeProviderModule {
@Binds
@Singleton
fun themeProvider(to: ThemeProviderImpl): ThemeProvider
}