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:
commit
a96746d5f5
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
@ -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/"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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?> =
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()"""
|
@ -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) }
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
@ -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": {
|
||||
|
129
app-compose/src/web/scss/core/_base.scss
Normal file
129
app-compose/src/web/scss/core/_base.scss
Normal 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, '"', """);
|
||||
$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;
|
||||
}
|
19
app-compose/src/web/scss/core/_colors.scss
Normal file
19
app-compose/src/web/scss/core/_colors.scss
Normal 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;
|
49
app-compose/src/web/scss/core/_core_vars.scss
Normal file
49
app-compose/src/web/scss/core/_core_vars.scss
Normal 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
|
||||
}
|
||||
}
|
91
app-compose/src/web/scss/facebook/core/_core_bg.scss
Normal file
91
app-compose/src/web/scss/facebook/core/_core_bg.scss
Normal 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;
|
||||
}
|
106
app-compose/src/web/scss/facebook/core/_core_border.scss
Normal file
106
app-compose/src/web/scss/facebook/core/_core_border.scss
Normal 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;
|
||||
}
|
4
app-compose/src/web/scss/facebook/core/_core_hider.scss
Normal file
4
app-compose/src/web/scss/facebook/core/_core_hider.scss
Normal file
@ -0,0 +1,4 @@
|
||||
[data-sigil=m_login_upsell],
|
||||
[data-sigil="m-loading-indicator-animate m-loading-indicator-root"] {
|
||||
display: none !important;
|
||||
}
|
20
app-compose/src/web/scss/facebook/core/_core_messages.scss
Normal file
20
app-compose/src/web/scss/facebook/core/_core_messages.scss
Normal 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;
|
||||
}
|
44
app-compose/src/web/scss/facebook/core/_core_text.scss
Normal file
44
app-compose/src/web/scss/facebook/core/_core_text.scss
Normal 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;
|
||||
}
|
6
app-compose/src/web/scss/facebook/core/_main.scss
Normal file
6
app-compose/src/web/scss/facebook/core/_main.scss
Normal 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
|
74
app-compose/src/web/scss/facebook/core/_svg.scss
Normal file
74
app-compose/src/web/scss/facebook/core/_svg.scss
Normal 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;
|
||||
}
|
51
app-compose/src/web/scss/facebook/core/core.scss
Normal file
51
app-compose/src/web/scss/facebook/core/core.scss
Normal 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;
|
||||
}
|
||||
}
|
1
app-compose/src/web/scss/facebook/themes/.gitignore
vendored
Normal file
1
app-compose/src/web/scss/facebook/themes/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
test.scss
|
2
app-compose/src/web/scss/facebook/themes/custom.scss
Normal file
2
app-compose/src/web/scss/facebook/themes/custom.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@import "../../palette/custom";
|
||||
@import "../core/main";
|
1
app-compose/src/web/scss/facebook/themes/default.scss
Normal file
1
app-compose/src/web/scss/facebook/themes/default.scss
Normal file
@ -0,0 +1 @@
|
||||
@import "../core/core_hider";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_amoled";
|
||||
@import "../core/main";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_dark";
|
||||
@import "../core/main";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_glass";
|
||||
@import "../core/main";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_light";
|
||||
@import "../core/main";
|
4
app-compose/src/web/scss/messenger/core/_core_bg.scss
Normal file
4
app-compose/src/web/scss/messenger/core/_core_bg.scss
Normal file
@ -0,0 +1,4 @@
|
||||
html, body, :root, #root,
|
||||
[style*="background-color: #FFFFFF"], [style*="background-color: #E4E6EB"] {
|
||||
background: $bg_transparent !important;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
[role="navigation"] {
|
||||
border-right: 2px solid $bg_opaque2 !important;
|
||||
}
|
15
app-compose/src/web/scss/messenger/core/_core_hider.scss
Normal file
15
app-compose/src/web/scss/messenger/core/_core_hider.scss
Normal 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;
|
||||
}
|
3
app-compose/src/web/scss/messenger/core/_core_text.scss
Normal file
3
app-compose/src/web/scss/messenger/core/_core_text.scss
Normal file
@ -0,0 +1,3 @@
|
||||
html, body, input {
|
||||
color: $text !important;
|
||||
}
|
3
app-compose/src/web/scss/messenger/core/_main.scss
Normal file
3
app-compose/src/web/scss/messenger/core/_main.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@import "core";
|
||||
|
||||
//this file is used as the base for all messenger themes
|
11
app-compose/src/web/scss/messenger/core/core.scss
Normal file
11
app-compose/src/web/scss/messenger/core/core.scss
Normal 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;
|
||||
}
|
1
app-compose/src/web/scss/messenger/themes/.gitignore
vendored
Normal file
1
app-compose/src/web/scss/messenger/themes/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
test.scss
|
2
app-compose/src/web/scss/messenger/themes/custom.scss
Normal file
2
app-compose/src/web/scss/messenger/themes/custom.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@import "../../palette/custom";
|
||||
@import "../core/main";
|
1
app-compose/src/web/scss/messenger/themes/default.scss
Normal file
1
app-compose/src/web/scss/messenger/themes/default.scss
Normal file
@ -0,0 +1 @@
|
||||
@import "../core/core_hider";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_amoled";
|
||||
@import "../core/main";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_dark";
|
||||
@import "../core/main";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_glass";
|
||||
@import "../core/main";
|
@ -0,0 +1,2 @@
|
||||
@import "../../palette/material_light";
|
||||
@import "../core/main";
|
13
app-compose/src/web/scss/palette/_custom.scss
Normal file
13
app-compose/src/web/scss/palette/_custom.scss
Normal 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$');
|
9
app-compose/src/web/scss/palette/_material_amoled.scss
Normal file
9
app-compose/src/web/scss/palette/_material_amoled.scss
Normal 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);
|
8
app-compose/src/web/scss/palette/_material_dark.scss
Normal file
8
app-compose/src/web/scss/palette/_material_dark.scss
Normal 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);
|
8
app-compose/src/web/scss/palette/_material_glass.scss
Normal file
8
app-compose/src/web/scss/palette/_material_glass.scss
Normal 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);
|
13
app-compose/src/web/scss/palette/_material_light.scss
Normal file
13
app-compose/src/web/scss/palette/_material_light.scss
Normal 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;
|
43
app-compose/src/web/ts/auto_resize_textarea.ts
Normal file
43
app-compose/src/web/ts/auto_resize_textarea.ts
Normal 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);
|
@ -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);
|
||||
|
@ -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
|
||||
|
15
app-compose/src/web/ts/click_debugger.ts
Normal file
15
app-compose/src/web/ts/click_debugger.ts
Normal 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);
|
145
app-compose/src/web/ts/context_a.ts
Normal file
145
app-compose/src/web/ts/context_a.ts
Normal 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);
|
27
app-compose/src/web/ts/document_watcher.ts
Normal file
27
app-compose/src/web/ts/document_watcher.ts
Normal 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);
|
@ -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);
|
7
app-compose/src/web/ts/header_badges.ts
Normal file
7
app-compose/src/web/ts/header_badges.ts
Normal 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);
|
61
app-compose/src/web/ts/horizontal_scrolling.ts
Normal file
61
app-compose/src/web/ts/horizontal_scrolling.ts
Normal 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);
|
||||
|
47
app-compose/src/web/ts/media.ts
Normal file
47
app-compose/src/web/ts/media.ts
Normal 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);
|
25
app-compose/src/web/ts/notif_msg.ts
Normal file
25
app-compose/src/web/ts/notif_msg.ts
Normal 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);
|
25
app-compose/src/web/ts/scroll_stop.ts
Normal file
25
app-compose/src/web/ts/scroll_stop.ts
Normal 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);
|
31
app-compose/src/web/ts/textarea_listener.ts
Normal file
31
app-compose/src/web/ts/textarea_listener.ts
Normal 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);
|
@ -16,7 +16,7 @@
|
||||
"allowUnreachableCode": true,
|
||||
"allowUnusedLabels": true,
|
||||
"removeComments": true,
|
||||
"outDir": "assets/frostcore/js"
|
||||
"outDir": "assets/frost/js"
|
||||
},
|
||||
"include": [
|
||||
"ts",
|
||||
|
7
app-compose/src/web/typings/browser.d.ts
vendored
7
app-compose/src/web/typings/browser.d.ts
vendored
@ -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>;
|
||||
}
|
40
app-compose/src/web/typings/frost.d.ts
vendored
40
app-compose/src/web/typings/frost.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user