Merge pull request #1948 from AllanWang/compose
@ -6,8 +6,6 @@
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11">
|
||||
<bytecodeTargetLevel target="17">
|
||||
<module name="Frost-for-Facebook.buildSrc" target="11" />
|
||||
<module name="Frost-for-Facebook.buildSrc.main" target="11" />
|
||||
<module name="Frost-for-Facebook.buildSrc.test" target="11" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
</project>
|
@ -1,6 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="/* * Copyright &#36;year 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/>. */" />
|
||||
<option name="myName" value="GPLv3" />
|
||||
</copyright>
|
||||
</component>
|
9
.idea/kotlinc.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Kotlin2JvmCompilerArguments">
|
||||
<option name="jvmTarget" value="17" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.21" />
|
||||
</component>
|
||||
</project>
|
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
@ -52,7 +51,7 @@
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
13
README.md
@ -11,7 +11,18 @@
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/com.pitchedapps.frost)
|
||||
|
||||
**This project is no longer actively maintained**. I still use it, but I've largely been off of Facebook for years. Bugs relating to logins are region dependent, and web wrappers don't have stable APIs, so please use at your own discretion.
|
||||
**This project is undergoing a full rewrite**. The latest snapshot of the old project is available at https://github.com/AllanWang/Frost-for-Facebook/tree/legacy.
|
||||
|
||||
While a rewrite isn't necessary to keep the project going, I wanted to take some time to learn new things, and also to support some big features:
|
||||
|
||||
* Views to compose. This will make things a lot more structured, and will simplify dynamic colors/themes, which I've included in my projects well before Material You.
|
||||
* WebView to GeckoView. This will add support for web extensions, and provide more functionality. Admittedly, this has been a huge pain to learn, and there are things that are far easier with webviews, but I think I'm at a point where I've answered my main questions.
|
||||
* Web extensions allows for actual ad blocks, and theme updates without pushing new apks
|
||||
* I intend on adding notification support for friend requests, though that could have been done with the old build
|
||||
|
||||
The direction I'm taking Frost v4.0.0 is to simplify a lot of things, and to leverage other extensions/libraries where possible. GeckoView also makes it easier to support multi account, in case I want to extend this beyond Facebook at some point in the future.
|
||||
|
||||
---
|
||||
|
||||
**Note** Some keystores are public for the sake of automatic builds and consistent signing across devices.
|
||||
This means that others can build apps with the same signature. The only valid download sources are through my github releases and F-Droid.
|
||||
|
1
app-compose/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
252
app-compose/build.gradle
Normal file
@ -0,0 +1,252 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "org.jetbrains.kotlin.android"
|
||||
id "kotlin-kapt"
|
||||
id "dagger.hilt.android.plugin"
|
||||
id "com.google.devtools.ksp"
|
||||
id "com.google.protobuf"
|
||||
id "app.cash.sqldelight"
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.pitchedapps.frost"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
multiDexEnabled = true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
// GeckoView
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1571175
|
||||
// Replaces `android.bundle.enableUncompressedNativeLibs = false` in gradle.properties
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-compiler
|
||||
kotlinCompilerExtensionVersion = "1.4.7"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
applicationIdSuffix ".compose.debug"
|
||||
versionNameSuffix "-compose-debug"
|
||||
signingConfig signingConfigs.debug
|
||||
resValue "string", "frost_name", "Frost Compose Debug"
|
||||
resValue "string", "frost_web", "Frost Compose Web Debug"
|
||||
|
||||
// kotlinOptions.freeCompilerArgs += compilerArgs
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
test.java.srcDirs += 'src/test/kotlin'
|
||||
androidTest.java.srcDirs += 'src/androidTest/kotlin'
|
||||
main.assets.srcDirs += ['src/web/assets']
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
applicationVariants.configureEach { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "${project.APP_ID}-${variant.buildType.name}.apk"
|
||||
}
|
||||
}
|
||||
|
||||
namespace 'com.pitchedapps.frost'
|
||||
|
||||
def testKeystoreFile = file('../files/test.keystore')
|
||||
def testPropFile = file('../files/test.properties')
|
||||
def withTestSigning = testKeystoreFile.exists() && testPropFile.exists()
|
||||
|
||||
def releaseKeystoreFile = file('../files/release.keystore')
|
||||
def releasePropFile = file('../files/release.properties')
|
||||
def withReleaseSigning = releaseKeystoreFile.exists() && releasePropFile.exists()
|
||||
|
||||
signingConfigs {
|
||||
|
||||
debug {
|
||||
storeFile file("../files/debug.keystore")
|
||||
storePassword "debugKey"
|
||||
keyAlias "debugKey"
|
||||
keyPassword "debugKey"
|
||||
}
|
||||
|
||||
if (withTestSigning) {
|
||||
def testProps = new Properties()
|
||||
testPropFile.withInputStream { testProps.load(it) }
|
||||
|
||||
test {
|
||||
storeFile testKeystoreFile
|
||||
storePassword testProps.getProperty('storePassword')
|
||||
keyAlias testProps.getProperty('keyAlias')
|
||||
keyPassword testProps.getProperty('keyPassword')
|
||||
}
|
||||
}
|
||||
|
||||
if (withReleaseSigning) {
|
||||
def releaseProps = new Properties()
|
||||
releasePropFile.withInputStream { releaseProps.load(it) }
|
||||
|
||||
release {
|
||||
storeFile releaseKeystoreFile
|
||||
storePassword releaseProps.getProperty('storePassword')
|
||||
keyAlias releaseProps.getProperty('keyAlias')
|
||||
keyPassword releaseProps.getProperty('keyPassword')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Select the Glean from GeckoView.
|
||||
// `service-sync-logins` requires Glean, which pulls in glean-native,
|
||||
// but that's also provided by geckoview-omni, so now we need to select which one to use.
|
||||
project.configurations.configureEach {
|
||||
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
|
||||
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
|
||||
if (toBeSelected != null) {
|
||||
select(toBeSelected)
|
||||
}
|
||||
because 'use GeckoView Glean instead of standalone Glean'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation "androidx.core:core-ktx:1.10.1"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "com.google.android.material:material:1.9.0"
|
||||
|
||||
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1"
|
||||
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
||||
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "com.google.truth:truth:1.1.3"
|
||||
testImplementation "org.json:json:20220320"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
|
||||
|
||||
def hilt = "2.46.1"
|
||||
implementation "com.google.dagger:hilt-android:${hilt}"
|
||||
kapt "com.google.dagger:hilt-android-compiler:${hilt}"
|
||||
|
||||
testImplementation "com.google.dagger:hilt-android:${hilt}"
|
||||
kaptTest "com.google.dagger:hilt-android-compiler:${hilt}"
|
||||
|
||||
def flogger = "0.7.1"
|
||||
implementation "com.google.flogger:flogger:${flogger}"
|
||||
implementation "com.google.flogger:flogger-system-backend:${flogger}"
|
||||
|
||||
def moshi = "1.14.0"
|
||||
implementation "com.squareup.moshi:moshi-kotlin:${moshi}"
|
||||
implementation "com.squareup.moshi:moshi-adapters:${moshi}"
|
||||
ksp "com.squareup.moshi:moshi-kotlin-codegen:${moshi}"
|
||||
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.11"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
||||
|
||||
// https://maven.mozilla.org/maven2/org/mozilla/components/browser-engine-gecko/maven-metadata.xml
|
||||
// https://github.com/mozilla-mobile/reference-browser/blob/master/buildSrc/src/main/java/AndroidComponents.kt
|
||||
// https://nightly.maven.mozilla.org/maven2/org/mozilla/components/browser-engine-gecko/maven-metadata.xml
|
||||
def mozillaAndroidComponents = "116.0.20230617040331"
|
||||
|
||||
implementation "org.mozilla.components:lib-state:${mozillaAndroidComponents}"
|
||||
// Kept for reference; not needed
|
||||
implementation "org.mozilla.components:browser-state:${mozillaAndroidComponents}"
|
||||
|
||||
// Compose
|
||||
def composeVersion = "1.4.3"
|
||||
|
||||
implementation("androidx.compose.ui:ui:${composeVersion}")
|
||||
implementation("androidx.activity:activity-compose:1.7.2")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
|
||||
// Tooling support (Previews, etc.)
|
||||
implementation("androidx.compose.ui:ui-tooling:${composeVersion}")
|
||||
// Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
|
||||
implementation("androidx.compose.foundation:foundation:${composeVersion}")
|
||||
// Material Design
|
||||
implementation("androidx.compose.material:material:${composeVersion}")
|
||||
implementation 'androidx.compose.material3:material3:1.2.0-alpha02'
|
||||
// Material design icons
|
||||
implementation("androidx.compose.material:material-icons-core:${composeVersion}")
|
||||
implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
|
||||
implementation("br.com.devsrsouza.compose.icons:font-awesome:1.1.0")
|
||||
|
||||
implementation("androidx.datastore:datastore:1.0.0")
|
||||
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 {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.22.3'
|
||||
}
|
||||
plugins {
|
||||
javalite {
|
||||
artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
|
||||
}
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option 'lite'
|
||||
}
|
||||
kotlin {
|
||||
option 'lite'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
// https://stackoverflow.com/q/69817925
|
||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
||||
}
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
databases {
|
||||
register("FrostDb") {
|
||||
packageName.set("com.pitchedapps.frost.db")
|
||||
schemaOutputDirectory.set(file("src/main/sqldelight/databases"))
|
||||
}
|
||||
}
|
||||
}
|
21
app-compose/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
142
app-compose/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<application
|
||||
android:name=".FrostApp"
|
||||
android:allowBackup="true"
|
||||
android:extractNativeLibs="false"
|
||||
android:fullBackupContent="@xml/frost_backup_rules"
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/frost_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/FrostTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:name=".StartActivity"
|
||||
android:exported="true"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/FrostTheme.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!--
|
||||
Config changes taken from Fenix with:
|
||||
locale
|
||||
addition
|
||||
|
||||
https://github.com/mozilla-mobile/fenix/blob/main/app/src/main/AndroidManifest.xml
|
||||
-->
|
||||
<activity
|
||||
android:name=".main.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout|locale"
|
||||
android:hardwareAccelerated="true" />
|
||||
|
||||
<!-- <activity-->
|
||||
<!-- android:name=".activities.FrostWebActivity"-->
|
||||
<!-- android:autoRemoveFromRecents="true"-->
|
||||
<!-- android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout|locale"-->
|
||||
<!-- android:exported="true"-->
|
||||
<!-- android:hardwareAccelerated="true"-->
|
||||
<!-- android:label="@string/frost_web"-->
|
||||
<!-- android:launchMode="singleInstance"-->
|
||||
<!-- android:taskAffinity="com.pitchedapps.frost.single.web"-->
|
||||
<!-- android:theme="@style/FrostTheme.Overlay.Slide">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.SEND" />-->
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <data android:mimeType="text/plain" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- <intent-filter-->
|
||||
<!-- android:autoVerify="true"-->
|
||||
<!-- tools:ignore="UnusedAttribute">-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
|
||||
<!-- <data-->
|
||||
<!-- android:host="m.facebook.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="m.facebook.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="mobile.facebook.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="mobile.facebook.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="touch.facebook.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="touch.facebook.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="fb.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="fb.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="fb.me"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="fb.me"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="facebook.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="facebook.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="www.facebook.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="www.facebook.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="messenger.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="messenger.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="www.messenger.com"-->
|
||||
<!-- android:scheme="http" />-->
|
||||
<!-- <data-->
|
||||
<!-- android:host="www.messenger.com"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </activity>-->
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import com.google.common.flogger.FluentLogger
|
||||
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()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
registerActivityLifecycleCallbacks(
|
||||
object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
logger.atFine().log("Activity %s destroyed", activity.localClassName)
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
logger.atFine().log("Activity %s created", activity.localClassName)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
MainScope().launch { setup() }
|
||||
}
|
||||
|
||||
private suspend fun setup() {
|
||||
JsAssets.load(this)
|
||||
frostJsInjectors.get().load()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.components.FrostDataStore
|
||||
import com.pitchedapps.frost.db.FrostDb
|
||||
import com.pitchedapps.frost.ext.FrostAccountId
|
||||
import com.pitchedapps.frost.ext.idData
|
||||
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.usecases.UseCases
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Start activity.
|
||||
*
|
||||
* This is the launcher activity, and should not be moved/renamed. The activity itself is transient,
|
||||
* and will launch another activity without history after doing initialization work.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
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)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val id = withContext(Dispatchers.IO) { getCurrentAccountId() }
|
||||
|
||||
logger.atInfo().log("Starting Frost with id %s", id)
|
||||
|
||||
// TODO load real tabs
|
||||
useCases.homeTabs.setHomeTabs(listOf(FbItem.Feed, FbItem.Menu))
|
||||
|
||||
launchActivity<MainActivity>(
|
||||
intentBuilder = {
|
||||
flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCurrentAccountId(): FrostAccountId {
|
||||
val currentId = dataStore.account.idData.firstOrNull()
|
||||
if (currentId != null) return currentId
|
||||
|
||||
val newId = getAnyAccountId()
|
||||
dataStore.account.updateData { it.toBuilder().setAccountId(newId).build() }
|
||||
return FrostAccountId(newId)
|
||||
}
|
||||
|
||||
private fun getAnyAccountId(): Long {
|
||||
val account = frostDb.accountsQueries.selectAll().executeAsOneOrNull()
|
||||
if (account != null) return account.id
|
||||
frostDb.accountsQueries.insertNew()
|
||||
return frostDb.accountsQueries.selectAll().executeAsOne().id
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Core injections.
|
||||
*
|
||||
* All injections here are providers to avoid cyclic dependencies.
|
||||
*/
|
||||
@Singleton
|
||||
class Core
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val storeProvider: Provider<FrostWebStore>,
|
||||
) {
|
||||
val store: FrostWebStore
|
||||
get() = storeProvider.get()
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import com.pitchedapps.frost.web.usecases.UseCases
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Main components containing other core components.
|
||||
*
|
||||
* Modelled off of Focus:
|
||||
* https://github.com/mozilla-mobile/focus-android/blob/main/app/src/main/java/org/mozilla/focus/Components.kt
|
||||
* but with hilt
|
||||
*/
|
||||
@Singleton
|
||||
class FrostComponents
|
||||
@Inject
|
||||
internal constructor(
|
||||
val core: Core,
|
||||
val useCases: UseCases,
|
||||
val dataStore: FrostDataStore,
|
||||
)
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.pitchedapps.frost.proto.Account
|
||||
import com.pitchedapps.frost.proto.settings.Appearance
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** DataStore injections. */
|
||||
@Singleton
|
||||
class FrostDataStore
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val accountProvider: Provider<DataStore<Account>>,
|
||||
private val appearanceProvider: Provider<DataStore<Appearance>>,
|
||||
) {
|
||||
val account: DataStore<Account>
|
||||
get() = accountProvider.get()
|
||||
|
||||
val appearance: DataStore<Appearance>
|
||||
get() = appearanceProvider.get()
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.compose
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
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. */
|
||||
@Composable
|
||||
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, transparent) {
|
||||
when {
|
||||
dynamicColor && isDarkTheme -> {
|
||||
dynamicDarkColorScheme(context)
|
||||
}
|
||||
dynamicColor && !isDarkTheme -> {
|
||||
dynamicLightColorScheme(context)
|
||||
}
|
||||
isDarkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(colorScheme = colorScheme) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
color = if (transparent) Color.Transparent else MaterialTheme.colorScheme.surface,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2021 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.compose.webview
|
||||
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
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.FrostWebView
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseAction
|
||||
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
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.ext.flow
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
|
||||
@FrostWebScoped
|
||||
class FrostWebCompose
|
||||
@Inject
|
||||
internal constructor(
|
||||
@FrostWeb val tabId: WebTargetId,
|
||||
private val store: FrostWebStore,
|
||||
private val client: FrostWebViewClient,
|
||||
private val chromeClient: FrostChromeClient,
|
||||
) {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
/**
|
||||
* Webview implementation in compose
|
||||
*
|
||||
* Based off of
|
||||
* https://github.com/google/accompanist/blob/main/web/src/main/java/com/google/accompanist/web/WebView.kt
|
||||
*
|
||||
* @param modifier A compose modifier
|
||||
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
||||
* the WebView back. navigation from outside the composable.
|
||||
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
||||
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they
|
||||
* will be subsequently overwritten after this lambda is called.
|
||||
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved if
|
||||
* you need to save and restore state in this WebView.
|
||||
*/
|
||||
@Composable
|
||||
fun WebView(
|
||||
modifier: Modifier = Modifier,
|
||||
captureBackPresses: Boolean = true,
|
||||
onCreated: (WebView) -> Unit = {},
|
||||
onDispose: (WebView) -> Unit = {},
|
||||
) {
|
||||
|
||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||
|
||||
logger.atInfo().log("Webview %s %s", tabId, webView?.hashCode())
|
||||
|
||||
webView?.let { wv ->
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val canGoBack by
|
||||
store.observeAsState(initialValue = false) { it[tabId]?.content?.canGoBack == true }
|
||||
|
||||
BackHandler(captureBackPresses && canGoBack) { wv.goBack() }
|
||||
|
||||
LaunchedEffect(wv, store) {
|
||||
fun storeFlow(action: suspend Flow<ContentState>.() -> Unit) = launch {
|
||||
store.flow(lifecycleOwner).mapNotNull { it[tabId]?.content }.action()
|
||||
}
|
||||
|
||||
storeFlow {
|
||||
mapNotNull { it.transientState.targetUrl }
|
||||
.distinctUntilChanged()
|
||||
.collect { url ->
|
||||
store.dispatch(LoadUrlResponseAction(url))
|
||||
wv.loadUrl(url)
|
||||
}
|
||||
}
|
||||
storeFlow {
|
||||
mapNotNull { it.transientState.navStep }
|
||||
.distinctUntilChanged()
|
||||
.filter { it != 0 }
|
||||
.collect { steps ->
|
||||
store.dispatch(WebStepResponseAction(steps))
|
||||
if (wv.canGoBackOrForward(steps)) {
|
||||
wv.goBackOrForward(steps)
|
||||
} else {
|
||||
logger.atWarning().log("web %s cannot go back %d steps", tabId, steps)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val childView =
|
||||
FrostWebView(context)
|
||||
.apply {
|
||||
onCreated(this)
|
||||
|
||||
logger.atInfo().log("Created webview for %s", tabId)
|
||||
|
||||
this.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
|
||||
// state.viewState?.let {
|
||||
// this.restoreState(it)
|
||||
// }
|
||||
|
||||
webChromeClient = chromeClient
|
||||
webViewClient = client
|
||||
|
||||
val url = store.state[tabId]?.content?.url
|
||||
if (url != null) loadUrl(url)
|
||||
}
|
||||
.also { webView = it }
|
||||
|
||||
val parentLayout = NestedScrollView(context)
|
||||
parentLayout.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
parentLayout.addView(childView)
|
||||
|
||||
parentLayout
|
||||
},
|
||||
modifier = modifier,
|
||||
onRelease = { parentFrame ->
|
||||
val wv = parentFrame.getChildAt(0) as WebView
|
||||
onDispose(wv)
|
||||
logger.atInfo().log("Released webview for %s", tabId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.ext
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.pitchedapps.frost.FrostApp
|
||||
import com.pitchedapps.frost.components.FrostComponents
|
||||
|
||||
/** Launches new activities. */
|
||||
inline fun <reified T : Activity> Context.launchActivity(
|
||||
clearStack: Boolean = false,
|
||||
bundleBuilder: Bundle.() -> Unit = {},
|
||||
intentBuilder: Intent.() -> Unit = {}
|
||||
) {
|
||||
val intent = Intent(this, T::class.java)
|
||||
if (clearStack) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
intent.intentBuilder()
|
||||
val bundle = Bundle()
|
||||
bundle.bundleBuilder()
|
||||
startActivity(intent, bundle.takeIf { !it.isEmpty })
|
||||
if (clearStack && this is Activity) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/** Component access through application context. */
|
||||
val Context.components: FrostComponents
|
||||
get() = (applicationContext as FrostApp).componentsProvider.get()
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.ext
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
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.
|
||||
*/
|
||||
data class FrostAccountId(val id: Long)
|
||||
|
||||
data class WebTargetId(val id: String)
|
||||
|
||||
/**
|
||||
* Representation of gecko context id.
|
||||
*
|
||||
* Id is used to split cookies between accounts. [GeckoContextId] must be fixed per
|
||||
* [FrostAccountId].
|
||||
*/
|
||||
@JvmInline value class GeckoContextId(val id: String)
|
||||
|
||||
/**
|
||||
* Helper to get [FrostAccountId] from account data store.
|
||||
*
|
||||
* If account id is not initialized, returns null.
|
||||
*/
|
||||
val DataStore<Account>.idData: Flow<FrostAccountId?>
|
||||
get() = data.map { if (it.hasAccountId()) FrostAccountId(it.accountId) else null }
|
||||
|
||||
/**
|
||||
* Convert accountId to contextId.
|
||||
*
|
||||
* Note that contextId cannot be modified, as it is linked to all cookie info. Account ids start at
|
||||
* 1, and it is important not to allow a default [GeckoContextId] with id 0. Doing so would be a
|
||||
* bug, and may cause users to mix logins from multiple Frost accounts.
|
||||
*/
|
||||
fun FrostAccountId.toContextId(): GeckoContextId {
|
||||
require(id > 0L) { "Invalid accountId $id" }
|
||||
return GeckoContextId(id = "frost-context-$id")
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.facebook
|
||||
|
||||
/** Created by Allan Wang on 2017-06-01. */
|
||||
const val FACEBOOK_COM = "facebook.com"
|
||||
const val MESSENGER_COM = "messenger.com"
|
||||
const val FBCDN_NET = "fbcdn.net"
|
||||
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 = "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/"
|
||||
|
||||
fun profilePictureUrl(id: Long) = "https://graph.facebook.com/$id/picture?type=large"
|
||||
|
||||
const val FB_LOGIN_URL = "${FB_URL_BASE}login"
|
||||
const val FB_HOME_URL = "${FB_URL_BASE}home.php"
|
||||
const val MESSENGER_THREAD_PREFIX = "$HTTPS_MESSENGER_COM/t/"
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.facebook
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons.Default as MaterialIcons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.Cake
|
||||
import androidx.compose.material.icons.filled.ChatBubble
|
||||
import androidx.compose.material.icons.filled.EventNote
|
||||
import androidx.compose.material.icons.filled.Flag
|
||||
import androidx.compose.material.icons.filled.Group
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Newspaper
|
||||
import androidx.compose.material.icons.filled.Note
|
||||
import androidx.compose.material.icons.filled.PersonAddAlt1
|
||||
import androidx.compose.material.icons.filled.Photo
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Store
|
||||
import androidx.compose.material.icons.filled.Today
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.main.MainTabItem
|
||||
import compose.icons.FontAwesomeIcons
|
||||
import compose.icons.fontawesomeicons.Solid
|
||||
import compose.icons.fontawesomeicons.solid.GlobeAmericas
|
||||
|
||||
/**
|
||||
* Fb page info.
|
||||
*
|
||||
* All pages here are independent, in that they can be loaded directly. [key] must be final, as it
|
||||
* is used to store tab info.
|
||||
*/
|
||||
enum class FbItem(
|
||||
val key: String,
|
||||
@StringRes val titleId: Int,
|
||||
val icon: ImageVector,
|
||||
val relativeUrl: String,
|
||||
) {
|
||||
ActivityLog(
|
||||
key = "activity_log",
|
||||
titleId = R.string.activity_log,
|
||||
icon = MaterialIcons.List,
|
||||
relativeUrl = "me/allactivity",
|
||||
),
|
||||
Birthdays(
|
||||
key = "birthdays",
|
||||
titleId = R.string.birthdays,
|
||||
icon = MaterialIcons.Cake,
|
||||
relativeUrl = "events/birthdays",
|
||||
),
|
||||
Events("events", R.string.events, MaterialIcons.EventNote, "events/upcoming"),
|
||||
Feed("feed", R.string.feed, MaterialIcons.Newspaper, ""),
|
||||
FeedMostRecent(
|
||||
"feed_most_recent",
|
||||
R.string.most_recent,
|
||||
MaterialIcons.History,
|
||||
"home.php?sk=h_chr",
|
||||
),
|
||||
FeedTopStories("feed_top_stories", R.string.top_stories, MaterialIcons.Star, "home.php?sk=h_nor"),
|
||||
Friends("friends", R.string.friends, MaterialIcons.PersonAddAlt1, "friends/center/requests"),
|
||||
Groups("groups", R.string.groups, MaterialIcons.Group, "groups"),
|
||||
Marketplace("marketplace", R.string.marketplace, MaterialIcons.Store, "marketplace"),
|
||||
Menu("menu", R.string.menu, MaterialIcons.Menu, "bookmarks"),
|
||||
Messages("messages", R.string.messages, MaterialIcons.ChatBubble, "messages"),
|
||||
Notes("notes", R.string.notes, MaterialIcons.Note, "notes"),
|
||||
Notifications(
|
||||
"notifications",
|
||||
R.string.notifications,
|
||||
FontAwesomeIcons.Solid.GlobeAmericas,
|
||||
"notifications",
|
||||
),
|
||||
OnThisDay("on_this_day", R.string.on_this_day, MaterialIcons.Today, "onthisday"),
|
||||
Pages("pages", R.string.pages, MaterialIcons.Flag, "pages"),
|
||||
Photos("photos", R.string.photos, MaterialIcons.Photo, "me/photos"),
|
||||
Profile("profile", R.string.profile, MaterialIcons.AccountCircle, "me"),
|
||||
Saved("saved", R.string.saved, MaterialIcons.Bookmark, "saved"),
|
||||
Settings("settings", R.string.settings, MaterialIcons.Settings, "settings"),
|
||||
;
|
||||
|
||||
val url = "$FB_URL_BASE$relativeUrl"
|
||||
|
||||
val isFeed: Boolean
|
||||
get() =
|
||||
when (this) {
|
||||
Feed,
|
||||
FeedMostRecent,
|
||||
FeedTopStories -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val values = values().associateBy { it.key }
|
||||
|
||||
fun fromKey(key: String) = values[key]
|
||||
|
||||
fun defaults() = listOf(Feed, Messages, Notifications, Menu)
|
||||
}
|
||||
}
|
||||
|
||||
/** Converts [FbItem] to [MainTabItem]. */
|
||||
fun FbItem.tab(context: Context, id: WebTargetId): MainTabItem =
|
||||
MainTabItem(
|
||||
id = id,
|
||||
title = context.getString(titleId),
|
||||
icon = icon,
|
||||
url = url,
|
||||
)
|
||||
|
||||
|
||||
/// ** Note that this url only works if a query (?q=) is provided */
|
||||
// _SEARCH("search", R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
|
||||
//
|
||||
/// ** Non mbasic search cannot be parsed. */
|
||||
// _SEARCH_PARSE(
|
||||
// R.string.kau_search,
|
||||
// GoogleMaterial.Icon.gmd_search,
|
||||
// "search/top",
|
||||
// prefix = FB_URL_MBASIC_BASE,
|
||||
// ),
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.facebook
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 21/12/17.
|
||||
*
|
||||
* Collection of regex matchers Input text must be properly unescaped
|
||||
*
|
||||
* See [StringEscapeUtils]
|
||||
*/
|
||||
|
||||
/** Matches the fb_dtsg component of a page containing it as a hidden value */
|
||||
val FB_DTSG_MATCHER: Regex by lazy { Regex("name=\"fb_dtsg\" value=\"(.*?)\"") }
|
||||
val FB_REV_MATCHER: Regex by lazy { Regex("\"app_version\":\"(.*?)\"") }
|
||||
|
||||
/** Matches user id from cookie */
|
||||
val FB_USER_MATCHER: Regex = Regex("c_user=([0-9]*);")
|
||||
|
||||
val FB_EPOCH_MATCHER: Regex = Regex(":([0-9]+)")
|
||||
val FB_NOTIF_ID_MATCHER: Regex = Regex("notif_([0-9]+)")
|
||||
val FB_MESSAGE_NOTIF_ID_MATCHER: Regex = Regex("(?:thread|user)_fbid_([0-9]+)")
|
||||
val FB_CSS_URL_MATCHER: Regex = Regex("url\\([\"|']?(.*?)[\"|']?\\)")
|
||||
val FB_JSON_URL_MATCHER: Regex = Regex("\"(http.*?)\"")
|
||||
val FB_IMAGE_ID_MATCHER: Regex = Regex("fbcdn.*?/[0-9]+_([0-9]+)_")
|
||||
val FB_REDIRECT_URL_MATCHER: Regex = Regex("url=(.*?fbcdn.*?)\"")
|
||||
|
||||
operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex)
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.facebook
|
||||
|
||||
import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/** [true] if url contains [FACEBOOK_COM] */
|
||||
inline val String?.isFacebookUrl
|
||||
get() = this != null && (contains(FACEBOOK_COM) || contains(FBCDN_NET))
|
||||
|
||||
inline val String?.isMessengerUrl
|
||||
get() = this != null && contains(MESSENGER_COM)
|
||||
|
||||
inline val String?.isFbCookie
|
||||
get() = this != null && contains("c_user")
|
||||
|
||||
/** [true] if url is a video and can be accepted by VideoViewer */
|
||||
inline val String.isVideoUrl
|
||||
get() = startsWith(VIDEO_REDIRECT) || (startsWith("https://video-") && contains(FBCDN_NET))
|
||||
|
||||
/** [true] if url directly leads to a usable image */
|
||||
inline val String.isImageUrl: Boolean
|
||||
get() {
|
||||
return contains(FBCDN_NET) && (contains(".png") || contains(".jpg"))
|
||||
}
|
||||
|
||||
/** [true] if url can be retrieved to get a direct image url */
|
||||
inline val String.isIndirectImageUrl: Boolean
|
||||
get() {
|
||||
return contains("/photo/view_full_size/") && contains("fbid=")
|
||||
}
|
||||
|
||||
/** [true] if url can be displayed in a different webview */
|
||||
inline val String?.isIndependent: Boolean
|
||||
get() {
|
||||
if (this == null || length < 5) return false // ignore short queries
|
||||
if (this[0] == '#' && !contains('/')) return false // ignore element values
|
||||
if (startsWith("http") && !isFacebookUrl) return true // ignore non facebook urls
|
||||
if (dependentSegments.any { contains(it) }) return false // ignore known dependent segments
|
||||
return true
|
||||
}
|
||||
|
||||
val dependentSegments =
|
||||
arrayOf(
|
||||
"photoset_token",
|
||||
"direct_action_execute",
|
||||
"messages/?pageNum",
|
||||
"sharer.php",
|
||||
"events/permalink",
|
||||
"events/feed/watch",
|
||||
/*
|
||||
* Add new members to groups
|
||||
*
|
||||
* No longer dependent again as of 12/20/2018
|
||||
*/
|
||||
// "madminpanel",
|
||||
/** Editing images */
|
||||
"/confirmation/?",
|
||||
/** Remove entry from "people you may know" */
|
||||
"/pymk/xout/",
|
||||
/*
|
||||
* Facebook messages have the following cases for the tid query
|
||||
* mid* or id* for newer threads, which can be launched in new windows
|
||||
* or a hash for old threads, which must be loaded on old threads
|
||||
*/
|
||||
"messages/read/?tid=id",
|
||||
"messages/read/?tid=mid",
|
||||
// For some reason townhall doesn't load independently
|
||||
// This will allow it to load, but going back unfortunately messes up the menu client
|
||||
// See https://github.com/AllanWang/Frost-for-Facebook/issues/1593
|
||||
"/townhall/"
|
||||
)
|
||||
|
||||
inline val String?.isExplicitIntent
|
||||
get() = this != null && (startsWith("intent://") || startsWith("market://"))
|
||||
|
||||
fun String.urlEncode(): String = URLEncoder.encode(this, StandardCharsets.UTF_8.name())
|
@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.facebook
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-07-07.
|
||||
*
|
||||
* Custom url builder so we can easily test it without the Android framework
|
||||
*/
|
||||
inline val String.formattedFbUrl: String
|
||||
get() = FbUrlFormatter(this).toString()
|
||||
|
||||
inline val Uri.formattedFbUri: Uri
|
||||
get() {
|
||||
val url = toString()
|
||||
return if (url.startsWith("http")) Uri.parse(url.formattedFbUrl) else this
|
||||
}
|
||||
|
||||
class FbUrlFormatter(url: String) {
|
||||
private val queries = mutableMapOf<String, String>()
|
||||
private val cleaned: String
|
||||
|
||||
/**
|
||||
* Formats all facebook urls
|
||||
*
|
||||
* The order is very important:
|
||||
* 1. Wrapper links (discardables) are stripped away, resulting in the actual link
|
||||
* 2. CSS encoding is converted to normal encoding
|
||||
* 3. Url is completely decoded
|
||||
* 4. Url is split into sections
|
||||
*/
|
||||
init {
|
||||
cleaned = clean(url)
|
||||
}
|
||||
|
||||
fun clean(url: String): String {
|
||||
if (url.isBlank()) return ""
|
||||
var cleanedUrl = url
|
||||
if (cleanedUrl.startsWith("#!")) cleanedUrl = cleanedUrl.substring(2)
|
||||
val urlRef = cleanedUrl
|
||||
discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) }
|
||||
val changed = cleanedUrl != urlRef
|
||||
converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
|
||||
try {
|
||||
cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name())
|
||||
} catch (e: Exception) {
|
||||
logger.atWarning().withCause(e).log("Failed url formatting")
|
||||
return url
|
||||
}
|
||||
cleanedUrl = cleanedUrl.replace("&", "&")
|
||||
if (changed && !cleanedUrl.contains("?")) // ensure we aren't missing '?'
|
||||
cleanedUrl = cleanedUrl.replaceFirst("&", "?")
|
||||
val qm = cleanedUrl.indexOf("?")
|
||||
if (qm > -1) {
|
||||
cleanedUrl.substring(qm + 1).split("&").forEach {
|
||||
val p = it.split("=")
|
||||
queries[p[0]] = p.elementAtOrNull(1) ?: ""
|
||||
}
|
||||
cleanedUrl = cleanedUrl.substring(0, qm)
|
||||
}
|
||||
discardableQueries.forEach { queries.remove(it) }
|
||||
// Convert desktop urls to mobile ones
|
||||
cleanedUrl = cleanedUrl.replace(WWW_FACEBOOK_COM, FACEBOOK_BASE_COM)
|
||||
if (cleanedUrl.startsWith("/")) cleanedUrl = FB_URL_BASE + cleanedUrl.substring(1)
|
||||
cleanedUrl =
|
||||
cleanedUrl.replaceFirst(
|
||||
".facebook.com//",
|
||||
".facebook.com/"
|
||||
) // sometimes we are given a bad url
|
||||
logger.atFinest().log("Formatted url from %s to %s", url, cleanedUrl)
|
||||
return cleanedUrl
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
buildString {
|
||||
append(cleaned)
|
||||
if (queries.isNotEmpty()) {
|
||||
append("?")
|
||||
queries.forEach { (k, v) ->
|
||||
if (v.isEmpty()) {
|
||||
append("${k.urlEncode()}&")
|
||||
} else {
|
||||
append("${k.urlEncode()}=${v.urlEncode()}&")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.removeSuffix("&")
|
||||
|
||||
fun toLogList(): List<String> {
|
||||
val list = mutableListOf(cleaned)
|
||||
queries.forEach { (k, v) -> list.add("\n- $k\t=\t$v") }
|
||||
list.add("\n\n${toString()}")
|
||||
return list
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
const val VIDEO_REDIRECT = "/video_redirect/?src="
|
||||
|
||||
/**
|
||||
* Items here are explicitly removed from the url Taken from FaceSlim
|
||||
* https://github.com/indywidualny/FaceSlim/blob/master/app/src/main/java/org/indywidualni/fblite/util/Miscellany.java
|
||||
*
|
||||
* Note: Typically, in this case, the redirect url should have all the necessary queries I am
|
||||
* unsure how Facebook reacts in all cases, so the ones after the redirect are appended on
|
||||
* afterwards That shouldn't break anything
|
||||
*/
|
||||
val discardable =
|
||||
arrayOf(
|
||||
"http://lm.facebook.com/l.php?u=",
|
||||
"https://lm.facebook.com/l.php?u=",
|
||||
"http://m.facebook.com/l.php?u=",
|
||||
"https://m.facebook.com/l.php?u=",
|
||||
"http://touch.facebook.com/l.php?u=",
|
||||
"https://touch.facebook.com/l.php?u=",
|
||||
VIDEO_REDIRECT
|
||||
)
|
||||
|
||||
/**
|
||||
* Queries that are not necessary for independent links
|
||||
*
|
||||
* acontext is not required for "friends interested in" notifications
|
||||
*/
|
||||
val discardableQueries =
|
||||
arrayOf(
|
||||
"ref",
|
||||
"refid",
|
||||
"SharedWith",
|
||||
"fbclid",
|
||||
"h",
|
||||
"_ft_",
|
||||
"_tn_",
|
||||
"_xt_",
|
||||
"bacr",
|
||||
"frefs",
|
||||
"hc_ref",
|
||||
"loc_ref",
|
||||
"pn_ref"
|
||||
)
|
||||
|
||||
val converter =
|
||||
listOf(
|
||||
"\\3C " to "%3C",
|
||||
"\\3E " to "%3E",
|
||||
"\\23 " to "%23",
|
||||
"\\25 " to "%25",
|
||||
"\\7B " to "%7B",
|
||||
"\\7D " to "%7D",
|
||||
"\\7C " to "%7C",
|
||||
"\\5C " to "%5C",
|
||||
"\\5E " to "%5E",
|
||||
"\\7E " to "%7E",
|
||||
"\\5B " to "%5B",
|
||||
"\\5D " to "%5D",
|
||||
"\\60 " to "%60",
|
||||
"\\3B " to "%3B",
|
||||
"\\2F " to "%2F",
|
||||
"\\3F " to "%3F",
|
||||
"\\3A " to "%3A",
|
||||
"\\40 " to "%40",
|
||||
"\\3D " to "%3D",
|
||||
"\\26 " to "%26",
|
||||
"\\24 " to "%24",
|
||||
"\\2B " to "%2B",
|
||||
"\\22 " to "%22",
|
||||
"\\2C " to "%2C",
|
||||
"\\20 " to "%20"
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.hilt
|
||||
|
||||
import android.content.Context
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||
import com.pitchedapps.frost.db.FrostDb
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Module containing SQL injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostDbModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun frostDb(@ApplicationContext context: Context): FrostDb {
|
||||
val driver: SqlDriver = AndroidSqliteDriver(FrostDb.Schema, context, "frost.db")
|
||||
return FrostDb(driver)
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.hilt
|
||||
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.web.FrostAdBlock
|
||||
import dagger.BindsOptionalOf
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier annotation class Frost
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface FrostBindModule {
|
||||
@BindsOptionalOf @Frost fun userAgent(): String
|
||||
|
||||
@BindsOptionalOf fun adBlock(): FrostAdBlock
|
||||
}
|
||||
|
||||
/** Module containing core Frost injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostModule {
|
||||
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
/**
|
||||
* Windows based user agent.
|
||||
*
|
||||
* Note that Facebook's mobile webpage for mobile user agents is completely different from the
|
||||
* desktop ones. All elements become divs, so nothing can be queried. There is a new UI too, but
|
||||
* it doesn't seem worth migrating all other logic over.
|
||||
*/
|
||||
private const val USER_AGENT_WINDOWS =
|
||||
"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/78.0.3904.70 Safari/537.36"
|
||||
|
||||
@Provides @Frost fun userAgent(): String = USER_AGENT_WINDOWS_FROST
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.hilt
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.BuildConfig
|
||||
import com.pitchedapps.frost.web.state.FrostLoggerMiddleware
|
||||
import com.pitchedapps.frost.web.state.FrostWebReducer
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Module containing core WebView injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object FrostWebViewModule {
|
||||
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
@Provides @Singleton fun cookieManager(): CookieManager = CookieManager.getInstance()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun frostWebStore(frostWebReducer: FrostWebReducer): FrostWebStore {
|
||||
val middleware = buildList { if (BuildConfig.DEBUG) add(FrostLoggerMiddleware()) }
|
||||
|
||||
return FrostWebStore(frostWebReducer = frostWebReducer, middleware = middleware)
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.hilt
|
||||
|
||||
import com.squareup.moshi.*
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import okio.Buffer
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Module containing Moshi injections. */
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object MoshiModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun moshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
// .add(ExtensionType.moshiFactory())
|
||||
.add(JSONObjectAdapter())
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
||||
private class JSONObjectAdapter {
|
||||
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): JSONObject? {
|
||||
// Handle map data only; ignore rest
|
||||
return (reader.readJsonValue() as? Map<*, *>)?.let { data ->
|
||||
try {
|
||||
JSONObject(data)
|
||||
} catch (e: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(writer: JsonWriter, value: JSONObject?) {
|
||||
value?.let { writer.value(Buffer().writeUtf8(value.toString())) }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.main
|
||||
|
||||
import android.os.Bundle
|
||||
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
|
||||
import javax.inject.Inject
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
|
||||
/**
|
||||
* Main activity.
|
||||
*
|
||||
* Contains tab layouts for browsing.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
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")
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
FrostTheme {
|
||||
// MainScreen(
|
||||
// tabs = tabs,
|
||||
// )
|
||||
|
||||
val tabs =
|
||||
store.observeAsState(initialValue = null) { it.homeTabs.map { it.tab } }.value
|
||||
?: return@FrostTheme
|
||||
|
||||
MainScreenWebView(homeTabs = tabs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.main
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
|
||||
/** Data representation of a single main tab entry. */
|
||||
data class MainTabItem(
|
||||
val id: WebTargetId,
|
||||
val title: String,
|
||||
val icon: ImageVector,
|
||||
val url: String
|
||||
)
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.main
|
||||
|
||||
import android.content.Context
|
||||
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.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.webview.FrostWebComposer
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@HiltViewModel
|
||||
class MainScreenViewModel
|
||||
@Inject
|
||||
internal constructor(
|
||||
@ApplicationContext context: Context,
|
||||
val components: FrostComponents,
|
||||
val store: FrostWebStore,
|
||||
val frostWebComposer: FrostWebComposer,
|
||||
) : ViewModel() {
|
||||
|
||||
val contextIdFlow: Flow<GeckoContextId?> =
|
||||
components.dataStore.account.idData.map { it?.toContextId() }
|
||||
|
||||
var tabIndex: Int by mutableStateOf(0)
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.main
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SelectHomeTab
|
||||
import com.pitchedapps.frost.webview.FrostWebComposer
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.ext.observeAsState
|
||||
|
||||
@Composable
|
||||
fun MainScreenWebView(modifier: Modifier = Modifier, homeTabs: List<MainTabItem>) {
|
||||
val vm: MainScreenViewModel = viewModel()
|
||||
|
||||
val selectedHomeTab by vm.store.observeAsState(initialValue = null) { it.selectedHomeTab }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = Color.Transparent,
|
||||
topBar = { MainTopBar(modifier = modifier) },
|
||||
bottomBar = {
|
||||
MainBottomBar(
|
||||
selectedTab = selectedHomeTab,
|
||||
items = homeTabs,
|
||||
onSelect = { vm.store.dispatch(SelectHomeTab(it)) },
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
MainScreenWebContainer(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
selectedTab = selectedHomeTab,
|
||||
items = homeTabs,
|
||||
store = vm.store,
|
||||
frostWebComposer = vm.frostWebComposer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainTopBar(modifier: Modifier = Modifier) {
|
||||
TopAppBar(modifier = modifier, title = { Text(text = "Title") })
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedTab: WebTargetId?,
|
||||
items: List<MainTabItem>,
|
||||
onSelect: (WebTargetId) -> Unit
|
||||
) {
|
||||
NavigationBar(modifier = modifier) {
|
||||
items.forEach { item ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(item.icon, contentDescription = item.title) },
|
||||
selected = selectedTab == item.id,
|
||||
onClick = { onSelect(item.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainScreenWebContainer(
|
||||
modifier: Modifier,
|
||||
selectedTab: WebTargetId?,
|
||||
items: List<MainTabItem>,
|
||||
store: FrostWebStore,
|
||||
frostWebComposer: FrostWebComposer
|
||||
) {
|
||||
val homeTabComposables = remember(items) { items.map { frostWebComposer.create(it.id) } }
|
||||
|
||||
PullRefresh(
|
||||
modifier = modifier,
|
||||
store = store,
|
||||
) {
|
||||
MainPager(selectedTab, items = homeTabComposables)
|
||||
// homeTabComposables.find { it.tabId == selectedTab }?.WebView()
|
||||
|
||||
// MultiViewContainer(store = store)
|
||||
|
||||
// SampleContainer(selectedTab = selectedTab, items = items)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MainPager(selectedTab: WebTargetId?, items: List<FrostWebCompose>) {
|
||||
|
||||
val pagerState = rememberPagerState { items.size }
|
||||
|
||||
LaunchedEffect(selectedTab, items) {
|
||||
val i = items.indexOfFirst { it.tabId == selectedTab }
|
||||
if (i != -1) {
|
||||
pagerState.scrollToPage(i)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
userScrollEnabled = false,
|
||||
beyondBoundsPageCount = 10, // Do not allow view release
|
||||
) { page ->
|
||||
items[page].WebView()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun PullRefresh(modifier: Modifier, store: FrostWebStore, content: @Composable () -> Unit) {
|
||||
val refreshScope = rememberCoroutineScope()
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
|
||||
fun refresh() =
|
||||
refreshScope.launch {
|
||||
refreshing = true
|
||||
delay(1500)
|
||||
refreshing = false
|
||||
}
|
||||
|
||||
val state = rememberPullRefreshState(refreshing, ::refresh)
|
||||
|
||||
Box(modifier.pullRefresh(state)) {
|
||||
content()
|
||||
|
||||
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.persistent
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.Serializer
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import com.pitchedapps.frost.proto.Account
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Singleton
|
||||
|
||||
private object AccountProtoSerializer : Serializer<Account> {
|
||||
override val defaultValue: Account = Account.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): Account {
|
||||
try {
|
||||
return Account.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: Account, output: OutputStream) = t.writeTo(output)
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AccountProtoModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDataStore(@ApplicationContext context: Context): DataStore<Account> =
|
||||
DataStoreFactory.create(
|
||||
AccountProtoSerializer,
|
||||
) {
|
||||
context.dataStoreFile("account.pb")
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.persistent.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.Serializer
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import com.pitchedapps.frost.proto.settings.Appearance
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Singleton
|
||||
|
||||
private object AppearanceProtoSerializer : Serializer<Appearance> {
|
||||
override val defaultValue: Appearance = Appearance.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): Appearance {
|
||||
try {
|
||||
return Appearance.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: Appearance, output: OutputStream) = t.writeTo(output)
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppearanceProtoModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDataStore(@ApplicationContext context: Context): DataStore<Appearance> =
|
||||
DataStoreFactory.create(
|
||||
AppearanceProtoSerializer,
|
||||
) {
|
||||
context.dataStoreFile("settings/appearance.pb")
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.webkit.WebView
|
||||
import androidx.core.view.NestedScrollingChild3
|
||||
import androidx.core.view.NestedScrollingChildHelper
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 20/12/17.
|
||||
*
|
||||
* Webview extension that handles nested scrolls
|
||||
*/
|
||||
open class NestedWebView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
||||
WebView(context, attrs, defStyleAttr), NestedScrollingChild3 {
|
||||
|
||||
// No JvmOverloads due to hilt
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
|
||||
private lateinit var childHelper: NestedScrollingChildHelper
|
||||
private var lastY: Int = 0
|
||||
private val scrollOffset = IntArray(2)
|
||||
private val scrollConsumed = IntArray(2)
|
||||
private var nestedOffsetY: Int = 0
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
||||
fun init() {
|
||||
// To avoid leaking constructor
|
||||
childHelper = NestedScrollingChildHelper(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle nested scrolling against SwipeRecyclerView Courtesy of takahirom
|
||||
*
|
||||
* https://github.com/takahirom/webview-in-coordinatorlayout/blob/master/app/src/main/java/com/github/takahirom/webview_in_coodinator_layout/NestedWebView.java
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
val event = MotionEvent.obtain(ev)
|
||||
val action = event.action
|
||||
if (action == MotionEvent.ACTION_DOWN) nestedOffsetY = 0
|
||||
val eventY = event.y.toInt()
|
||||
event.offsetLocation(0f, nestedOffsetY.toFloat())
|
||||
val returnValue: Boolean
|
||||
when (action) {
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
var deltaY = lastY - eventY
|
||||
// NestedPreScroll
|
||||
if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
|
||||
deltaY -= scrollConsumed[1]
|
||||
}
|
||||
lastY = eventY - scrollOffset[1]
|
||||
returnValue = super.onTouchEvent(event)
|
||||
// NestedScroll
|
||||
if (dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
|
||||
event.offsetLocation(0f, scrollOffset[1].toFloat())
|
||||
nestedOffsetY += scrollOffset[1]
|
||||
lastY -= scrollOffset[1]
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
returnValue = super.onTouchEvent(event)
|
||||
lastY = eventY
|
||||
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
returnValue = super.onTouchEvent(event)
|
||||
stopNestedScroll()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return returnValue
|
||||
}
|
||||
|
||||
/*
|
||||
* ---------------------------------------------
|
||||
* Nested Scrolling Content
|
||||
* ---------------------------------------------
|
||||
*/
|
||||
|
||||
override fun setNestedScrollingEnabled(enabled: Boolean) {
|
||||
childHelper.isNestedScrollingEnabled = enabled
|
||||
}
|
||||
|
||||
override fun isNestedScrollingEnabled() = childHelper.isNestedScrollingEnabled
|
||||
|
||||
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) = childHelper.stopNestedScroll(type)
|
||||
|
||||
override fun stopNestedScroll() = childHelper.stopNestedScroll()
|
||||
|
||||
override fun hasNestedScrollingParent(type: Int): Boolean =
|
||||
childHelper.hasNestedScrollingParent(type)
|
||||
|
||||
override fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent()
|
||||
|
||||
override fun dispatchNestedScroll(
|
||||
dxConsumed: Int,
|
||||
dyConsumed: Int,
|
||||
dxUnconsumed: Int,
|
||||
dyUnconsumed: Int,
|
||||
offsetInWindow: IntArray?,
|
||||
type: Int,
|
||||
consumed: IntArray
|
||||
) =
|
||||
childHelper.dispatchNestedScroll(
|
||||
dxConsumed,
|
||||
dyConsumed,
|
||||
dxUnconsumed,
|
||||
dyUnconsumed,
|
||||
offsetInWindow,
|
||||
type,
|
||||
consumed,
|
||||
)
|
||||
|
||||
override fun dispatchNestedScroll(
|
||||
dxConsumed: Int,
|
||||
dyConsumed: Int,
|
||||
dxUnconsumed: Int,
|
||||
dyUnconsumed: Int,
|
||||
offsetInWindow: IntArray?,
|
||||
type: Int
|
||||
) =
|
||||
childHelper.dispatchNestedScroll(
|
||||
dxConsumed,
|
||||
dyConsumed,
|
||||
dxUnconsumed,
|
||||
dyUnconsumed,
|
||||
offsetInWindow,
|
||||
type,
|
||||
)
|
||||
|
||||
override fun dispatchNestedScroll(
|
||||
dxConsumed: Int,
|
||||
dyConsumed: Int,
|
||||
dxUnconsumed: Int,
|
||||
dyUnconsumed: Int,
|
||||
offsetInWindow: IntArray?
|
||||
) =
|
||||
childHelper.dispatchNestedScroll(
|
||||
dxConsumed,
|
||||
dyConsumed,
|
||||
dxUnconsumed,
|
||||
dyUnconsumed,
|
||||
offsetInWindow,
|
||||
)
|
||||
|
||||
override fun dispatchNestedPreScroll(
|
||||
dx: Int,
|
||||
dy: Int,
|
||||
consumed: IntArray?,
|
||||
offsetInWindow: IntArray?,
|
||||
type: Int
|
||||
): Boolean = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
|
||||
|
||||
override fun dispatchNestedPreScroll(
|
||||
dx: Int,
|
||||
dy: Int,
|
||||
consumed: IntArray?,
|
||||
offsetInWindow: IntArray?
|
||||
) = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
|
||||
|
||||
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean) =
|
||||
childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
|
||||
|
||||
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float) =
|
||||
childHelper.dispatchNestedPreFling(velocityX, velocityY)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 android.text.TextUtils
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
interface FrostAdBlock {
|
||||
|
||||
val data: Set<String>
|
||||
|
||||
/**
|
||||
* Initialize ad block data.
|
||||
*
|
||||
* Required to be called once
|
||||
*/
|
||||
fun init()
|
||||
}
|
||||
|
||||
fun FrostAdBlock.isAd(url: String?): Boolean {
|
||||
url ?: return false
|
||||
val httpUrl = url.toHttpUrlOrNull() ?: return false
|
||||
return isAdHost(httpUrl.host)
|
||||
}
|
||||
|
||||
tailrec fun FrostAdBlock.isAdHost(host: String): Boolean {
|
||||
if (TextUtils.isEmpty(host)) return false
|
||||
val index = host.indexOf(".")
|
||||
if (index < 0 || index + 1 < host.length) return false
|
||||
if (data.contains(host)) return true
|
||||
return isAdHost(host.substring(index + 1))
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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 android.content.Context
|
||||
import android.webkit.CookieManager
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.facebook.FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.HTTPS_FACEBOOK_COM
|
||||
import com.pitchedapps.frost.facebook.HTTPS_MESSENGER_COM
|
||||
import com.pitchedapps.frost.facebook.MESSENGER_COM
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FrostCookie @Inject internal constructor(private val cookieManager: CookieManager) {
|
||||
|
||||
val fbCookie: String?
|
||||
get() = cookieManager.getCookie(HTTPS_FACEBOOK_COM)
|
||||
|
||||
val messengerCookie: String?
|
||||
get() = cookieManager.getCookie(HTTPS_MESSENGER_COM)
|
||||
|
||||
private suspend fun CookieManager.suspendSetWebCookie(domain: String, cookie: String?): Boolean {
|
||||
cookie ?: return true
|
||||
return withContext(NonCancellable) {
|
||||
// Save all cookies regardless of result, then check if all succeeded
|
||||
val result =
|
||||
cookie.split(";").map { async { setSingleWebCookie(domain, it) } }.awaitAll().all { it }
|
||||
logger.atInfo().log("Cookies set for %s, %b", domain, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.setSingleWebCookie(domain: String, cookie: String): Boolean =
|
||||
suspendCoroutine { cont ->
|
||||
setCookie(domain, cookie.trim()) { cont.resume(it) }
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||
removeAllCookies { cont.resume(it) }
|
||||
}
|
||||
|
||||
suspend fun save(id: Long) {
|
||||
logger.atInfo().log("Saving cookies for %d", id)
|
||||
// prefs.userId = id
|
||||
cookieManager.flush()
|
||||
// val cookie = CookieEntity(prefs.userId, null, webCookie)
|
||||
// cookieDao.save(cookie)
|
||||
}
|
||||
|
||||
suspend fun reset() {
|
||||
// prefs.userId = -1L
|
||||
withContext(Dispatchers.Main + NonCancellable) {
|
||||
with(cookieManager) {
|
||||
removeAllCookies()
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun switchUser(id: Long) {
|
||||
// val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
|
||||
// switchUser(cookie)
|
||||
}
|
||||
|
||||
// suspend fun switchUser(cookie: CookieEntity?) {
|
||||
// if (cookie?.cookie == null) {
|
||||
// L.d { "Switching User; null cookie" }
|
||||
// return
|
||||
// }
|
||||
// withContext(Dispatchers.Main + NonCancellable) {
|
||||
// L.d { "Switching User" }
|
||||
// prefs.userId = cookie.id
|
||||
// CookieManager.getInstance().apply {
|
||||
// removeAllCookies()
|
||||
// suspendSetWebCookie(FB_COOKIE_DOMAIN, cookie.cookie)
|
||||
// suspendSetWebCookie(MESSENGER_COOKIE_DOMAIN, cookie.cookieMessenger)
|
||||
// flush()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/** Helper function to remove the current cookies and launch the proper login page */
|
||||
suspend fun logout(context: Context, deleteCookie: Boolean = true) {
|
||||
// val cookies = arrayListOf<CookieEntity>()
|
||||
// if (context is Activity) cookies.addAll(context.cookies().filter { it.id != prefs.userId
|
||||
// })
|
||||
// logout(prefs.userId, deleteCookie)
|
||||
// context.launchLogin(cookies, true)
|
||||
}
|
||||
|
||||
/** Clear the cookies of the given id */
|
||||
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
|
||||
logger.atInfo().log("Logging out user %d", id)
|
||||
// TODO save cookies?
|
||||
if (deleteCookie) {
|
||||
// cookieDao.deleteById(id)
|
||||
logger.atInfo().log("Deleted cookies")
|
||||
}
|
||||
reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifications may come from different accounts, and we need to switch the cookies to load them
|
||||
* When coming back to the main app, switch back to our original account before continuing
|
||||
*/
|
||||
suspend fun switchBackUser() {
|
||||
// if (prefs.prevId == -1L) return
|
||||
// val prevId = prefs.prevId
|
||||
// prefs.prevId = -1L
|
||||
// if (prevId != prefs.userId) {
|
||||
// switchUser(prevId)
|
||||
// L.d { "Switch back user" }
|
||||
// L._d { "${prefs.userId} to $prevId" }
|
||||
// }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
|
||||
/** Domain information. Dot prefix still matters for Android browsers. */
|
||||
private const val FB_COOKIE_DOMAIN = ".$FACEBOOK_COM"
|
||||
private const val MESSENGER_COOKIE_DOMAIN = ".$MESSENGER_COM"
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.facebook.isFacebookUrl
|
||||
import com.pitchedapps.frost.facebook.isMessengerUrl
|
||||
import java.util.Optional
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
class FrostWebHelper @Inject internal constructor(frostAdBlock: Optional<FrostAdBlock>) {
|
||||
private val frostAdBlock = frostAdBlock.getOrNull()
|
||||
|
||||
/** Returns true if url should be intercepted (replaced with blank resource) */
|
||||
fun shouldInterceptUrl(url: String): Boolean {
|
||||
val httpUrl = url.toHttpUrlOrNull() ?: return false
|
||||
val host = httpUrl.host
|
||||
if (host.contains("facebook") || host.contains("fbcdn")) return false
|
||||
if (frostAdBlock?.isAdHost(host) == true) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if url should allow refreshes.
|
||||
*
|
||||
* Some urls are known to be invalid entrypoints, and cannot be refreshed. Others may contain
|
||||
* editable data without save state, so disabling swipe to refresh is preferred.
|
||||
*/
|
||||
fun allowUrlSwipeToRefresh(url: String): Boolean {
|
||||
if (url.isMessengerUrl) return false
|
||||
if (!url.isFacebookUrl) return true
|
||||
if (url.contains("soft=composer")) return false
|
||||
if (url.contains("sharer.php") || url.contains("sharer-dialog.php")) return false
|
||||
return true
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.state
|
||||
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
|
||||
typealias FrostWebMiddleware = Middleware<FrostWebState, FrostWebAction>
|
||||
|
||||
class FrostLoggerMiddleware : FrostWebMiddleware {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<FrostWebState, FrostWebAction>,
|
||||
next: (FrostWebAction) -> Unit,
|
||||
action: FrostWebAction
|
||||
) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
class FrostCookieMiddleware : FrostWebMiddleware {
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<FrostWebState, FrostWebAction>,
|
||||
next: (FrostWebAction) -> Unit,
|
||||
action: FrostWebAction
|
||||
) {
|
||||
when (action) {
|
||||
else -> next(action)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import mozilla.components.lib.state.Action
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
|
||||
*
|
||||
* For firefox example
|
||||
*/
|
||||
sealed interface FrostWebAction : Action
|
||||
|
||||
/**
|
||||
* [FrostWebAction] dispatched to indicate that the store is initialized and ready to use. This
|
||||
* action is dispatched automatically before any other action is processed. Its main purpose is to
|
||||
* trigger initialization logic in middlewares. The action itself has no effect on the
|
||||
* [FrostWebState].
|
||||
*/
|
||||
object InitAction : FrostWebAction
|
||||
|
||||
/** Actions affecting multiple tabs */
|
||||
sealed interface TabListAction : FrostWebAction {
|
||||
data class SetHomeTabs(val data: List<FbItem>, val selectedTab: Int? = 0) : TabListAction
|
||||
|
||||
data class SelectHomeTab(val id: WebTargetId) : TabListAction
|
||||
}
|
||||
|
||||
/** Action affecting a single tab */
|
||||
data class TabAction(val tabId: WebTargetId, val action: Action) : FrostWebAction {
|
||||
sealed interface Action
|
||||
|
||||
sealed interface ContentAction : Action {
|
||||
|
||||
/** Action indicating current url state. */
|
||||
data class UpdateUrlAction(val url: String) : ContentAction
|
||||
|
||||
/** Action indicating current title state. */
|
||||
data class UpdateTitleAction(val title: String?) : ContentAction
|
||||
|
||||
data class UpdateNavigationAction(val canGoBack: Boolean, val canGoForward: Boolean) :
|
||||
ContentAction
|
||||
|
||||
data class UpdateProgressAction(val progress: Int) : ContentAction
|
||||
}
|
||||
|
||||
/** Action triggered by user, leading to transient state changes. */
|
||||
sealed interface UserAction : Action {
|
||||
|
||||
/** Action to load new url. */
|
||||
data class LoadUrlAction(val url: String) : UserAction
|
||||
|
||||
object GoBackAction : UserAction
|
||||
|
||||
object GoForwardAction : UserAction
|
||||
}
|
||||
|
||||
/** Response triggered by webview, indicating [UserAction] fulfillment. */
|
||||
sealed interface ResponseAction : Action {
|
||||
|
||||
data class LoadUrlResponseAction(val url: String) : ResponseAction
|
||||
|
||||
data class WebStepResponseAction(val steps: Int) : ResponseAction
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.reducer.ContentStateReducer
|
||||
import com.pitchedapps.frost.web.state.reducer.TabListReducer
|
||||
import com.pitchedapps.frost.web.state.state.FloatingTabSessionState
|
||||
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
|
||||
import com.pitchedapps.frost.web.state.state.SessionState
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt
|
||||
*
|
||||
* For firefox example
|
||||
*/
|
||||
class FrostWebReducer
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val tabListReducer: TabListReducer,
|
||||
private val contentStateReducer: ContentStateReducer
|
||||
) {
|
||||
fun reduce(state: FrostWebState, action: FrostWebAction): FrostWebState {
|
||||
return when (action) {
|
||||
is InitAction -> state
|
||||
is TabListAction -> tabListReducer.reduce(state, action)
|
||||
is TabAction ->
|
||||
state.updateTabState(action.tabId) { session ->
|
||||
val newContent = contentStateReducer.reduce(session.content, action.action)
|
||||
session.createCopy(content = newContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Unchecked_Cast")
|
||||
internal fun FrostWebState.updateTabState(
|
||||
tabId: WebTargetId,
|
||||
update: (SessionState) -> SessionState,
|
||||
): FrostWebState {
|
||||
val floatingTabMatch = floatingTab?.takeIf { it.id == tabId }
|
||||
if (floatingTabMatch != null)
|
||||
return copy(floatingTab = update(floatingTabMatch) as FloatingTabSessionState)
|
||||
|
||||
val newHomeTabs = homeTabs.updateTabs(tabId, update) as List<HomeTabSessionState>?
|
||||
if (newHomeTabs != null) return copy(homeTabs = newHomeTabs)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the corresponding tab in the list and replaces it using [update].
|
||||
*
|
||||
* @param tabId ID of the tab to change.
|
||||
* @param update Returns a new version of the tab state.
|
||||
*/
|
||||
internal fun <T : SessionState> List<T>.updateTabs(
|
||||
tabId: WebTargetId,
|
||||
update: (T) -> T,
|
||||
): List<SessionState>? {
|
||||
val tabIndex = indexOfFirst { it.id == tabId }
|
||||
if (tabIndex == -1) return null
|
||||
return subList(0, tabIndex) + update(get(tabIndex)) + subList(tabIndex + 1, size)
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.FrostAccountId
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.state.FloatingTabSessionState
|
||||
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
|
||||
import mozilla.components.lib.state.State
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt
|
||||
*
|
||||
* for Firefox example.
|
||||
*/
|
||||
data class FrostWebState(
|
||||
val auth: AuthWebState = AuthWebState(),
|
||||
val selectedHomeTab: WebTargetId? = null,
|
||||
val homeTabs: List<HomeTabSessionState> = emptyList(),
|
||||
var floatingTab: FloatingTabSessionState? = null,
|
||||
) : State
|
||||
|
||||
/**
|
||||
* Auth web state.
|
||||
*
|
||||
* Unlike GeckoView, WebView currently has a singleton cookie manager.
|
||||
*
|
||||
* Cookies are tied to the entire app, rather than per tab.
|
||||
*
|
||||
* @param currentUser User based on loaded cookies
|
||||
* @param homeUser User selected for home screen
|
||||
*/
|
||||
data class AuthWebState(
|
||||
val currentUser: AuthUser = AuthUser.Unknown,
|
||||
val homeUser: AuthUser = AuthUser.Unknown,
|
||||
) {
|
||||
sealed interface AuthUser {
|
||||
data class User(val id: FrostAccountId) : AuthUser
|
||||
|
||||
data class Transitioning(val targetId: FrostAccountId?) : AuthUser
|
||||
|
||||
object LoggedOut : AuthUser
|
||||
|
||||
object Unknown : AuthUser
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.state.SessionState
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* See
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt
|
||||
*
|
||||
* For firefox example.
|
||||
*/
|
||||
class FrostWebStore(
|
||||
initialState: FrostWebState = FrostWebState(),
|
||||
frostWebReducer: FrostWebReducer,
|
||||
middleware: List<Middleware<FrostWebState, FrostWebAction>> = emptyList(),
|
||||
) :
|
||||
Store<FrostWebState, FrostWebAction>(
|
||||
initialState,
|
||||
frostWebReducer::reduce,
|
||||
middleware,
|
||||
"FrostStore",
|
||||
) {
|
||||
init {
|
||||
dispatch(InitAction)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun FrostWebState.get(tabId: WebTargetId): SessionState? {
|
||||
if (floatingTab?.id == tabId) return floatingTab
|
||||
return homeTabs.find { it.id == tabId }
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.state.helper
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebState
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.state.SessionState
|
||||
import mozilla.components.lib.state.Store
|
||||
import mozilla.components.lib.state.ext.observeAsComposableState
|
||||
|
||||
/**
|
||||
* Helper for allowing a component consumer to specify which tab a component should target.
|
||||
*
|
||||
* Based off of mozilla.components.browser.state.helper.Target:
|
||||
* https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt
|
||||
*/
|
||||
sealed class Target {
|
||||
/**
|
||||
* Looks up this target in the given [FrostWebStore] and returns the matching [SessionState] if
|
||||
* available. Otherwise returns `null`.
|
||||
*
|
||||
* @param store to lookup this target in.
|
||||
*/
|
||||
fun lookupIn(store: FrostWebStore): SessionState? = lookupIn(store.state)
|
||||
|
||||
/**
|
||||
* Looks up this target in the given [FrostWebState] and returns the matching [SessionState] if
|
||||
* available. Otherwise returns `null`.
|
||||
*
|
||||
* @param state to lookup this target in.
|
||||
*/
|
||||
abstract fun lookupIn(state: FrostWebState): SessionState?
|
||||
|
||||
/**
|
||||
* Observes this target and represents the mapped state (using [map]) via [State].
|
||||
*
|
||||
* Everytime the [Store] state changes and the result of the [observe] function changes for this
|
||||
* state, the returned [State] will be updated causing recomposition of every [State.value] usage.
|
||||
*
|
||||
* The [Store] observer will automatically be removed when this composable disposes or the current
|
||||
* [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
|
||||
*
|
||||
* @param store that should get observed
|
||||
* @param observe function that maps a [SessionState] to the (sub) state that should get observed
|
||||
* for changes.
|
||||
*/
|
||||
@Composable
|
||||
fun <R> observeAsComposableStateFrom(
|
||||
store: FrostWebStore,
|
||||
observe: (SessionState?) -> R,
|
||||
): State<SessionState?> {
|
||||
return store.observeAsComposableState(
|
||||
map = { state -> lookupIn(state) },
|
||||
observe = { state -> observe(lookupIn(state)) },
|
||||
)
|
||||
}
|
||||
|
||||
data class HomeTab(val id: WebTargetId) : Target() {
|
||||
override fun lookupIn(state: FrostWebState): SessionState? {
|
||||
return state.homeTabs.find { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
object FloatingTab : Target() {
|
||||
override fun lookupIn(state: FrostWebState): SessionState? {
|
||||
return state.floatingTab
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.state.reducer
|
||||
|
||||
import com.pitchedapps.frost.web.state.TabAction.Action
|
||||
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.web.state.TabAction.ContentAction.UpdateUrlAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.LoadUrlResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.ResponseAction.WebStepResponseAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.GoBackAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.GoForwardAction
|
||||
import com.pitchedapps.frost.web.state.TabAction.UserAction.LoadUrlAction
|
||||
import com.pitchedapps.frost.web.state.state.ContentState
|
||||
import com.pitchedapps.frost.web.state.state.TransientWebState
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ContentStateReducer @Inject internal constructor() {
|
||||
|
||||
fun reduce(state: ContentState, action: Action): ContentState {
|
||||
return when (action) {
|
||||
is UpdateUrlAction -> state.copy(url = action.url)
|
||||
is UpdateProgressAction -> state.copy(progress = action.progress)
|
||||
is UpdateNavigationAction ->
|
||||
state.copy(
|
||||
canGoBack = action.canGoBack,
|
||||
canGoForward = action.canGoForward,
|
||||
)
|
||||
is UpdateTitleAction -> state.copy(title = action.title)
|
||||
is UserAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
is ResponseAction ->
|
||||
state.copy(
|
||||
transientState =
|
||||
FrostTransientFulfillmentWebReducer.reduce(
|
||||
state.transientState,
|
||||
action,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object FrostTransientWebReducer {
|
||||
fun reduce(state: TransientWebState, action: UserAction): TransientWebState {
|
||||
return when (action) {
|
||||
is LoadUrlAction -> state.copy(targetUrl = action.url)
|
||||
is GoBackAction -> state.copy(navStep = state.navStep - 1)
|
||||
is GoForwardAction -> state.copy(navStep = state.navStep + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object FrostTransientFulfillmentWebReducer {
|
||||
fun reduce(state: TransientWebState, action: ResponseAction): TransientWebState {
|
||||
return when (action) {
|
||||
is LoadUrlResponseAction ->
|
||||
if (state.targetUrl == action.url) state.copy(targetUrl = null) else state
|
||||
is WebStepResponseAction -> state.copy(navStep = state.navStep - action.steps)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.state.reducer
|
||||
|
||||
import android.content.Context
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.facebook.tab
|
||||
import com.pitchedapps.frost.web.state.AuthWebState
|
||||
import com.pitchedapps.frost.web.state.FrostWebState
|
||||
import com.pitchedapps.frost.web.state.TabListAction
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SetHomeTabs
|
||||
import com.pitchedapps.frost.web.state.state.ContentState
|
||||
import com.pitchedapps.frost.web.state.state.HomeTabSessionState
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TabListReducer
|
||||
@Inject
|
||||
internal constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
fun reduce(state: FrostWebState, action: TabListAction): FrostWebState {
|
||||
return when (action) {
|
||||
is SetHomeTabs -> {
|
||||
val tabs =
|
||||
action.data.mapIndexed { i, fbItem -> fbItem.toHomeTabSession(context, i, state.auth) }
|
||||
val selectedTab = action.selectedTab?.let { HomeTabSessionState.homeTabId(it) }
|
||||
state.copy(
|
||||
homeTabs = tabs,
|
||||
selectedHomeTab = selectedTab,
|
||||
)
|
||||
}
|
||||
is TabListAction.SelectHomeTab -> state.copy(selectedHomeTab = action.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FbItem.toHomeTabSession(
|
||||
context: Context,
|
||||
i: Int,
|
||||
auth: AuthWebState
|
||||
): HomeTabSessionState =
|
||||
HomeTabSessionState(
|
||||
userId = auth.currentUser,
|
||||
content = ContentState(url = url),
|
||||
tab = tab(context, id = HomeTabSessionState.homeTabId(i)),
|
||||
)
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.state.state
|
||||
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.main.MainTabItem
|
||||
import com.pitchedapps.frost.web.state.AuthWebState.AuthUser
|
||||
|
||||
/** Data representation of single session. */
|
||||
interface SessionState {
|
||||
val id: WebTargetId
|
||||
val userId: AuthUser
|
||||
val content: ContentState
|
||||
|
||||
fun createCopy(
|
||||
id: WebTargetId = this.id,
|
||||
userId: AuthUser = this.userId,
|
||||
content: ContentState = this.content
|
||||
): SessionState
|
||||
}
|
||||
|
||||
/** Session for home screen, which includes nav bar data */
|
||||
data class HomeTabSessionState(
|
||||
override val userId: AuthUser,
|
||||
override val content: ContentState,
|
||||
val tab: MainTabItem,
|
||||
) : SessionState {
|
||||
|
||||
override val id: WebTargetId
|
||||
get() = tab.id
|
||||
|
||||
override fun createCopy(id: WebTargetId, userId: AuthUser, content: ContentState) =
|
||||
copy(userId = userId, content = content, tab = tab.copy(id = id))
|
||||
|
||||
companion object {
|
||||
fun homeTabId(index: Int): WebTargetId = WebTargetId("home-tab--$index")
|
||||
}
|
||||
}
|
||||
|
||||
data class FloatingTabSessionState(
|
||||
override val id: WebTargetId,
|
||||
override val userId: AuthUser,
|
||||
override val content: ContentState,
|
||||
) : SessionState {
|
||||
override fun createCopy(id: WebTargetId, userId: AuthUser, content: ContentState) =
|
||||
copy(id = id, userId = userId, content = content)
|
||||
}
|
||||
|
||||
/** Data relating to webview content */
|
||||
data class ContentState(
|
||||
val url: String,
|
||||
val title: String? = null,
|
||||
val progress: Int = 0,
|
||||
val loading: Boolean = false,
|
||||
val canGoBack: Boolean = false,
|
||||
val canGoForward: Boolean = false,
|
||||
val transientState: TransientWebState = TransientWebState(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Transient web state.
|
||||
*
|
||||
* While we typically don't want to store this, our webview is not a composable, and requires a
|
||||
* bridge to handle events.
|
||||
*
|
||||
* This state is not a list of pending actions, but rather a snapshot of the expected changes so
|
||||
* that conflicting events can be ignored.
|
||||
*
|
||||
* @param targetUrl url destination if nonnull
|
||||
* @param navStep pending steps. Positive = steps forward, negative = steps backward
|
||||
*/
|
||||
data class TransientWebState(
|
||||
val targetUrl: String? = null,
|
||||
val navStep: Int = 0,
|
||||
)
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.facebook.FbItem
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SelectHomeTab
|
||||
import com.pitchedapps.frost.web.state.TabListAction.SetHomeTabs
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use cases for the home screen. */
|
||||
class HomeTabsUseCases @Inject internal constructor(private val store: FrostWebStore) {
|
||||
|
||||
/**
|
||||
* Create the provided tabs.
|
||||
*
|
||||
* If there are existing tabs, they will be replaced.
|
||||
*/
|
||||
fun setHomeTabs(items: List<FbItem>) {
|
||||
store.dispatch(SetHomeTabs(items))
|
||||
}
|
||||
|
||||
/**
|
||||
* Select home tab based on index.
|
||||
*
|
||||
* If the index is OOB, the selected tab will be null.
|
||||
*/
|
||||
fun selectHomeTab(tabId: WebTargetId) {
|
||||
store.dispatch(SelectHomeTab(tabId))
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Collection of frost use cases.
|
||||
*
|
||||
* Note that included use cases are not lazily loaded.
|
||||
*/
|
||||
@Singleton
|
||||
class UseCases
|
||||
@Inject
|
||||
internal constructor(
|
||||
val homeTabs: HomeTabsUseCases,
|
||||
val tabs: TabUseCases,
|
||||
)
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import com.google.common.flogger.FluentLogger
|
||||
import com.pitchedapps.frost.ext.WebTargetId
|
||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||
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 */
|
||||
@FrostWebScoped
|
||||
class FrostChromeClient
|
||||
@Inject
|
||||
internal constructor(@FrostWeb private val tabId: WebTargetId, private val store: FrostWebStore) :
|
||||
WebChromeClient() {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
override fun getDefaultVideoPoster(): Bitmap? =
|
||||
super.getDefaultVideoPoster() ?: Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||
logger
|
||||
.atInfo()
|
||||
.log("Chrome Console %d: %s", consoleMessage.lineNumber(), consoleMessage.message())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(view: WebView, title: String) {
|
||||
super.onReceivedTitle(view, title)
|
||||
if (title.startsWith("http")) return
|
||||
store.dispatch(UpdateTitleAction(title))
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
// TODO remove?
|
||||
if (store.state[tabId]?.content?.progress == 100) return
|
||||
store.dispatch(UpdateProgressAction(newProgress))
|
||||
}
|
||||
|
||||
// override fun onShowFileChooser(
|
||||
// webView: WebView,
|
||||
// filePathCallback: ValueCallback<Array<Uri>?>,
|
||||
// fileChooserParams: FileChooserParams
|
||||
// ): Boolean {
|
||||
// callbacks.openMediaPicker(filePathCallback, fileChooserParams)
|
||||
// return true
|
||||
// }
|
||||
|
||||
// override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean
|
||||
// {
|
||||
// callbacks.onJsAlert(url, message, result)
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult):
|
||||
// Boolean {
|
||||
// callbacks.onJsConfirm(url, message, result)
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onJsBeforeUnload(
|
||||
// view: WebView,
|
||||
// url: String,
|
||||
// message: String,
|
||||
// result: JsResult
|
||||
// ): Boolean {
|
||||
// callbacks.onJsBeforeUnload(url, message, result)
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// override fun onJsPrompt(
|
||||
// view: WebView,
|
||||
// url: String,
|
||||
// message: String,
|
||||
// defaultValue: String?,
|
||||
// result: JsPromptResult
|
||||
// ): Boolean {
|
||||
// callbacks.onJsPrompt(url, message, defaultValue, result)
|
||||
// return true
|
||||
// }
|
||||
|
||||
// override fun onGeolocationPermissionsShowPrompt(
|
||||
// origin: String,
|
||||
// callback: GeolocationPermissions.Callback
|
||||
// ) {
|
||||
// L.i { "Requesting geolocation" }
|
||||
// context.kauRequestPermissions(PERMISSION_ACCESS_FINE_LOCATION) { granted, _ ->
|
||||
// L.i { "Geolocation response received; ${if (granted) "granted" else "denied"}" }
|
||||
// callback(origin, granted, true)
|
||||
// }
|
||||
// }
|
||||
|
||||
companion object {
|
||||
private val logger = FluentLogger.forEnclosingClass()
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2021 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 dagger.BindsInstance
|
||||
import dagger.hilt.DefineComponent
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.EntryPoints
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Scope
|
||||
|
||||
/**
|
||||
* Defines a new scope for Frost web related content.
|
||||
*
|
||||
* This is a subset of [dagger.hilt.android.scopes.ViewScoped]
|
||||
*/
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.CLASS)
|
||||
annotation class FrostWebScoped
|
||||
|
||||
@Qualifier annotation class FrostWeb
|
||||
|
||||
@FrostWebScoped @DefineComponent(parent = ViewModelComponent::class) interface FrostWebComponent
|
||||
|
||||
@DefineComponent.Builder
|
||||
interface FrostWebComponentBuilder {
|
||||
|
||||
fun tabId(@BindsInstance @FrostWeb tabId: WebTargetId): FrostWebComponentBuilder
|
||||
|
||||
fun build(): FrostWebComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* Using this injection seems to be buggy, leading to an invalid param tabId error:
|
||||
*
|
||||
* Cause: not a valid name: tabId-4xHwVBUParam
|
||||
*/
|
||||
class FrostWebComposer
|
||||
@Inject
|
||||
internal constructor(private val frostWebComponentBuilder: FrostWebComponentBuilder) {
|
||||
fun create(tabId: WebTargetId): FrostWebCompose {
|
||||
val component = frostWebComponentBuilder.tabId(tabId).build()
|
||||
return EntryPoints.get(component, FrostWebEntryPoint::class.java).compose()
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(FrostWebComponent::class)
|
||||
interface FrostWebEntryPoint {
|
||||
fun compose(): FrostWebCompose
|
||||
}
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.google.common.flogger.FluentLogger
|
||||
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.
|
||||
*
|
||||
* Collection of webview clients
|
||||
*/
|
||||
|
||||
/** The base of all webview clients Used to ensure that resources are properly intercepted */
|
||||
abstract class BaseWebViewClient : WebViewClient() {
|
||||
|
||||
protected abstract val webHelper: FrostWebHelper
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
val requestUrl = request.url?.toString() ?: return null
|
||||
return if (webHelper.shouldInterceptUrl(requestUrl)) BLANK_RESOURCE else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
val BLANK_RESOURCE =
|
||||
WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
/** The default webview client */
|
||||
@FrostWebScoped
|
||||
class FrostWebViewClient
|
||||
@Inject
|
||||
internal constructor(
|
||||
@FrostWeb private val tabId: WebTargetId,
|
||||
private val store: FrostWebStore,
|
||||
override val webHelper: FrostWebHelper,
|
||||
private val frostJsInjectors: FrostJsInjectors,
|
||||
) : BaseWebViewClient() {
|
||||
|
||||
private fun FrostWebStore.dispatch(action: TabAction.Action) {
|
||||
dispatch(TabAction(tabId = tabId, action = action))
|
||||
}
|
||||
|
||||
/** True if current url supports refresh. See [doUpdateVisitedHistory] for updates */
|
||||
internal var urlSupportsRefresh: Boolean = true
|
||||
|
||||
override fun doUpdateVisitedHistory(view: WebView, url: String, isReload: Boolean) {
|
||||
super.doUpdateVisitedHistory(view, url, isReload)
|
||||
urlSupportsRefresh = webHelper.allowUrlSwipeToRefresh(url)
|
||||
store.dispatch(
|
||||
UpdateNavigationAction(
|
||||
canGoBack = view.canGoBack(),
|
||||
canGoForward = view.canGoForward(),
|
||||
),
|
||||
)
|
||||
// web.parent.swipeAllowedByPage = urlSupportsRefresh
|
||||
// view.jsInject(JsAssets.AUTO_RESIZE_TEXTAREA.maybe(prefs.autoExpandTextBox), prefs = prefs)
|
||||
// v { "History $url; refresh $urlSupportsRefresh" }
|
||||
}
|
||||
|
||||
/** Main injections for facebook content */
|
||||
// protected open val facebookJsInjectors: List<InjectorContract> =
|
||||
// listOf(
|
||||
// // CssHider.CORE,
|
||||
// CssHider.HEADER,
|
||||
// CssHider.COMPOSER.maybe(!prefs.showComposer),
|
||||
// CssHider.STORIES.maybe(!prefs.showStories),
|
||||
// CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!prefs.showSuggestedFriends),
|
||||
// CssHider.SUGGESTED_GROUPS.maybe(!prefs.showSuggestedGroups),
|
||||
// CssHider.SUGGESTED_POSTS.maybe(!prefs.showSuggestedPosts),
|
||||
// themeProvider.injector(ThemeCategory.FACEBOOK),
|
||||
// CssHider.NON_RECENT.maybe(
|
||||
// (web.url?.contains("?sk=h_chr") ?: false) && prefs.aggressiveRecents
|
||||
// ),
|
||||
// CssHider.ADS,
|
||||
// CssHider.POST_ACTIONS.maybe(!prefs.showPostActions),
|
||||
// CssHider.POST_REACTIONS.maybe(!prefs.showPostReactions),
|
||||
// CssAsset.FullSizeImage.maybe(prefs.fullSizeImage),
|
||||
// JsAssets.DOCUMENT_WATCHER,
|
||||
// JsAssets.HORIZONTAL_SCROLLING,
|
||||
// JsAssets.AUTO_RESIZE_TEXTAREA.maybe(prefs.autoExpandTextBox),
|
||||
// JsAssets.CLICK_A,
|
||||
// JsAssets.CONTEXT_A,
|
||||
// JsAssets.MEDIA,
|
||||
// JsAssets.SCROLL_STOP,
|
||||
// )
|
||||
//
|
||||
// private fun WebView.facebookJsInject() {
|
||||
// jsInject(*facebookJsInjectors.toTypedArray(), prefs = prefs)
|
||||
// }
|
||||
//
|
||||
// private fun WebView.messengerJsInject() {
|
||||
// jsInject(themeProvider.injector(ThemeCategory.MESSENGER), prefs = prefs)
|
||||
// }
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
store.dispatch(UpdateProgressAction(0))
|
||||
store.dispatch(UpdateTitleAction(null))
|
||||
// v { "loading $url ${web.settings.userAgentString}" }
|
||||
// refresh.offer(true)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
when {
|
||||
url.isFacebookUrl -> frostJsInjectors.facebookInjectOnPageCommitVisible(view, url)
|
||||
}
|
||||
}
|
||||
// when {
|
||||
// url.isFacebookUrl -> {
|
||||
// v { "FB Page commit visible" }
|
||||
// view.facebookJsInject()
|
||||
// }
|
||||
// url.isMessengerUrl -> {
|
||||
// v { "Messenger Page commit visible" }
|
||||
// view.messengerJsInject()
|
||||
// }
|
||||
// else -> {
|
||||
// // refresh.offer(false)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
// if (!url.isFacebookUrl && !url.isMessengerUrl) {
|
||||
// refresh.offer(false)
|
||||
// return
|
||||
// }
|
||||
// onPageFinishedActions(url)
|
||||
}
|
||||
|
||||
// internal open fun onPageFinishedActions(url: String) {
|
||||
// if (url.startsWith("${FbItem.Messages.url}/read/") && prefs.messageScrollToBottom) {
|
||||
// web.pageDown(true)
|
||||
// }
|
||||
// injectAndFinish()
|
||||
// }
|
||||
|
||||
// Temp open
|
||||
// internal open fun injectAndFinish() {
|
||||
// v { "page finished reveal" }
|
||||
// // refresh.offer(false)
|
||||
// injectBackgroundColor()
|
||||
// when {
|
||||
// web.url.isFacebookUrl -> {
|
||||
// web.jsInject(
|
||||
// JsActions.LOGIN_CHECK,
|
||||
// JsAssets.TEXTAREA_LISTENER,
|
||||
// JsAssets.HEADER_BADGES.maybe(isMain),
|
||||
// prefs = prefs
|
||||
// )
|
||||
// web.facebookJsInject()
|
||||
// }
|
||||
// web.url.isMessengerUrl -> {
|
||||
// web.messengerJsInject()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun handleHtml(html: String?) {
|
||||
logger.atFine().log("Handle html: %s", html)
|
||||
}
|
||||
|
||||
fun emit(flag: Int) {
|
||||
logger.atInfo().log("Emit %d", flag)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format the request and launch it returns true to override the url returns false if we
|
||||
* are already in an overlaying activity
|
||||
*/
|
||||
// private fun WebView.launchRequest(request: WebResourceRequest): Boolean {
|
||||
// v { "Launching url: ${request.url}" }
|
||||
// return requestWebOverlay(request.url.toString())
|
||||
// }
|
||||
|
||||
// private fun launchImage(url: String, text: String? = null, cookie: String? = null): Boolean {
|
||||
// v { "Launching image: $url" }
|
||||
// web.context.launchImageActivity(url, text, cookie)
|
||||
// if (web.canGoBack()) web.goBack()
|
||||
// return true
|
||||
// }
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
logger.atFinest().log("Url loading: %s", request.url)
|
||||
val path = request.url?.path ?: return super.shouldOverrideUrlLoading(view, request)
|
||||
logger.atFinest().log("Url path: %s", path)
|
||||
val url = request.url.toString()
|
||||
if (url.isExplicitIntent) {
|
||||
// view.context.startActivityForUri(request.url)
|
||||
return true
|
||||
}
|
||||
// if (path.startsWith("/composer/")) {
|
||||
// return launchRequest(request)
|
||||
// }
|
||||
// if (url.isIndirectImageUrl) {
|
||||
// return launchImage(url.formattedFbUrl, cookie = fbCookie.webCookie)
|
||||
// }
|
||||
// if (url.isImageUrl) {
|
||||
// return launchImage(url.formattedFbUrl)
|
||||
// }
|
||||
// if (prefs.linksInDefaultApp && view.context.startActivityForUri(request.url)) {
|
||||
// return true
|
||||
// }
|
||||
// Convert desktop urls to mobile ones
|
||||
if (url.contains("https://www.facebook.com") && webHelper.allowUrlSwipeToRefresh(url)) {
|
||||
view.loadUrl(url.replace(WWW_FACEBOOK_COM, FACEBOOK_BASE_COM))
|
||||
return true
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private const val EMIT_THEME = 0b1
|
||||
private const val EMIT_ID = 0b10
|
||||
private const val EMIT_COMPLETE = EMIT_THEME or EMIT_ID
|
||||
private const val EMIT_FINISH = 0
|
@ -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) }
|
||||
}
|
9
app-compose/src/main/proto/account.proto
Normal file
@ -0,0 +1,9 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "com.pitchedapps.frost.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Account {
|
||||
// Current account id
|
||||
optional int64 account_id = 1;
|
||||
}
|
18
app-compose/src/main/proto/settings/appearance.proto
Normal file
@ -0,0 +1,18 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "com.pitchedapps.frost.proto.settings";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Appearance {
|
||||
// Tab identifiers for main screen
|
||||
repeated string main_tabs = 1;
|
||||
|
||||
enum MainTabLayout {
|
||||
TOP = 0;
|
||||
BOTTOM = 1;
|
||||
// Bare bones layout without custom tabs
|
||||
COMPACT = 2;
|
||||
}
|
||||
|
||||
MainTabLayout main_tab_layout = 2;
|
||||
}
|
BIN
app-compose/src/main/res/drawable-nodpi/splash_logo.9.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
9
app-compose/src/main/res/drawable/badge_background.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="10dp" />
|
||||
<solid android:color="@color/facebook_blue" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
14
app-compose/src/main/res/drawable/frost_f_200.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportHeight="177.16534"
|
||||
android:viewportWidth="177.16534">
|
||||
|
||||
<path
|
||||
android:pathData="M88.9405 31.61857l41.06143 23.88663M88.3467 31.59584l-31.358 18.2953M56.87005
|
||||
51.09542v94.26222M57.20726 87.2077l40.97656 23.86424"
|
||||
android:strokeColor="#fff"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeWidth="10" />
|
||||
</vector>
|
14
app-compose/src/main/res/drawable/frost_f_24.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="177.16534"
|
||||
android:viewportWidth="177.16534">
|
||||
|
||||
<path
|
||||
android:pathData="M88.9405 31.61857l41.06143 23.88663M88.3467 31.59584l-31.358 18.2953M56.87005
|
||||
51.09542v94.26222M57.20726 87.2077l40.97656 23.86424"
|
||||
android:strokeColor="#fff"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeWidth="10" />
|
||||
</vector>
|
10
app-compose/src/main/res/drawable/splash_screen.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/frost_splash_background" />
|
||||
<item android:bottom="@dimen/splash_logo">
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@drawable/splash_logo" />
|
||||
</item>
|
||||
</layer-list>
|
BIN
app-compose/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
app-compose/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
app-compose/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app-compose/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
app-compose/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app-compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
app-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
app-compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
8
app-compose/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="frost_splash_background">@color/facebook_blue</color>
|
||||
<color name="facebook_blue">#3b5998</color>
|
||||
<color name="facebook_blue_light">#6183C8</color>
|
||||
<color name="facebook_blue_dark">#2e4b86</color>
|
||||
<color name="frost_notification_accent">@color/facebook_blue</color>
|
||||
</resources>
|
3
app-compose/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<dimen name="splash_logo">16dp</dimen>
|
||||
</resources>
|
82
app-compose/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,82 @@
|
||||
<resources>
|
||||
<string name="feed">Feed</string>
|
||||
<string name="most_recent">Most Recent</string>
|
||||
<string name="top_stories">Top Stories</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="bookmarks">Bookmarks</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="friends">Friends</string>
|
||||
<string name="messages">Messages</string>
|
||||
<string name="messenger">Messenger</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
<string name="activity_log">Activity Log</string>
|
||||
<string name="pages">Pages</string>
|
||||
<string name="groups">Groups</string>
|
||||
<string name="saved">Saved</string>
|
||||
<string name="birthdays">Birthdays</string>
|
||||
<string name="chat">Chat</string>
|
||||
<string name="photos">Photos</string>
|
||||
<string name="marketplace">Marketplace</string>
|
||||
<string name="menu">Menu</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="notes">Notes</string>
|
||||
<string name="on_this_day">On This Day</string>
|
||||
|
||||
<string name="loading_account">Getting everything ready…</string>
|
||||
<string name="welcome">Welcome %s</string>
|
||||
<string name="select_facebook_account">Select Facebook Account</string>
|
||||
<string name="account_not_found">Current account is not in the database</string>
|
||||
|
||||
<string name="frost_notifications">Frost Notifications</string>
|
||||
<string name="requires_custom_theme">Requires custom theme</string>
|
||||
|
||||
<string name="share">Share</string>
|
||||
|
||||
<string name="web_overlay_swipe_hint">Swipe right to go back to the previous window.</string>
|
||||
<string name="profile_picture">Profile Picture</string>
|
||||
|
||||
<string name="new_message">New Message</string>
|
||||
<string name="no_text">No text</string>
|
||||
|
||||
<string name="show_all_results">Show All Results</string>
|
||||
|
||||
<string name="frost_description">Frost is a fully themable,
|
||||
fully functional alternative to the official Facebook app, made from scratch and proudly open sourced.</string>
|
||||
|
||||
<string name="faq_title">Frost FAQ</string>
|
||||
|
||||
<string name="open">Open</string>
|
||||
<string name="close">Close</string>
|
||||
|
||||
<string name="html_extraction_error">An error occurred in the html extraction.</string>
|
||||
<string name="html_extraction_cancelled">The request has been cancelled.</string>
|
||||
<string name="html_extraction_timeout">The request has timed out.</string>
|
||||
<string name="file_chooser_not_found">File chooser not found</string>
|
||||
|
||||
<string name="top_bar">Top Bar</string>
|
||||
<string name="bottom_bar">Bottom Bar</string>
|
||||
|
||||
<string name="pip" translatable="false">PIP</string>
|
||||
<string name="preview">Preview</string>
|
||||
<string name="options">Options</string>
|
||||
<string name="tab_customizer_instructions">Long press and drag to rearrange the top icons.</string>
|
||||
|
||||
<string name="no_new_notifications">No new notifications found</string>
|
||||
|
||||
<!--Biometrics-->
|
||||
<string name="biometrics_prompt_title">Authenticate Frost</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="yesterday">Yesterday</string>
|
||||
<!--
|
||||
Template used to display human readable string;
|
||||
For instance:
|
||||
Today at 1:23 PM
|
||||
Mar 13 at 9:00 AM
|
||||
|
||||
The first element is the day, and the second element is the time
|
||||
-->
|
||||
<string name="time_template">%1$s at %2$s</string>
|
||||
|
||||
<string name="disclaimer">Disclaimer</string>
|
||||
|
||||
</resources>
|
16
app-compose/src/main/res/values/strings_download.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_image">Pick Image</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="downloading">Downloading…</string>
|
||||
<string name="image_download_success">Image downloaded</string>
|
||||
<string name="image_download_fail">Image failed to download</string>
|
||||
<string name="image_share_failed">Failed to share image</string>
|
||||
|
||||
<string name="downloading_video">Downloading Video</string>
|
||||
<string name="downloaded_video">Video Downloaded</string>
|
||||
<string name="downloading_file">Downloading File</string>
|
||||
<string name="downloaded_file">File Downloaded</string>
|
||||
|
||||
<string name="error_invalid_download">Invalid download attempt</string>
|
||||
</resources>
|
14
app-compose/src/main/res/values/strings_errors.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="null_url_overlay">Empty URL given to overlay; exiting</string>
|
||||
<string name="invalid_share_url">Invalid Share URL</string>
|
||||
<string name="invalid_share_url_desc">You have shared a block of text that is not a URL. The text has been copied to your clipboard, so you may share it manually yourself.</string>
|
||||
<string name="no_download_manager">No Download Manager</string>
|
||||
<string name="no_download_manager_desc">The download manager is not enabled. Would you like to enable it to allow downloads?</string>
|
||||
<string name="error_generic">An error occurred.</string>
|
||||
<string name="video_load_failed">Failed to load video</string>
|
||||
<string name="error_notification">An error occurred when fetching notifications</string>
|
||||
<string name="error_sdk">Your device\'s SDK (%d) is incompatible. Frost only supports Lollipop (SDK 21) and above</string>
|
||||
<string name="error_webview">Your device does not seem to have a webview. Please add or enable one.</string>
|
||||
<string name="image_not_found">Image not found.</string>
|
||||
</resources>
|
17
app-compose/src/main/res/values/strings_intro.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="intro_welcome_to_frost">Welcome to Frost</string>
|
||||
<string name="intro_slide_to_continue">Slide to continue</string>
|
||||
<string name="intro_select_theme">Select a theme</string>
|
||||
|
||||
<string name="intro_multiple_accounts">Multiple Accounts</string>
|
||||
<string name="intro_multiple_accounts_desc">Add and switch accounts directly from the navigation bar.\nTap the current avatar to jump to your profile.</string>
|
||||
<string name="intro_easy_navigation">Easy Navigation</string>
|
||||
<string name="intro_easy_navigation_desc">Slide between views with a swipe, and click the tab icon to go back to the top.\nClick the icon again to reload the page.</string>
|
||||
<string name="intro_context_aware">Context Aware</string>
|
||||
<string name="intro_context_aware_desc">Long press links to copy and share them.\nLong press images to zoom and download.\nLong press cards to scroll horizontally.</string>
|
||||
|
||||
<string name="intro_end">Let\'s Begin!</string>
|
||||
<string name="intro_tap_to_exit">Tap anywhere to exit</string>
|
||||
</resources>
|
16
app-compose/src/main/res/values/strings_no_translate.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<resources>
|
||||
<string name="dev_name" translatable="false">Pitched Apps</string>
|
||||
<string name="translation_url" translatable="false">https://crwd.in/frost-for-facebook</string>
|
||||
<string name="github" translatable="false">GitHub</string>
|
||||
<string name="github_url" translatable="false">https://github.com/AllanWang/Frost-for-Facebook</string>
|
||||
<string name="github_downloads_url" translatable="false">https://github.com/AllanWang/Frost-for-Facebook/releases</string>
|
||||
<string name="fdroid_url" translatable="false">https://f-droid.org/en/packages/com.pitchedapps.frost</string>
|
||||
<string name="reddit_url" translatable="false">https://www.reddit.com/r/FrostForFacebook</string>
|
||||
<string name="frost_prefix" translatable="false">Frost for Facebook:</string>
|
||||
<string name="feedback" translatable="false">Feedback</string>
|
||||
<string name="bug_report" translatable="false">Bug Report</string>
|
||||
<string name="debug_report" translatable="false">Debug Report</string>
|
||||
<string name="theme_issue" translatable="false">Theme Issue</string>
|
||||
<string name="feature_request" translatable="false">Feature Request</string>
|
||||
<string name="reset_notif_epoch" translatable="false">Reset Notif Epoch</string>
|
||||
</resources>
|
8
app-compose/src/main/res/values/strings_play_store.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<resources>
|
||||
|
||||
<string name="restoring_purchases">Restoring purchases…</string>
|
||||
|
||||
<string name="uh_oh">Uh Oh</string>
|
||||
<string name="reload">Reload</string>
|
||||
</resources>
|
28
app-compose/src/main/res/values/strings_pref_appearance.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="theme_customization">Theme Customization</string>
|
||||
|
||||
<string name="theme">Theme</string>
|
||||
<string name="text_color">Text Color</string>
|
||||
<string name="accent_color">Accent Color</string>
|
||||
<string name="background_color">Background Color</string>
|
||||
<string name="header_color">Header Color</string>
|
||||
<string name="icon_color">Icon Color</string>
|
||||
|
||||
<string name="global_customization">Global Customization</string>
|
||||
|
||||
<string name="main_activity_layout">Main Activity Layout</string>
|
||||
<string name="main_activity_layout_desc">Set Main Activity Layout</string>
|
||||
|
||||
<string name="main_tabs">Main Activity Tabs</string>
|
||||
<string name="main_tabs_desc">Customize which tabs you\'ll see in your main activity</string>
|
||||
|
||||
<string name="tint_nav">Tint Nav Bar</string>
|
||||
<string name="tint_nav_desc">Navigation bar will be the same color as the header</string>
|
||||
<string name="web_text_scaling">Web Text Scaling</string>
|
||||
<string name="web_text_scaling_desc">Text Scaling Example; Long press the percentage text to reset.</string>
|
||||
|
||||
<string name="enforce_black_media_bg">Enforce black media background</string>
|
||||
<string name="enforce_black_media_bg_desc">Make media backgrounds black; default is the selected background above</string>
|
||||
</resources>
|
31
app-compose/src/main/res/values/strings_pref_behaviour.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="auto_refresh_feed">Auto Refresh Feed</string>
|
||||
<string name="auto_refresh_feed_desc">Refresh the feed after 30 minutes of inactivity</string>
|
||||
<string name="fancy_animations">Fancy Animations</string>
|
||||
<string name="fancy_animations_desc">Reveal webviews using ripples and animate transitions</string>
|
||||
<string name="overlay_swipe">Enable Overlays</string>
|
||||
<string name="overlay_swipe_desc">Pressing most links will launch in a new overlay so you can easily swipe back to the original page. Note that this does result in slightly longer loads given that the whole page is reloaded.</string>
|
||||
<string name="overlay_full_screen_swipe">Overlay Full Screen Swipe to Dismiss</string>
|
||||
<string name="overlay_full_screen_swipe_desc">Swipe right from anywhere on the overlaying web to close the browser. If disabled, only swiping from the left edge will move it.</string>
|
||||
<string name="open_links_in_default">Open Links in Default App</string>
|
||||
<string name="open_links_in_default_desc">When possible, open links in the default app rather than through the Frost web overlay</string>
|
||||
<string name="viewpager_swipe">Viewpager Swipe</string>
|
||||
<string name="viewpager_swipe_desc">Allow swiping between the pages in the main view to switch tabs. By default, the swiping automatically stops when you long press on an item, such as the like button. Disabling this will prevent page swiping altogether.</string>
|
||||
<string name="swipe_to_refresh">Swipe to Refresh</string>
|
||||
<string name="swipe_to_refresh_desc">Allow swiping down to refresh</string>
|
||||
<string name="search_bar">Search Bar</string>
|
||||
<string name="search_bar_desc">Enable the search bar instead of a search overlay</string>
|
||||
<string name="force_message_bottom">Force Message Bottom</string>
|
||||
<string name="force_message_bottom_desc">When loading a message thread, trigger a scroll to the bottom of the page rather than loading the page as is.</string>
|
||||
<string name="enable_pip">Enable PIP</string>
|
||||
<string name="enable_pip_desc">Enable picture in picture videos</string>
|
||||
<string name="autoplay_settings">Autoplay Settings</string>
|
||||
<string name="autoplay_settings_desc">Open Facebook\'s auto play settings. Note that it must be disabled for PIP to work.</string>
|
||||
<string name="exit_confirmation">Exit Confirmation</string>
|
||||
<string name="exit_confirmation_desc">Show confirmation dialog before exiting the app</string>
|
||||
<string name="auto_expand_text_box">Auto expand text box</string>
|
||||
<string name="auto_expand_text_box_desc">Increase text box height while typing. Disable if there are scroll issues.</string>
|
||||
|
||||
</resources>
|
23
app-compose/src/main/res/values/strings_pref_debug.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="debug_toast_enabled">Debugging section is enabled! Go back to settings.</string>
|
||||
<string name="debug_toast_already_enabled">Debugging section is already enabled. Go back to settings.</string>
|
||||
|
||||
<string name="debug_disclaimer_info">Though most private content is automatically removed in the report, some sensitive info may still be visible.
|
||||
\nPlease have a look at the debug report before sending it.
|
||||
\n\nClicking one of the options below will prepare an email response with the web page data.
|
||||
</string>
|
||||
|
||||
<string name="debug_incomplete">Incomplete report</string>
|
||||
|
||||
<string name="debug_report_email_title" translatable="false">Frost for Facebook: Debug Report</string>
|
||||
|
||||
<string name="debug_web">Debug from the Web</string>
|
||||
<string name="debug_web_desc">Navigate to the page with an issue and send the resources for debugging.</string>
|
||||
|
||||
<string name="parsing_data">Parsing Data</string>
|
||||
|
||||
<string name="debug_parsers">Debug Parsers</string>
|
||||
<string name="debug_parsers_desc">Launch one of the available parsers to debug its response data</string>
|
||||
</resources>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="experimental_disclaimer_info">Experimental features may be unstable and may never make it to production. Use at your own risk, send feedback, and feel free to disable them if they don\'t work well.</string>
|
||||
|
||||
<string name="verbose_logging">Verbose Logging</string>
|
||||
<string name="verbose_logging_desc">Enable verbose logging to help with crash reports. Logging will only be sent once an error is encountered, so repeat the issue to notify the dev. This will automatically be disabled if the app restarts.</string>
|
||||
<string name="restart_frost">Restart Frost</string>
|
||||
<string name="restart_frost_desc">Launch a cold restart for the application.</string>
|
||||
|
||||
</resources>
|
30
app-compose/src/main/res/values/strings_pref_feed.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="newsfeed_sort">Newsfeed Order</string>
|
||||
<string name="newsfeed_sort_desc">Defines the order in which the posts are shown</string>
|
||||
<string name="aggressive_recents">Aggressive Recents</string>
|
||||
<string name="aggressive_recents_desc">Filter out additional old posts from Facebook\'s original most recents feed. Disable this if your feed is empty.</string>
|
||||
<string name="composer">Status Composer</string>
|
||||
<string name="composer_desc">Show status composer in the feed</string>
|
||||
<string name="create_fab">Create FAB</string>
|
||||
<string name="create_fab_desc">Show FAB in feed to create new post</string>
|
||||
|
||||
<string name="suggested_friends">Suggested Friends</string>
|
||||
<string name="suggested_friends_desc">Show \"People You May Know\" in the feed</string>
|
||||
<string name="suggested_groups">Suggested Groups</string>
|
||||
<string name="suggested_groups_desc">Show \"Suggested Groups\" in the feed</string>
|
||||
<string name="suggested_posts">Suggested Posts</string>
|
||||
<string name="suggested_posts_desc">Show \"Suggested for you\" in the feed</string>
|
||||
<string name="show_stories">Show Stories</string>
|
||||
<string name="show_stories_desc">Show stories in the feed</string>
|
||||
<string name="show_post_actions">Show Post Actions</string>
|
||||
<string name="show_post_actions_desc">Show Like, Comment, and Share options</string>
|
||||
<string name="show_post_reactions">Show Post Reactions</string>
|
||||
<string name="show_post_reactions_desc">Show reaction counts to post</string>
|
||||
<string name="facebook_ads">Facebook Ads</string>
|
||||
<string name="facebook_ads_desc">Show native Facebook ads</string>
|
||||
<string name="full_size_image">Full Size Images</string>
|
||||
<string name="full_size_image_desc">Force news feed images to be full width</string>
|
||||
|
||||
</resources>
|
@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="network_media_on_metered">Disable images on metered network.</string>
|
||||
<string name="network_media_on_metered_desc">If a metered network is detected, Frost will automatically stop all images and videos from loading.</string>
|
||||
</resources>
|
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="notification_frequency">Notification Frequency</string>
|
||||
<string name="no_notifications">No Notifications</string>
|
||||
<string name="notification_keywords">Keywords</string>
|
||||
<string name="notification_keywords_desc">Does not notify when notification contains any of these keys.</string>
|
||||
<string name="add_keyword">Add Keyword</string>
|
||||
<string name="hint_keyword">Type keyword and press +</string>
|
||||
<string name="empty_keyword">Empty Keyword</string>
|
||||
<string name="notification_general">Enable general notifications</string>
|
||||
<string name="notification_general_desc">Get general notifications for your current account.</string>
|
||||
<string name="notification_general_all_accounts">Notify from all accounts</string>
|
||||
<string name="notification_general_all_accounts_desc">Get general notifications for every account that is logged in.</string>
|
||||
<string name="notification_messages">Enable message notifications</string>
|
||||
<string name="notification_messages_desc">Get instant message notifications for your current account.</string>
|
||||
<string name="notification_messages_all_accounts">Notify messages from all accounts</string>
|
||||
<string name="notification_messages_all_accounts_desc">Get instant message notifications from all accounts</string>
|
||||
<string name="notification_fetch_now">Fetch Notifications Now</string>
|
||||
<string name="notification_fetch_now_desc">Trigger the notification fetcher once.</string>
|
||||
<string name="notification_fetch_success">Fetching Notifications…</string>
|
||||
<string name="notification_fetch_fail">Couldn\'t fetch notifications</string>
|
||||
<string name="notification_sound">Notification sound</string>
|
||||
<string name="notification_channel">Customize notification channels</string>
|
||||
<string name="notification_channel_desc">Modify sound, vibration, priority, etc</string>
|
||||
|
||||
<string name="notification_ringtone">Notification Ringtone</string>
|
||||
<string name="message_ringtone">Message Ringtone</string>
|
||||
<string name="select_ringtone">Select Ringtone</string>
|
||||
|
||||
<string name="notification_vibrate">Notification vibration</string>
|
||||
<string name="notification_lights">Notification lights</string>
|
||||
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="security_disclaimer_info">Security preferences help protect access to Frost from the UI. However, note that local data is not encrypted, and can still be accessed by rooted users.</string>
|
||||
<string name="enable_biometrics">Enable biometrics</string>
|
||||
<string name="enable_biometrics_desc">Require biometric authentication after inactivity</string>
|
||||
</resources>
|
32
app-compose/src/main/res/values/strings_preferences.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<resources>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="appearance_desc">Theme, Items to display, etc</string>
|
||||
|
||||
<string name="notifications_desc">Frequency, filters, ringtones, etc</string>
|
||||
|
||||
<string name="newsfeed">News Feed</string>
|
||||
<string name="newsfeed_desc">Define what items appear in the newsfeed</string>
|
||||
|
||||
<string name="behaviour">Behaviour</string>
|
||||
<string name="behaviour_desc">Define how the app interacts in certain settings</string>
|
||||
|
||||
<string name="security">Security</string>
|
||||
<string name="security_desc">Lock screen, biometrics, etc</string>
|
||||
|
||||
<string name="network">Network</string>
|
||||
<string name="network_desc">Define options that affect metered networks</string>
|
||||
|
||||
<string name="experimental">Experimental</string>
|
||||
<string name="experimental_desc">Enable early access to potentially unstable features</string>
|
||||
|
||||
<string name="about_frost">About Frost for Facebook</string>
|
||||
<string name="about_frost_desc">Version, Credits, and FAQs</string>
|
||||
|
||||
<string name="help_translate">Help Translate</string>
|
||||
<string name="help_translate_desc">Frost is translated through crowdin. Contribute if you want it in your language!</string>
|
||||
|
||||
<string name="debug_frost">Frost Debugger</string>
|
||||
<string name="debug_frost_desc">Send html data to help with debugging.</string>
|
||||
|
||||
<string name="replay_intro">Replay Introduction</string>
|
||||
</resources>
|
12
app-compose/src/main/res/values/strings_web_context.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="share_link">Share Link</string>
|
||||
<string name="local_service_name" translatable="false">Local Frost Service</string>
|
||||
<string name="open_link">Open Link</string>
|
||||
<string name="copy_link">Copy Link</string>
|
||||
<string name="copy_text">Copy Text</string>
|
||||
<string name="debug_image_link_subject" translatable="false">Frost for Facebook: Image Link Debug</string>
|
||||
<string name="open_in_browser">Open in browser</string>
|
||||
</resources>
|
29
app-compose/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<resources>
|
||||
|
||||
<style name="FrostTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/facebook_blue</item>
|
||||
<item name="colorPrimaryDark">@color/facebook_blue_dark</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="FrostTheme.Transparent">
|
||||
<item name="android:windowShowWallpaper">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="FrostTheme.Overlay">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
<item name="android:colorBackgroundCacheHint">@null</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
</style>
|
||||
|
||||
<style name="FrostTheme.Splash" parent="Theme.Material3.Dark.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||
<item name="android:navigationBarColor">@color/frost_splash_background</item>
|
||||
<item name="colorPrimaryDark">@color/frost_splash_background</item>
|
||||
<item name="colorAccent">@color/frost_splash_background</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
12
app-compose/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-path
|
||||
name="external"
|
||||
path="/" />
|
||||
<cache-path
|
||||
name="cache"
|
||||
path="/" />
|
||||
<files-path
|
||||
name="files"
|
||||
path="/" />
|
||||
</paths>
|
9
app-compose/src/main/res/xml/frost_backup_rules.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude
|
||||
domain="database"
|
||||
path="Cookies.db" />
|
||||
<exclude
|
||||
domain="database"
|
||||
path="Cookies.db-journal" />
|
||||
</full-backup-content>
|
@ -0,0 +1,33 @@
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT,
|
||||
avatar TEXT,
|
||||
facebook_cookie TEXT,
|
||||
messenger_cookie TEXT
|
||||
);
|
||||
|
||||
count:
|
||||
SELECT COUNT(*) FROM account;
|
||||
|
||||
insertNew:
|
||||
INSERT INTO account DEFAULT VALUES;
|
||||
|
||||
insert:
|
||||
INSERT OR REPLACE INTO account (id, name, avatar, facebook_cookie, messenger_cookie) VALUES (?, ?, ?, ?, ?);
|
||||
|
||||
delete {
|
||||
DELETE FROM account WHERE id == (:id);
|
||||
DELETE FROM notifications WHERE id == (:id);
|
||||
}
|
||||
|
||||
select:
|
||||
SELECT *
|
||||
FROM account WHERE id == ?;
|
||||
|
||||
selectAll:
|
||||
SELECT *
|
||||
FROM account;
|
||||
|
||||
selectAllDisplay:
|
||||
SELECT id, name, avatar
|
||||
FROM account;
|
@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
facebook_notifications INTEGER,
|
||||
facebook_messages INTEGER,
|
||||
facebook_friends INTEGER,
|
||||
messenger_notifications INTEGER
|
||||
);
|
||||
|
||||
insertNew:
|
||||
INSERT OR IGNORE INTO notifications (id) VALUES (?);
|
||||
|
||||
insert:
|
||||
INSERT OR REPLACE INTO notifications (id,facebook_notifications, facebook_messages, facebook_friends, messenger_notifications) VALUES (?,?,?,?,?);
|
||||
|
||||
select:
|
||||
SELECT *
|
||||
FROM notifications WHERE id == ?;
|
||||
|
||||
selectAll:
|
||||
SELECT *
|
||||
FROM notifications;
|
28
app-compose/src/web/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
node_modules/
|
||||
.sass-cache/
|
||||
|
||||
*.js
|
||||
*.css
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
.idea/
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|