1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 12:02:33 +01:00

Merge pull request #1947 from AllanWang/js-injection

This commit is contained in:
Allan Wang 2023-06-20 23:53:07 -07:00 committed by GitHub
commit a96746d5f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 1939 additions and 294 deletions

View File

@ -206,6 +206,10 @@ dependencies {
implementation("com.google.protobuf:protobuf-kotlin-lite:3.23.3")
implementation("app.cash.sqldelight:android-driver:2.0.0-rc01")
// https://mvnrepository.com/artifact/org.apache.commons/commons-text
implementation("org.apache.commons:commons-text:1.10.0")
}
protobuf {

View File

@ -20,16 +20,21 @@ import android.app.Activity
import android.app.Application
import android.os.Bundle
import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.hilt.FrostComponents
import com.pitchedapps.frost.components.FrostComponents
import com.pitchedapps.frost.webview.injection.FrostJsInjectors
import com.pitchedapps.frost.webview.injection.assets.JsAssets
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
/** Frost Application. */
@HiltAndroidApp
class FrostApp : Application() {
@Inject lateinit var componentsProvider: Provider<FrostComponents>
@Inject lateinit var frostJsInjectors: Provider<FrostJsInjectors>
override fun onCreate() {
super.onCreate()
@ -57,6 +62,13 @@ class FrostApp : Application() {
},
)
}
MainScope().launch { setup() }
}
private suspend fun setup() {
JsAssets.load(this)
frostJsInjectors.get().load()
}
companion object {

View File

@ -18,7 +18,7 @@ package com.pitchedapps.frost
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.components.FrostDataStore
@ -29,9 +29,7 @@ import com.pitchedapps.frost.ext.launchActivity
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.main.MainActivity
import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.web.state.TabListAction
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
import com.pitchedapps.frost.web.usecases.UseCases
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
@ -46,13 +44,14 @@ import kotlinx.coroutines.withContext
* and will launch another activity without history after doing initialization work.
*/
@AndroidEntryPoint
class StartActivity : AppCompatActivity() {
class StartActivity : ComponentActivity() {
@Inject lateinit var frostDb: FrostDb
@Inject lateinit var dataStore: FrostDataStore
@Inject lateinit var store: FrostWebStore
@Inject lateinit var useCases: UseCases
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -63,22 +62,7 @@ class StartActivity : AppCompatActivity() {
logger.atInfo().log("Starting Frost with id %s", id)
// TODO load real tabs
store.dispatch(TabListAction.SetHomeTabs(data = listOf(FbItem.Feed, FbItem.Menu)))
// Test something scrollable
store.dispatch(
TabAction(
tabId = HomeTabSessionState.homeTabId(0),
TabAction.ContentAction.UpdateUrlAction(
"https://github.com/AllanWang/Frost-for-Facebook"
),
),
)
store.dispatch(
TabAction(
tabId = HomeTabSessionState.homeTabId(1),
TabAction.ContentAction.UpdateUrlAction("https://github.com/AllanWang/KAU"),
),
)
useCases.homeTabs.setHomeTabs(listOf(FbItem.Feed, FbItem.Menu))
launchActivity<MainActivity>(
intentBuilder = {

View File

@ -14,11 +14,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.hilt
package com.pitchedapps.frost.components
import com.pitchedapps.frost.components.Core
import com.pitchedapps.frost.components.FrostDataStore
import com.pitchedapps.frost.components.UseCases
import com.pitchedapps.frost.web.usecases.UseCases
import javax.inject.Inject
import javax.inject.Singleton

View File

@ -27,6 +27,7 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
/** Main Frost compose theme. */
@ -34,13 +35,14 @@ import androidx.compose.ui.platform.LocalContext
fun FrostTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(),
isDynamicColor: Boolean = true,
transparent: Boolean = true,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme =
remember(dynamicColor, isDarkTheme) {
remember(dynamicColor, isDarkTheme, transparent) {
when {
dynamicColor && isDarkTheme -> {
dynamicDarkColorScheme(context)
@ -53,5 +55,11 @@ fun FrostTheme(
}
}
MaterialTheme(colorScheme = colorScheme) { Surface(modifier = modifier, content = content) }
MaterialTheme(colorScheme = colorScheme) {
Surface(
modifier = modifier,
color = if (transparent) Color.Transparent else MaterialTheme.colorScheme.surface,
content = content,
)
}
}

View File

@ -31,7 +31,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.widget.NestedScrollView
import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.view.NestedWebView
import com.pitchedapps.frost.view.FrostWebView
import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
@ -39,7 +39,10 @@ import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseA
import com.pitchedapps.frost.web.state.get
import com.pitchedapps.frost.web.state.state.ContentState
import com.pitchedapps.frost.webview.FrostChromeClient
import com.pitchedapps.frost.webview.FrostWeb
import com.pitchedapps.frost.webview.FrostWebScoped
import com.pitchedapps.frost.webview.FrostWebViewClient
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@ -48,8 +51,11 @@ import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.flow
import mozilla.components.lib.state.ext.observeAsState
class FrostWebCompose(
val tabId: WebTargetId,
@FrostWebScoped
class FrostWebCompose
@Inject
internal constructor(
@FrostWeb val tabId: WebTargetId,
private val store: FrostWebStore,
private val client: FrostWebViewClient,
private val chromeClient: FrostChromeClient,
@ -126,7 +132,7 @@ class FrostWebCompose(
AndroidView(
factory = { context ->
val childView =
NestedWebView(context)
FrostWebView(context)
.apply {
onCreated(this)
@ -150,9 +156,6 @@ class FrostWebCompose(
}
.also { webView = it }
// Workaround a crash on certain devices that expect WebView to be
// wrapped in a ViewGroup.
// b/243567497
val parentLayout = NestedScrollView(context)
parentLayout.layoutParams =
FrameLayout.LayoutParams(
@ -176,17 +179,3 @@ class FrostWebCompose(
private val logger = FluentLogger.forEnclosingClass()
}
}
// override fun onReceivedError(
// view: WebView,
// request: WebResourceRequest?,
// error: WebResourceError?
// ) {
// super.onReceivedError(view, request, error)
//
// if (error != null) {
// state.errorsForCurrentRequest.add(WebViewError(request, error))
// }
// }
// }

View File

@ -21,7 +21,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.pitchedapps.frost.FrostApp
import com.pitchedapps.frost.hilt.FrostComponents
import com.pitchedapps.frost.components.FrostComponents
/** Launches new activities. */
inline fun <reified T : Activity> Context.launchActivity(

View File

@ -21,14 +21,19 @@ import com.pitchedapps.frost.proto.Account
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/*
* Cannot use inline value classes with Dagger due to Kapt:
* https://github.com/google/dagger/issues/2930
*/
/**
* Representation of unique frost account id.
*
* Account ids are identifiers specific to Frost, and group ids/info from other sites.
*/
@JvmInline value class FrostAccountId(val id: Long)
data class FrostAccountId(val id: Long)
@JvmInline value class WebTargetId(val id: String)
data class WebTargetId(val id: String)
/**
* Representation of gecko context id.

View File

@ -24,7 +24,7 @@ const val WWW_FACEBOOK_COM = "www.$FACEBOOK_COM"
const val WWW_MESSENGER_COM = "www.$MESSENGER_COM"
const val HTTPS_FACEBOOK_COM = "https://$WWW_FACEBOOK_COM"
const val HTTPS_MESSENGER_COM = "https://$WWW_MESSENGER_COM"
const val FACEBOOK_BASE_COM = "m.$FACEBOOK_COM"
const val FACEBOOK_BASE_COM = "touch.$FACEBOOK_COM"
const val FB_URL_BASE = "https://$FACEBOOK_BASE_COM/"
const val FACEBOOK_MBASIC_COM = "mbasic.$FACEBOOK_COM"
const val FB_URL_MBASIC_BASE = "https://$FACEBOOK_MBASIC_COM/"

View File

@ -24,7 +24,6 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Qualifier
import javax.inject.Singleton
@Qualifier annotation class Frost
@ -54,7 +53,7 @@ object FrostModule {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0"
private const val USER_AGENT_WINDOWS_FROST =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Safari/537.36"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36"
@Provides @Singleton @Frost fun userAgent(): String = USER_AGENT_WINDOWS_FROST
@Provides @Frost fun userAgent(): String = USER_AGENT_WINDOWS_FROST
}

View File

@ -21,6 +21,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.view.WindowCompat
import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.R
import com.pitchedapps.frost.compose.FrostTheme
import com.pitchedapps.frost.web.state.FrostWebStore
import dagger.hilt.android.AndroidEntryPoint
@ -38,6 +39,8 @@ class MainActivity : ComponentActivity() {
@Inject lateinit var store: FrostWebStore
override fun onCreate(savedInstanceState: Bundle?) {
// TODO make configurable
setTheme(R.style.FrostTheme_Transparent)
super.onCreate(savedInstanceState)
logger.atInfo().log("onCreate main activity")

View File

@ -21,10 +21,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.pitchedapps.frost.components.FrostComponents
import com.pitchedapps.frost.ext.GeckoContextId
import com.pitchedapps.frost.ext.idData
import com.pitchedapps.frost.ext.toContextId
import com.pitchedapps.frost.hilt.FrostComponents
import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.webview.FrostWebComposer
import dagger.hilt.android.lifecycle.HiltViewModel
@ -41,7 +41,6 @@ internal constructor(
val components: FrostComponents,
val store: FrostWebStore,
val frostWebComposer: FrostWebComposer,
// sample: FrostWebEntrySample,
) : ViewModel() {
val contextIdFlow: Flow<GeckoContextId?> =

View File

@ -41,6 +41,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pitchedapps.frost.compose.webview.FrostWebCompose
import com.pitchedapps.frost.ext.WebTargetId
@ -59,6 +60,7 @@ fun MainScreenWebView(modifier: Modifier = Modifier, homeTabs: List<MainTabItem>
Scaffold(
modifier = modifier,
containerColor = Color.Transparent,
topBar = { MainTopBar(modifier = modifier) },
bottomBar = {
MainBottomBar(

View File

@ -0,0 +1,54 @@
/*
* Copyright 2023 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import com.pitchedapps.frost.hilt.Frost
import dagger.hilt.android.AndroidEntryPoint
import java.util.Optional
import javax.inject.Inject
@AndroidEntryPoint
class FrostWebView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
NestedWebView(context, attrs, defStyleAttr) {
@Inject @Frost lateinit var userAgent: Optional<String>
init {
userAgent.ifPresent {
settings.userAgentString = it
println("Set user agent to $it")
}
with(settings) {
// noinspection SetJavaScriptEnabled
javaScriptEnabled = true
mediaPlaybackRequiresUserGesture = false // TODO check if we need this
allowFileAccess = true
// textZoom
domStorageEnabled = true
setLayerType(LAYER_TYPE_HARDWARE, null)
setBackgroundColor(Color.TRANSPARENT)
// Download listener
// JS Interface
}
}
}

View File

@ -30,7 +30,7 @@ import androidx.core.view.ViewCompat
*
* Webview extension that handles nested scrolls
*/
class NestedWebView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
open class NestedWebView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
WebView(context, attrs, defStyleAttr), NestedScrollingChild3 {
// No JvmOverloads due to hilt
@ -109,21 +109,17 @@ class NestedWebView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
override fun isNestedScrollingEnabled() = childHelper.isNestedScrollingEnabled
override fun startNestedScroll(axes: Int, type: Int): Boolean {
TODO("not implemented")
}
override fun startNestedScroll(axes: Int, type: Int): Boolean =
childHelper.startNestedScroll(axes, type)
override fun startNestedScroll(axes: Int) = childHelper.startNestedScroll(axes)
override fun stopNestedScroll(type: Int) {
TODO("not implemented")
}
override fun stopNestedScroll(type: Int) = childHelper.stopNestedScroll(type)
override fun stopNestedScroll() = childHelper.stopNestedScroll()
override fun hasNestedScrollingParent(type: Int): Boolean {
TODO("not implemented")
}
override fun hasNestedScrollingParent(type: Int): Boolean =
childHelper.hasNestedScrollingParent(type)
override fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent()

View File

@ -0,0 +1,66 @@
/*
* Copyright 2023 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.web
import com.pitchedapps.frost.components.FrostDataStore
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Singleton
/** Snapshot of UI options based on user preferences */
interface FrostWebUiOptions {
val theme: Theme
enum class Theme {
Original,
Light,
Dark,
Amoled,
Glass // Custom
}
}
/**
* Singleton to provide snapshots of [FrostWebUiOptions].
*
* This is a mutable class, and does not provide change listeners. We will update activities
* manually when needed.
*/
@Singleton
class FrostWebUiSnapshot(private val dataStore: FrostDataStore) {
@Volatile
var options: FrostWebUiOptions = defaultOptions()
private set
private val stale = AtomicBoolean(true)
private fun defaultOptions(): FrostWebUiOptions =
object : FrostWebUiOptions {
override val theme: FrostWebUiOptions.Theme = FrostWebUiOptions.Theme.Original
}
/** Fetch new snapshot and update other singletons */
suspend fun reload() {
if (!stale.getAndSet(false)) return
// todo load
}
fun markAsStale() {
stale.set(true)
}
}

View File

@ -28,10 +28,26 @@ class FrostLoggerMiddleware : FrostWebMiddleware {
next: (FrostWebAction) -> Unit,
action: FrostWebAction
) {
logger.atInfo().log("FrostWebAction: %s - %s", action::class.simpleName, action)
if (logInfo(action)) {
logger.atInfo().log("FrostWebAction: %s", action)
} else {
logger.atFine().log("FrostWebAction: %s", action)
}
next(action)
}
private fun logInfo(action: FrostWebAction): Boolean {
when (action) {
is TabAction ->
when (action.action) {
is TabAction.ContentAction.UpdateProgressAction -> return false
else -> {}
}
else -> {}
}
return true
}
companion object {
private val logger = FluentLogger.forEnclosingClass()
}

View File

@ -38,7 +38,7 @@ object InitAction : FrostWebAction
/** Actions affecting multiple tabs */
sealed interface TabListAction : FrostWebAction {
data class SetHomeTabs(val data: List<FbItem>) : TabListAction
data class SetHomeTabs(val data: List<FbItem>, val selectedTab: Int? = 0) : TabListAction
data class SelectHomeTab(val id: WebTargetId) : TabListAction
}

View File

@ -38,7 +38,11 @@ internal constructor(
is SetHomeTabs -> {
val tabs =
action.data.mapIndexed { i, fbItem -> fbItem.toHomeTabSession(context, i, state.auth) }
state.copy(homeTabs = tabs)
val selectedTab = action.selectedTab?.let { HomeTabSessionState.homeTabId(it) }
state.copy(
homeTabs = tabs,
selectedHomeTab = selectedTab,
)
}
is TabListAction.SelectHomeTab -> state.copy(selectedHomeTab = action.id)
}

View File

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.components.usecases
package com.pitchedapps.frost.web.usecases
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.facebook.FbItem
@ -31,7 +31,7 @@ class HomeTabsUseCases @Inject internal constructor(private val store: FrostWebS
*
* If there are existing tabs, they will be replaced.
*/
fun createHomeTabs(items: List<FbItem>) {
fun setHomeTabs(items: List<FbItem>) {
store.dispatch(SetHomeTabs(items))
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2023 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.web.usecases
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.web.state.TabAction
import javax.inject.Inject
class TabUseCases
@Inject
internal constructor(
private val store: FrostWebStore,
val requests: TabRequestUseCases,
val responses: TabResponseUseCases,
) {
fun updateUrl(tabId: WebTargetId, url: String) {
store.dispatch(TabAction(tabId = tabId, action = TabAction.ContentAction.UpdateUrlAction(url)))
}
fun updateTitle(tabId: WebTargetId, title: String?) {
store.dispatch(
TabAction(
tabId = tabId,
action = TabAction.ContentAction.UpdateTitleAction(title),
),
)
}
fun updateNavigation(tabId: WebTargetId, canGoBack: Boolean, canGoForward: Boolean) {
store.dispatch(
TabAction(
tabId = tabId,
action =
TabAction.ContentAction.UpdateNavigationAction(
canGoBack = canGoBack,
canGoForward = canGoForward,
),
),
)
}
}
class TabRequestUseCases @Inject internal constructor(private val store: FrostWebStore) {
fun requestUrl(tabId: WebTargetId, url: String) {
store.dispatch(TabAction(tabId = tabId, action = TabAction.UserAction.LoadUrlAction(url)))
}
fun goBack(tabId: WebTargetId) {
store.dispatch(TabAction(tabId = tabId, action = TabAction.UserAction.GoBackAction))
}
fun goForward(tabId: WebTargetId) {
store.dispatch(TabAction(tabId = tabId, action = TabAction.UserAction.GoForwardAction))
}
}
class TabResponseUseCases @Inject internal constructor(private val store: FrostWebStore) {
fun respondUrl(tabId: WebTargetId, url: String) {
store.dispatch(
TabAction(
tabId = tabId,
action = TabAction.ResponseAction.LoadUrlResponseAction(url),
),
)
}
fun respondSteps(tabId: WebTargetId, steps: Int) {
store.dispatch(
TabAction(
tabId = tabId,
action = TabAction.ResponseAction.WebStepResponseAction(steps),
),
)
}
}

View File

@ -14,9 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.components
package com.pitchedapps.frost.web.usecases
import com.pitchedapps.frost.components.usecases.HomeTabsUseCases
import javax.inject.Inject
import javax.inject.Singleton
@ -30,4 +29,5 @@ class UseCases
@Inject
internal constructor(
val homeTabs: HomeTabsUseCases,
val tabs: TabUseCases,
)

View File

@ -27,9 +27,13 @@ import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
import com.pitchedapps.frost.web.state.get
import javax.inject.Inject
/** The default chrome client */
class FrostChromeClient(private val tabId: WebTargetId, private val store: FrostWebStore) :
@FrostWebScoped
class FrostChromeClient
@Inject
internal constructor(@FrostWeb private val tabId: WebTargetId, private val store: FrostWebStore) :
WebChromeClient() {
private fun FrostWebStore.dispatch(action: TabAction.Action) {

View File

@ -16,18 +16,14 @@
*/
package com.pitchedapps.frost.webview
import android.webkit.WebViewClient
import com.pitchedapps.frost.compose.webview.FrostWebCompose
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.FrostWebHelper
import com.pitchedapps.frost.web.state.FrostWebStore
import dagger.BindsInstance
import dagger.Module
import dagger.Provides
import dagger.hilt.DefineComponent
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.hilt.android.components.ViewModelComponent
import javax.inject.Inject
import javax.inject.Qualifier
import javax.inject.Scope
@ -44,44 +40,32 @@ annotation class FrostWebScoped
@Qualifier annotation class FrostWeb
@FrostWebScoped @DefineComponent(parent = SingletonComponent::class) interface FrostWebComponent
@FrostWebScoped @DefineComponent(parent = ViewModelComponent::class) interface FrostWebComponent
@DefineComponent.Builder
interface FrostWebComponentBuilder {
@BindsInstance fun tabId(@FrostWeb tabId: WebTargetId): FrostWebComponentBuilder
fun tabId(@BindsInstance @FrostWeb tabId: WebTargetId): FrostWebComponentBuilder
fun build(): FrostWebComponent
}
@Module
@InstallIn(FrostWebComponent::class)
internal object FrostWebModule {
@Provides
fun client(
@FrostWeb tabId: WebTargetId,
store: FrostWebStore,
webHelper: FrostWebHelper
): WebViewClient = FrostWebViewClient(tabId, store, webHelper)
}
/**
* Using this injection seems to be buggy, leading to an invalid param tabId error:
*
* Cause: not a valid name: tabId-4xHwVBUParam
*/
class FrostWebEntrySample
class FrostWebComposer
@Inject
internal constructor(private val frostWebComponentBuilder: FrostWebComponentBuilder) {
fun test(tabId: WebTargetId): WebViewClient {
fun create(tabId: WebTargetId): FrostWebCompose {
val component = frostWebComponentBuilder.tabId(tabId).build()
return EntryPoints.get(component, FrostWebEntryPoint::class.java).client()
return EntryPoints.get(component, FrostWebEntryPoint::class.java).compose()
}
@EntryPoint
@InstallIn(FrostWebComponent::class)
interface FrostWebEntryPoint {
fun client(): WebViewClient
fun compose(): FrostWebCompose
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright 2023 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.webview
import com.pitchedapps.frost.compose.webview.FrostWebCompose
import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.web.FrostWebHelper
import com.pitchedapps.frost.web.state.FrostWebStore
import javax.inject.Inject
class FrostWebComposer
@Inject
internal constructor(
private val store: FrostWebStore,
private val webHelper: FrostWebHelper,
) {
fun create(tabId: WebTargetId): FrostWebCompose {
val client = FrostWebViewClient(tabId, store, webHelper)
val chromeClient = FrostChromeClient(tabId, store)
return FrostWebCompose(tabId, store, client, chromeClient)
}
}

View File

@ -17,6 +17,7 @@
package com.pitchedapps.frost.webview
import android.graphics.Bitmap
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
@ -26,13 +27,16 @@ import com.pitchedapps.frost.ext.WebTargetId
import com.pitchedapps.frost.facebook.FACEBOOK_BASE_COM
import com.pitchedapps.frost.facebook.WWW_FACEBOOK_COM
import com.pitchedapps.frost.facebook.isExplicitIntent
import com.pitchedapps.frost.facebook.isFacebookUrl
import com.pitchedapps.frost.web.FrostWebHelper
import com.pitchedapps.frost.web.state.FrostWebStore
import com.pitchedapps.frost.web.state.TabAction
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateNavigationAction
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateProgressAction
import com.pitchedapps.frost.web.state.TabAction.ContentAction.UpdateTitleAction
import com.pitchedapps.frost.webview.injection.FrostJsInjectors
import java.io.ByteArrayInputStream
import javax.inject.Inject
/**
* Created by Allan Wang on 2017-05-31.
@ -60,10 +64,14 @@ abstract class BaseWebViewClient : WebViewClient() {
}
/** The default webview client */
class FrostWebViewClient(
private val tabId: WebTargetId,
@FrostWebScoped
class FrostWebViewClient
@Inject
internal constructor(
@FrostWeb private val tabId: WebTargetId,
private val store: FrostWebStore,
override val webHelper: FrostWebHelper
override val webHelper: FrostWebHelper,
private val frostJsInjectors: FrostJsInjectors,
) : BaseWebViewClient() {
private fun FrostWebStore.dispatch(action: TabAction.Action) {
@ -130,19 +138,23 @@ class FrostWebViewClient(
// refresh.offer(true)
}
// private fun injectBackgroundColor() {
// web?.setBackgroundColor(
// when {
// isMain -> Color.TRANSPARENT
// web.url.isFacebookUrl -> themeProvider.bgColor.withAlpha(255)
// else -> Color.WHITE
// }
// )
// }
// private fun WebView.injectBackgroundColor(url: String?) {
// setBackgroundColor(
// when {
// isMain -> Color.TRANSPARENT
// url.isFacebookUrl -> themeProvider.bgColor.withAlpha(255)
// else -> Color.WHITE
// }
// )
// }
// override fun onPageCommitVisible(view: WebView, url: String?) {
// super.onPageCommitVisible(view, url)
// injectBackgroundColor()
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
when {
url.isFacebookUrl -> frostJsInjectors.facebookInjectOnPageCommitVisible(view, url)
}
}
// when {
// url.isFacebookUrl -> {
// v { "FB Page commit visible" }
@ -247,6 +259,14 @@ class FrostWebViewClient(
return super.shouldOverrideUrlLoading(view, request)
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
}
companion object {
private val logger = FluentLogger.forEnclosingClass()
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2023 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.webview.injection
import android.content.Context
import android.webkit.WebView
import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.webview.injection.assets.JsActions
import com.pitchedapps.frost.webview.injection.assets.JsAssets
import com.pitchedapps.frost.webview.injection.assets.inject
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
import java.io.FileNotFoundException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Singleton
class FrostJsInjectors
@Inject
internal constructor(
@ApplicationContext private val context: Context,
) {
@Volatile private var theme: JsInjector = JsInjector.EMPTY
fun facebookInjectOnPageCommitVisible(view: WebView, url: String?) {
logger.atInfo().log("inject page commit visible %b", theme != JsInjector.EMPTY)
listOf(theme, JsAssets.CLICK_A).inject(view)
}
private fun getTheme(): JsInjector {
return try {
val content =
context.assets
.open("frost/css/facebook/themes/material_glass.css")
.bufferedReader()
.use(BufferedReader::readText)
logger.atInfo().log("css %s", content)
JsBuilder().css(content).single("material_glass").build()
} catch (e: FileNotFoundException) {
logger.atSevere().withCause(e).log("CssAssets file not found")
JsActions.EMPTY
}
}
suspend fun load() {
withContext(Dispatchers.IO) { theme = getTheme() }
}
companion object {
private val logger = FluentLogger.forEnclosingClass()
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2023 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.webview.injection
import android.webkit.WebView
import org.apache.commons.text.StringEscapeUtils
interface JsInjector {
fun inject(webView: WebView)
companion object {
val EMPTY: JsInjector = EmptyJsInjector
operator fun invoke(content: String): JsInjector =
object : JsInjector {
override fun inject(webView: WebView) {
webView.evaluateJavascript(content, null)
}
}
}
}
private object EmptyJsInjector : JsInjector {
override fun inject(webView: WebView) {
// Noop
}
}
data class OneShotJsInjector(val tag: String, val injector: JsInjector) : JsInjector {
override fun inject(webView: WebView) {
// TODO
}
}
class JsBuilder {
private val css = StringBuilder()
private val js = StringBuilder()
private var tag: String? = null
fun css(css: String): JsBuilder {
this.css.append(StringEscapeUtils.escapeEcmaScript(css))
return this
}
fun js(content: String): JsBuilder {
this.js.append(content)
return this
}
fun single(tag: String): JsBuilder {
this.tag = tag // TODO TagObfuscator.obfuscateTag(tag)
return this
}
fun build() = JsInjector(toString())
override fun toString(): String {
val tag = this.tag
val builder =
StringBuilder().apply {
if (css.isNotBlank()) {
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
append("var a=document.createElement('style');")
append("a.innerHTML='$cssMin';")
if (tag != null) {
append("a.id='$tag';")
}
append("document.head.appendChild(a);")
}
if (js.isNotBlank()) {
append(js)
}
}
var content = builder.toString()
if (tag != null) {
content = singleInjector(tag, content)
}
return wrapAnonymous(content)
}
private fun wrapAnonymous(body: String) = "(function(){$body})();"
private fun singleInjector(tag: String, content: String) =
"""
if (!window.hasOwnProperty("$tag")) {
console.log("Registering $tag");
window.$tag = true;
$content
}
"""
.trimIndent()
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2020 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.webview.injection.assets
import android.webkit.WebView
import com.pitchedapps.frost.webview.injection.JsBuilder
import com.pitchedapps.frost.webview.injection.JsInjector
/** Small misc inline css assets */
enum class CssActions(private val content: String) : JsInjector {
FullSizeImage(
"div._4prr[style*=\"max-width\"][style*=\"max-height\"]{max-width:none !important;max-height:none !important}",
);
private val injector: JsInjector =
JsBuilder().css(content).single("css-small-assets-$name").build()
override fun inject(webView: WebView) {
injector.inject(webView)
}
}

View File

@ -0,0 +1,63 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.webview.injection.assets
import android.webkit.WebView
import com.pitchedapps.frost.webview.injection.JsBuilder
import com.pitchedapps.frost.webview.injection.JsInjector
/**
* Created by Allan Wang on 2017-05-31.
*
* List of elements to hide
*/
enum class CssHider(private vararg val items: String) : JsInjector {
CORE("[data-sigil=m_login_upsell]", "[role=progressbar]"),
HEADER(
"#header:not(.mFuturePageHeader):not(.titled)",
"#mJewelNav",
"[data-sigil=MTopBlueBarHeader]",
"#header-notices",
"[data-sigil*=m-promo-jewel-header]",
),
ADS("article[data-xt*=sponsor]", "article[data-store*=sponsor]", "article[data-ft*=sponsor]"),
PEOPLE_YOU_MAY_KNOW("article._d2r"),
SUGGESTED_GROUPS("article[data-ft*=\"ei\":]"),
// Is it really this simple?
SUGGESTED_POSTS("article[data-store*=recommendation]", "article[data-ft*=recommendation]"),
COMPOSER("#MComposer"),
MESSENGER("._s15", "[data-testid=info_panel]", "js_i"),
NON_RECENT("article:not([data-store*=actor_name])"),
STORIES(
"#MStoriesTray",
// Sub element with just the tray; title is not a part of this
"[data-testid=story_tray]",
),
POST_ACTIONS("footer [data-sigil=\"ufi-inline-actions\"]"),
POST_REACTIONS("footer [data-sigil=\"reactions-bling-bar\"]");
private val injector: JsInjector =
JsBuilder()
.css("${items.joinToString(separator = ",")}{display:none !important}")
.single("css-hider-$name")
.build()
override fun inject(webView: WebView) {
injector.inject(webView)
}
}

View File

@ -0,0 +1,55 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.webview.injection.assets
import android.webkit.WebView
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.webview.injection.JsInjector
/**
* Created by Allan Wang on 2017-05-31.
*
* Collection of short js functions that are embedded directly
*/
enum class JsActions(body: String) : JsInjector {
/**
* Redirects to login activity if create account is found see
* [com.pitchedapps.frost.web.FrostJSI.loadLogin]
*/
LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"),
BASE_HREF("""document.write("<base href='$FB_URL_BASE'/>");"""),
FETCH_BODY(
"""setTimeout(function(){var e=document.querySelector("main");e||(e=document.querySelector("body")),Frost.handleHtml(e.outerHTML)},1e2);""",
),
RETURN_BODY("return(document.getElementsByTagName('html')[0].innerHTML);"),
CREATE_POST(clickBySelector("#MComposer [onclick]")),
// CREATE_MSG(clickBySelector("a[rel=dialog]")),
/** Used as a pseudoinjector for maybe functions */
EMPTY("");
val function = "(function(){$body})();"
private val injector: JsInjector = JsInjector(function)
override fun inject(webView: WebView) {
injector.inject(webView)
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun clickBySelector(selector: String): String =
"""document.querySelector("$selector").click()"""

View File

@ -0,0 +1,81 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.webview.injection.assets
import android.content.Context
import android.webkit.WebView
import androidx.annotation.VisibleForTesting
import com.google.common.flogger.FluentLogger
import com.pitchedapps.frost.webview.injection.JsBuilder
import com.pitchedapps.frost.webview.injection.JsInjector
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Created by Allan Wang on 2017-05-31. Mapping of the available assets The enum name must match the
* js file name
*/
enum class JsAssets(private val singleLoad: Boolean = true) : JsInjector {
CLICK_A,
CONTEXT_A,
MEDIA,
HEADER_BADGES,
TEXTAREA_LISTENER,
NOTIF_MSG,
DOCUMENT_WATCHER,
HORIZONTAL_SCROLLING,
AUTO_RESIZE_TEXTAREA(singleLoad = false),
SCROLL_STOP,
;
@VisibleForTesting internal val file = "${name.lowercase(Locale.CANADA)}.js"
private fun injectorBlocking(context: Context): JsInjector {
return try {
val content =
context.assets.open("frost/js/$file").bufferedReader().use(BufferedReader::readText)
JsBuilder().js(content).run { if (singleLoad) single(name) else this }.build()
} catch (e: FileNotFoundException) {
logger.atWarning().withCause(e).log("JsAssets file not found")
JsInjector.EMPTY
}
}
private var injector: JsInjector = JsInjector.EMPTY
override fun inject(webView: WebView) {
injector.inject(webView)
}
private suspend fun load(context: Context) {
withContext(Dispatchers.IO) { injector = injectorBlocking(context) }
}
companion object {
private val logger = FluentLogger.forEnclosingClass()
suspend fun load(context: Context) =
withContext(Dispatchers.IO) { JsAssets.values().forEach { it.load(context) } }
}
}
fun List<JsInjector>.inject(webView: WebView) {
forEach { it.inject(webView) }
}

View File

@ -1,48 +0,0 @@
{
"manifest_version": 2,
"name": "frostcore",
"version": "1.0.0",
"description": "Core web extension for Frost",
"browser_specific_settings": {
"gecko": {
"id": "frost_gecko_core@pitchedapps"
}
},
"background": {
"scripts": [
"js/background/cookies.js"
]
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"js/frost.js"
]
},
{
"matches": [
"*://*.facebook.com/*"
],
"js": [
"js/click_a.js"
]
}
],
"permissions": [
"<all_urls>",
"activeTab",
"contextMenus",
"contextualIdentities",
"cookies",
"history",
"management",
"tabs",
"nativeMessaging",
"nativeMessagingFromContent",
"geckoViewAddons",
"webRequest"
]
}

View File

@ -1,7 +1,7 @@
{
"scripts": {
"compile": "tsc -p tsconfig.json && sass --no-source-map --style compressed --update scss:assets/frostcore/css",
"scss-watch": "sass --no-source-map --style compressed --update scss:assets/frostcore/css --watch"
"compile": "tsc -p tsconfig.json && sass --no-source-map --style compressed --update scss:assets/frost/css",
"scss-watch": "sass --no-source-map --style compressed --update scss:assets/frost/css --watch"
},
"license": "MPL-2.0",
"repository": {

View File

@ -0,0 +1,129 @@
@use 'sass:math';
@mixin placeholder {
::placeholder {
@content;
}
::-webkit-input-placeholder {
@content;
}
:-moz-placeholder {
@content;
}
::-moz-placeholder {
@content;
}
:-ms-input-placeholder {
@content;
}
::-ms-input-placeholder {
@content;
}
}
@mixin fill-available {
width: 100%;
max-width: -webkit-fill-available;
max-width: -moz-available;
max-width: fill-available;
}
@mixin keyframes($name) {
@-webkit-keyframes #{$name} {
@content;
}
@-moz-keyframes #{$name} {
@content;
}
//@-ms-keyframes #{$name} {
// @content;
//}
@keyframes #{$name} {
@content;
}
}
// Helper function to replace characters in a string
@function str-replace($string, $search, $replace: "") {
$index: str-index($string, $search);
@return if(
$index,
str-slice($string, 1, $index - 1) + $replace +
str-replace(str-slice($string, $index + str-length($search)), $search, $replace),
$string
);
}
// https://css-tricks.com/probably-dont-base64-svg/
// SVG optimization thanks to https://codepen.io/jakob-e/pen/doMoML
// Function to create an optimized svg url
// Version: 1.0.6
@function svg-url($svg) {
//
// Add missing namespace
//
@if not str-index($svg, xmlns) {
$svg: str-replace($svg, "<svg", '<svg xmlns="http://www.w3.org/2000/svg"');
}
//
// Chunk up string in order to avoid
// "stack level too deep" error
//
$encoded: "";
$slice: 2000;
$index: 0;
$loops: ceil(math.div(str-length($svg), $slice));
@for $i from 1 through $loops {
$chunk: str-slice($svg, $index, $index + $slice - 1);
//
// Encode
//
//$chunk: str-replace($chunk, '"', "&quot;");
$chunk: str-replace($chunk, "%", "%25");
$chunk: str-replace($chunk, "#", "%23");
$chunk: str-replace($chunk, "{", "%7B");
$chunk: str-replace($chunk, "}", "%7D");
$chunk: str-replace($chunk, "<", "%3C");
$chunk: str-replace($chunk, ">", "%3E");
//
// The maybe list
//
// Keep size and compile time down
// ... only add on documented fail
//
// $chunk: str-replace($chunk, '&', '%26');
// $chunk: str-replace($chunk, '|', '%7C');
// $chunk: str-replace($chunk, '[', '%5B');
// $chunk: str-replace($chunk, ']', '%5D');
// $chunk: str-replace($chunk, '^', '%5E');
// $chunk: str-replace($chunk, '`', '%60');
// $chunk: str-replace($chunk, ';', '%3B');
// $chunk: str-replace($chunk, '?', '%3F');
// $chunk: str-replace($chunk, ':', '%3A');
// $chunk: str-replace($chunk, '@', '%40');
// $chunk: str-replace($chunk, '=', '%3D');
$encoded: #{$encoded}#{$chunk};
$index: $index + $slice;
}
@return url("data:image/svg+xml,#{$encoded}");
}
// Background svg mixin
@mixin background-svg($svg, $extra: "no-repeat") {
background: svg-url($svg) unquote($extra) !important;
}

View File

@ -0,0 +1,19 @@
$bg_transparent: rgba(#f0f, 0.02) !default;
//Keep above as first line so partials aren't compiled
//Our default colors are test colors; production files should always import the actual colors
$text: #d7b0d7 !default;
$text_disabled: rgba($text, 0.6) !default;
// must be visible with accent as the background
$accent_text: #76d7c2 !default;
$link: #9266d5 !default;
$accent: #980008 !default;
$background: #451515 !default;
// background2 must be transparent
$background2: rgba(lighten($background, 35%), 0.35) !default; //Also change ratio in material_light
$bg_opaque: rgba($background, 1.0) !default;
$bg_opaque2: rgba($background2, 1.0) !default;
$card: #239645 !default;
$tint: #ff4682 !default; // must be different from $background
$divider: rgba($text, 0.3) !default;

View File

@ -0,0 +1,49 @@
:root, .__fb-light-mode {
--accent: #{$accent} !important;
--attachment-footer-background: #{$bg_opaque} !important;
--background-deemphasized: #{$background} !important;
--card-background: #{$bg_opaque} !important;
--card-background-flat: #{$bg_opaque} !important;
--comment-background: #{$bg_opaque2} !important;
--comment-footer-background: #{$bg_opaque} !important;
--darkreader-bg--fds-white: #{$bg_opaque} !important;
--darkreader-text--primary-text: #{$text} !important;
--darkreader-text--secondary-text: #{$text} !important;
--disabled-button-background: #{$divider} !important;
--disabled-button-text: #{$text_disabled} !important;
--disabled-icon: #{$text_disabled} !important;
--disabled-text: #{$text_disabled} !important;
--divider: #{$divider} !important;
--event-date: #{$accent} !important;
--fds-white: #{$background} !important;
--glimmer-spinner-icon: #{$accent} !important;
--hero-banner-background: #{$bg_opaque2} !important;
--highlight-bg: #{$bg_opaque2} !important;
--media-outer-border: #{$divider} !important;
--messenger-card-background: #{$bg_opaque} !important; // Main background; needs to be opaque to hide gradient used for sender card
--messenger-reply-background: #{$bg_opaque2} !important;
--nav-bar-background: #{$bg_opaque} !important;
--new-notification-background: #{$background2} !important;
--placeholder-icon: #{$text} !important;
--popover-background: #{$bg_opaque2} !important;
--primary-icon: #{$text} !important;
--primary-text: #{$text} !important;
--primary-button-text: #{$text} !important;
--primary-deemphasized-button-background: #{$divider} !important;
--primary-text-on-media: #{$text} !important;
--scroll-thumb: #{$accent} !important;
--secondary-icon: #{$text} !important;
--secondary-text: #{$text} !important;
--section-header-text: #{$text} !important;
--nav-bar-background-gradient-wash: #{$bg_opaque} !important;
--nav-bar-background-gradient: #{$bg_opaque} !important;
--placeholder-text: #{$text} !important; // Date
--surface-background: #{$bg_opaque2} !important; // Emoji background
--toggle-active-background: #{$bg_opaque2} !important;
--wash: #{$bg_opaque2} !important;
--web-wash: #{$bg_opaque2} !important;
[role="navigation"] {
--surface-background: #{$bg_opaque} !important; // Nav background
}
}

View File

@ -0,0 +1,91 @@
#viewport {
background: $background !important;
}
html, body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
[style*="background-color: #FFFFFF"], [style*="background-color: #E4E6EB"], ._9drh,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._7g4m, ._bub,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, ._53_-, ._4ut9,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
.tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, ._2wn5,
.al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._2bdb, ._3ci9, ._2_gy,
._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._6be7, ._-kp, ._-kq,
._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._5xp2, div.fullwidthMore,
._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._voz, ._vos, ._7i8m,
._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5_ee, ._3clk,
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._8r-n,
._5lp5, ._1ho1, ._39y9._39ya, ._59_m, ._6rny, ._9sh-, ._1zep, ._5snt, ._5fn5, ._5rmd, ._7nya,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li, ._7hkf, ._6vzz, ._3iyw ._37fb, ._5cqb,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu, ._7lcm, ._7kxh, ._6rvp, ._6rvq, ._6rvk,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy,
._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x {
background: $bg_transparent !important;
}
// card related
._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn, ._7kxe, [style*="background-color: #F5F8FF"],
._84lx, ._517h, ._59pe:focus, ._59pe:hover, ._m_1, ._3eqz, ._6m2, ._6q-c, ._61r- {
background: $card !important;
}
// unread related
.aclb {
background: $tint !important;
}
// contains images so must have background-color
._cv_, ._2sq8 {
background-color: $bg_transparent !important;
}
#page, ._8l7, ._-j8, ._-j9, ._6o5v, ._uwx, .touch ._uwx.mentions-input {
background: transparent !important;
}
.jewel, .flyout, ._52z5, ._13e_, ._5-lw, ._5c0e, .jx-result, ._336p, .mentions-suggest-item, ._2suk, ._-j7, ._4d0v, ._4d0m,
.mentions-suggest, ._1xoz, ._1xow, ._14v5 ._14v8, ._8s4y, ._55ws, ._6j_d,
// desktop sharing page
.uiBoxLightblue, .uiBoxWhite, .uiBoxGray, .uiTokenizer, .uiTypeahead, ._558b ._54ng, ._2_bh, ._509o, ._509o:hover {
background: $bg_opaque !important;
}
._403n, ._1-kc {
background: $bg_opaque2 !important;
}
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after, ._7hfd,
.timeline .timelinePublisher, .touched, .sharerAttachment,
.item a.primary.touched .primarywrap, ._537a, ._7cui, ._785,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
._10sb li.selected, ._2z4j, ._ib-, ._1bhl, ._5a5j, ._6--d, ._77p7,
._2b06, ._2tsf, ._3gka, .mCount, ._27vc, ._4pv-, ._6pk5, ._86nt,
._4qax, ._4756, ._w34, ._56bv::before, ._5769, ._34iv, ._z-w, ._t21, .mToken,
#addMembersTypeahead .mToken.mTokenWeakReference, ._4_d0 ._8-89,
.acbk {
background: $background2 !important;
}
.mQuestionsPollResultsBar .shaded, ._1r00 {
background: $accent !important;
}
._220g, ._1_y8:after, ._6pk6, ._9rc8,
._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before,
._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before {
background: $divider !important;
}
//fab
button ._v89 ._54k8._1fl1, ._7nyk, ._7nym, ._7nyn {
background: $accent !important;
}

View File

@ -0,0 +1,106 @@
//border between like and comment
._15kl::before, ._37fd .inlineComposerButton, ._1hb:before,
._pfn ._pfo::before,
._5j35::after, ._2k4b, ._3to7, ._4nw8 {
border-left: 1px solid $divider !important;
}
._4_d1, ._5cni, ._3jcq, ._1ho1 {
border-right: 1px solid $divider !important;
}
//above see more
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw, ._5rmd,
._2s1_:before, ._143z::before, ._143z::after, ._4d0x, ._5_gz, ._5_ev, ._63ur, ._6pi8,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid $divider !important;
}
._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._1t4h, ._37fd, ._1ha, ._3bg5 ._56do, ._8he,
._400s, ._5hoc, ._1bhn, ._5ag6, ._4pvz, ._31y5, ._7gxb, ._-kp, ._6_q3::after, ._3al1, ._4d0w, ._4d0k,
._301x, ._x08 ._x0a:after, ._36dc, ._6-l ._57jn, ._527k, ._g_k, ._7i8v, ._7k1c, ._2_gy,
._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, ._2bdc, ._3ci9, ._7i-0,
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5_g-,
._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6, ._2wn5, ._1ho1, ._1xk6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
border-bottom: 1px solid $divider !important;
}
.item a.primary.touched .primarywrap, ._4dwt ._5y33, ._1ih_, ._5_50, ._6beq, ._69aj, ._3iyw ._37fb, ._9drh,
._5fjv, ._3on6, ._2u4w, ._2om3, ._2ol-, ._5fjw, ._4z83, ._1gkq, ._4-dy, ._bub {
border-top: 1px solid $divider !important;
border-bottom: 1px solid $divider !important;
}
//friend card border
._d4i, ._f6s, .mentions-suggest-item, .mentions-suggest, .sharerAttachment,
.mToken, #addMembersTypeahead .mToken.mTokenWeakReference, .mQuestionsPollResultsBar,
._15q7, ._2q7v, ._4dwt ._16ii, ._3qdi::after, ._6q-c, ._61r-,
._2q7w, .acy, ._58ak, ._3t_l, ._4msa, ._3h8i, ._3clk, ._1kt6, ._1ksq, ._9sh-,
._1_y5, ._lr0, ._5hgt, ._2cpp, ._50uu, ._50uw, ._31yd, ._1e3d, ._3xz7, ._1xoz,
._4kcb, ._2lut, .jewel .touchable-notification.touched, .touchable-notification .touchable.touched,
.home-notification .touchable.touched, ._6beo ._6ber, ._7kxg,
._73ku ._73jw, ._6--d, ._26vk._56bt, ._3iyw ._2whz ._13-g, ._-jx,
._4e8n, ._uww, .mentions-placeholder, .mentions-shadow, .mentions-measurer, ._517h, ._59pe:focus, ._59pe:hover,
.uiBoxLightblue, .uiBoxWhite, ._558b ._54nc,
._5whq, ._59tt, ._41ft::after, .jx-tokenizer, ._3uqf, ._4756, ._1rrd, ._5n_f {
border: 1px solid $divider !important;
}
.mQuestionsPollResultsBar .shaded, ._1027._13sm {
border: 1px solid $text !important;
}
._3gka {
border: 1px dashed $divider !important;
}
//link card bottom border
._4o58::after, .acr, ._t21, ._2bdb, ._4ks>li,
.acw, .aclb, ._4qax, ._5h8f {
border-color: $divider !important;
}
// like, comment, share divider
._15ks ._15kl::before {
border-left: 1px solid transparent !important;
}
._56bf, .touch .btn {
border-radius: 0 !important;
border: 0 !important;
}
//page side tab layout
._2cis {
border-left: 10px solid $bg_transparent !important;
border-right: 10px solid $bg_transparent !important;
}
._2cir.selected, ._42rv, ._5zma, ._2x2s {
border-bottom: 3px solid $text !important;
}
._1ss6 {
border-left: 2px solid $text !important;
}
._484w.selected > ._6zf, ._5kqs::after, ._3lvo ._5xum._5xuk, ._x0b {
border-bottom: 1px solid $text !important;
}
._484w.selected ._6zf, ._7gxa, ._2wn2 {
border-bottom: 2px solid $accent !important;
}
// Small face previews
.facepile .facepileItem.facepileItemOverLapped .facepileItemRound, .facepile .facepileItem.facepileItemOverLapped.facepileItemRound, .facepile .facepileItem.facepileItemOverLapped .facepileMoreDotsRound {
border: 2px solid $bg_opaque2 !important;
}

View File

@ -0,0 +1,4 @@
[data-sigil=m_login_upsell],
[data-sigil="m-loading-indicator-animate m-loading-indicator-root"] {
display: none !important;
}

View File

@ -0,0 +1,20 @@
// Not all message related components are here; only the main ones.
// Borders for instance are merged into core_border
// Other person's message bubble
._34ee {
background: $background2 !important;
color: $text !important;
}
// Your message bubble; order matters
._34em ._34ee {
background: $accent !important;
color: $accent_text !important;
}
// Sticker page
._5as0, ._5cni, ._5as2 {
background: $bg_opaque !important;
}

View File

@ -0,0 +1,44 @@
html, body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._wn-,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, ._29e6, ._8wr8, ._52lz,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q, ._7iah, ._61mn ._61mo,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr, #mErrorView .message,
._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._7-1j, ._4ajz, ._m_1 ._2aha,
._1lf4, ._1hiz, ._xod, ._5ag5, ._zmk, ._3t_h, ._5lm6, ._3clv, ._3zlc, ._36rd, ._6oby, ._6_qk, ._9dr8,
._31zk, ._31zl, ._3xsa, ._3xs9, ._2-4s, ._2fzz ul, ._3z10, ._4mo, ._2om6, ._33r5, ._82y3, ._82y1, ._5rmf,
._43mh, .touch .btn, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._ctg, ._5300, ._5302, ._5_o0,
._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._7kx4, ._20zd, ._egh, ._egi,
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd, ._6obp, ._2iiu, ._1s06,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy, ._55i1, ._2wn6, ._1zep,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw, ._9si9,
textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._5fn5,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g, ._vbw,
._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc, ._6j_c, ._7iz_,
.uiStickyPlaceholderInput .placeholder, .mTypeahead span, ._4_d0 ._8-8a, ._6r12, ._5hoa, ._8r-l, ._7nyk, ._7nym, ._7nyn,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui, ._84lx [style*="color: rgb"],
._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj, ._1_yd, ._1_yc,
._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w, ._6zf, ._mhg, ._6r9_,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn, ._3iyw ._2whz ._13-g, ._6p6u, ._6p6v,
.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt, ._2bu3, ._2bu4, ._1ii2, ._1ii3,
._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._4yxo, ._6m3, ._6m7, ._6m3 ._1t62,
._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, ._t86 ._t87, ._t86 ._t88,
h1, h2, h3, h4, h5, h6 {
color: $text !important;
}
// Related to like buttons
a[data-sigil~="unlike"], a[style*="color: rgb(32, 120, 244)"], a[style*="color:#2078f4"],
strong > a, a > ._2vyr, ._15ks ._2q8z._2q8z, ._1e3e, .blueName, ._5kqs ._55sr, ._484w.selected ._6zf, ._6_qj, ._2wn3,
._by_, ._1r05 {
color: $accent !important;
}
._42nf ._42ng {
color: transparent !important;
}
// most links do not have a special color. We will highlight those in posts and messages
p > a, .msg span > a {
color: $link !important;
}

View File

@ -0,0 +1,6 @@
@import "core";
@import "svg";
//this file is used as the base for all themes
//given that svgs take a lot of characters, we won't compile them when testing
//therefore we use the core scss

View File

@ -0,0 +1,74 @@
// icons courtesy of https://material.io/icons/
$camera: '<svg xmlns="http://www.w3.org/2000/svg" fill="#{$text}" viewBox="0 -10 50 50"><circle cx="25" cy="23" r="3.2"/><path d="M22 13l-1.83 2H17c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V17c0-1.1-.9-2-2-2h-3.17L28 13h-6zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/><path fill="none" d="M13 11h24v24H13z"/></svg>';
// status upload image
._50uu {
@include background-svg($camera);
}
$video: '<svg xmlns="http://www.w3.org/2000/svg" fill="#{$text}" viewBox="0 0 50 50"><path fill="none" d="M13 26h24v24H13z"/><path d="M30 31.5V28c0-.55-.45-1-1-1H17c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>';
// status upload video
._50uw {
@include background-svg($video);
}
$like: '<svg xmlns="http://www.w3.org/2000/svg" fill="#{$text}" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z"/></svg>';
$like_selected: '<svg xmlns="http://www.w3.org/2000/svg" fill="#{$accent}" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z"/></svg>';
// 2018/12/29
// Previously ._15km ._15ko::before and ._15km ._15ko._77la::before; however, reaction changes no longer affect this element
// The robust measure seems to be the parent of a[data-sigil~="like-reaction-flyout"] along with [data-sigil~="like"] for an unliked post
// and [data-sigil~="unlike"] for a liked post
a._15ko::before {
@include background-svg($like);
background-position: center !important;
}
a._15ko._77la::before {
@include background-svg($like_selected);
background-position: center !important;
}
$comment: '<svg xmlns="http://www.w3.org/2000/svg" fill="#{$text}" viewBox="0 0 24 24"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/><path fill="none" d="M0 0h24v24H0z"/></svg>';
._15km ._15kq::before {
@include background-svg($comment);
background-position: center !important;
}
$share: '<svg xmlns="http://www.w3.org/2000/svg" fill="#{$text}" viewBox="0 0 24 24"><path d="M14 9V5l7 7-7 7v-4.1c-5 0-8.5 1.6-11 5.1 1-5 4-10 11-11z"/><path fill="none" d="M24 0H0v24h24z"/></svg>';
._15km ._15kr::before {
@include background-svg($share);
background-position: center !important;
}
$more_horiz: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path fill="#{$text}" d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>';
//$menus: ".sp_89zNula0Qh5",
//".sp_MP2OtCXORz9",
//".sp_NIWBacTn8LF",
//// 2018/12/31
//".sp_9ZFVhnFyWsw",
//// 2019/01/03
//".sp_SJIJjSlGEIO";
//
//$menu_collector: ();
//
//@each $menu in $menus {
// $menu_collector: append($menu_collector, unquote('#{$menu}'), 'comma');
// $menu_collector: append($menu_collector, unquote('#{$menu}_2x'), 'comma');
// $menu_collector: append($menu_collector, unquote('#{$menu}_3x'), 'comma');
//}
//
//#{$menu_collector} {
// @include background-svg($more_horiz);
// background-position: center !important;
//}
.story_body_container i.img[data-sigil*="story-popup-context"] {
@include background-svg($more_horiz);
background-position: center !important;
}

View File

@ -0,0 +1,51 @@
@import "../../core/colors";
@import "../../core/base";
@import "../../core/core_vars";
@import "core_text";
@import "core_bg";
@import "core_border";
@import "core_messages";
@import "core_hider";
//GLOBAL overrides; use with caution
*, *::after, *::before {
text-shadow: none !important;
box-shadow: none !important;
}
// .touch .btnS, button, ._94v, ._590n {
// box-shadow: none !important;
// }
@include placeholder {
color: $text_disabled !important;
}
.excessItem {
outline: $divider !important;
}
._3m1m {
background: linear-gradient(transparent, $bg_opaque) !important;
}
//new comment
@include keyframes(highlightFade) {
0%, 50% {
background: $background2;
}
100% {
background: $bg_transparent;
}
}
@include keyframes(chatHighlightAnimation) {
0%, 100% {
background: $bg_transparent;
}
50% {
background: $background2;
}
}

View File

@ -0,0 +1 @@
test.scss

View File

@ -0,0 +1,2 @@
@import "../../palette/custom";
@import "../core/main";

View File

@ -0,0 +1 @@
@import "../core/core_hider";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_amoled";
@import "../core/main";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_dark";
@import "../core/main";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_glass";
@import "../core/main";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_light";
@import "../core/main";

View File

@ -0,0 +1,4 @@
html, body, :root, #root,
[style*="background-color: #FFFFFF"], [style*="background-color: #E4E6EB"] {
background: $bg_transparent !important;
}

View File

@ -0,0 +1,3 @@
[role="navigation"] {
border-right: 2px solid $bg_opaque2 !important;
}

View File

@ -0,0 +1,15 @@
// Sizing adjustments
[role="navigation"] {
.rq0escxv.l9j0dhe7.du4w35lb.j83agx80.g5gj957u.rj1gh0hx.buofh1pr.hpfvmrgz.i1fnvgqd.bp9cbjyn.owycx6da.btwxx1t3.dflh9lhu.scb9dxdr.sj5x9vvc.cxgpxx05.sn0e7ne5.f6rbj1fe.l3ldwz01 /* New! Messenger app for windows */,
.rq0escxv.l9j0dhe7.du4w35lb.n851cfcs.aahdfvyu /* Search messenger */,
.wkznzc2l /* Top left chat + menu entry */ {
display: none !important;
}
}
header[role="banner"] /* login banner */,
._90px._9gb7 /* login bottom banner */,
.rq0escxv.l9j0dhe7.du4w35lb.j83agx80.cbu4d94t.pfnyh3mw.d2edcug0.hpfvmrgz.p8fzw8mz.pcp91wgn.iuny7tx3.ipjc6fyt /* Top bar call video info icons */,
.kuivcneq /* Right sidebar */ {
display: none !important;
}

View File

@ -0,0 +1,3 @@
html, body, input {
color: $text !important;
}

View File

@ -0,0 +1,3 @@
@import "core";
//this file is used as the base for all messenger themes

View File

@ -0,0 +1,11 @@
@import "../../core/colors";
@import "../../core/base";
@import "../../core/core_vars";
@import "core_text";
@import "core_bg";
@import "core_border";
@import "core_hider";
@include placeholder {
color: $text_disabled !important;
}

View File

@ -0,0 +1 @@
test.scss

View File

@ -0,0 +1,2 @@
@import "../../palette/custom";
@import "../core/main";

View File

@ -0,0 +1 @@
@import "../core/core_hider";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_amoled";
@import "../core/main";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_dark";
@import "../core/main";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_glass";
@import "../core/main";

View File

@ -0,0 +1,2 @@
@import "../../palette/material_light";
@import "../core/main";

View File

@ -0,0 +1,13 @@
$bg_transparent: unquote('$BT$');
$text: unquote('$T$');
$text_disabled: unquote('$TD$');
$link: unquote('$TT$');
$accent: unquote('$A$');
$accent_text: unquote('$AT$');
$background: unquote('$B$');
$background2: unquote('$BBT$');
$bg_opaque: unquote('$O$');
$bg_opaque2: unquote('$OO$');
$divider: unquote('$D$');
$card: unquote('$C$');
$tint: unquote('$TI$');

View File

@ -0,0 +1,9 @@
$text: #fff;
$accent_text: #fff;
$link: #5d86dd;
$accent: #5d86dd;
$background: #000;
$background2: rgba($background, 0.35);
$bg_transparent: $background;
$card: $background2;
$tint: rgba(#fff, 0.2);

View File

@ -0,0 +1,8 @@
$text: #fff;
$accent_text: #fff;
$link: #5d86dd;
$accent: #5d86dd;
$background: #303030;
$bg_transparent: $background;
$card: #353535;
$tint: rgba(#fff, 0.2);

View File

@ -0,0 +1,8 @@
$text: #fff;
$accent_text: #fff;
$link: #5d86dd;
$accent: #5d86dd;
$background: rgba(#000, 0.1);
$bg_transparent: transparent;
$card: rgba(#000, 0.25);
$tint: rgba(#fff, 0.15);

View File

@ -0,0 +1,13 @@
$text: #000;
$accent_text: #fff;
$link: #3b5998;
$accent: #3b5998;
$background: #fafafa;
// this is actually the inverse of material light (bg should be gray, cards should be white),
// but it looks better than the alternative
$background2: rgba(darken($background, 8%), 0.35);
$bg_transparent: $background;
$card: #fff;
$tint: #ddd;

View File

@ -0,0 +1,43 @@
// Credits to https://codepen.io/tomhodgins/pen/KgazaE
(function () {
const classTag = 'frostAutoExpand';
const textareas = <NodeListOf<HTMLTextAreaElement>>document.querySelectorAll(`textarea:not(.${classTag})`);
const dataAttribute = 'data-frost-minHeight';
const _frostAutoExpand = (el: HTMLElement) => {
if (!el.hasAttribute(dataAttribute)) {
el.setAttribute(dataAttribute, el.offsetHeight.toString());
}
// If no height is defined, have min bound to current height;
// otherwise we will allow for height decreases in case user deletes text
const minHeight = parseInt(el.getAttribute(dataAttribute) ?? '0');
// Save scroll position prior to height update
// See https://stackoverflow.com/a/18262927/4407321
const scrollLeft = window.pageXOffset ||
(document.documentElement || document.body.parentNode || document.body).scrollLeft;
const scrollTop = window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body).scrollTop;
el.style.height = 'inherit';
el.style.height = `${Math.max(el.scrollHeight, minHeight)}px`;
// Go to original scroll position
window.scrollTo(scrollLeft, scrollTop);
};
function _frostExpandAll() {
textareas.forEach(_frostAutoExpand);
}
textareas.forEach(el => {
el.classList.add(classTag)
const __frostAutoExpand = () => {
_frostAutoExpand(el)
};
el.addEventListener('paste', __frostAutoExpand)
el.addEventListener('input', __frostAutoExpand)
el.addEventListener('keyup', __frostAutoExpand)
});
window.addEventListener('load', _frostExpandAll)
window.addEventListener('resize', _frostExpandAll)
}).call(undefined);

View File

@ -1,44 +0,0 @@
async function updateCookies(changeInfo: browser.cookies._OnChangedChangeInfo) {
const application = "frostBackgroundChannel"
browser.runtime.sendNativeMessage(application, changeInfo)
}
async function readCookies() {
const application = "frostBackgroundChannel"
browser.runtime.sendNativeMessage(application, 'start cookie fetch')
// Testing with domains or urls didn't work
const cookies = await browser.cookies.getAll({});
const cookies2 = await browser.cookies.getAll({ storeId: "firefox-container-frost-context-1" })
const cookieStores = await browser.cookies.getAllCookieStores();
browser.runtime.sendNativeMessage(application, { name: "cookies", data: cookies.length, stores: cookieStores.map((s) => s.id), data2: cookies2.length, data3: cookies.filter(s => s.storeId != 'firefox-default').length })
}
async function handleMessage(request: any, sender: browser.runtime.MessageSender, sendResponse: (response?: any) => void) {
browser.runtime.sendNativeMessage("frostBackgroundChannel", 'pre send')
await new Promise(resolve => setTimeout(resolve, 1000));
browser.runtime.sendNativeMessage("frostBackgroundChannel", 'post send')
sendResponse({ received: request, asdf: "asdf" })
}
// Reading cookies with storeId might not be fully supported on Android
// https://stackoverflow.com/q/76505000/4407321
// Using manifest 3 stopped getAll from working
// Reading now always shows storeId as firefox-default
// Setting a cookie with a custom container does not seem to work
// browser.cookies.onChanged.addListener(updateCookies);
// browser.tabs.onActivated.addListener(readCookies);
// browser.runtime.onStartup.addListener(readCookies);
// browser.runtime.onMessage.addListener(handleMessage);

View File

@ -1,7 +1,6 @@
(async function () {
(function () {
let prevented = false;
/**
* Go up at most [depth] times, to retrieve a parent matching the provided predicate
* If one is found, it is returned immediately.
@ -40,29 +39,29 @@
/**
* Given event and target, return true if handled and false otherwise.
*/
type EventHandler = (e: Event, target: HTMLElement) => Promise<Boolean>
type EventHandler = (e: Event, target: HTMLElement) => Boolean
const _frostGeneral: EventHandler = async (e, target) => {
const _frostGeneral: EventHandler = (e, target) => {
// We now disable clicks for the main notification page
if (document.getElementById("notifications_list")) {
return false
}
const url = _parentUrl(target, 2);
return frost.loadUrl(url);
return Frost.loadUrl(url);
};
const _frostLaunchpadClick: EventHandler = async (e, target) => {
const _frostLaunchpadClick: EventHandler = (e, target) => {
if (!_parentEl(target, 6, (el) => el.id === 'launchpad')) {
return false
}
console.log('Clicked launchpad');
const url = _parentUrl(target, 5);
return frost.loadUrl(url);
return Frost.loadUrl(url);
};
const handlers: EventHandler[] = [_frostLaunchpadClick, _frostGeneral];
const _frostAClick = async (e: Event) => {
const _frostAClick = (e: Event) => {
if (prevented) {
console.log("Click intercept prevented");
return
@ -75,9 +74,8 @@
console.log("No element found");
return
}
// TODO cannot use await here; copy logic over here
for (const h of handlers) {
if (await h(e, target)) {
if (h(e, target)) {
e.stopPropagation();
e.preventDefault();
return

View File

@ -0,0 +1,15 @@
// For desktop only
(function () {
const _frostAContext = (e: Event) => {
// Commonality; check for valid target
const element = e.target || e.currentTarget || e.srcElement;
if (!(element instanceof Element)) {
console.log("No element found");
return
}
console.log(`Clicked element ${element.tagName} ${element.className}`);
};
document.addEventListener('contextmenu', _frostAContext, true);
}).call(undefined);

View File

@ -0,0 +1,145 @@
/**
* Context menu for links
* Largely mimics click_a.js
*/
(function () {
let longClick = false;
/**
* Go up at most [depth] times, to retrieve a parent matching the provided predicate
* If one is found, it is returned immediately.
* Otherwise, null is returned.
*/
function _parentEl(el: HTMLElement, depth: number, predicate: (el: HTMLElement) => boolean): HTMLElement | null {
for (let i = 0; i < depth + 1; i++) {
if (predicate(el)) {
return el
}
const parent = el.parentElement;
if (!parent) {
return null
}
el = parent
}
return null
}
/**
* Given event and target, return true if handled and false otherwise.
*/
type EventHandler = (e: Event, target: HTMLElement) => Boolean
const _frostCopyComment: EventHandler = (e, target) => {
if (!target.hasAttribute('data-commentid')) {
return false;
}
const text = target.innerText;
console.log(`Copy comment ${text}`);
Frost.contextMenu(null, text);
return true;
};
/**
* Posts should click a tag, with two parents up being div.story_body_container
*/
const _frostCopyPost: EventHandler = (e, target) => {
if (target.tagName !== 'A') {
return false;
}
const parent1 = target.parentElement;
if (!parent1 || parent1.tagName !== 'DIV') {
return false;
}
const parent2 = parent1.parentElement;
if (!parent2 || !parent2.classList.contains('story_body_container')) {
return false;
}
const url = target.getAttribute('href');
const text = parent1.innerText;
console.log(`Copy post ${url} ${text}`);
Frost.contextMenu(url, text);
return true;
};
const _getImageStyleUrl = (el: Element): string | null => {
// Emojis and special characters may be images from a span
const img = el.querySelector("[style*=\"background-image: url(\"]:not(span)");
if (!img) {
return null
}
return (<String>window.getComputedStyle(img, null).backgroundImage).trim().slice(4, -1);
};
/**
* Opens image activity for posts with just one image
*/
const _frostImage: EventHandler = (e, target) => {
const element = _parentEl(target, 2, (el) => el.tagName === 'A');
if (!element) {
return false;
}
const url = element.getAttribute('href');
if (!url || url === '#') {
return false;
}
const text = (<HTMLElement>element.parentElement).innerText;
// Check if image item exists, first in children and then in parent
const imageUrl = _getImageStyleUrl(element) || _getImageStyleUrl(<Element>element.parentElement);
if (imageUrl) {
console.log(`Context image: ${imageUrl}`);
Frost.loadImage(imageUrl, text);
return true;
}
// Check if true img exists
const img = element.querySelector("img[src*=scontent]");
if (img instanceof HTMLMediaElement) {
const imgUrl = img.src;
console.log(`Context img: ${imgUrl}`);
Frost.loadImage(imgUrl, text);
return true;
}
console.log(`Context content ${url} ${text}`);
Frost.contextMenu(url, text);
return true;
};
const handlers: EventHandler[] = [_frostImage, _frostCopyComment, _frostCopyPost];
const _frostAContext = (e: Event) => {
Frost.longClick(true);
longClick = true;
/**
* Don't handle context events while scrolling
*/
if (Frost.isScrolling()) {
console.log("Skip from scrolling");
return;
}
/*
* Commonality; check for valid target
*/
const target = e.target || e.currentTarget || e.srcElement;
if (!(target instanceof HTMLElement)) {
console.log("No element found");
return
}
for (const h of handlers) {
if (h(e, target)) {
e.stopPropagation();
e.preventDefault();
return
}
}
};
document.addEventListener('contextmenu', _frostAContext, true);
document.addEventListener('touchend', () => {
if (longClick) {
Frost.longClick(false);
longClick = false
}
}, true);
}).call(undefined);

View File

@ -0,0 +1,27 @@
// Emit key once half the viewport is covered
(function () {
const isReady = () => {
return document.body.scrollHeight > innerHeight + 100
};
if (isReady()) {
console.log('Already ready');
Frost.isReady();
return
}
console.log('Injected document watcher');
const observer = new MutationObserver(() => {
if (isReady()) {
observer.disconnect();
Frost.isReady();
console.log(`Documented surpassed height in ${performance.now()}`);
}
});
observer.observe(document, {
childList: true,
subtree: true
})
}).call(undefined);

View File

@ -1,21 +0,0 @@
/**
* Mobile browsers don't support modules, so I'm creating a shared variable.
*
* No idea if this is good practice.
*/
const frost = (function () {
const application = "frostChannel"
async function sendMessage<T>(message: ExtensionModel): Promise<T> {
return browser.runtime.sendNativeMessage(application, message)
}
async function loadUrl(url: string | null): Promise<boolean> {
if (url == null) return false
return sendMessage({ type: "url-click", url: url })
}
return {
sendMessage, loadUrl
}
}).call(undefined);

View File

@ -0,0 +1,7 @@
// Fetches the header contents if it exists
(function() {
const header = document.getElementById('header');
if (header) {
Frost.handleHeader(header.outerHTML);
}
}).call(undefined);

View File

@ -0,0 +1,61 @@
(function () {
/**
* Go up at most [depth] times, to retrieve a parent matching the provided predicate
* If one is found, it is returned immediately.
* Otherwise, null is returned.
*/
function _parentEl(el: HTMLElement, depth: number, predicate: (el: HTMLElement) => boolean): HTMLElement | null {
for (let i = 0; i < depth + 1; i++) {
if (predicate(el)) {
return el
}
const parent = el.parentElement;
if (!parent) {
return null
}
el = parent
}
return null
}
/**
* Check if element can scroll horizontally.
* We primarily rely on the overflow-x field.
* For performance reasons, we will check scrollWidth first to see if scrolling is a possibility
*/
function _canScrollHorizontally(el: HTMLElement): boolean {
/*
* Sometimes the offsetWidth is off by < 10px. We use the multiplier
* since the trays are typically more than 2 times greater
*/
if (el.scrollWidth > el.offsetWidth * 1.2) {
return true
}
const styles = window.getComputedStyle(el);
/*
* Works well in testing, but on mobile it just shows 'visible'
*/
return styles.overflowX === 'scroll';
}
const _frostCheckHorizontalScrolling = (e: Event) => {
const target = e.target || e.currentTarget || e.srcElement;
if (!(target instanceof HTMLElement)) {
return
}
const scrollable = _parentEl(target, 5, _canScrollHorizontally) !== null;
if (scrollable) {
console.log('Pause horizontal scrolling');
Frost.allowHorizontalScrolling(false);
}
};
const _frostResetHorizontalScrolling = (e: Event) => {
Frost.allowHorizontalScrolling(true)
};
document.addEventListener('touchstart', _frostCheckHorizontalScrolling, true);
document.addEventListener('touchend', _frostResetHorizontalScrolling, true);
}).call(undefined);

View File

@ -0,0 +1,47 @@
// Handles media events
(function () {
const _frostMediaClick = (e: Event) => {
const target = e.target || e.srcElement;
if (!(target instanceof HTMLElement)) {
return
}
let element: HTMLElement = target;
const dataset = element.dataset;
if (!dataset || !dataset.sigil || dataset.sigil.toLowerCase().indexOf('inlinevideo') == -1) {
return
}
let i = 0;
while (!element.hasAttribute('data-store')) {
if (++i > 2) {
return
}
element = <HTMLElement>element.parentNode;
}
const store = element.dataset.store;
if (!store) {
return
}
let dataStore;
try {
dataStore = JSON.parse(store)
} catch (e) {
return
}
const url = dataStore.src;
// !startsWith; see https://stackoverflow.com/a/36876507/4407321
if (!url || url.lastIndexOf('http', 0) !== 0) {
return
}
console.log(`Inline video ${url}`);
if (Frost.loadVideo(url, dataStore.animatedGifVideo || false)) {
e.stopPropagation()
}
};
document.addEventListener('click', _frostMediaClick, true);
}).call(undefined);

View File

@ -0,0 +1,25 @@
// Binds callback to an invisible webview to take in the search events
(function () {
let finished = false;
const x = new MutationObserver(() => {
const _f_thread = document.querySelector('#threadlist_rows');
if (!_f_thread) {
return
}
console.log(`Found message threads ${_f_thread.outerHTML}`);
Frost.handleHtml(_f_thread.outerHTML);
finished = true;
x.disconnect();
});
x.observe(document, {
childList: true,
subtree: true
});
setTimeout(() => {
if (!finished) {
finished = true;
console.log('Message thread timeout cancellation');
Frost.handleHtml("")
}
}, 20000);
}).call(undefined);

View File

@ -0,0 +1,25 @@
// Listen when scrolling events stop
(function () {
let scrollTimeout: number | undefined = undefined;
let scrolling: boolean = false;
window.addEventListener('scroll', function (event) {
if (!scrolling) {
Frost.setScrolling(true);
scrolling = true;
}
window.clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(function () {
if (scrolling) {
Frost.setScrolling(false);
scrolling = false;
}
}, 600);
// For our specific use case, we want to release other features pretty far after scrolling stops
// For general scrolling use cases, the delta can be much smaller
// My assumption for context menus is that the long press is 500ms
}, false);
}).call(undefined);

View File

@ -0,0 +1,31 @@
/*
* focus listener for textareas
* since swipe to refresh is quite sensitive, we will disable it
* when we detect a user typing
* note that this extends passed having a keyboard opened,
* as a user may still be reviewing his/her post
* swiping should automatically be reset on refresh
*/
(function () {
const _frostFocus = (e: Event) => {
const element = e.target || e.srcElement;
if (!(element instanceof Element)) {
return
}
console.log(`FrostJSI focus, ${element.tagName}`);
if (element.tagName === 'TEXTAREA') {
Frost.disableSwipeRefresh(true);
}
};
const _frostBlur = (e: Event) => {
const element = e.target || e.srcElement;
if (!(element instanceof Element)) {
return
}
console.log(`FrostJSI blur, ${element.tagName}`);
Frost.disableSwipeRefresh(false);
};
document.addEventListener("focus", _frostFocus, true);
document.addEventListener("blur", _frostBlur, true);
}).call(undefined);

View File

@ -16,7 +16,7 @@
"allowUnreachableCode": true,
"allowUnusedLabels": true,
"removeComments": true,
"outDir": "assets/frostcore/js"
"outDir": "assets/frost/js"
},
"include": [
"ts",

View File

@ -1,7 +0,0 @@
declare namespace browser.runtime {
interface Port {
postMessage: (message: string) => void;
postMessage: (message: ExtensionModel) => void;
}
function sendNativeMessage(application: string, message: ExtensionModel): Promise<any>;
}

View File

@ -1,11 +1,33 @@
type TestModel = {
type: 'test-model'
message: string
declare interface FrostJSI {
loadUrl(url: string | null): boolean
loadVideo(url: string | null, isGif: boolean): boolean
reloadBaseUrl(animate: boolean)
contextMenu(url: string | null, text: string | null)
longClick(start: boolean)
disableSwipeRefresh(disable: boolean)
loadLogin()
loadImage(imageUrl: string, text: string | null)
emit(flag: number)
isReady()
handleHtml(html: string | null)
handleHeader(html: string | null)
allowHorizontalScrolling(enable: boolean)
setScrolling(scrolling: boolean)
isScrolling(): boolean
}
type UrlClickModel = {
type: 'url-click'
url: string
}
type ExtensionModel = TestModel | UrlClickModel
declare var Frost: FrostJSI;