mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-08 12:02:33 +01:00
Apply ktfmt
This commit is contained in:
parent
bc1e1bda4f
commit
a319baa736
95
.editorconfig
Normal file
95
.editorconfig
Normal file
@ -0,0 +1,95 @@
|
||||
# https://raw.githubusercontent.com/facebookincubator/ktfmt/main/docs/editorconfig/.editorconfig-google
|
||||
#
|
||||
# This .editorconfig section approximates ktfmt's formatting rules. You can include it in an
|
||||
# existing .editorconfig file or use it standalone by copying it to <project root>/.editorconfig
|
||||
# and making sure your editor is set to read settings from .editorconfig files.
|
||||
#
|
||||
# It includes editor-specific config options for IntelliJ IDEA.
|
||||
#
|
||||
# If any option is wrong, PR are welcome
|
||||
|
||||
[{*.kt,*.kts}]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
indent_size = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_java_names_count_to_use_import_on_demand = 9999
|
||||
ij_kotlin_align_in_columns_case_branch = false
|
||||
ij_kotlin_align_multiline_binary_operation = false
|
||||
ij_kotlin_align_multiline_extends_list = false
|
||||
ij_kotlin_align_multiline_method_parentheses = false
|
||||
ij_kotlin_align_multiline_parameters = true
|
||||
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_assignment_wrap = normal
|
||||
ij_kotlin_blank_lines_after_class_header = 0
|
||||
ij_kotlin_blank_lines_around_block_when_branches = 0
|
||||
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
|
||||
ij_kotlin_block_comment_at_first_column = true
|
||||
ij_kotlin_call_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_call_parameters_right_paren_on_new_line = false
|
||||
ij_kotlin_call_parameters_wrap = on_every_item
|
||||
ij_kotlin_catch_on_new_line = false
|
||||
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
ij_kotlin_continuation_indent_for_chained_calls = true
|
||||
ij_kotlin_continuation_indent_for_expression_bodies = true
|
||||
ij_kotlin_continuation_indent_in_argument_lists = true
|
||||
ij_kotlin_continuation_indent_in_elvis = false
|
||||
ij_kotlin_continuation_indent_in_if_conditions = false
|
||||
ij_kotlin_continuation_indent_in_parameter_lists = false
|
||||
ij_kotlin_continuation_indent_in_supertype_lists = false
|
||||
ij_kotlin_else_on_new_line = false
|
||||
ij_kotlin_enum_constants_wrap = off
|
||||
ij_kotlin_extends_list_wrap = normal
|
||||
ij_kotlin_field_annotation_wrap = split_into_lines
|
||||
ij_kotlin_finally_on_new_line = false
|
||||
ij_kotlin_if_rparen_on_new_line = false
|
||||
ij_kotlin_import_nested_classes = false
|
||||
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
|
||||
ij_kotlin_keep_blank_lines_before_right_brace = 2
|
||||
ij_kotlin_keep_blank_lines_in_code = 2
|
||||
ij_kotlin_keep_blank_lines_in_declarations = 2
|
||||
ij_kotlin_keep_first_column_comment = true
|
||||
ij_kotlin_keep_indents_on_empty_lines = false
|
||||
ij_kotlin_keep_line_breaks = true
|
||||
ij_kotlin_lbrace_on_next_line = false
|
||||
ij_kotlin_line_comment_add_space = false
|
||||
ij_kotlin_line_comment_at_first_column = true
|
||||
ij_kotlin_method_annotation_wrap = split_into_lines
|
||||
ij_kotlin_method_call_chain_wrap = normal
|
||||
ij_kotlin_method_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_method_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_method_parameters_wrap = on_every_item
|
||||
ij_kotlin_name_count_to_use_star_import = 9999
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 9999
|
||||
ij_kotlin_parameter_annotation_wrap = off
|
||||
ij_kotlin_space_after_comma = true
|
||||
ij_kotlin_space_after_extend_colon = true
|
||||
ij_kotlin_space_after_type_colon = true
|
||||
ij_kotlin_space_before_catch_parentheses = true
|
||||
ij_kotlin_space_before_comma = false
|
||||
ij_kotlin_space_before_extend_colon = true
|
||||
ij_kotlin_space_before_for_parentheses = true
|
||||
ij_kotlin_space_before_if_parentheses = true
|
||||
ij_kotlin_space_before_lambda_arrow = true
|
||||
ij_kotlin_space_before_type_colon = false
|
||||
ij_kotlin_space_before_when_parentheses = true
|
||||
ij_kotlin_space_before_while_parentheses = true
|
||||
ij_kotlin_spaces_around_additive_operators = true
|
||||
ij_kotlin_spaces_around_assignment_operators = true
|
||||
ij_kotlin_spaces_around_equality_operators = true
|
||||
ij_kotlin_spaces_around_function_type_arrow = true
|
||||
ij_kotlin_spaces_around_logical_operators = true
|
||||
ij_kotlin_spaces_around_multiplicative_operators = true
|
||||
ij_kotlin_spaces_around_range = false
|
||||
ij_kotlin_spaces_around_relational_operators = true
|
||||
ij_kotlin_spaces_around_unary_operator = false
|
||||
ij_kotlin_spaces_around_when_arrow = true
|
||||
ij_kotlin_variable_annotation_wrap = off
|
||||
ij_kotlin_while_on_new_line = false
|
||||
ij_kotlin_wrap_elvis_expressions = 1
|
||||
ij_kotlin_wrap_expression_body_functions = 1
|
||||
ij_kotlin_wrap_first_method_in_call_chain = false
|
@ -27,15 +27,11 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class StartActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<StartActivity>(
|
||||
intentAction = {
|
||||
putExtra(ARG_URL, TEST_FORMATTED_URL)
|
||||
}
|
||||
)
|
||||
val activityRule =
|
||||
activityRule<StartActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,7 @@ import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [SingletonComponent::class],
|
||||
replaces = [PrefFactoryModule::class]
|
||||
)
|
||||
@TestInstallIn(components = [SingletonComponent::class], replaces = [PrefFactoryModule::class])
|
||||
object PrefFactoryTestModule {
|
||||
@Provides
|
||||
fun factory(): KPrefFactory = KPrefFactoryInMemory
|
||||
@Provides fun factory(): KPrefFactory = KPrefFactoryInMemory
|
||||
}
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class AboutActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<AboutActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<AboutActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class DebugActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<DebugActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<DebugActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -27,15 +27,11 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class FrostWebActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<FrostWebActivity>(
|
||||
intentAction = {
|
||||
putExtra(ARG_URL, TEST_FORMATTED_URL)
|
||||
}
|
||||
)
|
||||
val activityRule =
|
||||
activityRule<FrostWebActivity>(intentAction = { putExtra(ARG_URL, TEST_FORMATTED_URL) })
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -29,6 +29,11 @@ import com.pitchedapps.frost.utils.ARG_TEXT
|
||||
import com.pitchedapps.frost.utils.isIndirectImageUrl
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
@ -42,27 +47,17 @@ import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.Timeout
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@HiltAndroidTest
|
||||
class ImageActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<ImageActivity>(
|
||||
intentAction = {
|
||||
putExtra(ARG_IMAGE_URL, TEST_FORMATTED_URL)
|
||||
}
|
||||
)
|
||||
val activityRule =
|
||||
activityRule<ImageActivity>(intentAction = { putExtra(ARG_IMAGE_URL, TEST_FORMATTED_URL) })
|
||||
|
||||
@get:Rule(order = 2)
|
||||
val globalTimeout: Timeout = Timeout.seconds(15)
|
||||
@get:Rule(order = 2) val globalTimeout: Timeout = Timeout.seconds(15)
|
||||
|
||||
lateinit var mockServer: MockWebServer
|
||||
|
||||
@ -77,12 +72,14 @@ class ImageActivityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() = launchScenario(mockServer.url("image").toString()) {
|
||||
fun initializesSuccessfully() =
|
||||
launchScenario(mockServer.url("image").toString()) {
|
||||
// Verify no crash
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validImageTest() = launchScenario(mockServer.url("image").toString()) {
|
||||
fun validImageTest() =
|
||||
launchScenario(mockServer.url("image").toString()) {
|
||||
mockServer.takeRequest()
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
// assertEquals(
|
||||
@ -103,7 +100,8 @@ class ImageActivityTest {
|
||||
|
||||
@Test
|
||||
@Ignore("apparently this fails")
|
||||
fun invalidImageTest() = launchScenario(mockServer.url("text").toString()) {
|
||||
fun invalidImageTest() =
|
||||
launchScenario(mockServer.url("text").toString()) {
|
||||
mockServer.takeRequest()
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
assertTrue(binding.error.isVisible, "Error should be shown")
|
||||
@ -113,20 +111,18 @@ class ImageActivityTest {
|
||||
// fabAction,
|
||||
// "Text should not be a valid image format, error state expected"
|
||||
// )
|
||||
assertEquals(
|
||||
"Image format not supported",
|
||||
errorRef?.message,
|
||||
"Error message mismatch"
|
||||
)
|
||||
assertEquals("Image format not supported", errorRef?.message, "Error message mismatch")
|
||||
assertFalse(tempFile?.exists() == true, "Temp file should have been removed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun errorTest() = launchScenario(mockServer.url("error").toString()) {
|
||||
fun errorTest() =
|
||||
launchScenario(mockServer.url("error").toString()) {
|
||||
mockServer.takeRequest()
|
||||
assertEquals(1, mockServer.requestCount, "One http request expected")
|
||||
assertTrue(binding.error.isVisible, "Error should be shown")
|
||||
// assertEquals(FabStates.ERROR, fabAction, "Error response code, error state expected")
|
||||
// assertEquals(FabStates.ERROR, fabAction, "Error response code, error state
|
||||
// expected")
|
||||
assertEquals(
|
||||
"Unsuccessful response for image: Error mock response",
|
||||
errorRef?.message,
|
||||
@ -151,27 +147,21 @@ class ImageActivityTest {
|
||||
putExtra(ARG_TEXT, text)
|
||||
putExtra(ARG_COOKIE, cookie)
|
||||
}
|
||||
ActivityScenario.launch<ImageActivity>(intent).use {
|
||||
it.onActivity(action)
|
||||
}
|
||||
ActivityScenario.launch<ImageActivity>(intent).use { it.onActivity(action) }
|
||||
}
|
||||
|
||||
private fun mockServer(): MockWebServer {
|
||||
val img = Buffer()
|
||||
img.writeAll(getResource("bayer-pattern.jpg").source())
|
||||
return MockWebServer().apply {
|
||||
dispatcher = object : Dispatcher() {
|
||||
dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
when {
|
||||
request.path?.contains("text") == true -> MockResponse().setResponseCode(200)
|
||||
.setBody(
|
||||
"Valid mock text response"
|
||||
)
|
||||
request.path?.contains("image") == true -> MockResponse().setResponseCode(
|
||||
200
|
||||
).setBody(
|
||||
img
|
||||
)
|
||||
request.path?.contains("text") == true ->
|
||||
MockResponse().setResponseCode(200).setBody("Valid mock text response")
|
||||
request.path?.contains("image") == true ->
|
||||
MockResponse().setResponseCode(200).setBody(img)
|
||||
else -> MockResponse().setResponseCode(404).setBody("Error mock response")
|
||||
}
|
||||
}
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class IntroActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<IntroActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<IntroActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class LoginActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<LoginActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<LoginActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class MainActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<MainActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<MainActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class SelectorActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<SelectorActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<SelectorActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class SettingActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<SettingsActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<SettingsActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class TabCustomizerActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<TabCustomizerActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<TabCustomizerActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -25,11 +25,9 @@ import org.junit.Test
|
||||
@HiltAndroidTest
|
||||
class WebOverlayActivityTest {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hildAndroidRule = HiltAndroidRule(this)
|
||||
@get:Rule(order = 0) val hildAndroidRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val activityRule = activityRule<AboutActivity>()
|
||||
@get:Rule(order = 1) val activityRule = activityRule<AboutActivity>()
|
||||
|
||||
@Test
|
||||
fun initializesSuccessfully() {
|
||||
|
@ -20,9 +20,9 @@ import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
abstract class BaseDbTest {
|
||||
@ -32,12 +32,8 @@ abstract class BaseDbTest {
|
||||
@BeforeTest
|
||||
fun before() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val privateDb = Room.inMemoryDatabaseBuilder(
|
||||
context, FrostPrivateDatabase::class.java
|
||||
).build()
|
||||
val publicDb = Room.inMemoryDatabaseBuilder(
|
||||
context, FrostPublicDatabase::class.java
|
||||
).build()
|
||||
val privateDb = Room.inMemoryDatabaseBuilder(context, FrostPrivateDatabase::class.java).build()
|
||||
val publicDb = Room.inMemoryDatabaseBuilder(context, FrostPublicDatabase::class.java).build()
|
||||
db = FrostDatabase(privateDb, publicDb)
|
||||
}
|
||||
|
||||
|
@ -16,16 +16,18 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.db
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class CacheDbTest : BaseDbTest() {
|
||||
|
||||
private val dao get() = db.cacheDao()
|
||||
private val cookieDao get() = db.cookieDao()
|
||||
private val dao
|
||||
get() = db.cacheDao()
|
||||
private val cookieDao
|
||||
get() = db.cookieDao()
|
||||
|
||||
private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
|
||||
|
||||
|
@ -16,14 +16,15 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.db
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class CookieDbTest : BaseDbTest() {
|
||||
|
||||
private val dao get() = db.cookieDao()
|
||||
private val dao
|
||||
get() = db.cookieDao()
|
||||
|
||||
@Test
|
||||
fun basicCookie() {
|
||||
|
@ -21,8 +21,8 @@ import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CookieMigrationTest {
|
||||
@ -31,7 +31,8 @@ class CookieMigrationTest {
|
||||
|
||||
private val ALL_MIGRATIONS = arrayOf(COOKIES_MIGRATION_1_2)
|
||||
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
val helper: MigrationTestHelper =
|
||||
MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
FrostPrivateDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
@ -40,9 +41,7 @@ class CookieMigrationTest {
|
||||
@Test
|
||||
fun migrateAll() {
|
||||
// Create earliest version of the database.
|
||||
helper.createDatabase(TEST_DB, 1).apply {
|
||||
close()
|
||||
}
|
||||
helper.createDatabase(TEST_DB, 1).apply { close() }
|
||||
|
||||
// Open latest version of the database. Room will validate the schema
|
||||
// once all migrations execute.
|
||||
@ -50,7 +49,10 @@ class CookieMigrationTest {
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
FrostPrivateDatabase::class.java,
|
||||
TEST_DB
|
||||
).addMigrations(*ALL_MIGRATIONS).build().apply {
|
||||
)
|
||||
.addMigrations(*ALL_MIGRATIONS)
|
||||
.build()
|
||||
.apply {
|
||||
openHelper.writableDatabase
|
||||
close()
|
||||
}
|
||||
|
@ -18,20 +18,20 @@ package com.pitchedapps.frost.db
|
||||
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.facebook.defaultTabs
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class GenericDbTest : BaseDbTest() {
|
||||
|
||||
private val dao get() = db.genericDao()
|
||||
private val dao
|
||||
get() = db.genericDao()
|
||||
|
||||
/**
|
||||
* Note that order is also preserved here
|
||||
*/
|
||||
/** Note that order is also preserved here */
|
||||
@Test
|
||||
fun save() {
|
||||
val tabs = listOf(
|
||||
val tabs =
|
||||
listOf(
|
||||
FbItem.ACTIVITY_LOG,
|
||||
FbItem.BIRTHDAYS,
|
||||
FbItem.EVENTS,
|
||||
@ -49,9 +49,7 @@ class GenericDbTest : BaseDbTest() {
|
||||
|
||||
@Test
|
||||
fun defaultRetrieve() {
|
||||
runBlocking {
|
||||
assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed")
|
||||
}
|
||||
runBlocking { assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed") }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -19,19 +19,21 @@ package com.pitchedapps.frost.db
|
||||
import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
|
||||
import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
|
||||
import com.pitchedapps.frost.services.NotificationContent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class NotificationDbTest : BaseDbTest() {
|
||||
|
||||
private val dao get() = db.notifDao()
|
||||
private val dao
|
||||
get() = db.notifDao()
|
||||
|
||||
private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
|
||||
|
||||
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) = NotificationContent(
|
||||
private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) =
|
||||
NotificationContent(
|
||||
data = cookie,
|
||||
id = id,
|
||||
href = "",
|
||||
@ -94,7 +96,8 @@ class NotificationDbTest : BaseDbTest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary key is both id and userId, in the event that the same notification to multiple users has the same id
|
||||
* Primary key is both id and userId, in the event that the same notification to multiple users
|
||||
* has the same id
|
||||
*/
|
||||
@Test
|
||||
fun primaryKeyCheck() {
|
||||
@ -130,18 +133,10 @@ class NotificationDbTest : BaseDbTest() {
|
||||
// Unique unsorted ids
|
||||
val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
-1L,
|
||||
dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL),
|
||||
"Default epoch failed"
|
||||
)
|
||||
assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed")
|
||||
db.cookieDao().save(cookie)
|
||||
dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
|
||||
assertEquals(
|
||||
99L,
|
||||
dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL),
|
||||
"Latest epoch failed"
|
||||
)
|
||||
assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,8 @@
|
||||
package com.pitchedapps.frost.facebook
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class FbCookieTest {
|
||||
|
||||
|
@ -28,8 +28,7 @@ import java.io.InputStream
|
||||
val context: Context
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
fun getAsset(asset: String): InputStream =
|
||||
context.assets.open(asset)
|
||||
fun getAsset(asset: String): InputStream = context.assets.open(asset)
|
||||
|
||||
private class Helper
|
||||
|
||||
@ -40,8 +39,7 @@ inline fun <reified A : Activity> activityRule(
|
||||
intentAction: Intent.() -> Unit = {},
|
||||
activityOptions: Bundle? = null
|
||||
): ActivityScenarioRule<A> {
|
||||
val intent =
|
||||
Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
|
||||
val intent = Intent(ApplicationProvider.getApplicationContext(), A::class.java).also(intentAction)
|
||||
return ActivityScenarioRule(intent, activityOptions)
|
||||
}
|
||||
|
||||
|
@ -37,23 +37,17 @@ import dagger.hilt.android.HiltAndroidApp
|
||||
import java.util.Random
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-28.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-05-28. */
|
||||
@HiltAndroidApp
|
||||
class FrostApp : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
@Inject
|
||||
lateinit var cookieDao: CookieDao
|
||||
@Inject lateinit var cookieDao: CookieDao
|
||||
|
||||
@Inject
|
||||
lateinit var notifDao: NotificationDao
|
||||
@Inject lateinit var notifDao: NotificationDao
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@ -72,7 +66,8 @@ class FrostApp : Application() {
|
||||
BigImageViewer.initialize(GlideImageLoader.with(this, httpClient))
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
registerActivityLifecycleCallbacks(
|
||||
object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
@ -88,7 +83,8 @@ class FrostApp : Application() {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
L.d { "Activity ${activity.localClassName} created" }
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,7 +94,8 @@ class FrostApp : Application() {
|
||||
L.shouldLog = {
|
||||
when (it) {
|
||||
Log.VERBOSE -> BuildConfig.DEBUG
|
||||
Log.INFO, Log.ERROR -> true
|
||||
Log.INFO,
|
||||
Log.ERROR -> true
|
||||
else -> BuildConfig.DEBUG || prefs.verboseLogging
|
||||
}
|
||||
}
|
||||
|
@ -45,30 +45,23 @@ import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.launchNewTask
|
||||
import com.pitchedapps.frost.utils.loadAssets
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-28.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-05-28. */
|
||||
@AndroidEntryPoint
|
||||
class StartActivity : KauBaseActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var fbCookie: FbCookie
|
||||
@Inject lateinit var fbCookie: FbCookie
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
@Inject
|
||||
lateinit var cookieDao: CookieDao
|
||||
@Inject lateinit var cookieDao: CookieDao
|
||||
|
||||
@Inject
|
||||
lateinit var genericDao: GenericDao
|
||||
@Inject lateinit var genericDao: GenericDao
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -106,10 +99,13 @@ class StartActivity : KauBaseActivity() {
|
||||
cookies.isEmpty() -> launchNewTask<LoginActivity>()
|
||||
// Has cookies but no selected account
|
||||
prefs.userId == -1L -> launchNewTask<SelectorActivity>(cookies)
|
||||
else -> startActivity<MainActivity>(
|
||||
else ->
|
||||
startActivity<MainActivity>(
|
||||
intentBuilder = {
|
||||
putParcelableArrayListExtra(EXTRA_COOKIES, cookies)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
)
|
||||
@ -121,21 +117,18 @@ class StartActivity : KauBaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showInvalidWebView() =
|
||||
showInvalidView(R.string.error_webview)
|
||||
private fun showInvalidWebView() = showInvalidView(R.string.error_webview)
|
||||
|
||||
private fun showInvalidSdkView() {
|
||||
val text = String.format(string(R.string.error_sdk), Build.VERSION.SDK_INT)
|
||||
showInvalidView(text)
|
||||
}
|
||||
|
||||
private fun showInvalidView(textRes: Int) =
|
||||
showInvalidView(string(textRes))
|
||||
private fun showInvalidView(textRes: Int) = showInvalidView(string(textRes))
|
||||
|
||||
private fun showInvalidView(text: String) {
|
||||
setContentView(R.layout.activity_invalid)
|
||||
findViewById<ImageView>(R.id.invalid_icon)
|
||||
.setIcon(GoogleMaterial.Icon.gmd_adb, -1, Color.WHITE)
|
||||
findViewById<ImageView>(R.id.invalid_icon).setIcon(GoogleMaterial.Icon.gmd_adb, -1, Color.WHITE)
|
||||
findViewById<TextView>(R.id.invalid_text).text = text
|
||||
}
|
||||
}
|
||||
|
@ -52,17 +52,13 @@ import com.pitchedapps.frost.utils.L
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-26.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-26. */
|
||||
@AndroidEntryPoint
|
||||
class AboutActivity : AboutActivityBase(null) {
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
override fun Configs.buildConfigs() {
|
||||
textColor = themeProvider.textColor
|
||||
@ -79,19 +75,17 @@ class AboutActivity : AboutActivityBase(null) {
|
||||
var clickCount = 0
|
||||
|
||||
override fun postInflateMainPage(adapter: FastItemThemedAdapter<GenericItem>) {
|
||||
/**
|
||||
* Frost may not be a library but we're conveying the same info
|
||||
*/
|
||||
val frost = Library(
|
||||
/** Frost may not be a library but we're conveying the same info */
|
||||
val frost =
|
||||
Library(
|
||||
uniqueId = "com.pitchedapps.frost",
|
||||
name = string(R.string.frost_name),
|
||||
developers = listOf(
|
||||
Developer(name = string(R.string.dev_name), organisationUrl = null)
|
||||
),
|
||||
developers = listOf(Developer(name = string(R.string.dev_name), organisationUrl = null)),
|
||||
website = string(R.string.github_url),
|
||||
description = string(R.string.frost_description),
|
||||
artifactVersion = BuildConfig.VERSION_NAME,
|
||||
licenses = setOf(
|
||||
licenses =
|
||||
setOf(
|
||||
License(
|
||||
spdxId = "gplv3",
|
||||
name = "GNU GPL v3",
|
||||
@ -106,10 +100,7 @@ class AboutActivity : AboutActivityBase(null) {
|
||||
adapter.onClickListener = { _, _, item, _ ->
|
||||
if (item is LibraryIItem) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastClick > 500)
|
||||
clickCount = 1
|
||||
else
|
||||
clickCount++
|
||||
if (now - lastClick > 500) clickCount = 1 else clickCount++
|
||||
lastClick = now
|
||||
if (clickCount == 8) {
|
||||
if (!prefs.debugSettings) {
|
||||
@ -126,8 +117,7 @@ class AboutActivity : AboutActivityBase(null) {
|
||||
}
|
||||
|
||||
class AboutLinks :
|
||||
AbstractItem<AboutLinks.ViewHolder>(),
|
||||
ThemableIItem by ThemableIItemDelegate() {
|
||||
AbstractItem<AboutLinks.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
|
||||
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
|
||||
|
||||
override val layoutRes: Int
|
||||
@ -151,9 +141,9 @@ class AboutActivity : AboutActivityBase(null) {
|
||||
|
||||
/**
|
||||
* There are a lot of constraints to be added to each item just to have them chained properly
|
||||
* My as well do it programmatically
|
||||
* Initializing the viewholder will setup the icons, scale type and background of all icons,
|
||||
* link their click listeners and chain them together via a horizontal spread
|
||||
* My as well do it programmatically Initializing the viewholder will setup the icons, scale
|
||||
* type and background of all icons, link their click listeners and chain them together via a
|
||||
* horizontal spread
|
||||
*/
|
||||
init {
|
||||
val c = itemView.context
|
||||
@ -161,21 +151,17 @@ class AboutActivity : AboutActivityBase(null) {
|
||||
|
||||
val icons: Array<Pair<Int, () -> Unit>> =
|
||||
arrayOf(R.drawable.ic_fdroid_24 to { c.startLink(R.string.fdroid_url) })
|
||||
val iicons: Array<Pair<IIcon, () -> Unit>> = arrayOf(
|
||||
val iicons: Array<Pair<IIcon, () -> Unit>> =
|
||||
arrayOf(
|
||||
GoogleMaterial.Icon.gmd_file_download to { c.startLink(R.string.github_downloads_url) },
|
||||
CommunityMaterial.Icon3.cmd_reddit to { c.startLink(R.string.reddit_url) },
|
||||
CommunityMaterial.Icon2.cmd_github to { c.startLink(R.string.github_url) }
|
||||
)
|
||||
|
||||
images =
|
||||
(
|
||||
icons.map { (icon, onClick) -> c.drawable(icon) to onClick } + iicons.map { (icon, onClick) ->
|
||||
icon.toDrawable(
|
||||
c,
|
||||
32
|
||||
) to onClick
|
||||
}
|
||||
).mapIndexed { i, (icon, onClick) ->
|
||||
(icons.map { (icon, onClick) -> c.drawable(icon) to onClick } +
|
||||
iicons.map { (icon, onClick) -> icon.toDrawable(c, 32) to onClick })
|
||||
.mapIndexed { i, (icon, onClick) ->
|
||||
ImageView(c).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(size, size)
|
||||
id = 109389 + i
|
||||
|
@ -28,27 +28,19 @@ import com.pitchedapps.frost.utils.ActivityThemer
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-12.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-12. */
|
||||
@AndroidEntryPoint
|
||||
abstract class BaseActivity : KauBaseActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var fbCookie: FbCookie
|
||||
@Inject lateinit var fbCookie: FbCookie
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
@Inject
|
||||
lateinit var activityThemer: ActivityThemer
|
||||
@Inject lateinit var activityThemer: ActivityThemer
|
||||
|
||||
/**
|
||||
* Inherited consumer to customize back press
|
||||
*/
|
||||
/** Inherited consumer to customize back press */
|
||||
protected open fun backConsumer(): Boolean = false
|
||||
|
||||
final override fun onBackPressed() {
|
||||
|
@ -130,9 +130,9 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.qualifiers.ActivityContext
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 20/12/17.
|
||||
@ -141,27 +141,20 @@ import kotlin.math.abs
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
abstract class BaseMainActivity :
|
||||
BaseActivity(),
|
||||
MainActivityContract,
|
||||
VideoViewHolder,
|
||||
SearchViewHolder {
|
||||
BaseActivity(), MainActivityContract, VideoViewHolder, SearchViewHolder {
|
||||
|
||||
/**
|
||||
* Note that tabs themselves are initialized through a coroutine during onCreate
|
||||
*/
|
||||
/** Note that tabs themselves are initialized through a coroutine during onCreate */
|
||||
protected val adapter: SectionsPagerAdapter = SectionsPagerAdapter()
|
||||
override val frameWrapper: FrameLayout get() = drawerWrapperBinding.mainContainer
|
||||
override val frameWrapper: FrameLayout
|
||||
get() = drawerWrapperBinding.mainContainer
|
||||
lateinit var drawerWrapperBinding: ActivityMainDrawerWrapperBinding
|
||||
lateinit var contentBinding: ActivityMainContentBinding
|
||||
|
||||
@Inject
|
||||
lateinit var cookieDao: CookieDao
|
||||
@Inject lateinit var cookieDao: CookieDao
|
||||
|
||||
@Inject
|
||||
lateinit var genericDao: GenericDao
|
||||
@Inject lateinit var genericDao: GenericDao
|
||||
|
||||
@Inject
|
||||
lateinit var webFileChooser: WebFileChooser
|
||||
@Inject lateinit var webFileChooser: WebFileChooser
|
||||
|
||||
interface ActivityMainContentBinding {
|
||||
val root: View
|
||||
@ -186,7 +179,8 @@ abstract class BaseMainActivity :
|
||||
val start = System.currentTimeMillis()
|
||||
drawerWrapperBinding = ActivityMainDrawerWrapperBinding.inflate(layoutInflater)
|
||||
setContentView(drawerWrapperBinding.root)
|
||||
contentBinding = when (prefs.mainActivityLayout) {
|
||||
contentBinding =
|
||||
when (prefs.mainActivityLayout) {
|
||||
MainActivityLayout.TOP_BAR -> {
|
||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@ -226,9 +220,7 @@ abstract class BaseMainActivity :
|
||||
}
|
||||
onNestedCreate(savedInstanceState)
|
||||
L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" }
|
||||
launch {
|
||||
adapter.setPages(genericDao.getTabs())
|
||||
}
|
||||
launch { adapter.setPages(genericDao.getTabs()) }
|
||||
controlWebview = WebView(this)
|
||||
if (BuildConfig.VERSION_CODE > prefs.versionCode) {
|
||||
prefs.prevVersionCode = prefs.versionCode
|
||||
@ -251,9 +243,7 @@ abstract class BaseMainActivity :
|
||||
lastAccessTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
* Injector to handle creation for sub classes
|
||||
*/
|
||||
/** Injector to handle creation for sub classes */
|
||||
protected abstract fun onNestedCreate(savedInstanceState: Bundle?)
|
||||
|
||||
private var hasFab = false
|
||||
@ -291,8 +281,11 @@ abstract class BaseMainActivity :
|
||||
|
||||
private fun ActivityMainDrawerWrapperBinding.initDrawer() {
|
||||
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this@BaseMainActivity, drawer, contentBinding.toolbar,
|
||||
val toggle =
|
||||
ActionBarDrawerToggle(
|
||||
this@BaseMainActivity,
|
||||
drawer,
|
||||
contentBinding.toolbar,
|
||||
R.string.open,
|
||||
R.string.close
|
||||
)
|
||||
@ -364,9 +357,7 @@ abstract class BaseMainActivity :
|
||||
fab.setOnClickListener { clickEvent() }
|
||||
if (shouldShow) {
|
||||
if (fab.isShown) {
|
||||
fab.fadeScaleTransition {
|
||||
setIcon(iicon, color = themeProvider.iconColor)
|
||||
}
|
||||
fab.fadeScaleTransition { setIcon(iicon, color = themeProvider.iconColor) }
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -396,23 +387,24 @@ abstract class BaseMainActivity :
|
||||
private var orderedAccounts: List<CookieEntity> = cookies()
|
||||
private var pendingUpdate: Boolean = false
|
||||
private val binding = ViewNavHeaderBinding.inflate(layoutInflater)
|
||||
val root: View get() = binding.root
|
||||
private val optionsBackground = themeProvider.bgColor.withMinAlpha(200).colorToForeground(
|
||||
0.1f
|
||||
)
|
||||
val root: View
|
||||
get() = binding.root
|
||||
private val optionsBackground = themeProvider.bgColor.withMinAlpha(200).colorToForeground(0.1f)
|
||||
|
||||
init {
|
||||
setPrimary(prefs.userId)
|
||||
binding.updateAccounts()
|
||||
with(drawerWrapperBinding) {
|
||||
drawer.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
|
||||
drawer.addDrawerListener(
|
||||
object : DrawerLayout.SimpleDrawerListener() {
|
||||
override fun onDrawerClosed(drawerView: View) {
|
||||
if (drawerView !== navigation) return
|
||||
if (!pendingUpdate) return
|
||||
pendingUpdate = false
|
||||
binding.updateAccounts()
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
with(binding) {
|
||||
optionsContainer.setBackgroundColor(optionsBackground)
|
||||
@ -423,39 +415,24 @@ abstract class BaseMainActivity :
|
||||
if (showOptions) {
|
||||
animator.apply {
|
||||
withAnimator(optionsContainer.height, 0) {
|
||||
optionsContainer.updateLayoutParams {
|
||||
height = it
|
||||
}
|
||||
}
|
||||
withAnimator(arrow.rotation, 0f) {
|
||||
arrow.rotation = it
|
||||
}
|
||||
withEndAction {
|
||||
optionsContainer.gone()
|
||||
optionsContainer.updateLayoutParams { height = it }
|
||||
}
|
||||
withAnimator(arrow.rotation, 0f) { arrow.rotation = it }
|
||||
withEndAction { optionsContainer.gone() }
|
||||
}
|
||||
} else {
|
||||
optionsContainer.visible()
|
||||
animator.apply {
|
||||
withAnimator(
|
||||
optionsContainer.height,
|
||||
optionsContainer.unboundedHeight
|
||||
) {
|
||||
optionsContainer.updateLayoutParams {
|
||||
height = it
|
||||
}
|
||||
withAnimator(optionsContainer.height, optionsContainer.unboundedHeight) {
|
||||
optionsContainer.updateLayoutParams { height = it }
|
||||
}
|
||||
withEndAction {
|
||||
// Sometimes, height remains the same as measured during collapse
|
||||
// if the animations are disabled.
|
||||
// We will resolve this by always falling back to wrap content afterwards
|
||||
optionsContainer.updateLayoutParams {
|
||||
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
withAnimator(arrow.rotation, 180f) {
|
||||
arrow.rotation = it
|
||||
optionsContainer.updateLayoutParams { height = ViewGroup.LayoutParams.WRAP_CONTENT }
|
||||
}
|
||||
withAnimator(arrow.rotation, 180f) { arrow.rotation = it }
|
||||
}
|
||||
}
|
||||
showOptions = !showOptions
|
||||
@ -496,10 +473,7 @@ abstract class BaseMainActivity :
|
||||
)
|
||||
positiveButton(R.string.kau_yes) {
|
||||
this@BaseMainActivity.launch {
|
||||
fbCookie.logout(
|
||||
this@BaseMainActivity,
|
||||
deleteCookie = true
|
||||
)
|
||||
fbCookie.logout(this@BaseMainActivity, deleteCookie = true)
|
||||
}
|
||||
}
|
||||
negativeButton(R.string.kau_no)
|
||||
@ -510,15 +484,11 @@ abstract class BaseMainActivity :
|
||||
}
|
||||
with(optionsAddAccount) {
|
||||
setOptionsIcon(GoogleMaterial.Icon.gmd_add)
|
||||
setOnClickListener {
|
||||
launchNewTask<LoginActivity>(clearStack = false)
|
||||
}
|
||||
setOnClickListener { launchNewTask<LoginActivity>(clearStack = false) }
|
||||
}
|
||||
with(optionsManageAccount) {
|
||||
setOptionsIcon(GoogleMaterial.Icon.gmd_settings)
|
||||
setOnClickListener {
|
||||
launchNewTask<SelectorActivity>(cookies(), false)
|
||||
}
|
||||
setOnClickListener { launchNewTask<SelectorActivity>(cookies(), false) }
|
||||
}
|
||||
arrow.setImageDrawable(
|
||||
GoogleMaterial.Icon.gmd_arrow_drop_down.toDrawable(
|
||||
@ -553,56 +523,37 @@ abstract class BaseMainActivity :
|
||||
val accountSize = dimenPixelSize(R.dimen.drawer_account_avatar_size)
|
||||
val textColor = themeProvider.textColor
|
||||
orderedAccounts.forEach { cookie ->
|
||||
val tv =
|
||||
TextView(
|
||||
this@BaseMainActivity,
|
||||
null,
|
||||
0,
|
||||
R.style.Main_DrawerAccountUserOptions
|
||||
)
|
||||
glide.load(profilePictureUrl(cookie.id)).transform(FrostGlide.circleCrop)
|
||||
.into(object : CustomTarget<Drawable>(accountSize, accountSize) {
|
||||
val tv = TextView(this@BaseMainActivity, null, 0, R.style.Main_DrawerAccountUserOptions)
|
||||
glide
|
||||
.load(profilePictureUrl(cookie.id))
|
||||
.transform(FrostGlide.circleCrop)
|
||||
.into(
|
||||
object : CustomTarget<Drawable>(accountSize, accountSize) {
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
placeholder,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(placeholder, null, null, null)
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
transition: Transition<in Drawable>?
|
||||
) {
|
||||
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
resource,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(resource, null, null, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
tv.text = cookie.name
|
||||
tv.setTextColor(textColor)
|
||||
tv.background = createNavDrawable(themeProvider.accentColor, optionsBackground)
|
||||
tv.setOnClickListener {
|
||||
switchAccount(cookie.id)
|
||||
}
|
||||
tv.setOnClickListener { switchAccount(cookie.id) }
|
||||
optionsAccountsContainer.addView(tv)
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeDrawer() {
|
||||
with(drawerWrapperBinding) {
|
||||
drawer.closeDrawer(navigation)
|
||||
}
|
||||
with(drawerWrapperBinding) { drawer.closeDrawer(navigation) }
|
||||
}
|
||||
|
||||
private fun ImageView.setAccount(
|
||||
cookie: CookieEntity?,
|
||||
primary: Boolean
|
||||
) {
|
||||
private fun ImageView.setAccount(cookie: CookieEntity?, primary: Boolean) {
|
||||
if (cookie == null) {
|
||||
invisible()
|
||||
setOnClickListener(null)
|
||||
@ -645,7 +596,8 @@ abstract class BaseMainActivity :
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
contentBinding.toolbar.tint(themeProvider.iconColor)
|
||||
setMenuIcons(
|
||||
menu, themeProvider.iconColor,
|
||||
menu,
|
||||
themeProvider.iconColor,
|
||||
R.id.action_settings to GoogleMaterial.Icon.gmd_settings,
|
||||
R.id.action_search to GoogleMaterial.Icon.gmd_search
|
||||
)
|
||||
@ -658,8 +610,7 @@ abstract class BaseMainActivity :
|
||||
bindSearchView(menu, R.id.action_search, themeProvider.iconColor) {
|
||||
textCallback = { query, searchView ->
|
||||
val results = searchViewCache[query]
|
||||
if (results != null)
|
||||
searchView.results = results
|
||||
if (results != null) searchView.results = results
|
||||
else {
|
||||
val data = SearchParser.query(fbCookie.webCookie, query)?.data?.results
|
||||
if (data != null) {
|
||||
@ -679,13 +630,9 @@ abstract class BaseMainActivity :
|
||||
}
|
||||
}
|
||||
textDebounceInterval = 300
|
||||
searchCallback =
|
||||
{ query, _ ->
|
||||
launchWebOverlay(
|
||||
"${FbItem._SEARCH.url}/?q=${query.urlEncode()}",
|
||||
fbCookie,
|
||||
prefs
|
||||
); true
|
||||
searchCallback = { query, _ ->
|
||||
launchWebOverlay("${FbItem._SEARCH.url}/?q=${query.urlEncode()}", fbCookie, prefs)
|
||||
true
|
||||
}
|
||||
closeListener = { _ -> searchViewCache.clear() }
|
||||
foregroundColor = themeProvider.textColor
|
||||
@ -702,11 +649,8 @@ abstract class BaseMainActivity :
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
intent.putParcelableArrayListExtra(EXTRA_COOKIES, cookies())
|
||||
val bundle =
|
||||
ActivityOptions.makeCustomAnimation(
|
||||
this,
|
||||
R.anim.kau_slide_in_right,
|
||||
R.anim.kau_fade_out
|
||||
).toBundle()
|
||||
ActivityOptions.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out)
|
||||
.toBundle()
|
||||
startActivityForResult(intent, ACTIVITY_SETTINGS, bundle)
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
@ -796,9 +740,7 @@ abstract class BaseMainActivity :
|
||||
}
|
||||
|
||||
override fun collapseAppBar() {
|
||||
with(contentBinding) {
|
||||
appbar.post { appbar.setExpanded(false) }
|
||||
}
|
||||
with(contentBinding) { appbar.post { appbar.setExpanded(false) } }
|
||||
}
|
||||
|
||||
override fun backConsumer(): Boolean {
|
||||
@ -827,7 +769,9 @@ abstract class BaseMainActivity :
|
||||
inline val currentFragment: BaseFragment?
|
||||
get() {
|
||||
val viewpager = contentBinding.viewpager
|
||||
return supportFragmentManager.findFragmentByTag("android:switcher:${viewpager.id}:${viewpager.currentItem}") as BaseFragment?
|
||||
return supportFragmentManager.findFragmentByTag(
|
||||
"android:switcher:${viewpager.id}:${viewpager.currentItem}"
|
||||
) as BaseFragment?
|
||||
}
|
||||
|
||||
override fun reloadFragment(fragment: BaseFragment) {
|
||||
@ -840,9 +784,7 @@ abstract class BaseMainActivity :
|
||||
|
||||
private val forcedFallbacks = mutableSetOf<String>()
|
||||
|
||||
/**
|
||||
* Update page list and prompt reload
|
||||
*/
|
||||
/** Update page list and prompt reload */
|
||||
fun setPages(pages: List<FbItem>) {
|
||||
this.pages.clear()
|
||||
this.pages.addAll(pages)
|
||||
@ -851,11 +793,12 @@ abstract class BaseMainActivity :
|
||||
tabs.removeAllTabs()
|
||||
this@SectionsPagerAdapter.pages.forEachIndexed { index, fbItem ->
|
||||
tabs.addTab(
|
||||
tabs.newTab()
|
||||
tabs
|
||||
.newTab()
|
||||
.setCustomView(
|
||||
BadgedIcon(this@BaseMainActivity).apply {
|
||||
iicon = fbItem.icon
|
||||
}.also {
|
||||
BadgedIcon(this@BaseMainActivity)
|
||||
.apply { iicon = fbItem.icon }
|
||||
.also {
|
||||
it.setAllAlpha(if (index == 0) SELECTED_TAB_ALPHA else UNSELECTED_TAB_ALPHA)
|
||||
}
|
||||
)
|
||||
@ -865,9 +808,7 @@ abstract class BaseMainActivity :
|
||||
viewpager.setCurrentItem(0, false)
|
||||
viewpager.offscreenPageLimit = pages.size
|
||||
// todo check if post is necessary
|
||||
viewpager.post {
|
||||
fragmentEmit(0)
|
||||
} // trigger hook so title is set
|
||||
viewpager.post { fragmentEmit(0) } // trigger hook so title is set
|
||||
}
|
||||
}
|
||||
|
||||
@ -878,8 +819,7 @@ abstract class BaseMainActivity :
|
||||
fun restoreInstanceState(savedInstanceState: Bundle) {
|
||||
forcedFallbacks.clear()
|
||||
forcedFallbacks.addAll(
|
||||
savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK)
|
||||
?: emptyList()
|
||||
savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK) ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
@ -920,8 +860,7 @@ abstract class BaseMainActivity :
|
||||
get() {
|
||||
if (prefs.mainActivityLayout == MainActivityLayout.BOTTOM_BAR)
|
||||
lowerVideoPaddingPointF.set(0f, contentBinding.toolbar.height.toFloat())
|
||||
else
|
||||
lowerVideoPaddingPointF.set(0f, 0f)
|
||||
else lowerVideoPaddingPointF.set(0f, 0f)
|
||||
return lowerVideoPaddingPointF
|
||||
}
|
||||
|
||||
@ -939,5 +878,7 @@ object MainActivityModule {
|
||||
@ActivityScoped
|
||||
fun contract(@ActivityContext context: Context): MainActivityContract =
|
||||
(context as? BaseMainActivity)
|
||||
?: throw IllegalArgumentException("${context::class.java.simpleName} does not implement MainActivityContract")
|
||||
?: throw IllegalArgumentException(
|
||||
"${context::class.java.simpleName} does not implement MainActivityContract"
|
||||
)
|
||||
}
|
||||
|
@ -35,15 +35,13 @@ import com.pitchedapps.frost.utils.ActivityThemer
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.createFreshDir
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 05/01/18.
|
||||
*/
|
||||
/** Created by Allan Wang on 05/01/18. */
|
||||
@AndroidEntryPoint
|
||||
class DebugActivity : KauBaseActivity() {
|
||||
|
||||
@ -54,11 +52,9 @@ class DebugActivity : KauBaseActivity() {
|
||||
fun baseDir(context: Context) = File(context.externalCacheDir, "offline_debug")
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var activityThemer: ActivityThemer
|
||||
@Inject lateinit var activityThemer: ActivityThemer
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
lateinit var binding: ActivityDebugBinding
|
||||
|
||||
@ -77,9 +73,7 @@ class DebugActivity : KauBaseActivity() {
|
||||
}
|
||||
setTitle(R.string.debug_frost)
|
||||
|
||||
activityThemer.setFrostColors {
|
||||
toolbar(toolbar)
|
||||
}
|
||||
activityThemer.setFrostColors { toolbar(toolbar) }
|
||||
debugWebview.loadUrl(FbItem.FEED.url)
|
||||
debugWebview.onPageFinished = { swipeRefresh.isRefreshing = false }
|
||||
|
||||
@ -101,19 +95,15 @@ class DebugActivity : KauBaseActivity() {
|
||||
parent.createFreshDir()
|
||||
|
||||
val body: String? = suspendCoroutine { cont ->
|
||||
debugWebview.evaluateJavascript(JsActions.RETURN_BODY.function) {
|
||||
cont.resume(it)
|
||||
}
|
||||
debugWebview.evaluateJavascript(JsActions.RETURN_BODY.function) { cont.resume(it) }
|
||||
}
|
||||
|
||||
val hasScreenshot: Boolean =
|
||||
debugWebview.getScreenshot(File(parent, "screenshot.png"))
|
||||
val hasScreenshot: Boolean = debugWebview.getScreenshot(File(parent, "screenshot.png"))
|
||||
|
||||
val intent = Intent()
|
||||
intent.putExtra(RESULT_URL, debugWebview.url)
|
||||
intent.putExtra(RESULT_SCREENSHOT, hasScreenshot)
|
||||
if (body != null)
|
||||
intent.putExtra(RESULT_BODY, body)
|
||||
if (body != null) intent.putExtra(RESULT_BODY, body)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
@ -136,9 +126,6 @@ class DebugActivity : KauBaseActivity() {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.debugWebview.canGoBack())
|
||||
binding.debugWebview.goBack()
|
||||
else
|
||||
super.onBackPressed()
|
||||
if (binding.debugWebview.canGoBack()) binding.debugWebview.goBack() else super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
@ -62,39 +62,32 @@ import com.pitchedapps.frost.utils.frostUriFromFile
|
||||
import com.pitchedapps.frost.utils.isIndirectImageUrl
|
||||
import com.pitchedapps.frost.utils.logFrostEvent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-07-15.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-07-15. */
|
||||
@AndroidEntryPoint
|
||||
class ImageActivity : KauBaseActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var activityThemer: ActivityThemer
|
||||
@Inject lateinit var activityThemer: ActivityThemer
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
@Volatile
|
||||
internal var errorRef: Throwable? = null
|
||||
@Volatile internal var errorRef: Throwable? = null
|
||||
|
||||
/**
|
||||
* Reference to the temporary file path
|
||||
*/
|
||||
internal val tempFile: File? get() = binding.imagePhoto.currentImageFile
|
||||
/** Reference to the temporary file path */
|
||||
internal val tempFile: File?
|
||||
get() = binding.imagePhoto.currentImageFile
|
||||
|
||||
private lateinit var dragHelper: ViewDragHelper
|
||||
|
||||
@ -110,8 +103,7 @@ class ImageActivity : KauBaseActivity() {
|
||||
private var bottomBehavior: BottomSheetBehavior<View>? = null
|
||||
|
||||
private val baseBackgroundColor: Int
|
||||
get() = if (prefs.blackMediaBg) Color.BLACK
|
||||
else themeProvider.bgColor.withMinAlpha(235)
|
||||
get() = if (prefs.blackMediaBg) Color.BLACK else themeProvider.bgColor.withMinAlpha(235)
|
||||
|
||||
private fun loadError(e: Throwable) {
|
||||
if (e.message?.contains("<!DOCTYPE html>") == true) {
|
||||
@ -121,10 +113,7 @@ class ImageActivity : KauBaseActivity() {
|
||||
}
|
||||
errorRef = e
|
||||
e.logFrostEvent("Image load error")
|
||||
with(binding) {
|
||||
if (imageProgress.isVisible)
|
||||
imageProgress.fadeOut()
|
||||
}
|
||||
with(binding) { if (imageProgress.isVisible) imageProgress.fadeOut() }
|
||||
tempFile?.delete()
|
||||
binding.error.fadeIn()
|
||||
}
|
||||
@ -135,13 +124,13 @@ class ImageActivity : KauBaseActivity() {
|
||||
return finish()
|
||||
}
|
||||
L.i { "Displaying image" }
|
||||
trueImageUrl = async(Dispatchers.IO) {
|
||||
val result = if (!imageUrl.isIndirectImageUrl) imageUrl
|
||||
trueImageUrl =
|
||||
async(Dispatchers.IO) {
|
||||
val result =
|
||||
if (!imageUrl.isIndirectImageUrl) imageUrl
|
||||
else cookie?.getFullSizedImageUrl(imageUrl) ?: imageUrl
|
||||
if (result != imageUrl)
|
||||
L.v { "Launching image with true url $result" }
|
||||
else
|
||||
L.v { "Launching image with url $result" }
|
||||
if (result != imageUrl) L.v { "Launching image with true url $result" }
|
||||
else L.v { "Launching image with url $result" }
|
||||
result
|
||||
}
|
||||
binding = ActivityImageBinding.inflate(layoutInflater)
|
||||
@ -154,14 +143,16 @@ class ImageActivity : KauBaseActivity() {
|
||||
|
||||
private fun ActivityImageBinding.showImage(url: String) {
|
||||
imagePhoto.showImage(Uri.parse(url))
|
||||
imagePhoto.setImageShownCallback(object : ImageShownCallback {
|
||||
imagePhoto.setImageShownCallback(
|
||||
object : ImageShownCallback {
|
||||
override fun onThumbnailShown() {}
|
||||
|
||||
override fun onMainImageShown() {
|
||||
imageProgress.fadeOut()
|
||||
imagePhoto.animate().alpha(1f).scaleXY(1f).start()
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ActivityImageBinding.init() {
|
||||
@ -172,12 +163,12 @@ class ImageActivity : KauBaseActivity() {
|
||||
imageText.gone()
|
||||
} else {
|
||||
imageText.setTextColor(if (prefs.blackMediaBg) Color.WHITE else themeProvider.textColor)
|
||||
imageText.setBackgroundColor(
|
||||
baseBackgroundColor.colorToForeground(0.2f).withAlpha(255)
|
||||
)
|
||||
imageText.setBackgroundColor(baseBackgroundColor.colorToForeground(0.2f).withAlpha(255))
|
||||
imageText.text = text
|
||||
bottomBehavior = BottomSheetBehavior.from<View>(imageText).apply {
|
||||
addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
bottomBehavior =
|
||||
BottomSheetBehavior.from<View>(imageText).apply {
|
||||
addBottomSheetCallback(
|
||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
imageText.alpha = slideOffset / 2 + 0.5f
|
||||
}
|
||||
@ -185,7 +176,8 @@ class ImageActivity : KauBaseActivity() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
// No op
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
imageText.bringToFront()
|
||||
}
|
||||
@ -202,14 +194,11 @@ class ImageActivity : KauBaseActivity() {
|
||||
invisible()
|
||||
setState(FabStates.ERROR)
|
||||
}
|
||||
download.apply {
|
||||
setState(FabStates.DOWNLOAD)
|
||||
}
|
||||
share.apply {
|
||||
setState(FabStates.SHARE)
|
||||
}
|
||||
download.apply { setState(FabStates.DOWNLOAD) }
|
||||
share.apply { setState(FabStates.SHARE) }
|
||||
|
||||
imagePhoto.setImageLoaderCallback(object : ImageLoader.Callback {
|
||||
imagePhoto.setImageLoaderCallback(
|
||||
object : ImageLoader.Callback {
|
||||
override fun onCacheHit(imageType: Int, image: File?) {}
|
||||
|
||||
override fun onCacheMiss(imageType: Int, image: File?) {}
|
||||
@ -225,12 +214,12 @@ class ImageActivity : KauBaseActivity() {
|
||||
override fun onFail(error: Exception) {
|
||||
loadError(error)
|
||||
}
|
||||
})
|
||||
|
||||
activityThemer.setFrostColors {
|
||||
themeWindow = false
|
||||
}
|
||||
dragHelper = ViewDragHelper.create(imageDrag, ViewDragCallback()).apply {
|
||||
)
|
||||
|
||||
activityThemer.setFrostColors { themeWindow = false }
|
||||
dragHelper =
|
||||
ViewDragHelper.create(imageDrag, ViewDragCallback()).apply {
|
||||
setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP or ViewDragHelper.EDGE_BOTTOM)
|
||||
}
|
||||
imageDrag.dragHelper = dragHelper
|
||||
@ -251,13 +240,7 @@ class ImageActivity : KauBaseActivity() {
|
||||
|
||||
override fun getViewVerticalDragRange(child: View): Int = child.height
|
||||
|
||||
override fun onViewPositionChanged(
|
||||
changedView: View,
|
||||
left: Int,
|
||||
top: Int,
|
||||
dx: Int,
|
||||
dy: Int
|
||||
) {
|
||||
override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
|
||||
super.onViewPositionChanged(changedView, left, top, dx, dy)
|
||||
with(binding) {
|
||||
// make sure that we are using the proper axis
|
||||
@ -284,7 +267,8 @@ class ImageActivity : KauBaseActivity() {
|
||||
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
|
||||
val overScrolled = scrollPercent > scrollThreshold
|
||||
val maxOffset = releasedChild.height + 10
|
||||
val finalTop = when {
|
||||
val finalTop =
|
||||
when {
|
||||
scrollToTop && (overScrolled || yvel < -dragHelper.minVelocity) -> -maxOffset
|
||||
!scrollToTop && (overScrolled || yvel > dragHelper.minVelocity) -> maxOffset
|
||||
else -> 0
|
||||
@ -315,7 +299,9 @@ internal enum class FabStates(
|
||||
ERROR(GoogleMaterial.Icon.gmd_error, { Color.WHITE }, Color.RED) {
|
||||
override fun onClick(activity: ImageActivity) {
|
||||
val err =
|
||||
activity.errorRef?.takeIf { it !is FileNotFoundException && it.message != "Image failed to decode using JPEG decoder" }
|
||||
activity.errorRef?.takeIf {
|
||||
it !is FileNotFoundException && it.message != "Image failed to decode using JPEG decoder"
|
||||
}
|
||||
?: return
|
||||
activity.materialDialog {
|
||||
title(R.string.kau_error)
|
||||
@ -339,7 +325,8 @@ internal enum class FabStates(
|
||||
val file = activity.tempFile ?: return
|
||||
try {
|
||||
val photoURI = activity.frostUriFromFile(file)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SEND).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(Intent.EXTRA_STREAM, photoURI)
|
||||
type = "image/png"
|
||||
@ -354,29 +341,28 @@ internal enum class FabStates(
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the fab look
|
||||
* If it's in view, give it some animations
|
||||
* Change the fab look If it's in view, give it some animations
|
||||
*
|
||||
* TODO investigate what is wrong with fadeScaleTransition
|
||||
*
|
||||
* https://github.com/AllanWang/KAU/issues/184
|
||||
*
|
||||
*/
|
||||
fun update(fab: FloatingActionButton, themeProvider: ThemeProvider) {
|
||||
val tint =
|
||||
if (backgroundTint != Int.MAX_VALUE) backgroundTint else themeProvider.accentColor
|
||||
val tint = if (backgroundTint != Int.MAX_VALUE) backgroundTint else themeProvider.accentColor
|
||||
val iconColor = iconColorProvider(themeProvider)
|
||||
if (fab.isHidden) {
|
||||
fab.setIcon(iicon, color = iconColor)
|
||||
fab.backgroundTintList = ColorStateList.valueOf(tint)
|
||||
fab.show()
|
||||
} else {
|
||||
fab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
fab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
fab.setIcon(iicon, color = iconColor)
|
||||
fab.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,30 +55,23 @@ import com.pitchedapps.frost.utils.launchNewTask
|
||||
import com.pitchedapps.frost.utils.loadAssets
|
||||
import com.pitchedapps.frost.widgets.NotificationWidget
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-07-25.
|
||||
*
|
||||
* A beautiful intro activity
|
||||
* Phone showcases are drawn via layers
|
||||
* A beautiful intro activity Phone showcases are drawn via layers
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class IntroActivity :
|
||||
KauBaseActivity(),
|
||||
ViewPager.PageTransformer,
|
||||
ViewPager.OnPageChangeListener {
|
||||
class IntroActivity : KauBaseActivity(), ViewPager.PageTransformer, ViewPager.OnPageChangeListener {
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
@Inject
|
||||
lateinit var activityThemer: ActivityThemer
|
||||
@Inject lateinit var activityThemer: ActivityThemer
|
||||
|
||||
lateinit var binding: ActivityIntroBinding
|
||||
private var barHasNext = true
|
||||
@ -132,9 +125,8 @@ class IntroActivity :
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformations are mainly handled on a per view basis
|
||||
* This makes the first fragment fade out as the second fragment comes in
|
||||
* All fragments are locked in position
|
||||
* Transformations are mainly handled on a per view basis This makes the first fragment fade out
|
||||
* as the second fragment comes in All fragments are locked in position
|
||||
*/
|
||||
override fun transformPage(page: View, position: Float) {
|
||||
// only apply to adjacent pages
|
||||
@ -155,28 +147,22 @@ class IntroActivity :
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
)
|
||||
binding.ripple.ripple(blue, x, y, 600) {
|
||||
postDelayed(1000) { finish() }
|
||||
}
|
||||
binding.ripple.ripple(blue, x, y, 600) { postDelayed(1000) { finish() } }
|
||||
val lastView: View? = fragments.last().view
|
||||
arrayOf<View?>(
|
||||
binding.skip, binding.indicator, binding.next,
|
||||
binding.skip,
|
||||
binding.indicator,
|
||||
binding.next,
|
||||
lastView?.findViewById(R.id.intro_title),
|
||||
lastView?.findViewById(R.id.intro_desc)
|
||||
).forEach {
|
||||
it?.animate()?.alpha(0f)?.setDuration(600)?.start()
|
||||
}
|
||||
)
|
||||
.forEach { it?.animate()?.alpha(0f)?.setDuration(600)?.start() }
|
||||
if (themeProvider.textColor != Color.WHITE) {
|
||||
val f = lastView?.findViewById<ImageView>(R.id.intro_image)?.drawable
|
||||
if (f != null)
|
||||
ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
addUpdateListener {
|
||||
f.setTint(
|
||||
themeProvider.textColor.blendWith(
|
||||
Color.WHITE,
|
||||
it.animatedValue as Float
|
||||
)
|
||||
)
|
||||
f.setTint(themeProvider.textColor.blendWith(Color.WHITE, it.animatedValue as Float))
|
||||
}
|
||||
duration = 600
|
||||
start()
|
||||
@ -211,13 +197,11 @@ class IntroActivity :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
}
|
||||
override fun onPageScrollStateChanged(state: Int) {}
|
||||
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||
fragments[position].onPageScrolled(positionOffset)
|
||||
if (position + 1 < fragments.size)
|
||||
fragments[position + 1].onPageScrolled(positionOffset - 1)
|
||||
if (position + 1 < fragments.size) fragments[position + 1].onPageScrolled(positionOffset - 1)
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
|
@ -49,6 +49,9 @@ import com.pitchedapps.frost.web.FrostEmitter
|
||||
import com.pitchedapps.frost.web.LoginWebView
|
||||
import com.pitchedapps.frost.web.asFrostEmitter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.net.UnknownHostException
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@ -63,18 +66,12 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.net.UnknownHostException
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-01.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-01. */
|
||||
@AndroidEntryPoint
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var cookieDao: CookieDao
|
||||
@Inject lateinit var cookieDao: CookieDao
|
||||
|
||||
private val toolbar: Toolbar by bindView(R.id.toolbar)
|
||||
private val web: LoginWebView by bindView(R.id.login_webview)
|
||||
@ -84,7 +81,8 @@ class LoginActivity : BaseActivity() {
|
||||
|
||||
private lateinit var profileLoader: RequestManager
|
||||
|
||||
private val refreshMutableFlow = MutableSharedFlow<Boolean>(
|
||||
private val refreshMutableFlow =
|
||||
MutableSharedFlow<Boolean>(
|
||||
extraBufferCapacity = 10,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
@ -98,15 +96,10 @@ class LoginActivity : BaseActivity() {
|
||||
setContentView(R.layout.activity_login)
|
||||
setSupportActionBar(toolbar)
|
||||
setTitle(R.string.kau_login)
|
||||
activityThemer.setFrostColors {
|
||||
toolbar(toolbar)
|
||||
}
|
||||
activityThemer.setFrostColors { toolbar(toolbar) }
|
||||
profileLoader = GlideApp.with(profile)
|
||||
|
||||
refreshFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach { swipeRefresh.isRefreshing = it }
|
||||
.launchIn(this)
|
||||
refreshFlow.distinctUntilChanged().onEach { swipeRefresh.isRefreshing = it }.launchIn(this)
|
||||
|
||||
launch {
|
||||
val cookie = web.loadLogin { refresh(it != 100) }.await()
|
||||
@ -153,16 +146,17 @@ class LoginActivity : BaseActivity() {
|
||||
*/
|
||||
val cookies = ArrayList(cookieDao.selectAll())
|
||||
delay(1000)
|
||||
if (prefs.intro)
|
||||
launchNewTask<IntroActivity>(cookies, true)
|
||||
else
|
||||
launchNewTask<MainActivity>(cookies, true)
|
||||
if (prefs.intro) launchNewTask<IntroActivity>(cookies, true)
|
||||
else launchNewTask<MainActivity>(cookies, true)
|
||||
}
|
||||
|
||||
private suspend fun loadProfile(id: Long): Boolean = withMainContext {
|
||||
suspendCancellableCoroutine<Boolean> { cont ->
|
||||
profileLoader.load(profilePictureUrl(id))
|
||||
.transform(FrostGlide.circleCrop).listener(object : RequestListener<Drawable> {
|
||||
profileLoader
|
||||
.load(profilePictureUrl(id))
|
||||
.transform(FrostGlide.circleCrop)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
@ -184,18 +178,19 @@ class LoginActivity : BaseActivity() {
|
||||
cont.resume(false)
|
||||
return false
|
||||
}
|
||||
}).into(profile)
|
||||
}
|
||||
)
|
||||
.into(profile)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadUsername(cookie: CookieEntity): String? = withContext(Dispatchers.IO) {
|
||||
val result: String? = try {
|
||||
withTimeout(5000) {
|
||||
frostJsoup(cookie.cookie, FbItem.PROFILE.url).title()
|
||||
}
|
||||
private suspend fun loadUsername(cookie: CookieEntity): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val result: String? =
|
||||
try {
|
||||
withTimeout(5000) { frostJsoup(cookie.cookie, FbItem.PROFILE.url).title() }
|
||||
} catch (e: Exception) {
|
||||
if (e !is UnknownHostException)
|
||||
e.logFrostEvent("Fetch username failed")
|
||||
if (e !is UnknownHostException) e.logFrostEvent("Fetch username failed")
|
||||
null
|
||||
}
|
||||
|
||||
|
@ -40,10 +40,8 @@ import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class MainActivity : BaseMainActivity() {
|
||||
|
||||
private val fragmentMutableFlow = MutableSharedFlow<Int>(
|
||||
extraBufferCapacity = 10,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
private val fragmentMutableFlow =
|
||||
MutableSharedFlow<Int>(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
override val fragmentFlow: SharedFlow<Int> = fragmentMutableFlow.asSharedFlow()
|
||||
override val fragmentEmit: FrostEmitter<Int> = fragmentMutableFlow.asFrostEmitter()
|
||||
|
||||
@ -59,7 +57,8 @@ class MainActivity : BaseMainActivity() {
|
||||
}
|
||||
|
||||
private fun ActivityMainContentBinding.setupViewPager() {
|
||||
viewpager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
||||
viewpager.addOnPageChangeListener(
|
||||
object : ViewPager.SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
if (lastPosition == position) {
|
||||
@ -89,12 +88,14 @@ class MainActivity : BaseMainActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ActivityMainContentBinding.setupTabs() {
|
||||
viewpager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
|
||||
tabs.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
|
||||
tabs.addOnTabSelectedListener(
|
||||
object : TabLayout.ViewPagerOnTabSelectedListener(viewpager) {
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
super.onTabReselected(tab)
|
||||
currentFragment?.onTabClick()
|
||||
@ -104,14 +105,12 @@ class MainActivity : BaseMainActivity() {
|
||||
super.onTabSelected(tab)
|
||||
(tab.customView as BadgedIcon).badgeText = null
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
headerFlow
|
||||
.filter { it.isNotBlank() }
|
||||
.mapNotNull { html ->
|
||||
BadgeParser.parseFromData(
|
||||
cookie = fbCookie.webCookie,
|
||||
text = html
|
||||
)?.data
|
||||
BadgeParser.parseFromData(cookie = fbCookie.webCookie, text = html)?.data
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
@ -32,9 +32,7 @@ import com.pitchedapps.frost.utils.launchNewTask
|
||||
import com.pitchedapps.frost.views.AccountItem
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-04.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-04. */
|
||||
class SelectorActivity : BaseActivity() {
|
||||
|
||||
val recycler: RecyclerView by bindView(R.id.selector_recycler)
|
||||
@ -49,7 +47,8 @@ class SelectorActivity : BaseActivity() {
|
||||
recycler.adapter = adapter
|
||||
adapter.add(cookies().map { AccountItem(it, themeProvider) })
|
||||
adapter.add(AccountItem(null, themeProvider)) // add account
|
||||
adapter.addEventHook(object : ClickEventHook<AccountItem>() {
|
||||
adapter.addEventHook(
|
||||
object : ClickEventHook<AccountItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? =
|
||||
(viewHolder as? AccountItem.ViewHolder)?.itemView
|
||||
|
||||
@ -60,12 +59,14 @@ class SelectorActivity : BaseActivity() {
|
||||
item: AccountItem
|
||||
) {
|
||||
if (item.cookie == null) this@SelectorActivity.launchNewTask<LoginActivity>()
|
||||
else launch {
|
||||
else
|
||||
launch {
|
||||
fbCookie.switchUser(item.cookie)
|
||||
launchNewTask<MainActivity>(cookies())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
activityThemer.setFrostColors {
|
||||
text(text)
|
||||
background(container)
|
||||
|
@ -59,30 +59,23 @@ import com.pitchedapps.frost.utils.frostNavigationBar
|
||||
import com.pitchedapps.frost.utils.launchNewTask
|
||||
import com.pitchedapps.frost.utils.loadAssets
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-06.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-06. */
|
||||
@AndroidEntryPoint
|
||||
class SettingsActivity : KPrefActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var fbCookie: FbCookie
|
||||
@Inject lateinit var fbCookie: FbCookie
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var themeProvider: ThemeProvider
|
||||
@Inject lateinit var themeProvider: ThemeProvider
|
||||
|
||||
@Inject
|
||||
lateinit var notifDao: NotificationDao
|
||||
@Inject lateinit var notifDao: NotificationDao
|
||||
|
||||
@Inject
|
||||
lateinit var activityThemer: ActivityThemer
|
||||
@Inject lateinit var activityThemer: ActivityThemer
|
||||
|
||||
private var resultFlag = Activity.RESULT_CANCELED
|
||||
|
||||
@ -99,8 +92,7 @@ class SettingsActivity : KPrefActivity() {
|
||||
if (fetchRingtone(requestCode, resultCode, data)) return
|
||||
when (requestCode) {
|
||||
ACTIVITY_REQUEST_TABS -> {
|
||||
if (resultCode == Activity.RESULT_OK)
|
||||
shouldRestartMain()
|
||||
if (resultCode == Activity.RESULT_OK) shouldRestartMain()
|
||||
return
|
||||
}
|
||||
ACTIVITY_REQUEST_DEBUG -> {
|
||||
@ -113,21 +105,15 @@ class SettingsActivity : KPrefActivity() {
|
||||
reloadList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ringtone and save uri
|
||||
* Returns [true] if consumed, [false] otherwise
|
||||
*/
|
||||
/** Fetch ringtone and save uri Returns [true] if consumed, [false] otherwise */
|
||||
private fun fetchRingtone(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode and REQUEST_RINGTONE != REQUEST_RINGTONE || resultCode != Activity.RESULT_OK) return false
|
||||
if (requestCode and REQUEST_RINGTONE != REQUEST_RINGTONE || resultCode != Activity.RESULT_OK)
|
||||
return false
|
||||
val uri = data?.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||
val uriString: String = uri?.toString() ?: ""
|
||||
if (uri != null) {
|
||||
try {
|
||||
grantUriPermission(
|
||||
"com.android.systemui",
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
grantUriPermission("com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "grantUriPermission" }
|
||||
}
|
||||
@ -189,9 +175,7 @@ class SettingsActivity : KPrefActivity() {
|
||||
onClick = {
|
||||
startActivityForResult<AboutActivity>(
|
||||
9,
|
||||
bundleBuilder = {
|
||||
withSceneTransitionAnimation(this@SettingsActivity)
|
||||
}
|
||||
bundleBuilder = { withSceneTransitionAnimation(this@SettingsActivity) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -240,13 +224,9 @@ class SettingsActivity : KPrefActivity() {
|
||||
}
|
||||
|
||||
fun themeExterior(animate: Boolean = true) {
|
||||
if (animate) bgCanvas.fade(themeProvider.bgColor)
|
||||
else bgCanvas.set(themeProvider.bgColor)
|
||||
if (animate) toolbarCanvas.ripple(
|
||||
themeProvider.headerColor,
|
||||
RippleCanvas.MIDDLE,
|
||||
RippleCanvas.END
|
||||
)
|
||||
if (animate) bgCanvas.fade(themeProvider.bgColor) else bgCanvas.set(themeProvider.bgColor)
|
||||
if (animate)
|
||||
toolbarCanvas.ripple(themeProvider.headerColor, RippleCanvas.MIDDLE, RippleCanvas.END)
|
||||
else toolbarCanvas.set(themeProvider.headerColor)
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
}
|
||||
@ -265,7 +245,8 @@ class SettingsActivity : KPrefActivity() {
|
||||
menuInflater.inflate(R.menu.menu_settings, menu)
|
||||
toolbar.tint(themeProvider.iconColor)
|
||||
setMenuIcons(
|
||||
menu, themeProvider.iconColor,
|
||||
menu,
|
||||
themeProvider.iconColor,
|
||||
R.id.action_github to CommunityMaterial.Icon2.cmd_github,
|
||||
R.id.action_changelog to GoogleMaterial.Icon.gmd_info
|
||||
)
|
||||
|
@ -43,19 +43,16 @@ import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.iitems.TabIItem
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Collections
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 26/11/17.
|
||||
*/
|
||||
/** Created by Allan Wang on 26/11/17. */
|
||||
@AndroidEntryPoint
|
||||
class TabCustomizerActivity : BaseActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var genericDao: GenericDao
|
||||
@Inject lateinit var genericDao: GenericDao
|
||||
|
||||
private val adapter = FastItemAdapter<TabIItem>()
|
||||
|
||||
@ -91,7 +88,10 @@ class TabCustomizerActivity : BaseActivity() {
|
||||
|
||||
bindSwapper(adapter, tabRecycler)
|
||||
|
||||
adapter.onClickListener = { view, _, _, _ -> view!!.wobble(); true }
|
||||
adapter.onClickListener = { view, _, _, _ ->
|
||||
view!!.wobble()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
@ -109,9 +109,7 @@ class TabCustomizerActivity : BaseActivity() {
|
||||
fabCancel.setIcon(GoogleMaterial.Icon.gmd_close, themeProvider.iconColor)
|
||||
fabCancel.backgroundTintList = ColorStateList.valueOf(themeProvider.accentColor)
|
||||
fabCancel.setOnClickListener { finish() }
|
||||
activityThemer.setFrostColors {
|
||||
themeWindow = true
|
||||
}
|
||||
activityThemer.setFrostColors { themeWindow = true }
|
||||
}
|
||||
|
||||
private fun View.wobble() = startAnimation(wobble(context))
|
||||
@ -121,7 +119,8 @@ class TabCustomizerActivity : BaseActivity() {
|
||||
ItemTouchHelper(dragCallback).attachToRecyclerView(recycler)
|
||||
}
|
||||
|
||||
private fun swapper(adapter: FastItemAdapter<*>) = object : ItemTouchCallback {
|
||||
private fun swapper(adapter: FastItemAdapter<*>) =
|
||||
object : ItemTouchCallback {
|
||||
override fun itemTouchOnMove(oldPosition: Int, newPosition: Int): Boolean {
|
||||
Collections.swap(adapter.adapterItems, oldPosition, newPosition)
|
||||
adapter.notifyAdapterDataSetChanged()
|
||||
@ -131,10 +130,8 @@ class TabCustomizerActivity : BaseActivity() {
|
||||
override fun itemTouchDropped(oldPosition: Int, newPosition: Int) = Unit
|
||||
}
|
||||
|
||||
private class TabDragCallback(
|
||||
directions: Int,
|
||||
itemTouchCallback: ItemTouchCallback
|
||||
) : SimpleDragCallback(directions, itemTouchCallback) {
|
||||
private class TabDragCallback(directions: Int, itemTouchCallback: ItemTouchCallback) :
|
||||
SimpleDragCallback(directions, itemTouchCallback) {
|
||||
|
||||
private var draggingView: TabIItem.ViewHolder? = null
|
||||
|
||||
|
@ -64,6 +64,7 @@ import com.pitchedapps.frost.views.FrostContentWeb
|
||||
import com.pitchedapps.frost.views.FrostVideoViewer
|
||||
import com.pitchedapps.frost.views.FrostWebView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -71,7 +72,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-01.
|
||||
@ -83,8 +83,8 @@ import javax.inject.Inject
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used by notifications. Unlike the other overlays, this runs as a singleInstance
|
||||
* Going back will bring you back to the previous app
|
||||
* Used by notifications. Unlike the other overlays, this runs as a singleInstance Going back will
|
||||
* bring you back to the previous app
|
||||
*/
|
||||
class FrostWebActivity : WebOverlayActivityBase() {
|
||||
|
||||
@ -110,8 +110,8 @@ class FrostWebActivity : WebOverlayActivityBase() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse the action url
|
||||
* Returns [true] if no action exists or if the action has been consumed, [false] if we need to notify the user of a bad action
|
||||
* Attempts to parse the action url Returns [true] if no action exists or if the action has been
|
||||
* consumed, [false] if we need to notify the user of a bad action
|
||||
*/
|
||||
private fun parseActionSend(): Boolean {
|
||||
if (intent.action != Intent.ACTION_SEND || intent.type != "text/plain") return true
|
||||
@ -133,27 +133,26 @@ class FrostWebActivity : WebOverlayActivityBase() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant that forces a mobile user agent. This is largely internal,
|
||||
* and is only necessary when we are launching from an existing [WebOverlayActivityBase]
|
||||
* Variant that forces a mobile user agent. This is largely internal, and is only necessary when we
|
||||
* are launching from an existing [WebOverlayActivityBase]
|
||||
*/
|
||||
class WebOverlayMobileActivity : WebOverlayActivityBase(USER_AGENT_MOBILE_CONST)
|
||||
|
||||
/**
|
||||
* Variant that forces a desktop user agent. This is largely internal,
|
||||
* and is only necessary when we are launching from an existing [WebOverlayActivityBase]
|
||||
* Variant that forces a desktop user agent. This is largely internal, and is only necessary when we
|
||||
* are launching from an existing [WebOverlayActivityBase]
|
||||
*/
|
||||
class WebOverlayDesktopActivity : WebOverlayActivityBase(USER_AGENT_DESKTOP_CONST)
|
||||
|
||||
/**
|
||||
* Internal overlay for the app; this is tied with the main task and is singleTop as opposed to singleInstance
|
||||
* Internal overlay for the app; this is tied with the main task and is singleTop as opposed to
|
||||
* singleInstance
|
||||
*/
|
||||
class WebOverlayActivity : WebOverlayActivityBase()
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT) :
|
||||
BaseActivity(),
|
||||
FrostContentContainer,
|
||||
VideoViewHolder {
|
||||
BaseActivity(), FrostContentContainer, VideoViewHolder {
|
||||
|
||||
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
|
||||
val toolbar: Toolbar by bindView(R.id.overlay_toolbar)
|
||||
@ -162,17 +161,14 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
|
||||
get() = content.coreView
|
||||
private val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
|
||||
|
||||
@Inject
|
||||
lateinit var webFileChooser: WebFileChooser
|
||||
@Inject lateinit var webFileChooser: WebFileChooser
|
||||
|
||||
private inline val urlTest: String?
|
||||
get() = intent.getStringExtra(ARG_URL) ?: intent.dataString
|
||||
|
||||
lateinit var swipeBack: SwipeBackContract
|
||||
|
||||
/**
|
||||
* Nonnull variant; verify by checking [urlTest]
|
||||
*/
|
||||
/** Nonnull variant; verify by checking [urlTest] */
|
||||
override val baseUrl: String
|
||||
get() = urlTest!!.formattedFbUrl
|
||||
|
||||
@ -240,9 +236,8 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage url loadings
|
||||
* This is usually only called when multiple listeners are added and inject the same url
|
||||
* We will avoid reloading if the url is the same
|
||||
* Manage url loadings This is usually only called when multiple listeners are added and inject
|
||||
* the same url We will avoid reloading if the url is the same
|
||||
*/
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
@ -256,14 +251,11 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
|
||||
}
|
||||
|
||||
override fun backConsumer(): Boolean {
|
||||
if (!web.onBackPressed())
|
||||
finishSlideOut()
|
||||
if (!web.onBackPressed()) finishSlideOut()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Our theme for the overlay should be fully opaque
|
||||
*/
|
||||
/** Our theme for the overlay should be fully opaque */
|
||||
fun theme() {
|
||||
val opaqueAccent = themeProvider.headerColor.withAlpha(255)
|
||||
statusBarColor = opaqueAccent.darken()
|
||||
@ -309,7 +301,8 @@ abstract class WebOverlayActivityBase(private val userAgent: String = USER_AGENT
|
||||
R.id.action_copy_link -> copyToClipboard(url)
|
||||
R.id.action_share -> shareText(url)
|
||||
R.id.action_open_in_browser -> startLink(url)
|
||||
else -> if (!OverlayContext.onOptionsItemSelected(web, item.itemId))
|
||||
else ->
|
||||
if (!OverlayContext.onOptionsItemSelected(web, item.itemId))
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
|
@ -31,9 +31,7 @@ interface MainActivityContract : MainFabContract {
|
||||
fun setTitle(res: Int)
|
||||
fun setTitle(text: CharSequence)
|
||||
|
||||
/**
|
||||
* Available on all threads
|
||||
*/
|
||||
/** Available on all threads */
|
||||
fun collapseAppBar()
|
||||
|
||||
fun reloadFragment(fragment: BaseFragment)
|
||||
|
@ -16,29 +16,18 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.contracts
|
||||
|
||||
/**
|
||||
* Functions that will modify the current ui
|
||||
*/
|
||||
/** Functions that will modify the current ui */
|
||||
interface DynamicUiContract {
|
||||
|
||||
/**
|
||||
* Change all necessary view components to the new theme
|
||||
* Also propagate where applicable
|
||||
*/
|
||||
/** Change all necessary view components to the new theme Also propagate where applicable */
|
||||
fun reloadTheme()
|
||||
|
||||
/**
|
||||
* Change theme without propagation
|
||||
*/
|
||||
/** Change theme without propagation */
|
||||
fun reloadThemeSelf()
|
||||
|
||||
/**
|
||||
* Change text size & propagate
|
||||
*/
|
||||
/** Change text size & propagate */
|
||||
fun reloadTextSize()
|
||||
|
||||
/**
|
||||
* Change text size without propagation
|
||||
*/
|
||||
/** Change text size without propagation */
|
||||
fun reloadTextSizeSelf()
|
||||
}
|
||||
|
@ -35,9 +35,7 @@ import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-07-04.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-07-04. */
|
||||
private const val MEDIA_CHOOSER_RESULT = 67
|
||||
|
||||
interface WebFileChooser {
|
||||
@ -46,17 +44,13 @@ interface WebFileChooser {
|
||||
fileChooserParams: WebChromeClient.FileChooserParams
|
||||
)
|
||||
|
||||
fun onActivityResultWeb(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
intent: Intent?
|
||||
): Boolean
|
||||
fun onActivityResultWeb(requestCode: Int, resultCode: Int, intent: Intent?): Boolean
|
||||
}
|
||||
|
||||
class WebFileChooserImpl @Inject internal constructor(
|
||||
private val activity: Activity,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : WebFileChooser {
|
||||
class WebFileChooserImpl
|
||||
@Inject
|
||||
internal constructor(private val activity: Activity, private val themeProvider: ThemeProvider) :
|
||||
WebFileChooser {
|
||||
private var filePathCallback: ValueCallback<Array<Uri>?>? = null
|
||||
|
||||
override fun openMediaPicker(
|
||||
@ -81,11 +75,7 @@ class WebFileChooserImpl @Inject internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResultWeb(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
intent: Intent?
|
||||
): Boolean {
|
||||
override fun onActivityResultWeb(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
|
||||
L.d { "FileChooser On activity results web $requestCode" }
|
||||
if (requestCode != MEDIA_CHOOSER_RESULT) return false
|
||||
val data = intent?.data
|
||||
@ -98,7 +88,5 @@ class WebFileChooserImpl @Inject internal constructor(
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
interface WebFileChooserModule {
|
||||
@Binds
|
||||
@ActivityScoped
|
||||
fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
|
||||
@Binds @ActivityScoped fun webFileChooser(to: WebFileChooserImpl): WebFileChooser
|
||||
}
|
||||
|
@ -22,53 +22,37 @@ import com.pitchedapps.frost.web.FrostEmitter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 20/12/17.
|
||||
*/
|
||||
/** Created by Allan Wang on 20/12/17. */
|
||||
|
||||
/**
|
||||
* Contract for the underlying parent,
|
||||
* binds to activities & fragments
|
||||
*/
|
||||
/** Contract for the underlying parent, binds to activities & fragments */
|
||||
interface FrostContentContainer : CoroutineScope {
|
||||
|
||||
val baseUrl: String
|
||||
|
||||
val baseEnum: FbItem?
|
||||
|
||||
/**
|
||||
* Update toolbar title
|
||||
*/
|
||||
/** Update toolbar title */
|
||||
fun setTitle(title: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract for components shared among
|
||||
* all content providers
|
||||
*/
|
||||
/** Contract for components shared among all content providers */
|
||||
interface FrostContentParent : DynamicUiContract {
|
||||
|
||||
val scope: CoroutineScope
|
||||
|
||||
val core: FrostContentCore
|
||||
|
||||
/**
|
||||
* Observable to get data on whether view is refreshing or not
|
||||
*/
|
||||
/** Observable to get data on whether view is refreshing or not */
|
||||
val refreshFlow: SharedFlow<Boolean>
|
||||
|
||||
val refreshEmit: FrostEmitter<Boolean>
|
||||
|
||||
/**
|
||||
* Observable to get data on refresh progress, with range [0, 100]
|
||||
*/
|
||||
/** Observable to get data on refresh progress, with range [0, 100] */
|
||||
val progressFlow: SharedFlow<Int>
|
||||
|
||||
val progressEmit: FrostEmitter<Int>
|
||||
|
||||
/**
|
||||
* Observable to get new title data (unique values only)
|
||||
*/
|
||||
/** Observable to get new title data (unique values only) */
|
||||
val titleFlow: SharedFlow<String>
|
||||
|
||||
val titleEmit: FrostEmitter<String>
|
||||
@ -77,104 +61,73 @@ interface FrostContentParent : DynamicUiContract {
|
||||
|
||||
var baseEnum: FbItem?
|
||||
|
||||
val swipeEnabled: Boolean get() = swipeAllowedByPage && !swipeDisabledByAction
|
||||
val swipeEnabled: Boolean
|
||||
get() = swipeAllowedByPage && !swipeDisabledByAction
|
||||
|
||||
/**
|
||||
* Temporary disable swiping based on action
|
||||
*/
|
||||
/** Temporary disable swiping based on action */
|
||||
var swipeDisabledByAction: Boolean
|
||||
|
||||
/**
|
||||
* Decides if swipe should be allowed for the current page
|
||||
*/
|
||||
/** Decides if swipe should be allowed for the current page */
|
||||
var swipeAllowedByPage: Boolean
|
||||
|
||||
/**
|
||||
* Binds the container to self
|
||||
* this will also handle all future bindings
|
||||
* Must be called by container!
|
||||
* Binds the container to self this will also handle all future bindings Must be called by
|
||||
* container!
|
||||
*/
|
||||
fun bind(container: FrostContentContainer)
|
||||
|
||||
/**
|
||||
* Signal that the contract will not be used again
|
||||
* Clean up resources where applicable
|
||||
*/
|
||||
/** Signal that the contract will not be used again Clean up resources where applicable */
|
||||
fun destroy()
|
||||
|
||||
/**
|
||||
* Hook onto the refresh observable for one cycle
|
||||
* Animate toggles between the fancy ripple and the basic fade
|
||||
* The cycle only starts on the first load since
|
||||
* there may have been another process when this is registered
|
||||
* Hook onto the refresh observable for one cycle Animate toggles between the fancy ripple and the
|
||||
* basic fade The cycle only starts on the first load since there may have been another process
|
||||
* when this is registered
|
||||
*
|
||||
* Returns true to proceed with load
|
||||
* In some cases when the url has not changed,
|
||||
* it may not be advisable to proceed with the load
|
||||
* For those cases, we will return false to stop it
|
||||
* Returns true to proceed with load In some cases when the url has not changed, it may not be
|
||||
* advisable to proceed with the load For those cases, we will return false to stop it
|
||||
*/
|
||||
fun registerTransition(urlChanged: Boolean, animate: Boolean): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Underlying contract for the content itself
|
||||
*/
|
||||
/** Underlying contract for the content itself */
|
||||
interface FrostContentCore : DynamicUiContract {
|
||||
|
||||
val scope: CoroutineScope
|
||||
get() = parent.scope
|
||||
|
||||
/**
|
||||
* Reference to parent
|
||||
* Bound through calling [FrostContentParent.bind]
|
||||
*/
|
||||
/** Reference to parent Bound through calling [FrostContentParent.bind] */
|
||||
val parent: FrostContentParent
|
||||
|
||||
/**
|
||||
* Initializes view through given [container]
|
||||
*
|
||||
* The content may be free to extract other data from
|
||||
* the container if necessary
|
||||
* The content may be free to extract other data from the container if necessary
|
||||
*/
|
||||
fun bind(parent: FrostContentParent, container: FrostContentContainer): View
|
||||
|
||||
/**
|
||||
* Call to reload wrapped data
|
||||
*/
|
||||
/** Call to reload wrapped data */
|
||||
fun reload(animate: Boolean)
|
||||
|
||||
/**
|
||||
* Call to reload base data
|
||||
*/
|
||||
/** Call to reload base data */
|
||||
fun reloadBase(animate: Boolean)
|
||||
|
||||
/**
|
||||
* If possible, remove anything in the view stack
|
||||
* Applies namely to webviews
|
||||
*/
|
||||
/** If possible, remove anything in the view stack Applies namely to webviews */
|
||||
fun clearHistory()
|
||||
|
||||
/**
|
||||
* Should be called when a back press is triggered
|
||||
* Return [true] if consumed, [false] otherwise
|
||||
* Should be called when a back press is triggered Return [true] if consumed, [false] otherwise
|
||||
*/
|
||||
fun onBackPressed(): Boolean
|
||||
|
||||
val currentUrl: String
|
||||
|
||||
/**
|
||||
* Condition to help pause certain background resources
|
||||
*/
|
||||
/** Condition to help pause certain background resources */
|
||||
var active: Boolean
|
||||
|
||||
/**
|
||||
* Triggered when view is within viewpager
|
||||
* and tab is clicked
|
||||
*/
|
||||
/** Triggered when view is within viewpager and tab is clicked */
|
||||
fun onTabClicked()
|
||||
|
||||
/**
|
||||
* Signal destruction to release some content manually
|
||||
*/
|
||||
/** Signal destruction to release some content manually */
|
||||
fun destroy()
|
||||
}
|
||||
|
@ -22,14 +22,14 @@ import android.widget.TextView
|
||||
/**
|
||||
* Created by Allan Wang on 2017-11-07.
|
||||
*
|
||||
* Should be implemented by all views in [com.pitchedapps.frost.activities.MainActivity]
|
||||
* to allow for instant view reloading
|
||||
* Should be implemented by all views in [com.pitchedapps.frost.activities.MainActivity] to allow
|
||||
* for instant view reloading
|
||||
*/
|
||||
interface FrostThemable {
|
||||
|
||||
/**
|
||||
* Change all necessary view components to the new theme
|
||||
* and call whatever other children that also implement [FrostThemable]
|
||||
* Change all necessary view components to the new theme and call whatever other children that
|
||||
* also implement [FrostThemable]
|
||||
*/
|
||||
fun reloadTheme()
|
||||
|
||||
|
@ -24,24 +24,17 @@ import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.views.FrostVideoContainerContract
|
||||
import com.pitchedapps.frost.views.FrostVideoViewer
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-11-10.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-11-10. */
|
||||
interface VideoViewHolder : FrameWrapper, FrostVideoContainerContract {
|
||||
|
||||
var videoViewer: FrostVideoViewer?
|
||||
|
||||
fun showVideo(url: String) = showVideo(url, false)
|
||||
|
||||
/**
|
||||
* Create new viewer and reuse existing one
|
||||
* The url will be formatted upon loading
|
||||
*/
|
||||
/** Create new viewer and reuse existing one The url will be formatted upon loading */
|
||||
fun showVideo(url: String, repeat: Boolean) {
|
||||
if (videoViewer != null)
|
||||
videoViewer?.setVideo(url, repeat)
|
||||
else
|
||||
videoViewer = FrostVideoViewer.showVideo(url, repeat, this)
|
||||
if (videoViewer != null) videoViewer?.setVideo(url, repeat)
|
||||
else videoViewer = FrostVideoViewer.showVideo(url, repeat, this)
|
||||
}
|
||||
|
||||
fun videoOnStop() = videoViewer?.pause()
|
||||
|
@ -26,24 +26,20 @@ import androidx.room.Query
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-30.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-05-30. */
|
||||
|
||||
/**
|
||||
* Generic cache to store serialized content
|
||||
*/
|
||||
/** Generic cache to store serialized content */
|
||||
@Entity(
|
||||
tableName = "frost_cache",
|
||||
primaryKeys = ["id", "type"],
|
||||
foreignKeys = [
|
||||
foreignKeys =
|
||||
[
|
||||
ForeignKey(
|
||||
entity = CookieEntity::class,
|
||||
parentColumns = ["cookie_id"],
|
||||
childColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)]
|
||||
)
|
||||
@Parcelize
|
||||
data class CacheEntity(
|
||||
@ -59,24 +55,17 @@ interface CacheDao {
|
||||
@Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type")
|
||||
fun _select(id: Long, type: String): CacheEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun _insertCache(cache: CacheEntity)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _insertCache(cache: CacheEntity)
|
||||
|
||||
@Query("DELETE FROM frost_cache WHERE id = :id AND type = :type")
|
||||
fun _delete(id: Long, type: String)
|
||||
}
|
||||
|
||||
suspend fun CacheDao.select(id: Long, type: String) = dao {
|
||||
_select(id, type)
|
||||
}
|
||||
suspend fun CacheDao.select(id: Long, type: String) = dao { _select(id, type) }
|
||||
|
||||
suspend fun CacheDao.delete(id: Long, type: String) = dao {
|
||||
_delete(id, type)
|
||||
}
|
||||
suspend fun CacheDao.delete(id: Long, type: String) = dao { _delete(id, type) }
|
||||
|
||||
/**
|
||||
* Returns true if successful, given that there are constraints to the insertion
|
||||
*/
|
||||
/** Returns true if successful, given that there are constraints to the insertion */
|
||||
suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = dao {
|
||||
try {
|
||||
_insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents))
|
||||
|
@ -28,16 +28,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.pitchedapps.frost.prefs.Prefs
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-30.
|
||||
*/
|
||||
|
||||
/** Created by Allan Wang on 2017-05-30. */
|
||||
@Entity(tableName = "cookies")
|
||||
@Parcelize
|
||||
data class CookieEntity(
|
||||
@androidx.room.PrimaryKey
|
||||
@ColumnInfo(name = "cookie_id")
|
||||
val id: Long,
|
||||
@androidx.room.PrimaryKey @ColumnInfo(name = "cookie_id") val id: Long,
|
||||
val name: String?,
|
||||
val cookie: String?,
|
||||
val cookieMessenger: String? = null // Version 2
|
||||
@ -51,35 +46,38 @@ data class CookieEntity(
|
||||
@Dao
|
||||
interface CookieDao {
|
||||
|
||||
@Query("SELECT * FROM cookies")
|
||||
fun _selectAll(): List<CookieEntity>
|
||||
@Query("SELECT * FROM cookies") fun _selectAll(): List<CookieEntity>
|
||||
|
||||
@Query("SELECT * FROM cookies WHERE cookie_id = :id")
|
||||
fun _selectById(id: Long): CookieEntity?
|
||||
@Query("SELECT * FROM cookies WHERE cookie_id = :id") fun _selectById(id: Long): CookieEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun _save(cookie: CookieEntity)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(cookie: CookieEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun _save(cookies: List<CookieEntity>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(cookies: List<CookieEntity>)
|
||||
|
||||
@Query("DELETE FROM cookies WHERE cookie_id = :id")
|
||||
fun _deleteById(id: Long)
|
||||
@Query("DELETE FROM cookies WHERE cookie_id = :id") fun _deleteById(id: Long)
|
||||
|
||||
@Query("UPDATE cookies SET cookieMessenger = :cookie WHERE cookie_id = :id")
|
||||
fun _updateMessengerCookie(id: Long, cookie: String?)
|
||||
}
|
||||
|
||||
suspend fun CookieDao.selectAll() = dao { _selectAll() }
|
||||
suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) }
|
||||
suspend fun CookieDao.save(cookie: CookieEntity) = dao { _save(cookie) }
|
||||
suspend fun CookieDao.save(cookies: List<CookieEntity>) = dao { _save(cookies) }
|
||||
suspend fun CookieDao.deleteById(id: Long) = dao { _deleteById(id) }
|
||||
suspend fun CookieDao.currentCookie(prefs: Prefs) = selectById(prefs.userId)
|
||||
suspend fun CookieDao.updateMessengerCookie(id: Long, cookie: String?) =
|
||||
dao { _updateMessengerCookie(id, cookie) }
|
||||
|
||||
val COOKIES_MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) }
|
||||
|
||||
suspend fun CookieDao.save(cookie: CookieEntity) = dao { _save(cookie) }
|
||||
|
||||
suspend fun CookieDao.save(cookies: List<CookieEntity>) = dao { _save(cookies) }
|
||||
|
||||
suspend fun CookieDao.deleteById(id: Long) = dao { _deleteById(id) }
|
||||
|
||||
suspend fun CookieDao.currentCookie(prefs: Prefs) = selectById(prefs.userId)
|
||||
|
||||
suspend fun CookieDao.updateMessengerCookie(id: Long, cookie: String?) = dao {
|
||||
_updateMessengerCookie(id, cookie)
|
||||
}
|
||||
|
||||
val COOKIES_MIGRATION_1_2 =
|
||||
object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE cookies ADD COLUMN cookieMessenger TEXT")
|
||||
}
|
||||
|
@ -20,9 +20,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Wraps dao calls to work with coroutines
|
||||
* Non transactional queries were supposed to be fixed in https://issuetracker.google.com/issues/69474692,
|
||||
* but it still requires dispatch from a non ui thread.
|
||||
* This avoids that constraint
|
||||
* Wraps dao calls to work with coroutines Non transactional queries were supposed to be fixed in
|
||||
* https://issuetracker.google.com/issues/69474692, but it still requires dispatch from a non ui
|
||||
* thread. This avoids that constraint
|
||||
*/
|
||||
suspend inline fun <T> dao(crossinline block: () -> T) = withContext(Dispatchers.IO) { block() }
|
||||
|
@ -60,16 +60,11 @@ interface FrostDao : FrostPrivateDao, FrostPublicDao {
|
||||
fun close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Composition of all database interfaces
|
||||
*/
|
||||
/** Composition of all database interfaces */
|
||||
class FrostDatabase(
|
||||
private val privateDb: FrostPrivateDatabase,
|
||||
private val publicDb: FrostPublicDatabase
|
||||
) :
|
||||
FrostDao,
|
||||
FrostPrivateDao by privateDb,
|
||||
FrostPublicDao by publicDb {
|
||||
) : FrostDao, FrostPrivateDao by privateDb, FrostPublicDao by publicDb {
|
||||
|
||||
override fun close() {
|
||||
privateDb.close()
|
||||
@ -86,14 +81,21 @@ class FrostDatabase(
|
||||
}
|
||||
|
||||
fun create(context: Context): FrostDatabase {
|
||||
val privateDb = Room.databaseBuilder(
|
||||
context, FrostPrivateDatabase::class.java,
|
||||
val privateDb =
|
||||
Room.databaseBuilder(
|
||||
context,
|
||||
FrostPrivateDatabase::class.java,
|
||||
FrostPrivateDatabase.DATABASE_NAME
|
||||
).addMigrations(COOKIES_MIGRATION_1_2).frostBuild()
|
||||
val publicDb = Room.databaseBuilder(
|
||||
context, FrostPublicDatabase::class.java,
|
||||
)
|
||||
.addMigrations(COOKIES_MIGRATION_1_2)
|
||||
.frostBuild()
|
||||
val publicDb =
|
||||
Room.databaseBuilder(
|
||||
context,
|
||||
FrostPublicDatabase::class.java,
|
||||
FrostPublicDatabase.DATABASE_NAME
|
||||
).frostBuild()
|
||||
)
|
||||
.frostBuild()
|
||||
return FrostDatabase(privateDb, publicDb)
|
||||
}
|
||||
}
|
||||
|
@ -25,31 +25,20 @@ import androidx.room.Query
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.facebook.defaultTabs
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-30.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-05-30. */
|
||||
|
||||
/**
|
||||
* Generic cache to store serialized content
|
||||
*/
|
||||
/** Generic cache to store serialized content */
|
||||
@Entity(tableName = "frost_generic")
|
||||
data class GenericEntity(
|
||||
@PrimaryKey
|
||||
val type: String,
|
||||
val contents: String
|
||||
)
|
||||
data class GenericEntity(@PrimaryKey val type: String, val contents: String)
|
||||
|
||||
@Dao
|
||||
interface GenericDao {
|
||||
|
||||
@Query("SELECT contents FROM frost_generic WHERE type = :type")
|
||||
fun _select(type: String): String?
|
||||
@Query("SELECT contents FROM frost_generic WHERE type = :type") fun _select(type: String): String?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun _save(entity: GenericEntity)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) fun _save(entity: GenericEntity)
|
||||
|
||||
@Query("DELETE FROM frost_generic WHERE type = :type")
|
||||
fun _delete(type: String)
|
||||
@Query("DELETE FROM frost_generic WHERE type = :type") fun _delete(type: String)
|
||||
|
||||
companion object {
|
||||
const val TYPE_TABS = "generic_tabs"
|
||||
@ -65,9 +54,6 @@ suspend fun GenericDao.saveTabs(tabs: List<FbItem>) = dao {
|
||||
|
||||
suspend fun GenericDao.getTabs(): List<FbItem> = dao {
|
||||
val allTabs = FbItem.values.map { it.name to it }.toMap()
|
||||
_select(GenericDao.TYPE_TABS)
|
||||
?.split(",")
|
||||
?.mapNotNull { allTabs[it] }
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
_select(GenericDao.TYPE_TABS)?.split(",")?.mapNotNull { allTabs[it] }?.takeIf { it.isNotEmpty() }
|
||||
?: defaultTabs()
|
||||
}
|
||||
|
@ -32,19 +32,18 @@ import com.pitchedapps.frost.utils.L
|
||||
@Entity(
|
||||
tableName = "notifications",
|
||||
primaryKeys = ["notif_id", "userId"],
|
||||
foreignKeys = [
|
||||
foreignKeys =
|
||||
[
|
||||
ForeignKey(
|
||||
entity = CookieEntity::class,
|
||||
parentColumns = ["cookie_id"],
|
||||
childColumns = ["userId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
)],
|
||||
indices = [Index("notif_id"), Index("userId")]
|
||||
)
|
||||
data class NotificationEntity(
|
||||
@ColumnInfo(name = "notif_id")
|
||||
val id: Long,
|
||||
@ColumnInfo(name = "notif_id") val id: Long,
|
||||
val userId: Long,
|
||||
val href: String,
|
||||
val title: String?,
|
||||
@ -72,12 +71,11 @@ data class NotificationEntity(
|
||||
}
|
||||
|
||||
data class NotificationContentEntity(
|
||||
@Embedded
|
||||
val cookie: CookieEntity,
|
||||
@Embedded
|
||||
val notif: NotificationEntity
|
||||
@Embedded val cookie: CookieEntity,
|
||||
@Embedded val notif: NotificationEntity
|
||||
) {
|
||||
fun toNotifContent() = NotificationContent(
|
||||
fun toNotifContent() =
|
||||
NotificationContent(
|
||||
data = cookie,
|
||||
id = notif.id,
|
||||
href = notif.href,
|
||||
@ -92,14 +90,16 @@ data class NotificationContentEntity(
|
||||
@Dao
|
||||
interface NotificationDao {
|
||||
|
||||
/**
|
||||
* Note that notifications are guaranteed to be ordered by descending timestamp
|
||||
*/
|
||||
/** Note that notifications are guaranteed to be ordered by descending timestamp */
|
||||
@Transaction
|
||||
@Query("SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC")
|
||||
@Query(
|
||||
"SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC"
|
||||
)
|
||||
fun _selectNotifications(userId: Long, type: String): List<NotificationContentEntity>
|
||||
|
||||
@Query("SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1")
|
||||
@Query(
|
||||
"SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1"
|
||||
)
|
||||
fun _selectEpoch(userId: Long, type: String): Long?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@ -108,12 +108,9 @@ interface NotificationDao {
|
||||
@Query("DELETE FROM notifications WHERE userId = :userId AND type = :type")
|
||||
fun _deleteNotifications(userId: Long, type: String)
|
||||
|
||||
@Query("DELETE FROM notifications")
|
||||
fun _deleteAll()
|
||||
@Query("DELETE FROM notifications") fun _deleteAll()
|
||||
|
||||
/**
|
||||
* It is assumed that the notification batch comes from the same user
|
||||
*/
|
||||
/** It is assumed that the notification batch comes from the same user */
|
||||
@Transaction
|
||||
fun _saveNotifications(type: String, notifs: List<NotificationContent>) {
|
||||
val userId = notifs.firstOrNull()?.data?.id ?: return
|
||||
@ -131,13 +128,9 @@ fun NotificationDao.selectNotificationsSync(userId: Long, type: String): List<No
|
||||
suspend fun NotificationDao.selectNotifications(
|
||||
userId: Long,
|
||||
type: String
|
||||
): List<NotificationContent> = dao {
|
||||
selectNotificationsSync(userId, type)
|
||||
}
|
||||
): List<NotificationContent> = dao { selectNotificationsSync(userId, type) }
|
||||
|
||||
/**
|
||||
* Returns true if successful, given that there are constraints to the insertion
|
||||
*/
|
||||
/** Returns true if successful, given that there are constraints to the insertion */
|
||||
suspend fun NotificationDao.saveNotifications(
|
||||
type: String,
|
||||
notifs: List<NotificationContent>
|
||||
|
@ -26,6 +26,12 @@ import com.pitchedapps.frost.utils.createFreshDir
|
||||
import com.pitchedapps.frost.utils.createFreshFile
|
||||
import com.pitchedapps.frost.utils.frostJsoup
|
||||
import com.pitchedapps.frost.utils.unescapeHtml
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -37,12 +43,6 @@ import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.Entities
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 04/01/18.
|
||||
@ -56,17 +56,15 @@ class OfflineWebsite(
|
||||
private val cookie: String = "",
|
||||
baseUrl: String? = null,
|
||||
private val html: String? = null,
|
||||
/**
|
||||
* Directory that holds all the files
|
||||
*/
|
||||
/** Directory that holds all the files */
|
||||
val baseDir: File,
|
||||
private val userAgent: String = USER_AGENT
|
||||
) {
|
||||
|
||||
/**
|
||||
* Supplied url without the queries
|
||||
*/
|
||||
private val baseUrl: String = baseUrl ?: run {
|
||||
/** Supplied url without the queries */
|
||||
private val baseUrl: String =
|
||||
baseUrl
|
||||
?: run {
|
||||
val url: HttpUrl = url.toHttpUrlOrNull() ?: throw IllegalArgumentException("Malformed url")
|
||||
return@run "${url.scheme}://${url.host}"
|
||||
}
|
||||
@ -88,18 +86,15 @@ class OfflineWebsite(
|
||||
|
||||
private val cssQueue = mutableSetOf<String>()
|
||||
|
||||
private fun request(url: String) = Request.Builder()
|
||||
.header("Cookie", cookie)
|
||||
.header("User-Agent", userAgent)
|
||||
.url(url)
|
||||
.get()
|
||||
.call()
|
||||
private fun request(url: String) =
|
||||
Request.Builder().header("Cookie", cookie).header("User-Agent", userAgent).url(url).get().call()
|
||||
|
||||
/**
|
||||
* Caller to bind callbacks and start the load
|
||||
* Callback is guaranteed to be called unless the load is cancelled
|
||||
* Caller to bind callbacks and start the load Callback is guaranteed to be called unless the load
|
||||
* is cancelled
|
||||
*/
|
||||
suspend fun load(progress: (Int) -> Unit = {}): Boolean = withContext(Dispatchers.IO) {
|
||||
suspend fun load(progress: (Int) -> Unit = {}): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
reset()
|
||||
|
||||
L.v { "Saving $url to ${baseDir.absolutePath}" }
|
||||
@ -181,8 +176,7 @@ class OfflineWebsite(
|
||||
fileQueue.clean().forEachIndexed { index, url ->
|
||||
yield()
|
||||
fileProgress(index)
|
||||
if (!downloadFile(url))
|
||||
return@withContext false
|
||||
if (!downloadFile(url)) return@withContext false
|
||||
}
|
||||
|
||||
yield()
|
||||
@ -199,7 +193,6 @@ class OfflineWebsite(
|
||||
}
|
||||
|
||||
ZipOutputStream(FileOutputStream(zip)).use { out ->
|
||||
|
||||
fun File.zip(name: String = this.name) {
|
||||
if (!isFile) return
|
||||
inputStream().use { file ->
|
||||
@ -209,10 +202,8 @@ class OfflineWebsite(
|
||||
out.closeEntry()
|
||||
delete()
|
||||
}
|
||||
baseDir.listFiles { file -> file != zip }
|
||||
?.forEach { it.zip() }
|
||||
assetDir.listFiles()
|
||||
?.forEach { it.zip("assets/${it.name}") }
|
||||
baseDir.listFiles { file -> file != zip }?.forEach { it.zip() }
|
||||
assetDir.listFiles()?.forEach { it.zip("assets/${it.name}") }
|
||||
|
||||
assetDir.delete()
|
||||
}
|
||||
@ -238,7 +229,8 @@ class OfflineWebsite(
|
||||
return try {
|
||||
val file = File(assetDir, fileName(url))
|
||||
file.createNewFile()
|
||||
val stream = request(url).execute().body?.byteStream()
|
||||
val stream =
|
||||
request(url).execute().body?.byteStream()
|
||||
?: throw IllegalArgumentException("Response body not found for $url")
|
||||
file.copyFromInputStream(stream)
|
||||
true
|
||||
@ -253,11 +245,15 @@ class OfflineWebsite(
|
||||
val file = File(assetDir, fileName(url))
|
||||
file.createNewFile()
|
||||
|
||||
var content = request(url).execute().body?.string()
|
||||
var content =
|
||||
request(url).execute().body?.string()
|
||||
?: throw IllegalArgumentException("Response body not found for $url")
|
||||
val links = FB_CSS_URL_MATCHER.findAll(content).mapNotNull { it[1] }
|
||||
val absLinks = links.mapNotNull {
|
||||
val newUrl = when {
|
||||
val absLinks =
|
||||
links
|
||||
.mapNotNull {
|
||||
val newUrl =
|
||||
when {
|
||||
it.startsWith("http") -> it
|
||||
it.startsWith("/") -> "$baseUrl$it"
|
||||
else -> return@mapNotNull null
|
||||
@ -266,7 +262,8 @@ class OfflineWebsite(
|
||||
// so the url does not point to another subfolder
|
||||
content = content.replace(it, fileName(newUrl))
|
||||
newUrl
|
||||
}.toSet()
|
||||
}
|
||||
.toSet()
|
||||
|
||||
file.writeText(content)
|
||||
absLinks
|
||||
@ -290,35 +287,28 @@ class OfflineWebsite(
|
||||
private inline val String.isValid
|
||||
get() = startsWith("http")
|
||||
|
||||
/**
|
||||
* Fetch the previously discovered filename
|
||||
* or create a new one
|
||||
* This is thread-safe
|
||||
*/
|
||||
/** Fetch the previously discovered filename or create a new one This is thread-safe */
|
||||
private fun fileName(url: String): String {
|
||||
val mapped = urlMapper[url]
|
||||
if (mapped != null) return mapped
|
||||
|
||||
val candidate = url.substringBefore("?").trim('/')
|
||||
.substringAfterLast("/").shorten()
|
||||
val candidate = url.substringBefore("?").trim('/').substringAfterLast("/").shorten()
|
||||
|
||||
val index = atomicInt.getAndIncrement()
|
||||
|
||||
var newUrl = "a${index}_$candidate"
|
||||
|
||||
/**
|
||||
* This is primarily for zipping up and sending via emails
|
||||
* As .js files typically aren't allowed, we'll simply make everything txt files
|
||||
* This is primarily for zipping up and sending via emails As .js files typically aren't
|
||||
* allowed, we'll simply make everything txt files
|
||||
*/
|
||||
if (newUrl.endsWith(".js"))
|
||||
newUrl = "$newUrl.txt"
|
||||
if (newUrl.endsWith(".js")) newUrl = "$newUrl.txt"
|
||||
|
||||
urlMapper[url] = newUrl
|
||||
return newUrl
|
||||
}
|
||||
|
||||
private fun String.shorten() =
|
||||
if (length <= 10) this else substring(length - 10)
|
||||
private fun String.shorten() = if (length <= 10) this else substring(length - 10)
|
||||
|
||||
private fun Set<String>.clean(): List<String> =
|
||||
filter(String::isNotBlank).filter { it.startsWith("http") }
|
||||
|
@ -20,9 +20,7 @@ import androidx.annotation.StringRes
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-23.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-23. */
|
||||
enum class FeedSort(@StringRes val textRes: Int, val item: FbItem) {
|
||||
DEFAULT(R.string.kau_default, FbItem.FEED),
|
||||
MOST_RECENT(R.string.most_recent, FbItem.FEED_MOST_RECENT),
|
||||
|
@ -19,26 +19,14 @@ package com.pitchedapps.frost.enums
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.injectors.ThemeProvider
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-08-19.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-08-19. */
|
||||
enum class MainActivityLayout(
|
||||
val titleRes: Int,
|
||||
val backgroundColor: (ThemeProvider) -> Int,
|
||||
val iconColor: (ThemeProvider) -> Int
|
||||
) {
|
||||
|
||||
TOP_BAR(
|
||||
R.string.top_bar,
|
||||
{ it.headerColor },
|
||||
{ it.iconColor }
|
||||
),
|
||||
|
||||
BOTTOM_BAR(
|
||||
R.string.bottom_bar,
|
||||
{ it.bgColor },
|
||||
{ it.textColor }
|
||||
);
|
||||
TOP_BAR(R.string.top_bar, { it.headerColor }, { it.iconColor }),
|
||||
BOTTOM_BAR(R.string.bottom_bar, { it.bgColor }, { it.textColor });
|
||||
|
||||
companion object {
|
||||
val values = values() // save one instance
|
||||
|
@ -30,19 +30,15 @@ import com.pitchedapps.frost.views.FrostWebView
|
||||
/**
|
||||
* Created by Allan Wang on 2017-09-16.
|
||||
*
|
||||
* Options for [WebOverlayActivityBase] to give more info as to what kind of
|
||||
* overlay is present.
|
||||
* Options for [WebOverlayActivityBase] to give more info as to what kind of overlay is present.
|
||||
*
|
||||
* For now, this is able to add new menu options upon first load
|
||||
*/
|
||||
enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<OverlayContext> {
|
||||
|
||||
NOTIFICATION(FrostMenuItem(R.id.action_notification, FbItem.NOTIFICATIONS)),
|
||||
MESSAGE(FrostMenuItem(R.id.action_messages, FbItem.MESSAGES));
|
||||
|
||||
/**
|
||||
* Inject the [menuItem] in the order that they are given at the front of the menu
|
||||
*/
|
||||
/** Inject the [menuItem] in the order that they are given at the front of the menu */
|
||||
fun onMenuCreate(context: Context, menu: Menu) {
|
||||
menuItem?.addToMenu(context, menu, 0)
|
||||
}
|
||||
@ -53,8 +49,8 @@ enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<Ove
|
||||
companion object : EnumCompanion<OverlayContext>("frost_arg_overlay_context", values()) {
|
||||
|
||||
/**
|
||||
* Execute selection call for an item by id
|
||||
* Returns [true] if selection was consumed, [false] otherwise
|
||||
* Execute selection call for an item by id Returns [true] if selection was consumed, [false]
|
||||
* otherwise
|
||||
*/
|
||||
fun onOptionsItemSelected(web: FrostWebView, id: Int): Boolean {
|
||||
val item = values.firstOrNull { id == it.menuItem?.id }?.menuItem ?: return false
|
||||
@ -64,9 +60,7 @@ enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<Ove
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame for an injectable menu item
|
||||
*/
|
||||
/** Frame for an injectable menu item */
|
||||
class FrostMenuItem(
|
||||
val id: Int,
|
||||
val fbItem: FbItem,
|
||||
|
@ -23,9 +23,7 @@ import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.prefs.sections.ThemePrefs
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-14.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-14. */
|
||||
const val FACEBOOK_BLUE = 0xff3b5998.toInt()
|
||||
const val BLUE_LIGHT = 0xff5d86dd.toInt()
|
||||
|
||||
@ -38,7 +36,6 @@ enum class Theme(
|
||||
val headerColorGetter: (ThemePrefs) -> Int,
|
||||
val iconColorGetter: (ThemePrefs) -> Int
|
||||
) {
|
||||
|
||||
DEFAULT(
|
||||
R.string.kau_default,
|
||||
"default",
|
||||
@ -48,7 +45,6 @@ enum class Theme(
|
||||
{ FACEBOOK_BLUE },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
LIGHT(
|
||||
R.string.kau_light,
|
||||
"material_light",
|
||||
@ -58,7 +54,6 @@ enum class Theme(
|
||||
{ FACEBOOK_BLUE },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
DARK(
|
||||
R.string.kau_dark,
|
||||
"material_dark",
|
||||
@ -68,7 +63,6 @@ enum class Theme(
|
||||
{ 0xff2e4b86.toInt() },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
AMOLED(
|
||||
R.string.kau_amoled,
|
||||
"material_amoled",
|
||||
@ -78,7 +72,6 @@ enum class Theme(
|
||||
{ Color.BLACK },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
GLASS(
|
||||
R.string.kau_glass,
|
||||
"material_glass",
|
||||
@ -88,7 +81,6 @@ enum class Theme(
|
||||
{ 0xb3000000.toInt() },
|
||||
{ Color.WHITE }
|
||||
),
|
||||
|
||||
CUSTOM(
|
||||
R.string.kau_custom,
|
||||
"custom",
|
||||
@ -99,8 +91,7 @@ enum class Theme(
|
||||
{ it.customIconColor }
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
internal val file = file?.let { "$it.css" }
|
||||
@VisibleForTesting internal val file = file?.let { "$it.css" }
|
||||
|
||||
companion object {
|
||||
val values = values() // save one instance
|
||||
@ -109,9 +100,8 @@ enum class Theme(
|
||||
}
|
||||
|
||||
enum class ThemeCategory {
|
||||
FACEBOOK, MESSENGER
|
||||
;
|
||||
FACEBOOK,
|
||||
MESSENGER;
|
||||
|
||||
@VisibleForTesting
|
||||
internal val folder = name.toLowerCase(Locale.CANADA)
|
||||
@VisibleForTesting internal val folder = name.toLowerCase(Locale.CANADA)
|
||||
}
|
||||
|
@ -16,10 +16,7 @@
|
||||
*/
|
||||
package com.pitchedapps.frost.facebook
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-01.
|
||||
*/
|
||||
|
||||
/** 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"
|
||||
@ -31,7 +28,9 @@ const val FACEBOOK_BASE_COM = "m.$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/"
|
||||
@ -53,15 +52,11 @@ const val USER_AGENT_DESKTOP_CONST =
|
||||
|
||||
const val USER_AGENT = USER_AGENT_DESKTOP_CONST
|
||||
|
||||
/**
|
||||
* Animation transition delay, just to ensure that the styles
|
||||
* have properly set in
|
||||
*/
|
||||
/** Animation transition delay, just to ensure that the styles have properly set in */
|
||||
const val WEB_LOAD_DELAY = 50L
|
||||
|
||||
/**
|
||||
* Additional delay for transition when called from commit.
|
||||
* Note that transitions are also called from onFinish, so this value
|
||||
* will never make a load slower than it is
|
||||
* Additional delay for transition when called from commit. Note that transitions are also called
|
||||
* from onFinish, so this value will never make a load slower than it is
|
||||
*/
|
||||
const val WEB_COMMIT_LOAD_DELAY = 200L
|
||||
|
@ -28,53 +28,43 @@ import com.pitchedapps.frost.prefs.Prefs
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.cookies
|
||||
import com.pitchedapps.frost.utils.launchLogin
|
||||
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
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-30.
|
||||
*
|
||||
* The following component manages all cookie transfers.
|
||||
*/
|
||||
class FbCookie @Inject internal constructor(
|
||||
private val prefs: Prefs,
|
||||
private val cookieDao: CookieDao
|
||||
) {
|
||||
class FbCookie
|
||||
@Inject
|
||||
internal constructor(private val prefs: Prefs, private val cookieDao: CookieDao) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Domain information. Dot prefix still matters for Android browsers.
|
||||
*/
|
||||
/** 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"
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the facebook cookie if it exists
|
||||
* Note that this is a synchronized call
|
||||
*/
|
||||
/** Retrieves the facebook cookie if it exists Note that this is a synchronized call */
|
||||
val webCookie: String?
|
||||
get() = CookieManager.getInstance().getCookie(HTTPS_FACEBOOK_COM)
|
||||
|
||||
val messengerCookie: String?
|
||||
get() = CookieManager.getInstance().getCookie(HTTPS_MESSENGER_COM)
|
||||
|
||||
private suspend fun CookieManager.suspendSetWebCookie(
|
||||
domain: String,
|
||||
cookie: String?
|
||||
): Boolean {
|
||||
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 }
|
||||
val result =
|
||||
cookie.split(";").map { async { setSingleWebCookie(domain, it) } }.awaitAll().all { it }
|
||||
L.d { "Cookies set" }
|
||||
L._d { "Set $cookie\n\tResult $webCookie" }
|
||||
result
|
||||
@ -83,15 +73,11 @@ class FbCookie @Inject internal constructor(
|
||||
|
||||
private suspend fun CookieManager.setSingleWebCookie(domain: String, cookie: String): Boolean =
|
||||
suspendCoroutine { cont ->
|
||||
setCookie(domain, cookie.trim()) {
|
||||
cont.resume(it)
|
||||
}
|
||||
setCookie(domain, cookie.trim()) { cont.resume(it) }
|
||||
}
|
||||
|
||||
private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
|
||||
removeAllCookies {
|
||||
cont.resume(it)
|
||||
}
|
||||
removeAllCookies { cont.resume(it) }
|
||||
}
|
||||
|
||||
suspend fun save(id: Long) {
|
||||
@ -134,21 +120,15 @@ class FbCookie @Inject internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to remove the current cookies
|
||||
* and launch the proper login page
|
||||
*/
|
||||
/** 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 })
|
||||
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
|
||||
*/
|
||||
/** Clear the cookies of the given id */
|
||||
suspend fun logout(id: Long, deleteCookie: Boolean = true) {
|
||||
L.d { "Logging out user" }
|
||||
if (deleteCookie) {
|
||||
|
@ -35,7 +35,6 @@ enum class FbItem(
|
||||
val fragmentCreator: () -> BaseFragment = ::WebFragment,
|
||||
prefix: String = FB_URL_BASE
|
||||
) : EnumBundle<FbItem> {
|
||||
|
||||
ACTIVITY_LOG(R.string.activity_log, GoogleMaterial.Icon.gmd_list, "me/allactivity"),
|
||||
BIRTHDAYS(R.string.birthdays, GoogleMaterial.Icon.gmd_cake, "events/birthdays"),
|
||||
CHAT(R.string.chat, GoogleMaterial.Icon.gmd_chat, "buddylist"),
|
||||
@ -68,18 +67,10 @@ enum class FbItem(
|
||||
PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "me"),
|
||||
SAVED(R.string.saved, GoogleMaterial.Icon.gmd_bookmark, "saved"),
|
||||
|
||||
/**
|
||||
* Note that this url only works if a query (?q=) is provided
|
||||
*/
|
||||
_SEARCH(
|
||||
R.string.kau_search,
|
||||
GoogleMaterial.Icon.gmd_search,
|
||||
"search/top"
|
||||
),
|
||||
/** Note that this url only works if a query (?q=) is provided */
|
||||
_SEARCH(R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
|
||||
|
||||
/**
|
||||
* Non mbasic search cannot be parsed.
|
||||
*/
|
||||
/** Non mbasic search cannot be parsed. */
|
||||
_SEARCH_PARSE(
|
||||
R.string.kau_search,
|
||||
GoogleMaterial.Icon.gmd_search,
|
||||
@ -92,8 +83,11 @@ enum class FbItem(
|
||||
val url = "$prefix$relativeUrl"
|
||||
|
||||
val isFeed: Boolean
|
||||
get() = when (this) {
|
||||
FEED, FEED_MOST_RECENT, FEED_TOP_STORIES -> true
|
||||
get() =
|
||||
when (this) {
|
||||
FEED,
|
||||
FEED_MOST_RECENT,
|
||||
FEED_TOP_STORIES -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
@ -19,21 +19,16 @@ package com.pitchedapps.frost.facebook
|
||||
/**
|
||||
* Created by Allan Wang on 21/12/17.
|
||||
*
|
||||
* Collection of regex matchers
|
||||
* Input text must be properly unescaped
|
||||
* 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
|
||||
*/
|
||||
/** 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
|
||||
*/
|
||||
/** Matches user id from cookie */
|
||||
val FB_USER_MATCHER: Regex = Regex("c_user=([0-9]*);")
|
||||
|
||||
val FB_EPOCH_MATCHER: Regex = Regex(":([0-9]+)")
|
||||
|
@ -82,7 +82,8 @@ class FbUrlFormatter(url: String) {
|
||||
// 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(
|
||||
cleanedUrl =
|
||||
cleanedUrl.replaceFirst(
|
||||
".facebook.com//",
|
||||
".facebook.com/"
|
||||
) // sometimes we are given a bad url
|
||||
@ -90,7 +91,8 @@ class FbUrlFormatter(url: String) {
|
||||
return cleanedUrl
|
||||
}
|
||||
|
||||
override fun toString(): String = buildString {
|
||||
override fun toString(): String =
|
||||
buildString {
|
||||
append(cleaned)
|
||||
if (queries.isNotEmpty()) {
|
||||
append("?")
|
||||
@ -102,7 +104,8 @@ class FbUrlFormatter(url: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}.removeSuffix("&")
|
||||
}
|
||||
.removeSuffix("&")
|
||||
|
||||
fun toLogList(): List<String> {
|
||||
val list = mutableListOf(cleaned)
|
||||
@ -116,15 +119,15 @@ class FbUrlFormatter(url: String) {
|
||||
const val VIDEO_REDIRECT = "/video_redirect/?src="
|
||||
|
||||
/**
|
||||
* Items here are explicitly removed from the url
|
||||
* Taken from FaceSlim
|
||||
* 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
|
||||
* 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(
|
||||
val discardable =
|
||||
arrayOf(
|
||||
"http://lm.facebook.com/l.php?u=",
|
||||
"https://lm.facebook.com/l.php?u=",
|
||||
"http://m.facebook.com/l.php?u=",
|
||||
@ -139,7 +142,8 @@ class FbUrlFormatter(url: String) {
|
||||
*
|
||||
* acontext is not required for "friends interested in" notifications
|
||||
*/
|
||||
val discardableQueries = arrayOf(
|
||||
val discardableQueries =
|
||||
arrayOf(
|
||||
"ref",
|
||||
"refid",
|
||||
"SharedWith",
|
||||
@ -155,13 +159,32 @@ class FbUrlFormatter(url: String) {
|
||||
"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",
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
@ -29,7 +29,8 @@ data class FrostBadges(
|
||||
val notifications: String?
|
||||
) : ParseData {
|
||||
override val isEmpty: Boolean
|
||||
get() = feed.isNullOrEmpty() &&
|
||||
get() =
|
||||
feed.isNullOrEmpty() &&
|
||||
friends.isNullOrEmpty() &&
|
||||
messages.isNullOrEmpty() &&
|
||||
notifications.isNullOrEmpty()
|
||||
@ -43,14 +44,9 @@ private class BadgeParserImpl : FrostParserBase<FrostBadges>(false) {
|
||||
|
||||
override fun parseImpl(doc: Document): FrostBadges? {
|
||||
val header = doc.getElementById("header") ?: return null
|
||||
if (header.select("[data-sigil=count]").isEmpty())
|
||||
return null
|
||||
val (feed, requests, messages, notifications) = listOf(
|
||||
"feed",
|
||||
"requests",
|
||||
"messages",
|
||||
"notifications"
|
||||
)
|
||||
if (header.select("[data-sigil=count]").isEmpty()) return null
|
||||
val (feed, requests, messages, notifications) =
|
||||
listOf("feed", "requests", "messages", "notifications")
|
||||
.map { "[data-sigil*=$it] [data-sigil=count]" }
|
||||
.map { doc.select(it) }
|
||||
.map { e -> e?.getOrNull(0)?.ownText() }
|
||||
|
@ -30,45 +30,32 @@ import org.jsoup.select.Elements
|
||||
/**
|
||||
* Created by Allan Wang on 2017-10-06.
|
||||
*
|
||||
* Interface for a given parser
|
||||
* Use cases should be attached as delegates to objects that implement this interface
|
||||
* Interface for a given parser Use cases should be attached as delegates to objects that implement
|
||||
* this interface
|
||||
*
|
||||
* In all cases, parsing will be done from a JSoup document
|
||||
* Variants accepting strings are also permitted, and they will be converted to documents accordingly
|
||||
* The return type must be nonnull if no parsing errors occurred, as null signifies a parse error
|
||||
* If null really must be allowed, use Optionals
|
||||
* In all cases, parsing will be done from a JSoup document Variants accepting strings are also
|
||||
* permitted, and they will be converted to documents accordingly The return type must be nonnull if
|
||||
* no parsing errors occurred, as null signifies a parse error If null really must be allowed, use
|
||||
* Optionals
|
||||
*/
|
||||
interface FrostParser<out T : ParseData> {
|
||||
|
||||
/**
|
||||
* Name associated to parser
|
||||
* Purely for display
|
||||
*/
|
||||
/** Name associated to parser Purely for display */
|
||||
var nameRes: Int
|
||||
|
||||
/**
|
||||
* Url to request from
|
||||
*/
|
||||
/** Url to request from */
|
||||
val url: String
|
||||
|
||||
/**
|
||||
* Call parsing with default implementation using cookie
|
||||
*/
|
||||
/** Call parsing with default implementation using cookie */
|
||||
fun parse(cookie: String?): ParseResponse<T>?
|
||||
|
||||
/**
|
||||
* Call parsing with given document
|
||||
*/
|
||||
/** Call parsing with given document */
|
||||
fun parse(cookie: String?, document: Document): ParseResponse<T>?
|
||||
|
||||
/**
|
||||
* Call parsing using jsoup to fetch from given url
|
||||
*/
|
||||
/** Call parsing using jsoup to fetch from given url */
|
||||
fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>?
|
||||
|
||||
/**
|
||||
* Call parsing with given data
|
||||
*/
|
||||
/** Call parsing with given data */
|
||||
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
|
||||
}
|
||||
|
||||
@ -88,16 +75,19 @@ interface ParseNotification : ParseData {
|
||||
fun getUnreadNotifications(data: CookieEntity): List<NotificationContent>
|
||||
}
|
||||
|
||||
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder().apply {
|
||||
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) =
|
||||
StringBuilder()
|
||||
.apply {
|
||||
val tabs = "\t".repeat(indent)
|
||||
append("$tabs$tag: [\n\t$tabs")
|
||||
append(this@toJsonString.joinToString("\n\t$tabs"))
|
||||
append("\n$tabs]\n")
|
||||
}.toString()
|
||||
}
|
||||
.toString()
|
||||
|
||||
/**
|
||||
* T should have a readable toString() function
|
||||
* [redirectToText] dictates whether all data should be converted to text then back to document before parsing
|
||||
* T should have a readable toString() function [redirectToText] dictates whether all data should be
|
||||
* converted to text then back to document before parsing
|
||||
*/
|
||||
internal abstract class FrostParserBase<out T : ParseData>(private val redirectToText: Boolean) :
|
||||
FrostParser<T> {
|
||||
@ -116,8 +106,7 @@ internal abstract class FrostParserBase<out T : ParseData>(private val redirectT
|
||||
|
||||
override fun parse(cookie: String?, document: Document): ParseResponse<T>? {
|
||||
cookie ?: return null
|
||||
if (redirectToText)
|
||||
return parseFromData(cookie, document.toString())
|
||||
if (redirectToText) return parseFromData(cookie, document.toString())
|
||||
val data = parseImpl(document) ?: return null
|
||||
return ParseResponse(cookie, data)
|
||||
}
|
||||
@ -125,18 +114,20 @@ internal abstract class FrostParserBase<out T : ParseData>(private val redirectT
|
||||
protected abstract fun parseImpl(doc: Document): T?
|
||||
|
||||
/**
|
||||
* Attempts to find inner <i> element with some style containing a url
|
||||
* Returns the formatted url, or an empty string if nothing was found
|
||||
* Attempts to find inner <i> element with some style containing a url Returns the formatted url,
|
||||
* or an empty string if nothing was found
|
||||
*/
|
||||
protected fun Element.getInnerImgStyle(): String? =
|
||||
select("i.img[style*=url]").getStyleUrl()
|
||||
protected fun Element.getInnerImgStyle(): String? = select("i.img[style*=url]").getStyleUrl()
|
||||
|
||||
protected fun Elements.getStyleUrl(): String? =
|
||||
FB_CSS_URL_MATCHER.find(attr("style"))[1]?.formattedFbUrl
|
||||
|
||||
protected open fun textToDoc(text: String): Document? =
|
||||
if (!redirectToText) Jsoup.parse(text)
|
||||
else throw RuntimeException("${this::class.java.simpleName} requires text redirect but did not implement textToDoc")
|
||||
else
|
||||
throw RuntimeException(
|
||||
"${this::class.java.simpleName} requires text redirect but did not implement textToDoc"
|
||||
)
|
||||
|
||||
protected fun parseLink(element: Element?): FrostLink? {
|
||||
val a = element?.getElementsByTag("a")?.first() ?: return null
|
||||
|
@ -33,9 +33,8 @@ import org.jsoup.nodes.Element
|
||||
/**
|
||||
* Created by Allan Wang on 2017-10-06.
|
||||
*
|
||||
* In Facebook, messages are passed through scripts and loaded into view via react afterwards
|
||||
* We can parse out the content we want directly and load it ourselves
|
||||
*
|
||||
* In Facebook, messages are passed through scripts and loaded into view via react afterwards We can
|
||||
* parse out the content we want directly and load it ourselves
|
||||
*/
|
||||
object MessageParser : FrostParser<FrostMessages> by MessageParserImpl() {
|
||||
|
||||
@ -52,16 +51,22 @@ data class FrostMessages(
|
||||
override val isEmpty: Boolean
|
||||
get() = threads.isEmpty()
|
||||
|
||||
override fun toString() = StringBuilder().apply {
|
||||
override fun toString() =
|
||||
StringBuilder()
|
||||
.apply {
|
||||
append("FrostMessages {\n")
|
||||
append(threads.toJsonString("threads", 1))
|
||||
append("\tsee more: $seeMore\n")
|
||||
append(extraLinks.toJsonString("extra links", 1))
|
||||
append("}")
|
||||
}.toString()
|
||||
}
|
||||
.toString()
|
||||
|
||||
override fun getUnreadNotifications(data: CookieEntity) =
|
||||
threads.asSequence().filter(FrostThread::unread).map {
|
||||
threads
|
||||
.asSequence()
|
||||
.filter(FrostThread::unread)
|
||||
.map {
|
||||
with(it) {
|
||||
NotificationContent(
|
||||
data = data,
|
||||
@ -74,16 +79,14 @@ data class FrostMessages(
|
||||
unread = unread
|
||||
)
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* [id] user/thread id, or current time fallback
|
||||
* [img] parsed url for profile img
|
||||
* [time] time of message
|
||||
* [url] link to thread
|
||||
* [unread] true if image is unread, false otherwise
|
||||
* [content] optional string for thread
|
||||
* [id] user/thread id, or current time fallback [img] parsed url for profile img [time] time of
|
||||
* message [url] link to thread [unread] true if image is unread, false otherwise [content] optional
|
||||
* string for thread
|
||||
*/
|
||||
data class FrostThread(
|
||||
val id: Long,
|
||||
@ -122,14 +125,12 @@ private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
|
||||
override fun parseImpl(doc: Document): FrostMessages? {
|
||||
val threadList = doc.getElementById("threadlist_rows") ?: return null
|
||||
val threads: List<FrostThread> =
|
||||
threadList.getElementsByAttributeValueMatching(
|
||||
"id",
|
||||
".*${FB_MESSAGE_NOTIF_ID_MATCHER.pattern}.*"
|
||||
)
|
||||
threadList
|
||||
.getElementsByAttributeValueMatching("id", ".*${FB_MESSAGE_NOTIF_ID_MATCHER.pattern}.*")
|
||||
.mapNotNull(this::parseMessage)
|
||||
val seeMore = parseLink(doc.getElementById("see_older_threads"))
|
||||
val extraLinks = threadList.nextElementSibling()?.select("a")
|
||||
?.mapNotNull(this::parseLink) ?: emptyList()
|
||||
val extraLinks =
|
||||
threadList.nextElementSibling()?.select("a")?.mapNotNull(this::parseLink) ?: emptyList()
|
||||
return FrostMessages(threads, seeMore, extraLinks)
|
||||
}
|
||||
|
||||
@ -138,7 +139,8 @@ private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
|
||||
val abbr = element.getElementsByTag("abbr")
|
||||
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
|
||||
// fetch id
|
||||
val id = FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
||||
val id =
|
||||
FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
||||
val snippet = element.select("span.snippet").firstOrNull()
|
||||
val content = snippet?.text()?.trim()
|
||||
|
@ -26,29 +26,29 @@ import com.pitchedapps.frost.services.NotificationContent
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-12-25.
|
||||
*
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-12-25. */
|
||||
object NotifParser : FrostParser<FrostNotifs> by NotifParserImpl()
|
||||
|
||||
data class FrostNotifs(
|
||||
val notifs: List<FrostNotif>,
|
||||
val seeMore: FrostLink?
|
||||
) : ParseNotification {
|
||||
data class FrostNotifs(val notifs: List<FrostNotif>, val seeMore: FrostLink?) : ParseNotification {
|
||||
|
||||
override val isEmpty: Boolean
|
||||
get() = notifs.isEmpty()
|
||||
|
||||
override fun toString() = StringBuilder().apply {
|
||||
override fun toString() =
|
||||
StringBuilder()
|
||||
.apply {
|
||||
append("FrostNotifs {\n")
|
||||
append(notifs.toJsonString("notifs", 1))
|
||||
append("\tsee more: $seeMore\n")
|
||||
append("}")
|
||||
}.toString()
|
||||
}
|
||||
.toString()
|
||||
|
||||
override fun getUnreadNotifications(data: CookieEntity) =
|
||||
notifs.asSequence().filter(FrostNotif::unread).map {
|
||||
notifs
|
||||
.asSequence()
|
||||
.filter(FrostNotif::unread)
|
||||
.map {
|
||||
with(it) {
|
||||
NotificationContent(
|
||||
data = data,
|
||||
@ -61,18 +61,15 @@ data class FrostNotifs(
|
||||
unread = unread
|
||||
)
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* [id] notif id, or current time fallback
|
||||
* [img] parsed url for profile img
|
||||
* [time] time of message
|
||||
* [url] link to thread
|
||||
* [unread] true if image is unread, false otherwise
|
||||
* [content] optional string for thread
|
||||
* [timeString] text version of time from Facebook
|
||||
* [thumbnailUrl] optional thumbnail url if existent
|
||||
* [id] notif id, or current time fallback [img] parsed url for profile img [time] time of message
|
||||
* [url] link to thread [unread] true if image is unread, false otherwise [content] optional string
|
||||
* for thread [timeString] text version of time from Facebook [thumbnailUrl] optional thumbnail url
|
||||
* if existent
|
||||
*/
|
||||
data class FrostNotif(
|
||||
val id: Long,
|
||||
@ -93,7 +90,8 @@ private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
|
||||
|
||||
override fun parseImpl(doc: Document): FrostNotifs? {
|
||||
val notificationList = doc.getElementById("notifications_list") ?: return null
|
||||
val notifications = notificationList
|
||||
val notifications =
|
||||
notificationList
|
||||
.getElementsByAttributeValueMatching("id", ".*${FB_NOTIF_ID_MATCHER.pattern}.*")
|
||||
.mapNotNull(this::parseNotif)
|
||||
val seeMore =
|
||||
@ -107,12 +105,12 @@ private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
|
||||
val abbr = element.getElementsByTag("abbr")
|
||||
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
|
||||
// fetch id
|
||||
val id = FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
||||
val id =
|
||||
FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
|
||||
?: System.currentTimeMillis() % FALLBACK_TIME_MOD
|
||||
val img = element.getInnerImgStyle()
|
||||
val timeString = abbr.text()
|
||||
val content =
|
||||
a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove
|
||||
val content = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() // remove
|
||||
val thumbnail = element.selectFirst("img.thumbnail")?.attr("src")
|
||||
return FrostNotif(
|
||||
id = id,
|
||||
|
@ -27,13 +27,10 @@ import com.pitchedapps.frost.utils.urlEncode
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-10-09.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-10-09. */
|
||||
object SearchParser : FrostParser<FrostSearches> by SearchParserImpl() {
|
||||
fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? {
|
||||
val url =
|
||||
"${FbItem._SEARCH_PARSE.url}/?q=${if (input.isNotBlank()) input.urlEncode() else "a"}"
|
||||
val url = "${FbItem._SEARCH_PARSE.url}/?q=${if (input.isNotBlank()) input.urlEncode() else "a"}"
|
||||
L._i { "Search Query $url" }
|
||||
return parseFromUrl(cookie, url)
|
||||
}
|
||||
@ -49,17 +46,19 @@ data class FrostSearches(val results: List<FrostSearch>) : ParseData {
|
||||
override val isEmpty: Boolean
|
||||
get() = results.isEmpty()
|
||||
|
||||
override fun toString() = StringBuilder().apply {
|
||||
override fun toString() =
|
||||
StringBuilder()
|
||||
.apply {
|
||||
append("FrostSearches {\n")
|
||||
append(results.toJsonString("results", 1))
|
||||
append("}")
|
||||
}.toString()
|
||||
}
|
||||
.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* As far as I'm aware, all links are independent, so the queries don't matter
|
||||
* A lot of it is tracking information, which I'll strip away
|
||||
* Other text items are formatted for safety
|
||||
* As far as I'm aware, all links are independent, so the queries don't matter A lot of it is
|
||||
* tracking information, which I'll strip away Other text items are formatted for safety
|
||||
*
|
||||
* Note that it's best to create search results from [create]
|
||||
*/
|
||||
@ -68,7 +67,8 @@ data class FrostSearch(val href: String, val title: String, val description: Str
|
||||
fun toSearchItem() = SearchItem(href, title, description)
|
||||
|
||||
companion object {
|
||||
fun create(href: String, title: String, description: String?) = FrostSearch(
|
||||
fun create(href: String, title: String, description: String?) =
|
||||
FrostSearch(
|
||||
with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) },
|
||||
title.format(),
|
||||
description?.format()
|
||||
@ -86,38 +86,31 @@ private class SearchParserImpl : FrostParserBase<FrostSearches>(false) {
|
||||
get() = replace(FACEBOOK_MBASIC_COM, FACEBOOK_BASE_COM)
|
||||
|
||||
override fun parseImpl(doc: Document): FrostSearches? {
|
||||
val container: Element = doc.getElementById("BrowseResultsContainer")
|
||||
?: doc.getElementById("root")
|
||||
?: return null
|
||||
val container: Element =
|
||||
doc.getElementById("BrowseResultsContainer") ?: doc.getElementById("root") ?: return null
|
||||
|
||||
return FrostSearches(
|
||||
container.select("table[role=presentation]").mapNotNull { el ->
|
||||
// Our assumption is that search entries start with an image, followed by general info
|
||||
// There may be other <td />s, but we will not be parsing them
|
||||
// Furthermore, the <td /> entry wraps a link, containing all the necessary info
|
||||
val a = el.select("td")
|
||||
.getOrNull(1)
|
||||
?.selectFirst("a")
|
||||
?: return@mapNotNull null
|
||||
val a = el.select("td").getOrNull(1)?.selectFirst("a") ?: return@mapNotNull null
|
||||
val url =
|
||||
a.attr("href").takeIf { it.isNotEmpty() }
|
||||
?.formattedFbUrl?.formattedSearchUrl
|
||||
a.attr("href").takeIf { it.isNotEmpty() }?.formattedFbUrl?.formattedSearchUrl
|
||||
?: return@mapNotNull null
|
||||
// Currently, children should all be <div /> elements, where the first entry is the name/title
|
||||
// Currently, children should all be <div /> elements, where the first entry is the
|
||||
// name/title
|
||||
// And the other entries are additional info.
|
||||
// There are also cases of nested tables, eg for the "join" button in groups.
|
||||
// Those elements have <span /> texts, so we will filter by div to ignore those
|
||||
val texts =
|
||||
a.children()
|
||||
.filter { childEl: Element -> childEl.tagName() == "div" && childEl.hasText() }
|
||||
a.children().filter { childEl: Element ->
|
||||
childEl.tagName() == "div" && childEl.hasText()
|
||||
}
|
||||
val title = texts.firstOrNull()?.text() ?: return@mapNotNull null
|
||||
val info = texts.takeIf { it.size > 1 }?.last()?.text()
|
||||
L.e { a }
|
||||
create(
|
||||
href = url,
|
||||
title = title,
|
||||
description = info
|
||||
).also { L.e { it } }
|
||||
create(href = url, title = title, description = info).also { L.e { it } }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -26,18 +26,13 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
val httpClient: OkHttpClient by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
if (BuildConfig.DEBUG)
|
||||
builder.addInterceptor(
|
||||
HttpLoggingInterceptor()
|
||||
.setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||
)
|
||||
builder.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
|
||||
builder.build()
|
||||
}
|
||||
|
||||
internal fun String?.requestBuilder(): Request.Builder {
|
||||
val builder = Request.Builder()
|
||||
.header("User-Agent", USER_AGENT)
|
||||
if (this != null)
|
||||
builder.header("Cookie", this)
|
||||
val builder = Request.Builder().header("User-Agent", USER_AGENT)
|
||||
if (this != null) builder.header("Cookie", this)
|
||||
// .cacheControl(CacheControl.FORCE_NETWORK)
|
||||
return builder
|
||||
}
|
||||
|
@ -24,19 +24,15 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 29/12/17.
|
||||
*/
|
||||
/** Created by Allan Wang on 29/12/17. */
|
||||
|
||||
/**
|
||||
* Attempts to get the fbcdn url of the supplied image redirect url
|
||||
*/
|
||||
/** Attempts to get the fbcdn url of the supplied image redirect url */
|
||||
suspend fun String.getFullSizedImageUrl(url: String, timeout: Long = 3000): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
withTimeout(timeout) {
|
||||
val redirect = requestBuilder().url(url).get().call()
|
||||
.execute().body?.string() ?: return@withTimeout null
|
||||
val redirect =
|
||||
requestBuilder().url(url).get().call().execute().body?.string() ?: return@withTimeout null
|
||||
FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -43,27 +43,23 @@ import com.pitchedapps.frost.utils.REQUEST_REFRESH
|
||||
import com.pitchedapps.frost.utils.REQUEST_TEXT_ZOOM
|
||||
import com.pitchedapps.frost.utils.frostEvent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.isActive
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-11-07.
|
||||
*
|
||||
* All fragments pertaining to the main view
|
||||
* Must be attached to activities implementing [MainActivityContract]
|
||||
* All fragments pertaining to the main view Must be attached to activities implementing
|
||||
* [MainActivityContract]
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
abstract class BaseFragment :
|
||||
Fragment(),
|
||||
CoroutineScope,
|
||||
FragmentContract,
|
||||
DynamicUiContract {
|
||||
abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, DynamicUiContract {
|
||||
|
||||
companion object {
|
||||
private const val ARG_POSITION = "arg_position"
|
||||
@ -78,26 +74,19 @@ abstract class BaseFragment :
|
||||
): BaseFragment {
|
||||
val fragment = if (useFallback) WebFragment() else base()
|
||||
val d = if (data == FbItem.FEED) FeedSort(prefs.feedSort).item else data
|
||||
fragment.withArguments(
|
||||
ARG_URL to d.url,
|
||||
ARG_POSITION to position
|
||||
)
|
||||
fragment.withArguments(ARG_URL to d.url, ARG_POSITION to position)
|
||||
d.put(fragment.requireArguments())
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
protected lateinit var mainContract: MainActivityContract
|
||||
@Inject protected lateinit var mainContract: MainActivityContract
|
||||
|
||||
@Inject
|
||||
protected lateinit var fbCookie: FbCookie
|
||||
@Inject protected lateinit var fbCookie: FbCookie
|
||||
|
||||
@Inject
|
||||
protected lateinit var prefs: Prefs
|
||||
@Inject protected lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
protected lateinit var themeProvider: ThemeProvider
|
||||
@Inject protected lateinit var themeProvider: ThemeProvider
|
||||
|
||||
open lateinit var job: Job
|
||||
override val coroutineContext: CoroutineContext
|
||||
@ -112,10 +101,7 @@ abstract class BaseFragment :
|
||||
set(value) {
|
||||
if (!isActive || value || this is WebFragment) return
|
||||
requireArguments().putBoolean(ARG_VALID, value)
|
||||
frostEvent(
|
||||
"Native Fallback",
|
||||
"Item" to baseEnum.name
|
||||
)
|
||||
frostEvent("Native Fallback", "Item" to baseEnum.name)
|
||||
mainContract.reloadFragment(this)
|
||||
}
|
||||
|
||||
@ -138,8 +124,11 @@ abstract class BaseFragment :
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(layoutRes, container, false)
|
||||
val content = view as? FrostContentParent
|
||||
?: throw IllegalArgumentException("layoutRes for fragment must return view implementing FrostContentParent")
|
||||
val content =
|
||||
view as? FrostContentParent
|
||||
?: throw IllegalArgumentException(
|
||||
"layoutRes for fragment must return view implementing FrostContentParent"
|
||||
)
|
||||
this.content = content
|
||||
content.bind(this)
|
||||
return view
|
||||
@ -198,7 +187,8 @@ abstract class BaseFragment :
|
||||
reloadTextSize()
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
override fun updateFab(contract: MainFabContract) {
|
||||
@ -207,9 +197,7 @@ abstract class BaseFragment :
|
||||
|
||||
protected fun FloatingActionButton.update(iicon: IIcon, click: () -> Unit) {
|
||||
if (isShown) {
|
||||
fadeScaleTransition {
|
||||
setIcon(iicon, themeProvider.iconColor)
|
||||
}
|
||||
fadeScaleTransition { setIcon(iicon, themeProvider.iconColor) }
|
||||
} else {
|
||||
setIcon(iicon, themeProvider.iconColor)
|
||||
show()
|
||||
|
@ -23,60 +23,44 @@ import com.pitchedapps.frost.contracts.MainActivityContract
|
||||
import com.pitchedapps.frost.contracts.MainFabContract
|
||||
import com.pitchedapps.frost.views.FrostRecyclerView
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-11-07.
|
||||
*/
|
||||
|
||||
/** Created by Allan Wang on 2017-11-07. */
|
||||
interface FragmentContract : FrostContentContainer {
|
||||
|
||||
val content: FrostContentParent?
|
||||
|
||||
/**
|
||||
* Defines whether the fragment is valid in the viewpager
|
||||
* or if it needs to be recreated
|
||||
* May be called from any thread to toggle status.
|
||||
* Note that calls beyond the fragment lifecycle will be ignored
|
||||
* Defines whether the fragment is valid in the viewpager or if it needs to be recreated May be
|
||||
* called from any thread to toggle status. Note that calls beyond the fragment lifecycle will be
|
||||
* ignored
|
||||
*/
|
||||
var valid: Boolean
|
||||
|
||||
/**
|
||||
* Helper to retrieve the core from [content]
|
||||
*/
|
||||
/** Helper to retrieve the core from [content] */
|
||||
val core: FrostContentCore?
|
||||
get() = content?.core
|
||||
|
||||
/**
|
||||
* Specifies position in Activity's viewpager
|
||||
*/
|
||||
/** Specifies position in Activity's viewpager */
|
||||
val position: Int
|
||||
|
||||
/**
|
||||
* Specifies whether if current load
|
||||
* will be fragment's first load
|
||||
* Specifies whether if current load will be fragment's first load
|
||||
*
|
||||
* Defaults to true
|
||||
*/
|
||||
var firstLoad: Boolean
|
||||
|
||||
/**
|
||||
* Called when the fragment is first visible
|
||||
* Typically, if [firstLoad] is true,
|
||||
* the fragment should call [reload] and make [firstLoad] false
|
||||
* Called when the fragment is first visible Typically, if [firstLoad] is true, the fragment
|
||||
* should call [reload] and make [firstLoad] false
|
||||
*/
|
||||
fun firstLoadRequest()
|
||||
|
||||
fun updateFab(contract: MainFabContract)
|
||||
|
||||
/**
|
||||
* Single callable action to be executed upon creation
|
||||
* Note that this call is not guaranteed
|
||||
*/
|
||||
/** Single callable action to be executed upon creation Note that this call is not guaranteed */
|
||||
fun post(action: (fragment: FragmentContract) -> Unit)
|
||||
|
||||
/**
|
||||
* Call whenever a fragment is attached so that it may listen
|
||||
* to activity emissions.
|
||||
*/
|
||||
/** Call whenever a fragment is attached so that it may listen to activity emissions. */
|
||||
fun attach(contract: MainActivityContract)
|
||||
|
||||
/*
|
||||
@ -95,10 +79,9 @@ interface RecyclerContentContract {
|
||||
fun bind(recyclerView: FrostRecyclerView)
|
||||
|
||||
/**
|
||||
* Completely handle data reloading, within a non-ui thread
|
||||
* The progress function allows optional emission of progress values (between 0 and 100)
|
||||
* and can be called from any thread.
|
||||
* Returns [true] for success, [false] otherwise
|
||||
* Completely handle data reloading, within a non-ui thread The progress function allows optional
|
||||
* emission of progress values (between 0 and 100) and can be called from any thread. Returns
|
||||
* [true] for success, [false] otherwise
|
||||
*/
|
||||
suspend fun reload(progress: (Int) -> Unit): Boolean
|
||||
}
|
||||
|
@ -32,9 +32,7 @@ import com.pitchedapps.frost.views.FrostRecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 27/12/17.
|
||||
*/
|
||||
/** Created by Allan Wang on 27/12/17. */
|
||||
abstract class RecyclerFragment<T, Item : GenericItem> : BaseFragment(), RecyclerContentContract {
|
||||
|
||||
override val layoutRes: Int = R.layout.view_content_recycler
|
||||
@ -51,7 +49,8 @@ abstract class RecyclerFragment<T, Item : GenericItem> : BaseFragment(), Recycle
|
||||
|
||||
final override suspend fun reload(progress: (Int) -> Unit): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
val data = try {
|
||||
val data =
|
||||
try {
|
||||
reloadImpl(progress)
|
||||
} catch (e: Exception) {
|
||||
L.e(e) { "Recycler reload fail $baseUrl" }
|
||||
@ -84,23 +83,19 @@ abstract class GenericRecyclerFragment<T, Item : GenericItem> : RecyclerFragment
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything to call for one time bindings
|
||||
* At this stage, all adapters will have FastAdapter references
|
||||
* Anything to call for one time bindings At this stage, all adapters will have FastAdapter
|
||||
* references
|
||||
*/
|
||||
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
||||
|
||||
/**
|
||||
* Create the fast adapter to bind to the recyclerview
|
||||
*/
|
||||
/** Create the fast adapter to bind to the recyclerview */
|
||||
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
||||
}
|
||||
|
||||
abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
|
||||
RecyclerFragment<Item, Item>() {
|
||||
|
||||
/**
|
||||
* The parser to make this all happen
|
||||
*/
|
||||
/** The parser to make this all happen */
|
||||
abstract val parser: FrostParser<T>
|
||||
|
||||
open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url)
|
||||
@ -116,14 +111,12 @@ abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything to call for one time bindings
|
||||
* At this stage, all adapters will have FastAdapter references
|
||||
* Anything to call for one time bindings At this stage, all adapters will have FastAdapter
|
||||
* references
|
||||
*/
|
||||
open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
|
||||
|
||||
/**
|
||||
* Create the fast adapter to bind to the recyclerview
|
||||
*/
|
||||
/** Create the fast adapter to bind to the recyclerview */
|
||||
open fun getAdapter(): GenericFastAdapter = fastAdapter(this.adapter)
|
||||
|
||||
override suspend fun reloadImpl(progress: (Int) -> Unit): List<Item>? =
|
||||
@ -132,7 +125,8 @@ abstract class FrostParserFragment<T : ParseData, Item : GenericItem> :
|
||||
val cookie = fbCookie.webCookie
|
||||
val doc = getDoc(cookie)
|
||||
progress(60)
|
||||
val response = try {
|
||||
val response =
|
||||
try {
|
||||
parser.parse(cookie, doc)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
|
@ -27,9 +27,12 @@ import com.pitchedapps.frost.views.FrostRecyclerView
|
||||
/**
|
||||
* Created by Allan Wang on 27/12/17.
|
||||
*
|
||||
* Retained as an example. Deletion made at https://github.com/AllanWang/Frost-for-Facebook/pull/1542
|
||||
* Retained as an example. Deletion made at
|
||||
* https://github.com/AllanWang/Frost-for-Facebook/pull/1542
|
||||
*/
|
||||
@Deprecated(message = "Retained as an example; currently does not support marking a notification as read")
|
||||
@Deprecated(
|
||||
message = "Retained as an example; currently does not support marking a notification as read"
|
||||
)
|
||||
class NotificationFragment : FrostParserFragment<FrostNotifs, NotificationIItem>() {
|
||||
|
||||
override val parser = NotifParser
|
||||
|
@ -31,17 +31,15 @@ import com.pitchedapps.frost.web.FrostWebViewClientMessenger
|
||||
/**
|
||||
* Created by Allan Wang on 27/12/17.
|
||||
*
|
||||
* Basic webfragment
|
||||
* Do not extend as this is always a fallback
|
||||
* Basic webfragment Do not extend as this is always a fallback
|
||||
*/
|
||||
class WebFragment : BaseFragment() {
|
||||
|
||||
override val layoutRes: Int = R.layout.view_content_web
|
||||
|
||||
/**
|
||||
* Given a webview, output a client
|
||||
*/
|
||||
fun client(web: FrostWebView) = when (baseEnum) {
|
||||
/** Given a webview, output a client */
|
||||
fun client(web: FrostWebView) =
|
||||
when (baseEnum) {
|
||||
FbItem.MESSENGER -> FrostWebViewClientMessenger(web)
|
||||
FbItem.MENU -> FrostWebViewClientMenu(web)
|
||||
else -> FrostWebViewClient(web)
|
||||
@ -54,9 +52,7 @@ class WebFragment : BaseFragment() {
|
||||
return super.updateFab(contract)
|
||||
}
|
||||
if (baseEnum.isFeed && prefs.showCreateFab) {
|
||||
contract.showFab(GoogleMaterial.Icon.gmd_edit) {
|
||||
JsActions.CREATE_POST.inject(web, prefs)
|
||||
}
|
||||
contract.showFab(GoogleMaterial.Icon.gmd_edit) { JsActions.CREATE_POST.inject(web, prefs) }
|
||||
return
|
||||
}
|
||||
super.updateFab(contract)
|
||||
|
@ -27,22 +27,23 @@ import com.bumptech.glide.load.resource.bitmap.CircleCrop
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.pitchedapps.frost.facebook.FbCookie
|
||||
import javax.inject.Inject
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 28/12/17.
|
||||
*
|
||||
* Collection of transformations
|
||||
* Each caller will generate a new one upon request
|
||||
* Collection of transformations Each caller will generate a new one upon request
|
||||
*/
|
||||
object FrostGlide {
|
||||
val circleCrop
|
||||
get() = CircleCrop()
|
||||
}
|
||||
|
||||
fun <T> RequestBuilder<T>.transform(vararg transformation: BitmapTransformation): RequestBuilder<T> =
|
||||
fun <T> RequestBuilder<T>.transform(
|
||||
vararg transformation: BitmapTransformation
|
||||
): RequestBuilder<T> =
|
||||
when (transformation.size) {
|
||||
0 -> this
|
||||
1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
|
||||
@ -56,16 +57,16 @@ class FrostGlideModule : AppGlideModule() {
|
||||
// registry.replace(GlideUrl::class.java,
|
||||
// InputStream::class.java,
|
||||
// OkHttpUrlLoader.Factory(getFrostHttpClient()))
|
||||
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory())
|
||||
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java,
|
||||
// HdImageLoadingFactory())
|
||||
}
|
||||
}
|
||||
|
||||
// private fun getFrostHttpClient(): OkHttpClient =
|
||||
// OkHttpClient.Builder().addInterceptor(FrostCookieInterceptor()).build()
|
||||
|
||||
class FrostCookieInterceptor @Inject internal constructor(
|
||||
private val fbCookie: FbCookie
|
||||
) : Interceptor {
|
||||
class FrostCookieInterceptor @Inject internal constructor(private val fbCookie: FbCookie) :
|
||||
Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val origRequest = chain.request()
|
||||
|
@ -32,13 +32,9 @@ import com.pitchedapps.frost.injectors.ThemeProvider
|
||||
import com.pitchedapps.frost.prefs.Prefs
|
||||
import com.pitchedapps.frost.utils.launchWebOverlay
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 30/12/17.
|
||||
*/
|
||||
/** Created by Allan Wang on 30/12/17. */
|
||||
|
||||
/**
|
||||
* Base contract for anything with a url that may be launched in a new overlay
|
||||
*/
|
||||
/** Base contract for anything with a url that may be launched in a new overlay */
|
||||
interface ClickableIItemContract {
|
||||
|
||||
val url: String?
|
||||
@ -51,39 +47,32 @@ interface ClickableIItemContract {
|
||||
companion object {
|
||||
fun bindEvents(adapter: IAdapter<GenericItem>, fbCookie: FbCookie, prefs: Prefs) {
|
||||
adapter.fastAdapter?.apply {
|
||||
selectExtension {
|
||||
isSelectable = false
|
||||
}
|
||||
selectExtension { isSelectable = false }
|
||||
onClickListener = { v, _, item, _ ->
|
||||
if (item is ClickableIItemContract) {
|
||||
item.click(v!!.context, fbCookie, prefs)
|
||||
true
|
||||
} else
|
||||
false
|
||||
} else false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic header item
|
||||
* Not clickable with an accent color
|
||||
*/
|
||||
/** Generic header item Not clickable with an accent color */
|
||||
open class HeaderIItem(
|
||||
val text: String?,
|
||||
itemId: Int = R.layout.iitem_header,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : KauIItem<HeaderIItem.ViewHolder>(
|
||||
) :
|
||||
KauIItem<HeaderIItem.ViewHolder>(
|
||||
R.layout.iitem_header,
|
||||
{ ViewHolder(it, themeProvider) },
|
||||
itemId
|
||||
) {
|
||||
|
||||
class ViewHolder(
|
||||
itemView: View,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : FastAdapter.ViewHolder<HeaderIItem>(itemView) {
|
||||
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
|
||||
FastAdapter.ViewHolder<HeaderIItem>(itemView) {
|
||||
|
||||
val text: TextView by bindView(R.id.item_header_text)
|
||||
|
||||
@ -99,22 +88,18 @@ open class HeaderIItem(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic text item
|
||||
* Clickable with text color
|
||||
*/
|
||||
/** Generic text item Clickable with text color */
|
||||
open class TextIItem(
|
||||
val text: String?,
|
||||
override val url: String?,
|
||||
itemId: Int = R.layout.iitem_text,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : KauIItem<TextIItem.ViewHolder>(R.layout.iitem_text, { ViewHolder(it, themeProvider) }, itemId),
|
||||
) :
|
||||
KauIItem<TextIItem.ViewHolder>(R.layout.iitem_text, { ViewHolder(it, themeProvider) }, itemId),
|
||||
ClickableIItemContract {
|
||||
|
||||
class ViewHolder(
|
||||
itemView: View,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : FastAdapter.ViewHolder<TextIItem>(itemView) {
|
||||
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
|
||||
FastAdapter.ViewHolder<TextIItem>(itemView) {
|
||||
|
||||
val text: TextView by bindView(R.id.item_text_view)
|
||||
|
||||
|
@ -41,15 +41,15 @@ import com.pitchedapps.frost.prefs.Prefs
|
||||
import com.pitchedapps.frost.utils.isIndependent
|
||||
import com.pitchedapps.frost.utils.launchWebOverlay
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 27/12/17.
|
||||
*/
|
||||
/** Created by Allan Wang on 27/12/17. */
|
||||
class NotificationIItem(
|
||||
val notification: FrostNotif,
|
||||
val cookie: String,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : KauIItem<NotificationIItem.ViewHolder>(
|
||||
R.layout.iitem_notification, { ViewHolder(it, themeProvider) }
|
||||
) :
|
||||
KauIItem<NotificationIItem.ViewHolder>(
|
||||
R.layout.iitem_notification,
|
||||
{ ViewHolder(it, themeProvider) }
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@ -60,23 +60,19 @@ class NotificationIItem(
|
||||
themeProvider: ThemeProvider
|
||||
) {
|
||||
adapter.fastAdapter?.apply {
|
||||
selectExtension {
|
||||
isSelectable = false
|
||||
}
|
||||
selectExtension { isSelectable = false }
|
||||
onClickListener = { v, _, item, position ->
|
||||
val notif = item.notification
|
||||
if (notif.unread) {
|
||||
adapter.set(
|
||||
position,
|
||||
NotificationIItem(
|
||||
notif.copy(unread = false),
|
||||
item.cookie,
|
||||
themeProvider
|
||||
)
|
||||
NotificationIItem(notif.copy(unread = false), item.cookie, themeProvider)
|
||||
)
|
||||
}
|
||||
// TODO temp fix. If url is dependent, we cannot load it directly
|
||||
v!!.context.launchWebOverlay(
|
||||
v!!
|
||||
.context
|
||||
.launchWebOverlay(
|
||||
if (notif.url.isIndependent) notif.url else FbItem.NOTIFICATIONS.url,
|
||||
fbCookie,
|
||||
prefs
|
||||
@ -95,10 +91,7 @@ class NotificationIItem(
|
||||
override fun areItemsTheSame(oldItem: NotificationIItem, newItem: NotificationIItem) =
|
||||
oldItem.notification.id == newItem.notification.id
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: NotificationIItem,
|
||||
newItem: NotificationIItem
|
||||
) =
|
||||
override fun areContentsTheSame(oldItem: NotificationIItem, newItem: NotificationIItem) =
|
||||
oldItem.notification == newItem.notification
|
||||
|
||||
override fun getChangePayload(
|
||||
@ -111,10 +104,8 @@ class NotificationIItem(
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(
|
||||
itemView: View,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : FastAdapter.ViewHolder<NotificationIItem>(itemView) {
|
||||
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
|
||||
FastAdapter.ViewHolder<NotificationIItem>(itemView) {
|
||||
|
||||
private val frame: ViewGroup by bindView(R.id.item_frame)
|
||||
private val avatar: ImageView by bindView(R.id.item_avatar)
|
||||
@ -127,7 +118,8 @@ class NotificationIItem(
|
||||
|
||||
override fun bindView(item: NotificationIItem, payloads: List<Any>) {
|
||||
val notif = item.notification
|
||||
frame.background = createSimpleRippleDrawable(
|
||||
frame.background =
|
||||
createSimpleRippleDrawable(
|
||||
themeProvider.textColor,
|
||||
themeProvider.nativeBgColor(notif.unread)
|
||||
)
|
||||
@ -135,11 +127,8 @@ class NotificationIItem(
|
||||
date.setTextColor(themeProvider.textColor.withAlpha(150))
|
||||
|
||||
val glide = glide
|
||||
glide.load(notif.img)
|
||||
.transform(FrostGlide.circleCrop)
|
||||
.into(avatar)
|
||||
if (notif.thumbnailUrl != null)
|
||||
glide.load(notif.thumbnailUrl).into(thumbnail.visible())
|
||||
glide.load(notif.img).transform(FrostGlide.circleCrop).into(avatar)
|
||||
if (notif.thumbnailUrl != null) glide.load(notif.thumbnailUrl).into(thumbnail.visible())
|
||||
|
||||
content.text = notif.content
|
||||
date.text = notif.timeString
|
||||
|
@ -31,22 +31,15 @@ import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.facebook.FbItem
|
||||
import com.pitchedapps.frost.injectors.ThemeProvider
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 26/11/17.
|
||||
*/
|
||||
/** Created by Allan Wang on 26/11/17. */
|
||||
class TabIItem(val item: FbItem, private val themeProvider: ThemeProvider) :
|
||||
KauIItem<TabIItem.ViewHolder>(
|
||||
R.layout.iitem_tab_preview,
|
||||
{ ViewHolder(it, themeProvider) }
|
||||
),
|
||||
KauIItem<TabIItem.ViewHolder>(R.layout.iitem_tab_preview, { ViewHolder(it, themeProvider) }),
|
||||
IDraggable {
|
||||
|
||||
override val isDraggable: Boolean = true
|
||||
|
||||
class ViewHolder(
|
||||
itemView: View,
|
||||
private val themeProvider: ThemeProvider
|
||||
) : FastAdapter.ViewHolder<TabIItem>(itemView) {
|
||||
class ViewHolder(itemView: View, private val themeProvider: ThemeProvider) :
|
||||
FastAdapter.ViewHolder<TabIItem>(itemView) {
|
||||
|
||||
val image: ImageView by bindView(R.id.image)
|
||||
val text: TextView by bindView(R.id.text)
|
||||
@ -55,8 +48,7 @@ class TabIItem(val item: FbItem, private val themeProvider: ThemeProvider) :
|
||||
val isInToolbar = adapterPosition < 4
|
||||
val color = if (isInToolbar) themeProvider.iconColor else themeProvider.textColor
|
||||
image.setIcon(item.item.icon, 20, color)
|
||||
if (isInToolbar)
|
||||
text.invisible()
|
||||
if (isInToolbar) text.invisible()
|
||||
else {
|
||||
text.visible().setText(item.item.titleId)
|
||||
text.setTextColor(color.withAlpha(200))
|
||||
|
@ -19,17 +19,18 @@ package com.pitchedapps.frost.injectors
|
||||
import android.webkit.WebView
|
||||
import com.pitchedapps.frost.prefs.Prefs
|
||||
|
||||
/**
|
||||
* Small misc inline css assets
|
||||
*/
|
||||
/** Small misc inline css assets */
|
||||
enum class CssAsset(private val content: String) : InjectorContract {
|
||||
FullSizeImage("div._4prr[style*=\"max-width\"][style*=\"max-height\"]{max-width:none !important;max-height:none !important}"),
|
||||
FullSizeImage(
|
||||
"div._4prr[style*=\"max-width\"][style*=\"max-height\"]{max-width:none !important;max-height:none !important}"
|
||||
),
|
||||
|
||||
/*
|
||||
* Remove top margin and hide some contents from the top bar and home page (as it's our base url)
|
||||
*/
|
||||
Menu("#bookmarks_flyout{margin-top:0 !important}#m_news_feed_stream,#MComposer{display:none !important}")
|
||||
;
|
||||
Menu(
|
||||
"#bookmarks_flyout{margin-top:0 !important}#m_news_feed_stream,#MComposer{display:none !important}"
|
||||
);
|
||||
|
||||
val injector: JsInjector by lazy {
|
||||
JsBuilder().css(content).single("css-small-assets-$name").build()
|
||||
|
@ -33,10 +33,7 @@ enum class CssHider(private vararg val items: String) : InjectorContract {
|
||||
"#header-notices",
|
||||
"[data-sigil*=m-promo-jewel-header]"
|
||||
),
|
||||
ADS(
|
||||
"article[data-xt*=sponsor]",
|
||||
"article[data-store*=sponsor]"
|
||||
),
|
||||
ADS("article[data-xt*=sponsor]", "article[data-store*=sponsor]"),
|
||||
PEOPLE_YOU_MAY_KNOW("article._d2r"),
|
||||
SUGGESTED_GROUPS("article[data-ft*=\"ei\":]"),
|
||||
COMPOSER("#MComposer"),
|
||||
@ -47,19 +44,15 @@ enum class CssHider(private vararg val items: String) : InjectorContract {
|
||||
// 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\"]"
|
||||
)
|
||||
;
|
||||
POST_ACTIONS("footer [data-sigil=\"ufi-inline-actions\"]"),
|
||||
POST_REACTIONS("footer [data-sigil=\"reactions-bling-bar\"]");
|
||||
|
||||
val injector: JsInjector by lazy {
|
||||
JsBuilder().css("${items.joinToString(separator = ",")}{display:none !important}")
|
||||
.single("css-hider-$name").build()
|
||||
JsBuilder()
|
||||
.css("${items.joinToString(separator = ",")}{display:none !important}")
|
||||
.single("css-hider-$name")
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun inject(webView: WebView, prefs: Prefs) =
|
||||
injector.inject(webView, prefs)
|
||||
override fun inject(webView: WebView, prefs: Prefs) = injector.inject(webView, prefs)
|
||||
}
|
||||
|
@ -27,24 +27,23 @@ import com.pitchedapps.frost.prefs.Prefs
|
||||
*/
|
||||
enum class JsActions(body: String) : InjectorContract {
|
||||
/**
|
||||
* Redirects to login activity if create account is found
|
||||
* see [com.pitchedapps.frost.web.FrostJSI.loadLogin]
|
||||
* 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);"""),
|
||||
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
|
||||
*/
|
||||
/** Used as a pseudoinjector for maybe functions */
|
||||
EMPTY("");
|
||||
|
||||
val function = "(function(){$body})();"
|
||||
|
||||
override fun inject(webView: WebView, prefs: Prefs) =
|
||||
JsInjector(function).inject(webView, prefs)
|
||||
override fun inject(webView: WebView, prefs: Prefs) = JsInjector(function).inject(webView, prefs)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
|
@ -22,24 +22,32 @@ import androidx.annotation.VisibleForTesting
|
||||
import ca.allanwang.kau.kotlin.lazyContext
|
||||
import com.pitchedapps.frost.prefs.Prefs
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
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 css file name
|
||||
* Created by Allan Wang on 2017-05-31. Mapping of the available assets The enum name must match the
|
||||
* css file name
|
||||
*/
|
||||
enum class JsAssets(private val singleLoad: Boolean = true) : InjectorContract {
|
||||
MENU, MENU_QUICK(singleLoad = false), CLICK_A, CONTEXT_A, MEDIA, HEADER_BADGES, TEXTAREA_LISTENER, NOTIF_MSG,
|
||||
DOCUMENT_WATCHER, HORIZONTAL_SCROLLING, AUTO_RESIZE_TEXTAREA(singleLoad = false), SCROLL_STOP,
|
||||
MENU,
|
||||
MENU_QUICK(singleLoad = false),
|
||||
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.toLowerCase(Locale.CANADA)}.js"
|
||||
@VisibleForTesting internal val file = "${name.toLowerCase(Locale.CANADA)}.js"
|
||||
private val injector = lazyContext {
|
||||
try {
|
||||
val content = it.assets.open("js/$file").bufferedReader().use(BufferedReader::readText)
|
||||
@ -56,9 +64,7 @@ enum class JsAssets(private val singleLoad: Boolean = true) : InjectorContract {
|
||||
companion object {
|
||||
// Ensures that all non themes and the selected theme are loaded
|
||||
suspend fun load(context: Context) {
|
||||
withContext(Dispatchers.IO) {
|
||||
values().forEach { it.injector.invoke(context) }
|
||||
}
|
||||
withContext(Dispatchers.IO) { values().forEach { it.injector.invoke(context) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,8 @@ import androidx.annotation.VisibleForTesting
|
||||
import com.pitchedapps.frost.prefs.Prefs
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.web.FrostWebViewClient
|
||||
import org.apache.commons.text.StringEscapeUtils
|
||||
import kotlin.random.Random
|
||||
import org.apache.commons.text.StringEscapeUtils
|
||||
|
||||
class JsBuilder {
|
||||
private val css = StringBuilder()
|
||||
@ -49,7 +49,8 @@ class JsBuilder {
|
||||
|
||||
override fun toString(): String {
|
||||
val tag = this.tag
|
||||
val builder = StringBuilder().apply {
|
||||
val builder =
|
||||
StringBuilder().apply {
|
||||
append("!function(){")
|
||||
if (css.isNotBlank()) {
|
||||
val cssMin = css.replace(Regex("\\s*\n\\s*"), "")
|
||||
@ -71,51 +72,40 @@ class JsBuilder {
|
||||
return content
|
||||
}
|
||||
|
||||
private fun singleInjector(tag: String, content: String) = StringBuilder().apply {
|
||||
private fun singleInjector(tag: String, content: String) =
|
||||
StringBuilder()
|
||||
.apply {
|
||||
append("if (!window.hasOwnProperty(\"$tag\")) {")
|
||||
append("console.log(\"Registering $tag\");")
|
||||
append("window.$tag = true;")
|
||||
append(content)
|
||||
append("}")
|
||||
}.toString()
|
||||
}
|
||||
.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract for all injectors to allow it to interact properly with a webview
|
||||
*/
|
||||
/** Contract for all injectors to allow it to interact properly with a webview */
|
||||
interface InjectorContract {
|
||||
fun inject(webView: WebView, prefs: Prefs)
|
||||
|
||||
/**
|
||||
* Toggle the injector (usually through Prefs
|
||||
* If false, will fallback to an empty action
|
||||
*/
|
||||
/** Toggle the injector (usually through Prefs If false, will fallback to an empty action */
|
||||
fun maybe(enable: Boolean): InjectorContract = if (enable) this else JsActions.EMPTY
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to inject multiple functions simultaneously with a single callback
|
||||
*/
|
||||
/** Helper method to inject multiple functions simultaneously with a single callback */
|
||||
fun WebView.jsInject(vararg injectors: InjectorContract, prefs: Prefs) {
|
||||
injectors.asSequence().filter { it != JsActions.EMPTY }.forEach {
|
||||
it.inject(this, prefs)
|
||||
}
|
||||
injectors.asSequence().filter { it != JsActions.EMPTY }.forEach { it.inject(this, prefs) }
|
||||
}
|
||||
|
||||
fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract, prefs: Prefs) =
|
||||
web.jsInject(*injectors, prefs = prefs)
|
||||
|
||||
/**
|
||||
* Wrapper class to convert a function into an injector
|
||||
*/
|
||||
/** Wrapper class to convert a function into an injector */
|
||||
class JsInjector(val function: String) : InjectorContract {
|
||||
override fun inject(webView: WebView, prefs: Prefs) =
|
||||
webView.evaluateJavascript(function, null)
|
||||
override fun inject(webView: WebView, prefs: Prefs) = webView.evaluateJavascript(function, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper object to obfuscate window tags for JS injection.
|
||||
*/
|
||||
/** Helper object to obfuscate window tags for JS injection. */
|
||||
@VisibleForTesting
|
||||
internal object TagObfuscator {
|
||||
|
||||
|
@ -35,12 +35,12 @@ import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileNotFoundException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface ThemeProvider {
|
||||
val textColor: Int
|
||||
@ -61,9 +61,7 @@ interface ThemeProvider {
|
||||
|
||||
val isCustomTheme: Boolean
|
||||
|
||||
/**
|
||||
* Note that while this can be loaded from any thread, it is typically done through [preload]]
|
||||
*/
|
||||
/** Note that while this can be loaded from any thread, it is typically done through [preload]] */
|
||||
fun injector(category: ThemeCategory): InjectorContract
|
||||
|
||||
fun setTheme(id: Int)
|
||||
@ -74,13 +72,13 @@ interface ThemeProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides [InjectorContract] for each [ThemeCategory].
|
||||
* Can be reloaded to take in changes from [Prefs]
|
||||
* Provides [InjectorContract] for each [ThemeCategory]. Can be reloaded to take in changes from
|
||||
* [Prefs]
|
||||
*/
|
||||
class ThemeProviderImpl @Inject internal constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val prefs: Prefs
|
||||
) : ThemeProvider {
|
||||
class ThemeProviderImpl
|
||||
@Inject
|
||||
internal constructor(@ApplicationContext private val context: Context, private val prefs: Prefs) :
|
||||
ThemeProvider {
|
||||
|
||||
private var theme: Theme = Theme.values[prefs.theme]
|
||||
set(value) {
|
||||
@ -97,7 +95,8 @@ class ThemeProviderImpl @Inject internal constructor(
|
||||
get() = theme.accentColorGetter(prefs)
|
||||
|
||||
override val accentColorForWhite: Int
|
||||
get() = when {
|
||||
get() =
|
||||
when {
|
||||
accentColor.isColorVisibleOn(Color.WHITE) -> accentColor
|
||||
textColor.isColorVisibleOn(Color.WHITE) -> textColor
|
||||
else -> FACEBOOK_BLUE
|
||||
@ -106,9 +105,8 @@ class ThemeProviderImpl @Inject internal constructor(
|
||||
override val nativeBgColor: Int
|
||||
get() = bgColor.withAlpha(30)
|
||||
|
||||
override fun nativeBgColor(unread: Boolean) = bgColor
|
||||
.colorToForeground(if (unread) 0.7f else 0.0f)
|
||||
.withAlpha(30)
|
||||
override fun nativeBgColor(unread: Boolean) =
|
||||
bgColor.colorToForeground(if (unread) 0.7f else 0.0f).withAlpha(30)
|
||||
|
||||
override val bgColor: Int
|
||||
get() = theme.backgroundColorGetter(prefs)
|
||||
@ -125,24 +123,22 @@ class ThemeProviderImpl @Inject internal constructor(
|
||||
override fun injector(category: ThemeCategory): InjectorContract =
|
||||
injectors.getOrPut(category) { createInjector(category) }
|
||||
|
||||
/**
|
||||
* Note that while this can be loaded from any thread, it is typically done through [preload]
|
||||
*/
|
||||
/** Note that while this can be loaded from any thread, it is typically done through [preload] */
|
||||
private fun createInjector(category: ThemeCategory): InjectorContract {
|
||||
val file = theme.file ?: return JsActions.EMPTY
|
||||
try {
|
||||
var content =
|
||||
context.assets.open("css/${category.folder}/themes/$file").bufferedReader()
|
||||
context.assets
|
||||
.open("css/${category.folder}/themes/$file")
|
||||
.bufferedReader()
|
||||
.use(BufferedReader::readText)
|
||||
if (theme == Theme.CUSTOM) {
|
||||
val bt = if (Color.alpha(bgColor) == 255)
|
||||
bgColor.toRgbaString()
|
||||
else
|
||||
"transparent"
|
||||
val bt = if (Color.alpha(bgColor) == 255) bgColor.toRgbaString() else "transparent"
|
||||
|
||||
val bb = bgColor.colorToForeground(0.35f)
|
||||
|
||||
content = content
|
||||
content =
|
||||
content
|
||||
.replace("\$T\$", textColor.toRgbaString())
|
||||
.replace("\$TT\$", textColor.colorToBackground(0.05f).toRgbaString())
|
||||
.replace("\$TD\$", textColor.adjustAlpha(0.6f).toRgbaString())
|
||||
@ -185,7 +181,5 @@ class ThemeProviderImpl @Inject internal constructor(
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface ThemeProviderModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun themeProvider(to: ThemeProviderImpl): ThemeProvider
|
||||
@Binds @Singleton fun themeProvider(to: ThemeProviderImpl): ThemeProvider
|
||||
}
|
||||
|
@ -24,19 +24,17 @@ import com.pitchedapps.frost.activities.IntroActivity
|
||||
import com.pitchedapps.frost.databinding.IntroThemeBinding
|
||||
import com.pitchedapps.frost.enums.Theme
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-07-28.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-07-28. */
|
||||
class IntroFragmentTheme : BaseIntroFragment(R.layout.intro_theme) {
|
||||
|
||||
private lateinit var binding: IntroThemeBinding
|
||||
|
||||
val themeList
|
||||
get() = with(binding) {
|
||||
listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass)
|
||||
}
|
||||
get() =
|
||||
with(binding) { listOf(introThemeLight, introThemeDark, introThemeAmoled, introThemeGlass) }
|
||||
|
||||
override fun viewArray(): Array<Array<out View>> = with(binding) {
|
||||
override fun viewArray(): Array<Array<out View>> =
|
||||
with(binding) {
|
||||
arrayOf(
|
||||
arrayOf(title),
|
||||
arrayOf(introThemeLight, introThemeDark),
|
||||
@ -57,9 +55,7 @@ class IntroFragmentTheme : BaseIntroFragment(R.layout.intro_theme) {
|
||||
introThemeGlass.setThemeClick(Theme.GLASS)
|
||||
val currentTheme = prefs.theme - 1
|
||||
if (currentTheme in 0..3)
|
||||
themeList.forEachIndexed { index, v ->
|
||||
v.scaleXY = if (index == currentTheme) 1.6f else 0.8f
|
||||
}
|
||||
themeList.forEachIndexed { index, v -> v.scaleXY = if (index == currentTheme) 1.6f else 0.8f }
|
||||
}
|
||||
|
||||
private fun View.setThemeClick(theme: Theme) {
|
||||
|
@ -32,18 +32,17 @@ import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.utils.launchTabCustomizerActivity
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-07-28.
|
||||
*/
|
||||
abstract class BaseImageIntroFragment(
|
||||
val titleRes: Int,
|
||||
val imageRes: Int,
|
||||
val descRes: Int
|
||||
) : BaseIntroFragment(R.layout.intro_image) {
|
||||
/** Created by Allan Wang on 2017-07-28. */
|
||||
abstract class BaseImageIntroFragment(val titleRes: Int, val imageRes: Int, val descRes: Int) :
|
||||
BaseIntroFragment(R.layout.intro_image) {
|
||||
|
||||
val imageDrawable: LayerDrawable by lazyResettableRegistered { image.drawable as LayerDrawable }
|
||||
val phone: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone) }
|
||||
val screen: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone_screen) }
|
||||
val phone: Drawable by lazyResettableRegistered {
|
||||
imageDrawable.findDrawableByLayerId(R.id.intro_phone)
|
||||
}
|
||||
val screen: Drawable by lazyResettableRegistered {
|
||||
imageDrawable.findDrawableByLayerId(R.id.intro_phone_screen)
|
||||
}
|
||||
val icon: ImageView by bindViewResettable(R.id.intro_button)
|
||||
|
||||
override fun viewArray(): Array<Array<out View>> = arrayOf(arrayOf(title), arrayOf(desc))
|
||||
@ -78,17 +77,16 @@ abstract class BaseImageIntroFragment(
|
||||
}
|
||||
|
||||
fun firstImageFragmentTransition(offset: Float) {
|
||||
if (offset < 0)
|
||||
image.alpha = 1 + offset
|
||||
if (offset < 0) image.alpha = 1 + offset
|
||||
}
|
||||
|
||||
fun lastImageFragmentTransition(offset: Float) {
|
||||
if (offset > 0)
|
||||
image.alpha = 1 - offset
|
||||
if (offset > 0) image.alpha = 1 - offset
|
||||
}
|
||||
}
|
||||
|
||||
class IntroAccountFragment : BaseImageIntroFragment(
|
||||
class IntroAccountFragment :
|
||||
BaseImageIntroFragment(
|
||||
R.string.intro_multiple_accounts,
|
||||
R.drawable.intro_phone_nav,
|
||||
R.string.intro_multiple_accounts_desc
|
||||
@ -96,7 +94,11 @@ class IntroAccountFragment : BaseImageIntroFragment(
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
super.themeFragmentImpl()
|
||||
themeImageComponent(themeProvider.iconColor, R.id.intro_phone_avatar_1, R.id.intro_phone_avatar_2)
|
||||
themeImageComponent(
|
||||
themeProvider.iconColor,
|
||||
R.id.intro_phone_avatar_1,
|
||||
R.id.intro_phone_avatar_2
|
||||
)
|
||||
themeImageComponent(themeProvider.bgColor.colorToForeground(), R.id.intro_phone_nav)
|
||||
themeImageComponent(themeProvider.headerColor, R.id.intro_phone_header)
|
||||
}
|
||||
@ -107,16 +109,17 @@ class IntroAccountFragment : BaseImageIntroFragment(
|
||||
}
|
||||
}
|
||||
|
||||
class IntroTabTouchFragment : BaseImageIntroFragment(
|
||||
R.string.intro_easy_navigation, R.drawable.intro_phone_tab, R.string.intro_easy_navigation_desc
|
||||
class IntroTabTouchFragment :
|
||||
BaseImageIntroFragment(
|
||||
R.string.intro_easy_navigation,
|
||||
R.drawable.intro_phone_tab,
|
||||
R.string.intro_easy_navigation_desc
|
||||
) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
icon.visible().setIcon(GoogleMaterial.Icon.gmd_edit, 24)
|
||||
icon.setOnClickListener {
|
||||
activity?.launchTabCustomizerActivity()
|
||||
}
|
||||
icon.setOnClickListener { activity?.launchTabCustomizerActivity() }
|
||||
}
|
||||
|
||||
override fun themeFragmentImpl() {
|
||||
@ -133,7 +136,8 @@ class IntroTabTouchFragment : BaseImageIntroFragment(
|
||||
}
|
||||
}
|
||||
|
||||
class IntroTabContextFragment : BaseImageIntroFragment(
|
||||
class IntroTabContextFragment :
|
||||
BaseImageIntroFragment(
|
||||
R.string.intro_context_aware,
|
||||
R.drawable.intro_phone_long_press,
|
||||
R.string.intro_context_aware_desc
|
||||
|
@ -45,17 +45,13 @@ import kotlin.math.abs
|
||||
* Contains the base, start, and end fragments
|
||||
*/
|
||||
|
||||
/**
|
||||
* The core intro fragment for all other fragments
|
||||
*/
|
||||
/** The core intro fragment for all other fragments */
|
||||
@AndroidEntryPoint
|
||||
abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
|
||||
|
||||
@Inject
|
||||
protected lateinit var prefs: Prefs
|
||||
@Inject protected lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
protected lateinit var themeProvider: ThemeProvider
|
||||
@Inject protected lateinit var themeProvider: ThemeProvider
|
||||
|
||||
val screenWidth
|
||||
get() = resources.displayMetrics.widthPixels
|
||||
@ -67,8 +63,7 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
|
||||
val increment = maxTranslation / views.size
|
||||
views.forEachIndexed { i, group ->
|
||||
group.forEach {
|
||||
it.translationX =
|
||||
if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
|
||||
it.translationX = if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment
|
||||
it.alpha = 1 - abs(offset)
|
||||
}
|
||||
}
|
||||
@ -112,7 +107,9 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
|
||||
}
|
||||
|
||||
protected open fun themeFragmentImpl() {
|
||||
(view as? ViewGroup)?.children?.forEach { (it as? TextView)?.setTextColor(themeProvider.textColor) }
|
||||
(view as? ViewGroup)?.children?.forEach {
|
||||
(it as? TextView)?.setTextColor(themeProvider.textColor)
|
||||
}
|
||||
}
|
||||
|
||||
protected val viewArray: Array<Array<out View>> by lazyResettableRegistered { viewArray() }
|
||||
@ -131,8 +128,7 @@ abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() {
|
||||
if (view != null) onPageSelectedImpl()
|
||||
}
|
||||
|
||||
protected open fun onPageSelectedImpl() {
|
||||
}
|
||||
protected open fun onPageSelectedImpl() {}
|
||||
}
|
||||
|
||||
class IntroFragmentWelcome : BaseIntroFragment(R.layout.intro_welcome) {
|
||||
|
@ -108,8 +108,8 @@ class OldPrefs @Inject internal constructor(factory: KPrefFactory) :
|
||||
var enablePip: Boolean by kpref("enable_pip", true)
|
||||
|
||||
/**
|
||||
* Despite the naming, this toggle currently only enables debug logging.
|
||||
* Verbose is never logged in release builds.
|
||||
* Despite the naming, this toggle currently only enables debug logging. Verbose is never logged
|
||||
* in release builds.
|
||||
*/
|
||||
var verboseLogging: Boolean by kpref("verbose_logging", false)
|
||||
|
||||
|
@ -41,7 +41,8 @@ import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* [Prefs] is no longer an actual pref, but we will expose the reset function as it is used elsewhere
|
||||
* [Prefs] is no longer an actual pref, but we will expose the reset function as it is used
|
||||
* elsewhere
|
||||
*/
|
||||
interface PrefsBase {
|
||||
fun reset()
|
||||
@ -49,22 +50,19 @@ interface PrefsBase {
|
||||
}
|
||||
|
||||
interface Prefs :
|
||||
BehaviourPrefs,
|
||||
CorePrefs,
|
||||
FeedPrefs,
|
||||
NotifPrefs,
|
||||
ThemePrefs,
|
||||
ShowcasePrefs,
|
||||
PrefsBase
|
||||
BehaviourPrefs, CorePrefs, FeedPrefs, NotifPrefs, ThemePrefs, ShowcasePrefs, PrefsBase
|
||||
|
||||
class PrefsImpl @Inject internal constructor(
|
||||
class PrefsImpl
|
||||
@Inject
|
||||
internal constructor(
|
||||
private val behaviourPrefs: BehaviourPrefs,
|
||||
private val corePrefs: CorePrefs,
|
||||
private val feedPrefs: FeedPrefs,
|
||||
private val notifPrefs: NotifPrefs,
|
||||
private val themePrefs: ThemePrefs,
|
||||
private val showcasePrefs: ShowcasePrefs
|
||||
) : Prefs,
|
||||
) :
|
||||
Prefs,
|
||||
BehaviourPrefs by behaviourPrefs,
|
||||
CorePrefs by corePrefs,
|
||||
FeedPrefs by feedPrefs,
|
||||
@ -94,33 +92,19 @@ class PrefsImpl @Inject internal constructor(
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PrefModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun behaviour(to: BehaviourPrefsImpl): BehaviourPrefs
|
||||
@Binds @Singleton fun behaviour(to: BehaviourPrefsImpl): BehaviourPrefs
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun core(to: CorePrefsImpl): CorePrefs
|
||||
@Binds @Singleton fun core(to: CorePrefsImpl): CorePrefs
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun feed(to: FeedPrefsImpl): FeedPrefs
|
||||
@Binds @Singleton fun feed(to: FeedPrefsImpl): FeedPrefs
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun notif(to: NotifPrefsImpl): NotifPrefs
|
||||
@Binds @Singleton fun notif(to: NotifPrefsImpl): NotifPrefs
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun theme(to: ThemePrefsImpl): ThemePrefs
|
||||
@Binds @Singleton fun theme(to: ThemePrefsImpl): ThemePrefs
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun showcase(to: ShowcasePrefsImpl): ShowcasePrefs
|
||||
@Binds @Singleton fun showcase(to: ShowcasePrefsImpl): ShowcasePrefs
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun prefs(to: PrefsImpl): Prefs
|
||||
@Binds @Singleton fun prefs(to: PrefsImpl): Prefs
|
||||
}
|
||||
|
||||
@Module
|
||||
|
@ -49,62 +49,42 @@ interface BehaviourPrefs : PrefsBase {
|
||||
var autoExpandTextBox: Boolean
|
||||
}
|
||||
|
||||
class BehaviourPrefsImpl @Inject internal constructor(
|
||||
class BehaviourPrefsImpl
|
||||
@Inject
|
||||
internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.behaviour", factory), BehaviourPrefs {
|
||||
|
||||
override var biometricsEnabled: Boolean by kpref(
|
||||
"biometrics_enabled",
|
||||
oldPrefs.biometricsEnabled /* false */
|
||||
)
|
||||
override var biometricsEnabled: Boolean by
|
||||
kpref("biometrics_enabled", oldPrefs.biometricsEnabled /* false */)
|
||||
|
||||
override var overlayEnabled: Boolean by kpref(
|
||||
"overlay_enabled",
|
||||
oldPrefs.overlayEnabled /* true */
|
||||
)
|
||||
override var overlayEnabled: Boolean by
|
||||
kpref("overlay_enabled", oldPrefs.overlayEnabled /* true */)
|
||||
|
||||
override var overlayFullScreenSwipe: Boolean by kpref(
|
||||
"overlay_full_screen_swipe",
|
||||
oldPrefs.overlayFullScreenSwipe /* true */
|
||||
)
|
||||
override var overlayFullScreenSwipe: Boolean by
|
||||
kpref("overlay_full_screen_swipe", oldPrefs.overlayFullScreenSwipe /* true */)
|
||||
|
||||
override var viewpagerSwipe: Boolean by kpref(
|
||||
"viewpager_swipe",
|
||||
oldPrefs.viewpagerSwipe /* true */
|
||||
)
|
||||
override var viewpagerSwipe: Boolean by
|
||||
kpref("viewpager_swipe", oldPrefs.viewpagerSwipe /* true */)
|
||||
|
||||
override var loadMediaOnMeteredNetwork: Boolean by kpref(
|
||||
"media_on_metered_network",
|
||||
oldPrefs.loadMediaOnMeteredNetwork /* true */
|
||||
)
|
||||
override var loadMediaOnMeteredNetwork: Boolean by
|
||||
kpref("media_on_metered_network", oldPrefs.loadMediaOnMeteredNetwork /* true */)
|
||||
|
||||
override var debugSettings: Boolean by kpref(
|
||||
"debug_settings",
|
||||
oldPrefs.debugSettings /* false */
|
||||
)
|
||||
override var debugSettings: Boolean by kpref("debug_settings", oldPrefs.debugSettings /* false */)
|
||||
|
||||
override var linksInDefaultApp: Boolean by kpref(
|
||||
"link_in_default_app",
|
||||
oldPrefs.linksInDefaultApp /* false */
|
||||
)
|
||||
override var linksInDefaultApp: Boolean by
|
||||
kpref("link_in_default_app", oldPrefs.linksInDefaultApp /* false */)
|
||||
|
||||
override var blackMediaBg: Boolean by kpref("black_media_bg", oldPrefs.blackMediaBg /* false */)
|
||||
|
||||
override var autoRefreshFeed: Boolean by kpref(
|
||||
"auto_refresh_feed",
|
||||
oldPrefs.autoRefreshFeed /* false */
|
||||
)
|
||||
override var autoRefreshFeed: Boolean by
|
||||
kpref("auto_refresh_feed", oldPrefs.autoRefreshFeed /* false */)
|
||||
|
||||
override var showCreateFab: Boolean by kpref(
|
||||
"show_create_fab",
|
||||
oldPrefs.showCreateFab /* true */
|
||||
)
|
||||
override var showCreateFab: Boolean by kpref("show_create_fab", oldPrefs.showCreateFab /* true */)
|
||||
|
||||
override var fullSizeImage: Boolean by kpref(
|
||||
"full_size_image",
|
||||
oldPrefs.fullSizeImage /* false */
|
||||
)
|
||||
override var fullSizeImage: Boolean by
|
||||
kpref("full_size_image", oldPrefs.fullSizeImage /* false */)
|
||||
|
||||
override var autoExpandTextBox: Boolean by kpref("auto_expand_text_box", true)
|
||||
}
|
||||
|
@ -41,8 +41,8 @@ interface CorePrefs : PrefsBase {
|
||||
var identifier: Int
|
||||
|
||||
/**
|
||||
* Despite the naming, this toggle currently only enables debug logging.
|
||||
* Verbose is never logged in release builds.
|
||||
* Despite the naming, this toggle currently only enables debug logging. Verbose is never logged
|
||||
* in release builds.
|
||||
*/
|
||||
var verboseLogging: Boolean
|
||||
|
||||
@ -55,7 +55,9 @@ interface CorePrefs : PrefsBase {
|
||||
var messageScrollToBottom: Boolean
|
||||
}
|
||||
|
||||
class CorePrefsImpl @Inject internal constructor(
|
||||
class CorePrefsImpl
|
||||
@Inject
|
||||
internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.core", factory), CorePrefs {
|
||||
@ -71,31 +73,22 @@ class CorePrefsImpl @Inject internal constructor(
|
||||
|
||||
override var versionCode: Int by kpref("version_code", oldPrefs.versionCode /* -1 */)
|
||||
|
||||
override var prevVersionCode: Int by kpref(
|
||||
"prev_version_code",
|
||||
oldPrefs.prevVersionCode /* -1 */
|
||||
)
|
||||
override var prevVersionCode: Int by kpref("prev_version_code", oldPrefs.prevVersionCode /* -1 */)
|
||||
|
||||
override var installDate: Long by kpref("install_date", oldPrefs.installDate /* -1L */)
|
||||
|
||||
override var identifier: Int by kpref("identifier", oldPrefs.identifier /* -1 */)
|
||||
|
||||
override var verboseLogging: Boolean by kpref(
|
||||
"verbose_logging",
|
||||
oldPrefs.verboseLogging /* false */
|
||||
)
|
||||
override var verboseLogging: Boolean by
|
||||
kpref("verbose_logging", oldPrefs.verboseLogging /* false */)
|
||||
|
||||
override var enablePip: Boolean by kpref("enable_pip", oldPrefs.enablePip /* true */)
|
||||
|
||||
override var exitConfirmation: Boolean by kpref(
|
||||
"exit_confirmation",
|
||||
oldPrefs.exitConfirmation /* true */
|
||||
)
|
||||
override var exitConfirmation: Boolean by
|
||||
kpref("exit_confirmation", oldPrefs.exitConfirmation /* true */)
|
||||
|
||||
override var animate: Boolean by kpref("fancy_animations", oldPrefs.animate /* true */)
|
||||
|
||||
override var messageScrollToBottom: Boolean by kpref(
|
||||
"message_scroll_to_bottom",
|
||||
oldPrefs.messageScrollToBottom /* false */
|
||||
)
|
||||
override var messageScrollToBottom: Boolean by
|
||||
kpref("message_scroll_to_bottom", oldPrefs.messageScrollToBottom /* false */)
|
||||
}
|
||||
|
@ -50,49 +50,32 @@ interface FeedPrefs : PrefsBase {
|
||||
var showPostReactions: Boolean
|
||||
}
|
||||
|
||||
class FeedPrefsImpl @Inject internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs
|
||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.feed", factory), FeedPrefs {
|
||||
class FeedPrefsImpl @Inject internal constructor(factory: KPrefFactory, oldPrefs: OldPrefs) :
|
||||
KPref("${BuildConfig.APPLICATION_ID}.prefs.feed", factory), FeedPrefs {
|
||||
|
||||
override var webTextScaling: Int by kpref("web_text_scaling", oldPrefs.webTextScaling /* 100 */)
|
||||
|
||||
override var feedSort: Int by kpref(
|
||||
"feed_sort",
|
||||
oldPrefs.feedSort /* FeedSort.DEFAULT.ordinal */
|
||||
)
|
||||
override var feedSort: Int by kpref("feed_sort", oldPrefs.feedSort /* FeedSort.DEFAULT.ordinal */)
|
||||
|
||||
override var aggressiveRecents: Boolean by kpref(
|
||||
"aggressive_recents",
|
||||
oldPrefs.aggressiveRecents /* false */
|
||||
)
|
||||
override var aggressiveRecents: Boolean by
|
||||
kpref("aggressive_recents", oldPrefs.aggressiveRecents /* false */)
|
||||
|
||||
override var showComposer: Boolean by kpref(
|
||||
"status_composer_feed",
|
||||
oldPrefs.showComposer /* true */
|
||||
)
|
||||
override var showComposer: Boolean by
|
||||
kpref("status_composer_feed", oldPrefs.showComposer /* true */)
|
||||
|
||||
override var showSuggestedFriends: Boolean by kpref(
|
||||
"suggested_friends_feed",
|
||||
oldPrefs.showSuggestedFriends /* true */
|
||||
)
|
||||
override var showSuggestedFriends: Boolean by
|
||||
kpref("suggested_friends_feed", oldPrefs.showSuggestedFriends /* true */)
|
||||
|
||||
override var showSuggestedGroups: Boolean by kpref(
|
||||
"suggested_groups_feed",
|
||||
oldPrefs.showSuggestedGroups /* true */
|
||||
)
|
||||
override var showSuggestedGroups: Boolean by
|
||||
kpref("suggested_groups_feed", oldPrefs.showSuggestedGroups /* true */)
|
||||
|
||||
override var showFacebookAds: Boolean by kpref(
|
||||
"facebook_ads",
|
||||
oldPrefs.showFacebookAds /* false */
|
||||
)
|
||||
override var showFacebookAds: Boolean by
|
||||
kpref("facebook_ads", oldPrefs.showFacebookAds /* false */)
|
||||
|
||||
override var showStories: Boolean by kpref("show_stories", oldPrefs.showStories /* true */)
|
||||
|
||||
override var mainActivityLayoutType: Int by kpref(
|
||||
"main_activity_layout_type",
|
||||
oldPrefs.mainActivityLayoutType /* 0 */
|
||||
)
|
||||
override var mainActivityLayoutType: Int by
|
||||
kpref("main_activity_layout_type", oldPrefs.mainActivityLayoutType /* 0 */)
|
||||
|
||||
override val mainActivityLayout: MainActivityLayout
|
||||
get() = MainActivityLayout(mainActivityLayoutType)
|
||||
|
@ -47,63 +47,43 @@ interface NotifPrefs : PrefsBase {
|
||||
var notificationFreq: Long
|
||||
}
|
||||
|
||||
class NotifPrefsImpl @Inject internal constructor(
|
||||
class NotifPrefsImpl
|
||||
@Inject
|
||||
internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.notif", factory), NotifPrefs {
|
||||
|
||||
override var notificationKeywords: Set<String> by kpref(
|
||||
"notification_keywords",
|
||||
oldPrefs.notificationKeywords /* mutableSetOf() */
|
||||
)
|
||||
override var notificationKeywords: Set<String> by
|
||||
kpref("notification_keywords", oldPrefs.notificationKeywords /* mutableSetOf() */)
|
||||
|
||||
override var notificationsGeneral: Boolean by kpref(
|
||||
"notification_general",
|
||||
oldPrefs.notificationsGeneral /* true */
|
||||
)
|
||||
override var notificationsGeneral: Boolean by
|
||||
kpref("notification_general", oldPrefs.notificationsGeneral /* true */)
|
||||
|
||||
override var notificationAllAccounts: Boolean by kpref(
|
||||
"notification_all_accounts",
|
||||
oldPrefs.notificationAllAccounts /* true */
|
||||
)
|
||||
override var notificationAllAccounts: Boolean by
|
||||
kpref("notification_all_accounts", oldPrefs.notificationAllAccounts /* true */)
|
||||
|
||||
override var notificationsInstantMessages: Boolean by kpref(
|
||||
"notification_im",
|
||||
oldPrefs.notificationsInstantMessages /* true */
|
||||
)
|
||||
override var notificationsInstantMessages: Boolean by
|
||||
kpref("notification_im", oldPrefs.notificationsInstantMessages /* true */)
|
||||
|
||||
override var notificationsImAllAccounts: Boolean by kpref(
|
||||
"notification_im_all_accounts",
|
||||
oldPrefs.notificationsImAllAccounts /* false */
|
||||
)
|
||||
override var notificationsImAllAccounts: Boolean by
|
||||
kpref("notification_im_all_accounts", oldPrefs.notificationsImAllAccounts /* false */)
|
||||
|
||||
override var notificationVibrate: Boolean by kpref(
|
||||
"notification_vibrate",
|
||||
oldPrefs.notificationVibrate /* true */
|
||||
)
|
||||
override var notificationVibrate: Boolean by
|
||||
kpref("notification_vibrate", oldPrefs.notificationVibrate /* true */)
|
||||
|
||||
override var notificationSound: Boolean by kpref(
|
||||
"notification_sound",
|
||||
oldPrefs.notificationSound /* true */
|
||||
)
|
||||
override var notificationSound: Boolean by
|
||||
kpref("notification_sound", oldPrefs.notificationSound /* true */)
|
||||
|
||||
override var notificationRingtone: String by kpref(
|
||||
"notification_ringtone",
|
||||
oldPrefs.notificationRingtone /* "" */
|
||||
)
|
||||
override var notificationRingtone: String by
|
||||
kpref("notification_ringtone", oldPrefs.notificationRingtone /* "" */)
|
||||
|
||||
override var messageRingtone: String by kpref(
|
||||
"message_ringtone",
|
||||
oldPrefs.messageRingtone /* "" */
|
||||
)
|
||||
override var messageRingtone: String by
|
||||
kpref("message_ringtone", oldPrefs.messageRingtone /* "" */)
|
||||
|
||||
override var notificationLights: Boolean by kpref(
|
||||
"notification_lights",
|
||||
oldPrefs.notificationLights /* true */
|
||||
)
|
||||
override var notificationLights: Boolean by
|
||||
kpref("notification_lights", oldPrefs.notificationLights /* true */)
|
||||
|
||||
override var notificationFreq: Long by kpref(
|
||||
"notification_freq",
|
||||
oldPrefs.notificationFreq /* 15L */
|
||||
)
|
||||
override var notificationFreq: Long by
|
||||
kpref("notification_freq", oldPrefs.notificationFreq /* 15L */)
|
||||
}
|
||||
|
@ -23,9 +23,7 @@ import com.pitchedapps.frost.prefs.PrefsBase
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ShowcasePrefs : PrefsBase {
|
||||
/**
|
||||
* Check if this is the first time launching the web overlay; show snackbar if true
|
||||
*/
|
||||
/** Check if this is the first time launching the web overlay; show snackbar if true */
|
||||
val firstWebOverlay: Boolean
|
||||
|
||||
val intro: Boolean
|
||||
@ -36,9 +34,8 @@ interface ShowcasePrefs : PrefsBase {
|
||||
*
|
||||
* Showcase prefs that offer one time helpers to guide new users
|
||||
*/
|
||||
class ShowcasePrefsImpl @Inject internal constructor(
|
||||
factory: KPrefFactory
|
||||
) : KPref("${BuildConfig.APPLICATION_ID}.showcase", factory), ShowcasePrefs {
|
||||
class ShowcasePrefsImpl @Inject internal constructor(factory: KPrefFactory) :
|
||||
KPref("${BuildConfig.APPLICATION_ID}.showcase", factory), ShowcasePrefs {
|
||||
|
||||
override val firstWebOverlay: Boolean by kprefSingle("first_web_overlay")
|
||||
|
||||
|
@ -39,41 +39,30 @@ interface ThemePrefs : PrefsBase {
|
||||
var tintNavBar: Boolean
|
||||
}
|
||||
|
||||
class ThemePrefsImpl @Inject internal constructor(
|
||||
class ThemePrefsImpl
|
||||
@Inject
|
||||
internal constructor(
|
||||
factory: KPrefFactory,
|
||||
oldPrefs: OldPrefs,
|
||||
) : KPref("${BuildConfig.APPLICATION_ID}.prefs.theme", factory), ThemePrefs {
|
||||
|
||||
/**
|
||||
* Note that this is purely for the pref storage. Updating themes should use
|
||||
* ThemeProvider
|
||||
*/
|
||||
/** Note that this is purely for the pref storage. Updating themes should use ThemeProvider */
|
||||
override var theme: Int by kpref("theme", oldPrefs.theme /* 0 */)
|
||||
|
||||
override var customTextColor: Int by kpref(
|
||||
"color_text",
|
||||
oldPrefs.customTextColor /* 0xffeceff1.toInt() */
|
||||
)
|
||||
override var customTextColor: Int by
|
||||
kpref("color_text", oldPrefs.customTextColor /* 0xffeceff1.toInt() */)
|
||||
|
||||
override var customAccentColor: Int by kpref(
|
||||
"color_accent",
|
||||
oldPrefs.customAccentColor /* 0xff0288d1.toInt() */
|
||||
)
|
||||
override var customAccentColor: Int by
|
||||
kpref("color_accent", oldPrefs.customAccentColor /* 0xff0288d1.toInt() */)
|
||||
|
||||
override var customBackgroundColor: Int by kpref(
|
||||
"color_bg",
|
||||
oldPrefs.customBackgroundColor /* 0xff212121.toInt() */
|
||||
)
|
||||
override var customBackgroundColor: Int by
|
||||
kpref("color_bg", oldPrefs.customBackgroundColor /* 0xff212121.toInt() */)
|
||||
|
||||
override var customHeaderColor: Int by kpref(
|
||||
"color_header",
|
||||
oldPrefs.customHeaderColor /* 0xff01579b.toInt() */
|
||||
)
|
||||
override var customHeaderColor: Int by
|
||||
kpref("color_header", oldPrefs.customHeaderColor /* 0xff01579b.toInt() */)
|
||||
|
||||
override var customIconColor: Int by kpref(
|
||||
"color_icons",
|
||||
oldPrefs.customIconColor /* 0xffeceff1.toInt() */
|
||||
)
|
||||
override var customIconColor: Int by
|
||||
kpref("color_icons", oldPrefs.customIconColor /* 0xffeceff1.toInt() */)
|
||||
|
||||
override var tintNavBar: Boolean by kpref("tint_nav_bar", oldPrefs.tintNavBar /* true */)
|
||||
}
|
||||
|
@ -20,9 +20,9 @@ import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import androidx.annotation.CallSuper
|
||||
import ca.allanwang.kau.utils.ContextHelper
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BaseJobService : JobService(), CoroutineScope {
|
||||
|
||||
@ -32,9 +32,7 @@ abstract class BaseJobService : JobService(), CoroutineScope {
|
||||
|
||||
protected val startTime = System.currentTimeMillis()
|
||||
|
||||
/**
|
||||
* Note that if a job plans on running asynchronously, it should return true
|
||||
*/
|
||||
/** Note that if a job plans on running asynchronously, it should return true */
|
||||
@CallSuper
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
job = Job()
|
||||
|
@ -60,12 +60,11 @@ import kotlin.math.abs
|
||||
private val _40_DP = 40.dpToPx
|
||||
|
||||
private val pendingIntentFlagUpdateCurrent: Int
|
||||
get() = PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
get() =
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
|
||||
/**
|
||||
* Enum to handle notification creations
|
||||
*/
|
||||
/** Enum to handle notification creations */
|
||||
enum class NotificationType(
|
||||
val channelId: String,
|
||||
private val overlayContext: OverlayContext,
|
||||
@ -73,7 +72,6 @@ enum class NotificationType(
|
||||
private val parser: FrostParser<ParseNotification>,
|
||||
private val ringtoneProvider: (Prefs) -> String
|
||||
) {
|
||||
|
||||
GENERAL(
|
||||
NOTIF_CHANNEL_GENERAL,
|
||||
OverlayContext.NOTIFICATION,
|
||||
@ -81,7 +79,6 @@ enum class NotificationType(
|
||||
NotifParser,
|
||||
{ it.notificationRingtone }
|
||||
),
|
||||
|
||||
MESSAGE(
|
||||
NOTIF_CHANNEL_MESSAGES,
|
||||
OverlayContext.MESSAGE,
|
||||
@ -92,9 +89,7 @@ enum class NotificationType(
|
||||
|
||||
private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}"
|
||||
|
||||
/**
|
||||
* Optional binder to return the request bundle builder
|
||||
*/
|
||||
/** Optional binder to return the request bundle builder */
|
||||
internal open fun bindRequest(
|
||||
content: NotificationContent,
|
||||
cookie: String
|
||||
@ -109,12 +104,10 @@ enum class NotificationType(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread data from designated parser
|
||||
* Display notifications for those after old epoch
|
||||
* Save new epoch
|
||||
* Get unread data from designated parser Display notifications for those after old epoch Save new
|
||||
* epoch
|
||||
*
|
||||
* Returns the number of notifications generated,
|
||||
* or -1 if an error occurred
|
||||
* Returns the number of notifications generated, or -1 if an error occurred
|
||||
*/
|
||||
suspend fun fetch(
|
||||
context: Context,
|
||||
@ -122,7 +115,8 @@ enum class NotificationType(
|
||||
prefs: Prefs,
|
||||
notifDao: NotificationDao
|
||||
): Int {
|
||||
val response = try {
|
||||
val response =
|
||||
try {
|
||||
parser.parse(data.cookie)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
@ -132,17 +126,14 @@ enum class NotificationType(
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the text doesn't contain any blacklisted keywords
|
||||
*/
|
||||
/** Checks that the text doesn't contain any blacklisted keywords */
|
||||
fun validText(text: String?): Boolean {
|
||||
val t = text ?: return true
|
||||
return prefs.notificationKeywords.none {
|
||||
t.contains(it, true)
|
||||
}
|
||||
return prefs.notificationKeywords.none { t.contains(it, true) }
|
||||
}
|
||||
|
||||
val notifContents = response.data.getUnreadNotifications(data).filter { notif ->
|
||||
val notifContents =
|
||||
response.data.getUnreadNotifications(data).filter { notif ->
|
||||
validText(notif.title) && validText(notif.text)
|
||||
}
|
||||
if (notifContents.isEmpty()) return 0
|
||||
@ -173,8 +164,7 @@ enum class NotificationType(
|
||||
val notifs = newNotifContents.map { createNotification(context, it) }
|
||||
|
||||
frostEvent("Notifications", "Type" to name, "Count" to notifs.size)
|
||||
if (notifs.size > 1)
|
||||
summaryNotification(context, userId, notifs.size).notify(context)
|
||||
if (notifs.size > 1) summaryNotification(context, userId, notifs.size).notify(context)
|
||||
val ringtone = ringtoneProvider(prefs)
|
||||
notifs.forEachIndexed { i, notif ->
|
||||
// Ring at most twice
|
||||
@ -184,7 +174,8 @@ enum class NotificationType(
|
||||
}
|
||||
|
||||
fun debugNotification(context: Context, data: CookieEntity) {
|
||||
val content = NotificationContent(
|
||||
val content =
|
||||
NotificationContent(
|
||||
data,
|
||||
System.currentTimeMillis(),
|
||||
"https://github.com/AllanWang/Frost-for-Facebook",
|
||||
@ -197,9 +188,7 @@ enum class NotificationType(
|
||||
createNotification(context, content).notify(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach content related data to an intent
|
||||
*/
|
||||
/** Attach content related data to an intent */
|
||||
fun putContentExtra(intent: Intent, content: NotificationContent): Intent {
|
||||
// We will show the notification page for dependent urls. We can trigger a click next time
|
||||
intent.data =
|
||||
@ -209,8 +198,7 @@ enum class NotificationType(
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic content for the provided type and user id.
|
||||
* No content related data is added
|
||||
* Create a generic content for the provided type and user id. No content related data is added
|
||||
*/
|
||||
fun createCommonIntent(context: Context, userId: Long): Intent {
|
||||
val intent = Intent(context, FrostWebActivity::class.java)
|
||||
@ -219,9 +207,7 @@ enum class NotificationType(
|
||||
return intent
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and submit a new notification with the given [content]
|
||||
*/
|
||||
/** Create and submit a new notification with the given [content] */
|
||||
private fun createNotification(
|
||||
context: Context,
|
||||
content: NotificationContent
|
||||
@ -232,7 +218,9 @@ enum class NotificationType(
|
||||
val group = "${groupPrefix}_${data.id}"
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
|
||||
val notifBuilder = context.frostNotification(channelId)
|
||||
val notifBuilder =
|
||||
context
|
||||
.frostNotification(channelId)
|
||||
.setContentTitle(title ?: context.string(R.string.frost_name))
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
@ -245,7 +233,8 @@ enum class NotificationType(
|
||||
|
||||
if (profileUrl != null) {
|
||||
try {
|
||||
val profileImg = GlideApp.with(context)
|
||||
val profileImg =
|
||||
GlideApp.with(context)
|
||||
.asBitmap()
|
||||
.load(profileUrl)
|
||||
.transform(FrostGlide.circleCrop)
|
||||
@ -261,9 +250,9 @@ enum class NotificationType(
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a summary notification to wrap the previous ones
|
||||
* This will always produce sound, vibration, and lights based on preferences
|
||||
* and will only show if we have at least 2 notifications
|
||||
* Create a summary notification to wrap the previous ones This will always produce sound,
|
||||
* vibration, and lights based on preferences and will only show if we have at least 2
|
||||
* notifications
|
||||
*/
|
||||
private fun summaryNotification(context: Context, userId: Long, count: Int): FrostNotification {
|
||||
val intent = createCommonIntent(context, userId)
|
||||
@ -271,7 +260,9 @@ enum class NotificationType(
|
||||
val group = "${groupPrefix}_$userId"
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, intent, pendingIntentFlagUpdateCurrent)
|
||||
val notifBuilder = context.frostNotification(channelId)
|
||||
val notifBuilder =
|
||||
context
|
||||
.frostNotification(channelId)
|
||||
.setContentTitle(context.string(R.string.frost_name))
|
||||
.setContentText("$count ${context.string(fbItem.titleId)}")
|
||||
.setGroup(group)
|
||||
@ -287,9 +278,7 @@ enum class NotificationType(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification data holder
|
||||
*/
|
||||
/** Notification data holder */
|
||||
data class NotificationContent(
|
||||
// TODO replace data with userId?
|
||||
val data: CookieEntity,
|
||||
@ -306,8 +295,8 @@ data class NotificationContent(
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for a complete notification builder and identifier
|
||||
* which can be immediately notified when given a [Context]
|
||||
* Wrapper for a complete notification builder and identifier which can be immediately notified when
|
||||
* given a [Context]
|
||||
*/
|
||||
data class FrostNotification(
|
||||
private val tag: String,
|
||||
@ -338,5 +327,4 @@ fun Context.scheduleNotificationsFromPrefs(prefs: Prefs): Boolean {
|
||||
fun Context.scheduleNotifications(minutes: Long): Boolean =
|
||||
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
|
||||
|
||||
fun Context.fetchNotifications(): Boolean =
|
||||
fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
|
||||
fun Context.fetchNotifications(): Boolean = fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
|
||||
|
@ -30,32 +30,29 @@ import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.frostEvent
|
||||
import com.pitchedapps.frost.widgets.NotificationWidget
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-14.
|
||||
*
|
||||
* Service to manage notifications
|
||||
* Will periodically check through all accounts in the db and send notifications when appropriate
|
||||
* Service to manage notifications Will periodically check through all accounts in the db and send
|
||||
* notifications when appropriate
|
||||
*
|
||||
* All fetching is done through parsers
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class NotificationService : BaseJobService() {
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
@Inject
|
||||
lateinit var notifDao: NotificationDao
|
||||
@Inject lateinit var notifDao: NotificationDao
|
||||
|
||||
@Inject
|
||||
lateinit var cookieDao: CookieDao
|
||||
@Inject lateinit var cookieDao: CookieDao
|
||||
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
super.onStopJob(params)
|
||||
@ -66,11 +63,12 @@ class NotificationService : BaseJobService() {
|
||||
private var preparedFinish = false
|
||||
|
||||
private fun prepareFinish(abrupt: Boolean) {
|
||||
if (preparedFinish)
|
||||
return
|
||||
if (preparedFinish) return
|
||||
preparedFinish = true
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" }
|
||||
L.i {
|
||||
"Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms"
|
||||
}
|
||||
frostEvent(
|
||||
"NotificationTime",
|
||||
"Type" to (if (abrupt) "Service force stop" else "Service"),
|
||||
@ -86,8 +84,7 @@ class NotificationService : BaseJobService() {
|
||||
try {
|
||||
sendNotifications(params)
|
||||
} finally {
|
||||
if (!isActive)
|
||||
prepareFinish(false)
|
||||
if (!isActive) prepareFinish(false)
|
||||
jobFinished(params, false)
|
||||
}
|
||||
}
|
||||
@ -104,13 +101,9 @@ class NotificationService : BaseJobService() {
|
||||
for (cookie in cookies) {
|
||||
yield()
|
||||
val current = cookie.id == currentId
|
||||
if (prefs.notificationsGeneral &&
|
||||
(current || prefs.notificationAllAccounts)
|
||||
)
|
||||
if (prefs.notificationsGeneral && (current || prefs.notificationAllAccounts))
|
||||
notifCount += fetch(jobId, NotificationType.GENERAL, cookie)
|
||||
if (prefs.notificationsInstantMessages &&
|
||||
(current || prefs.notificationsImAllAccounts)
|
||||
)
|
||||
if (prefs.notificationsInstantMessages && (current || prefs.notificationsImAllAccounts))
|
||||
notifCount += fetch(jobId, NotificationType.MESSAGE, cookie)
|
||||
}
|
||||
|
||||
@ -123,8 +116,8 @@ class NotificationService : BaseJobService() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented fetch to also notify when an error occurs
|
||||
* Also normalized the output to return the number of notifications received
|
||||
* Implemented fetch to also notify when an error occurs Also normalized the output to return the
|
||||
* number of notifications received
|
||||
*/
|
||||
private suspend fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int {
|
||||
val count = type.fetch(this, cookie, prefs, notifDao)
|
||||
@ -142,7 +135,8 @@ class NotificationService : BaseJobService() {
|
||||
}
|
||||
|
||||
private fun generalNotification(id: Int, textRes: Int, withDefaults: Boolean) {
|
||||
val notifBuilder = frostNotification(NOTIF_CHANNEL_GENERAL)
|
||||
val notifBuilder =
|
||||
frostNotification(NOTIF_CHANNEL_GENERAL)
|
||||
.setFrostAlert(this, withDefaults, prefs.notificationRingtone, prefs)
|
||||
.setContentTitle(string(R.string.frost_name))
|
||||
.setContentText(string(textRes))
|
||||
|
@ -36,9 +36,7 @@ import com.pitchedapps.frost.prefs.Prefs
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.frostUri
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 07/04/18.
|
||||
*/
|
||||
/** Created by Allan Wang on 07/04/18. */
|
||||
const val NOTIF_CHANNEL_GENERAL = "general"
|
||||
const val NOTIF_CHANNEL_MESSAGES = "messages"
|
||||
|
||||
@ -50,12 +48,11 @@ fun setupNotificationChannels(c: Context, themeProvider: ThemeProvider) {
|
||||
manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName, themeProvider)
|
||||
manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg", themeProvider)
|
||||
manager.notificationChannels
|
||||
.filter {
|
||||
it.id != NOTIF_CHANNEL_GENERAL &&
|
||||
it.id != NOTIF_CHANNEL_MESSAGES
|
||||
}
|
||||
.filter { it.id != NOTIF_CHANNEL_GENERAL && it.id != NOTIF_CHANNEL_MESSAGES }
|
||||
.forEach { manager.deleteNotificationChannel(it.id) }
|
||||
L.d { "Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups" }
|
||||
L.d {
|
||||
"Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups"
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@ -64,10 +61,7 @@ private fun NotificationManager.createNotificationChannel(
|
||||
name: String,
|
||||
themeProvider: ThemeProvider
|
||||
): NotificationChannel {
|
||||
val channel = NotificationChannel(
|
||||
id,
|
||||
name, NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
channel.enableLights(true)
|
||||
channel.lightColor = themeProvider.accentColor
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
@ -76,8 +70,7 @@ private fun NotificationManager.createNotificationChannel(
|
||||
}
|
||||
|
||||
fun Context.frostNotification(id: String) =
|
||||
NotificationCompat.Builder(this, id)
|
||||
.apply {
|
||||
NotificationCompat.Builder(this, id).apply {
|
||||
setSmallIcon(R.drawable.frost_f_24)
|
||||
setAutoCancel(true)
|
||||
setOnlyAlertOnce(true)
|
||||
@ -86,9 +79,8 @@ fun Context.frostNotification(id: String) =
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictates whether a notification should have sound/vibration/lights or not
|
||||
* Delegates to channels if Android O and up
|
||||
* Otherwise uses our provided preferences
|
||||
* Dictates whether a notification should have sound/vibration/lights or not Delegates to channels
|
||||
* if Android O and up Otherwise uses our provided preferences
|
||||
*/
|
||||
fun NotificationCompat.Builder.setFrostAlert(
|
||||
context: Context,
|
||||
@ -131,15 +123,15 @@ fun JobInfo.Builder.setExtras(id: Int): JobInfo.Builder {
|
||||
}
|
||||
|
||||
/**
|
||||
* interval is # of min, which must be at least 15
|
||||
* returns false if an error occurs; true otherwise
|
||||
* interval is # of min, which must be at least 15 returns false if an error occurs; true otherwise
|
||||
*/
|
||||
inline fun <reified T : JobService> Context.scheduleJob(id: Int, minutes: Long): Boolean {
|
||||
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
scheduler.cancel(id)
|
||||
if (minutes < 0L) return true
|
||||
val serviceComponent = ComponentName(this, T::class.java)
|
||||
val builder = JobInfo.Builder(id, serviceComponent)
|
||||
val builder =
|
||||
JobInfo.Builder(id, serviceComponent)
|
||||
.setPeriodic(minutes * 60000)
|
||||
.setExtras(id)
|
||||
.setPersisted(true)
|
||||
@ -152,13 +144,12 @@ inline fun <reified T : JobService> Context.scheduleJob(id: Int, minutes: Long):
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Run notification job right now
|
||||
*/
|
||||
/** Run notification job right now */
|
||||
inline fun <reified T : JobService> Context.fetchJob(id: Int): Boolean {
|
||||
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
val serviceComponent = ComponentName(this, T::class.java)
|
||||
val builder = JobInfo.Builder(id, serviceComponent)
|
||||
val builder =
|
||||
JobInfo.Builder(id, serviceComponent)
|
||||
.setMinimumLatency(0L)
|
||||
.setExtras(id)
|
||||
.setOverrideDeadline(2000L)
|
||||
|
@ -32,8 +32,7 @@ import javax.inject.Inject
|
||||
@AndroidEntryPoint
|
||||
class UpdateReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var prefs: Prefs
|
||||
@Inject lateinit var prefs: Prefs
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
||||
|
@ -35,11 +35,8 @@ import com.pitchedapps.frost.utils.frostSnackbar
|
||||
import com.pitchedapps.frost.utils.launchTabCustomizerActivity
|
||||
import com.pitchedapps.frost.views.KPrefTextSeekbar
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-29.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-29. */
|
||||
fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
|
||||
header(R.string.theme_customization)
|
||||
|
||||
text(R.string.theme, prefs::theme, { themeProvider.setTheme(it) }) {
|
||||
@ -62,9 +59,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
}
|
||||
}
|
||||
}
|
||||
textGetter = {
|
||||
string(Theme(it).textRes)
|
||||
}
|
||||
textGetter = { string(Theme(it).textRes) }
|
||||
}
|
||||
|
||||
fun KPrefColorPicker.KPrefColorContract.dependsOnCustom() {
|
||||
@ -78,7 +73,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.text_color, prefs::customTextColor,
|
||||
R.string.text_color,
|
||||
prefs::customTextColor,
|
||||
{
|
||||
prefs.customTextColor = it
|
||||
reload()
|
||||
@ -91,7 +87,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.accent_color, prefs::customAccentColor,
|
||||
R.string.accent_color,
|
||||
prefs::customAccentColor,
|
||||
{
|
||||
prefs.customAccentColor = it
|
||||
reload()
|
||||
@ -104,7 +101,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.background_color, prefs::customBackgroundColor,
|
||||
R.string.background_color,
|
||||
prefs::customBackgroundColor,
|
||||
{
|
||||
prefs.customBackgroundColor = it
|
||||
bgCanvas.ripple(it, duration = 500L)
|
||||
@ -118,7 +116,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.header_color, prefs::customHeaderColor,
|
||||
R.string.header_color,
|
||||
prefs::customHeaderColor,
|
||||
{
|
||||
prefs.customHeaderColor = it
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
@ -132,7 +131,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
}
|
||||
|
||||
colorPicker(
|
||||
R.string.icon_color, prefs::customIconColor,
|
||||
R.string.icon_color,
|
||||
prefs::customIconColor,
|
||||
{
|
||||
prefs.customIconColor = it
|
||||
invalidateOptionsMenu()
|
||||
@ -174,7 +174,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.tint_nav, prefs::tintNavBar,
|
||||
R.string.tint_nav,
|
||||
prefs::tintNavBar,
|
||||
{
|
||||
prefs.tintNavBar = it
|
||||
frostNavigationBar(prefs, themeProvider)
|
||||
@ -188,7 +189,8 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
KPrefTextSeekbar(
|
||||
KPrefSeekbar.KPrefSeekbarBuilder(
|
||||
globalOptions,
|
||||
R.string.web_text_scaling, prefs::webTextScaling
|
||||
R.string.web_text_scaling,
|
||||
prefs::webTextScaling
|
||||
) {
|
||||
prefs.webTextScaling = it
|
||||
setFrostResult(REQUEST_TEXT_ZOOM)
|
||||
@ -196,12 +198,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
)
|
||||
)
|
||||
|
||||
checkbox(
|
||||
R.string.enforce_black_media_bg, prefs::blackMediaBg,
|
||||
{
|
||||
prefs.blackMediaBg = it
|
||||
}
|
||||
) {
|
||||
checkbox(R.string.enforce_black_media_bg, prefs::blackMediaBg, { prefs.blackMediaBg = it }) {
|
||||
descRes = R.string.enforce_black_media_bg_desc
|
||||
}
|
||||
}
|
||||
|
@ -20,23 +20,30 @@ import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
|
||||
import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.activities.SettingsActivity
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-30.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-30. */
|
||||
fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
|
||||
checkbox(R.string.auto_refresh_feed, prefs::autoRefreshFeed, { prefs.autoRefreshFeed = it }) {
|
||||
descRes = R.string.auto_refresh_feed_desc
|
||||
}
|
||||
|
||||
checkbox(R.string.fancy_animations, prefs::animate, { prefs.animate = it; animate = it }) {
|
||||
checkbox(
|
||||
R.string.fancy_animations,
|
||||
prefs::animate,
|
||||
{
|
||||
prefs.animate = it
|
||||
animate = it
|
||||
}
|
||||
) {
|
||||
descRes = R.string.fancy_animations_desc
|
||||
}
|
||||
|
||||
checkbox(
|
||||
R.string.overlay_swipe,
|
||||
prefs::overlayEnabled,
|
||||
{ prefs.overlayEnabled = it; shouldRefreshMain() }
|
||||
{
|
||||
prefs.overlayEnabled = it
|
||||
shouldRefreshMain()
|
||||
}
|
||||
) {
|
||||
descRes = R.string.overlay_swipe_desc
|
||||
}
|
||||
@ -72,7 +79,10 @@ fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
checkbox(
|
||||
R.string.auto_expand_text_box,
|
||||
prefs::autoExpandTextBox,
|
||||
{ prefs.autoExpandTextBox = it; shouldRefreshMain() }
|
||||
{
|
||||
prefs.autoExpandTextBox = it
|
||||
shouldRefreshMain()
|
||||
}
|
||||
) {
|
||||
descRes = R.string.auto_expand_text_box_desc
|
||||
}
|
||||
|
@ -40,34 +40,29 @@ import com.pitchedapps.frost.prefs.Prefs
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.frostUriFromFile
|
||||
import com.pitchedapps.frost.utils.sendFrostEmail
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-30.
|
||||
*
|
||||
* A sub pref section that is enabled through a hidden preference
|
||||
* Each category will load a page, extract the contents, remove private info, and create a report
|
||||
* A sub pref section that is enabled through a hidden preference Each category will load a page,
|
||||
* extract the contents, remove private info, and create a report
|
||||
*/
|
||||
fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
|
||||
plainText(R.string.disclaimer) {
|
||||
descRes = R.string.debug_disclaimer_info
|
||||
}
|
||||
plainText(R.string.disclaimer) { descRes = R.string.debug_disclaimer_info }
|
||||
|
||||
plainText(R.string.debug_web) {
|
||||
descRes = R.string.debug_web_desc
|
||||
onClick =
|
||||
{ this@getDebugPrefs.startActivityForResult<DebugActivity>(ACTIVITY_REQUEST_DEBUG) }
|
||||
onClick = { this@getDebugPrefs.startActivityForResult<DebugActivity>(ACTIVITY_REQUEST_DEBUG) }
|
||||
}
|
||||
|
||||
plainText(R.string.debug_parsers) {
|
||||
descRes = R.string.debug_parsers_desc
|
||||
onClick = {
|
||||
|
||||
val parsers = arrayOf(NotifParser, MessageParser, SearchParser)
|
||||
|
||||
materialDialog {
|
||||
@ -86,7 +81,8 @@ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
cancelOnTouchOutside(false)
|
||||
}
|
||||
|
||||
attempt = launch(Dispatchers.IO) {
|
||||
attempt =
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
val data = parser.parse(fbCookie.webCookie)
|
||||
withMainContext {
|
||||
@ -116,7 +112,8 @@ private const val ZIP_NAME = "debug"
|
||||
|
||||
fun SettingsActivity.sendDebug(url: String, html: String?) {
|
||||
|
||||
val downloader = OfflineWebsite(
|
||||
val downloader =
|
||||
OfflineWebsite(
|
||||
url,
|
||||
cookie = fbCookie.webCookie ?: "",
|
||||
baseUrl = FB_URL_BASE,
|
||||
@ -139,21 +136,15 @@ fun SettingsActivity.sendDebug(url: String, html: String?) {
|
||||
// progressFlow.onEach { md.setProgress(it) }.launchIn(this)
|
||||
|
||||
launchMain {
|
||||
val success = downloader.loadAndZip(ZIP_NAME) {
|
||||
progressFlow.tryEmit(it)
|
||||
}
|
||||
val success = downloader.loadAndZip(ZIP_NAME) { progressFlow.tryEmit(it) }
|
||||
md.dismiss()
|
||||
if (success) {
|
||||
val zipUri = frostUriFromFile(
|
||||
File(downloader.baseDir, "$ZIP_NAME.zip")
|
||||
)
|
||||
val zipUri = frostUriFromFile(File(downloader.baseDir, "$ZIP_NAME.zip"))
|
||||
L.i { "Sending debug zip with uri $zipUri" }
|
||||
sendFrostEmail(R.string.debug_report_email_title, prefs = prefs) {
|
||||
addItem("Url", url)
|
||||
addAttachment(zipUri)
|
||||
extras = {
|
||||
type = "application/zip"
|
||||
}
|
||||
extras = { type = "application/zip" }
|
||||
}
|
||||
} else {
|
||||
toast(R.string.error_generic)
|
||||
|
@ -23,21 +23,17 @@ import com.pitchedapps.frost.R
|
||||
import com.pitchedapps.frost.activities.SettingsActivity
|
||||
import com.pitchedapps.frost.utils.REQUEST_RESTART_APPLICATION
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-06-29.
|
||||
*/
|
||||
/** Created by Allan Wang on 2017-06-29. */
|
||||
fun SettingsActivity.getExperimentalPrefs(): KPrefAdapterBuilder.() -> Unit = {
|
||||
|
||||
plainText(R.string.disclaimer) {
|
||||
descRes = R.string.experimental_disclaimer_info
|
||||
}
|
||||
plainText(R.string.disclaimer) { descRes = R.string.experimental_disclaimer_info }
|
||||
|
||||
// Experimental content starts here ------------------
|
||||
|
||||
// Experimental content ends here --------------------
|
||||
|
||||
checkbox(
|
||||
R.string.verbose_logging, prefs::verboseLogging,
|
||||
R.string.verbose_logging,
|
||||
prefs::verboseLogging,
|
||||
{
|
||||
prefs.verboseLogging = it
|
||||
KL.shouldLog = { it != Log.VERBOSE }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user