diff --git a/app/schemas/com.jerboa.db.AppDB/35.json b/app/schemas/com.jerboa.db.AppDB/35.json new file mode 100644 index 000000000..f1ffc7d76 --- /dev/null +++ b/app/schemas/com.jerboa.db.AppDB/35.json @@ -0,0 +1,281 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "d5e8f8b3a1c9e2f4d6b7a0c3e5f1d2b4", + "entities": [ + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `current` INTEGER NOT NULL, `instance` TEXT NOT NULL, `name` TEXT NOT NULL, `jwt` TEXT NOT NULL, `default_listing_type` INTEGER NOT NULL DEFAULT 0, `default_sort_type` INTEGER NOT NULL DEFAULT 0, `verification_state` INTEGER NOT NULL DEFAULT 0, `is_admin` INTEGER NOT NULL, `is_mod` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jwt", + "columnName": "jwt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultListingType", + "columnName": "default_listing_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "defaultSortType", + "columnName": "default_sort_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "verificationState", + "columnName": "verification_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isAdmin", + "columnName": "is_admin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMod", + "columnName": "is_mod", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "AppSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `font_size` INTEGER NOT NULL DEFAULT 16, `theme` INTEGER NOT NULL DEFAULT 0, `theme_color` INTEGER NOT NULL DEFAULT 0, `post_view_mode` INTEGER NOT NULL DEFAULT 0, `show_bottom_nav` INTEGER NOT NULL DEFAULT 1, `post_navigation_gesture_mode` INTEGER NOT NULL DEFAULT 0, `show_collapsed_comment_content` INTEGER NOT NULL DEFAULT 0, `show_comment_action_bar_by_default` INTEGER NOT NULL DEFAULT 1, `show_voting_arrows_in_list_view` INTEGER NOT NULL DEFAULT 1, `show_parent_comment_navigation_buttons` INTEGER NOT NULL DEFAULT 0, `navigate_parent_comments_with_volume_buttons` INTEGER NOT NULL DEFAULT 0, `use_custom_tabs` INTEGER NOT NULL DEFAULT 1, `use_private_tabs` INTEGER NOT NULL DEFAULT 0, `secure_window` INTEGER NOT NULL DEFAULT 0, `blur_nsfw` INTEGER NOT NULL DEFAULT 1, `show_text_descriptions_in_navbar` INTEGER NOT NULL DEFAULT 1, `markAsReadOnScroll` INTEGER NOT NULL DEFAULT 0, `backConfirmationMode` INTEGER NOT NULL DEFAULT 1, `show_post_link_previews` INTEGER NOT NULL DEFAULT 1, `post_actionbar_mode` INTEGER NOT NULL DEFAULT 0, `auto_play_gifs` INTEGER NOT NULL DEFAULT 0, `swipe_to_action_preset` INTEGER NOT NULL DEFAULT 0, `last_version_code_viewed` INTEGER NOT NULL DEFAULT 0, `disable_video_autoplay` INTEGER NOT NULL DEFAULT 0, `low_bandwidth_mode` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontSize", + "columnName": "font_size", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "16" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "themeColor", + "columnName": "theme_color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "postViewMode", + "columnName": "post_view_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showBottomNav", + "columnName": "show_bottom_nav", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "postNavigationGestureMode", + "columnName": "post_navigation_gesture_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showCollapsedCommentContent", + "columnName": "show_collapsed_comment_content", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showCommentActionBarByDefault", + "columnName": "show_comment_action_bar_by_default", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showVotingArrowsInListView", + "columnName": "show_voting_arrows_in_list_view", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showParentCommentNavigationButtons", + "columnName": "show_parent_comment_navigation_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "navigateParentCommentsWithVolumeButtons", + "columnName": "navigate_parent_comments_with_volume_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "useCustomTabs", + "columnName": "use_custom_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "usePrivateTabs", + "columnName": "use_private_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "secureWindow", + "columnName": "secure_window", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blurNSFW", + "columnName": "blur_nsfw", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showTextDescriptionsInNavbar", + "columnName": "show_text_descriptions_in_navbar", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "markAsReadOnScroll", + "columnName": "markAsReadOnScroll", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "backConfirmationMode", + "columnName": "backConfirmationMode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showPostLinkPreviews", + "columnName": "show_post_link_previews", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "postActionBarMode", + "columnName": "post_actionbar_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "autoPlayGifs", + "columnName": "auto_play_gifs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "swipeToActionPreset", + "columnName": "swipe_to_action_preset", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastVersionCodeViewed", + "columnName": "last_version_code_viewed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "disableVideoAutoplay", + "columnName": "disable_video_autoplay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lowBandwidthMode", + "columnName": "low_bandwidth_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd5e8f8b3a1c9e2f4d6b7a0c3e5f1d2b4')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt index cace58147..30b872ae2 100644 --- a/app/src/main/java/com/jerboa/MainActivity.kt +++ b/app/src/main/java/com/jerboa/MainActivity.kt @@ -1,6 +1,7 @@ package com.jerboa import android.content.Intent +import android.net.ConnectivityManager import android.net.Uri import android.os.Build import android.os.Bundle @@ -23,6 +24,7 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import androidx.core.content.getSystemService import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument @@ -34,6 +36,7 @@ import com.jerboa.feat.BackConfirmation.addConfirmationDialog import com.jerboa.feat.BackConfirmation.addConfirmationToast import com.jerboa.feat.BackConfirmation.disposeConfirmation import com.jerboa.feat.BackConfirmationMode +import com.jerboa.feat.LowBandwidthMode import com.jerboa.feat.ShowConfirmationDialog import com.jerboa.model.AccountSettingsViewModel import com.jerboa.model.AccountSettingsViewModelFactory @@ -107,6 +110,10 @@ class MainActivity : AppCompatActivity() { triggerRebirth(ctx) } + val connectivityManager = ctx.getSystemService() + val lowBandwidthMode = + appSettings.lowBandwidthMode.toEnum().isActive(connectivityManager) + if (appSettings.autoPlayGifs) { Coil.setImageLoader((ctx.applicationContext as JerboaApplication).imageGifLoader) } else { @@ -152,6 +159,7 @@ class MainActivity : AppCompatActivity() { appState, appSettings.useCustomTabs, appSettings.usePrivateTabs, + lowBandwidthMode, object : BetterLinkMovementMethod.OnLinkLongClickListener { override fun onLongClick( textView: TextView, @@ -230,6 +238,7 @@ class MainActivity : AppCompatActivity() { appSettingsViewModel = appSettingsViewModel, appSettings = appSettings, drawerState = drawerState, + lowBandwidthMode = lowBandwidthMode, ) } @@ -259,6 +268,7 @@ class MainActivity : AppCompatActivity() { postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, ) } @@ -304,6 +314,7 @@ class MainActivity : AppCompatActivity() { postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, ) } @@ -347,6 +358,7 @@ class MainActivity : AppCompatActivity() { postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, ) } @@ -387,6 +399,7 @@ class MainActivity : AppCompatActivity() { postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, ) } @@ -535,6 +548,7 @@ class MainActivity : AppCompatActivity() { postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, ) } } @@ -570,6 +584,7 @@ class MainActivity : AppCompatActivity() { postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, ) } diff --git a/app/src/main/java/com/jerboa/Utils.kt b/app/src/main/java/com/jerboa/Utils.kt index a102d7a34..f4bbe65eb 100644 --- a/app/src/main/java/com/jerboa/Utils.kt +++ b/app/src/main/java/com/jerboa/Utils.kt @@ -1290,6 +1290,17 @@ fun ConnectivityManager?.isCurrentlyConnected(): Boolean = ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: true +fun ConnectivityManager?.isConnectionMetered(): Boolean = + this + ?.activeNetwork + ?.let(::getNetworkCapabilities) + ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + ?.not() + ?: false + +fun ConnectivityManager?.isDataSaverEnabled(): Boolean = + this?.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED + /** * When calling this, you must call ActivityResultLauncher.unregister() * on the returned ActivityResultLauncher when the launcher is no longer diff --git a/app/src/main/java/com/jerboa/db/AppDB.kt b/app/src/main/java/com/jerboa/db/AppDB.kt index 9e483f4ab..91a5dad08 100644 --- a/app/src/main/java/com/jerboa/db/AppDB.kt +++ b/app/src/main/java/com/jerboa/db/AppDB.kt @@ -44,6 +44,7 @@ const val DEFAULT_POST_ACTION_BAR_MODE = 0 const val DEFAULT_AUTO_PLAY_GIFS = 0 const val DEFAULT_SWIPE_TO_ACTION_PRESET = 0 const val DEFAULT_DISABLE_AUTO_PLAY = 0 +const val DEFAULT_LOW_BANDWIDTH_MODE = 0 val APP_SETTINGS_DEFAULT = AppSettings( @@ -72,10 +73,11 @@ val APP_SETTINGS_DEFAULT = autoPlayGifs = DEFAULT_AUTO_PLAY_GIFS.toBool(), swipeToActionPreset = DEFAULT_SWIPE_TO_ACTION_PRESET, disableVideoAutoplay = DEFAULT_DISABLE_AUTO_PLAY, + lowBandwidthMode = DEFAULT_LOW_BANDWIDTH_MODE, ) @Database( - version = 34, + version = 35, entities = [Account::class, AppSettings::class], exportSchema = true, ) diff --git a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt index 20e888a27..fb66fad88 100644 --- a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt +++ b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt @@ -566,6 +566,15 @@ val MIGRATION_33_34 = } } +val MIGRATION_34_35 = + object : Migration(34, 35) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE AppSettings ADD COLUMN low_bandwidth_mode INTEGER NOT NULL DEFAULT 0", + ) + } + } + // Don't forget to test your migration with `./gradlew app:connectAndroidTest` val MIGRATIONS_LIST = arrayOf( @@ -602,4 +611,5 @@ val MIGRATIONS_LIST = MIGRATION_31_32, MIGRATION_32_33, MIGRATION_33_34, + MIGRATION_34_35, ) diff --git a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt index 719826cd4..e38f8e8a4 100644 --- a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt +++ b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt @@ -8,6 +8,7 @@ import com.jerboa.db.DEFAULT_BACK_CONFIRMATION_MODE import com.jerboa.db.DEFAULT_BLUR_NSFW import com.jerboa.db.DEFAULT_DISABLE_AUTO_PLAY import com.jerboa.db.DEFAULT_LAST_VERSION_CODE_VIEWED +import com.jerboa.db.DEFAULT_LOW_BANDWIDTH_MODE import com.jerboa.db.DEFAULT_MARK_AS_READ_ON_SCROLL import com.jerboa.db.DEFAULT_NAVIGATE_PARENT_COMMENTS_WITH_VOLUME_BUTTONS import com.jerboa.db.DEFAULT_POST_ACTION_BAR_MODE @@ -152,4 +153,9 @@ data class AppSettings( defaultValue = DEFAULT_DISABLE_AUTO_PLAY.toString(), ) val disableVideoAutoplay: Int, + @ColumnInfo( + name = "low_bandwidth_mode", + defaultValue = DEFAULT_LOW_BANDWIDTH_MODE.toString(), + ) + val lowBandwidthMode: Int, ) diff --git a/app/src/main/java/com/jerboa/feat/LowBandwidthMode.kt b/app/src/main/java/com/jerboa/feat/LowBandwidthMode.kt new file mode 100644 index 000000000..080a5c736 --- /dev/null +++ b/app/src/main/java/com/jerboa/feat/LowBandwidthMode.kt @@ -0,0 +1,23 @@ +package com.jerboa.feat + +import android.net.ConnectivityManager +import androidx.annotation.StringRes +import com.jerboa.R +import com.jerboa.isConnectionMetered +import com.jerboa.isDataSaverEnabled + +enum class LowBandwidthMode( + @param:StringRes val resId: Int, +) { + Auto(R.string.low_bandwidth_mode_auto), + Always(R.string.low_bandwidth_mode_always), + Never(R.string.low_bandwidth_mode_never), + ; + + fun isActive(connectivityManager: ConnectivityManager?): Boolean = + when (this) { + Auto -> connectivityManager.isConnectionMetered() || connectivityManager.isDataSaverEnabled() + Always -> true + Never -> false + } +} diff --git a/app/src/main/java/com/jerboa/ui/components/common/MarkdownHelper.kt b/app/src/main/java/com/jerboa/ui/components/common/MarkdownHelper.kt index 412bd1c5e..c9f635c64 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/MarkdownHelper.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/MarkdownHelper.kt @@ -27,6 +27,7 @@ import com.jerboa.JerboaAppState import com.jerboa.convertSpToPx import com.jerboa.util.markwon.BetterLinkMovementMethod import com.jerboa.util.markwon.ForceHttpsPlugin +import com.jerboa.util.markwon.LinkOnlyImagesPlugin import com.jerboa.util.markwon.MarkwonLemmyLinkPlugin import com.jerboa.util.markwon.MarkwonSpoilerPlugin import com.jerboa.util.markwon.ScriptRewriteSupportPlugin @@ -80,11 +81,25 @@ object MarkdownHelper { appState: JerboaAppState, useCustomTabs: Boolean, usePrivateTabs: Boolean, + lowBandwidthMode: Boolean, onLongClick: BetterLinkMovementMethod.OnLinkLongClickListener, ) { val context = appState.navController.context val loader = context.imageLoader - // main markdown parser has coil + html on + val imagesPlugin = if (lowBandwidthMode) { + // Render `![alt](url)` as a plain clickable link — no network load. + LinkOnlyImagesPlugin() + } else { + ClickableCoilImagesPlugin.create(context, loader, appState) + } + val htmlPlugin = if (lowBandwidthMode) { + // HtmlPlugin's default `` handler would still load images; + // swap it for a no-op so inline HTML images are dropped. + HtmlPlugin.create { plugin -> plugin.addHandler(TagHandlerNoOp.create("img")) } + } else { + HtmlPlugin.create() + } + markwon = Markwon .builder(context) @@ -96,8 +111,8 @@ object MarkdownHelper { .usePlugin(MarkwonSpoilerPlugin(true)) .usePlugin(StrikethroughPlugin.create()) .usePlugin(TablePlugin.create(context)) - .usePlugin(ClickableCoilImagesPlugin.create(context, loader, appState)) - .usePlugin(HtmlPlugin.create()) + .usePlugin(imagesPlugin) + .usePlugin(htmlPlugin) // use TableAwareLinkMovementMethod to handle clicks inside tables, // wraps LinkMovementMethod internally .usePlugin( @@ -168,11 +183,12 @@ object MarkdownHelper { ) }, update = { textView -> - val md = markwon!!.toMarkdown(markdown) + val parser = markwon!! + val md = parser.toMarkdown(markdown) for (img in md.getSpans(0, md.length, AsyncDrawableSpan::class.java)) { img.drawable.initWithKnownDimensions(textView.width, textView.textSize) } - markwon!!.setParsedMarkdown(textView, md) + parser.setParsedMarkdown(textView, md) }, modifier = modifier, ) diff --git a/app/src/main/java/com/jerboa/ui/components/community/CommunityScreen.kt b/app/src/main/java/com/jerboa/ui/components/community/CommunityScreen.kt index 93707e49e..09e6b7c7e 100644 --- a/app/src/main/java/com/jerboa/ui/components/community/CommunityScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/community/CommunityScreen.kt @@ -89,6 +89,7 @@ fun CommunityScreen( postActionBarMode: PostActionBarMode, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { Log.d("jerboa", "got to community screen") @@ -455,6 +456,7 @@ fun CommunityScreen( showPostAppendRetry = communityViewModel.postsRes is ApiState.AppendingFailure, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/home/BottomNavScreen.kt b/app/src/main/java/com/jerboa/ui/components/home/BottomNavScreen.kt index 6b468265f..b4b840656 100644 --- a/app/src/main/java/com/jerboa/ui/components/home/BottomNavScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/home/BottomNavScreen.kt @@ -153,6 +153,7 @@ fun BottomNavScreen( appSettingsViewModel: AppSettingsViewModel, appSettings: AppSettings, drawerState: DrawerState, + lowBandwidthMode: Boolean, ) { val acc by accountViewModel.currentAccount.observeAsState(GuardAccount) val account by remember { @@ -265,6 +266,7 @@ fun BottomNavScreen( postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, padding = padding, ) } @@ -332,6 +334,7 @@ fun BottomNavScreen( postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, padding = padding, ) } @@ -355,6 +358,7 @@ fun BottomNavScreen( postActionBarMode = appSettings.postActionBarMode.toEnum(), swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), disableVideoAutoplay = appSettings.disableVideoAutoplay.toBool(), + lowBandwidthMode = lowBandwidthMode, padding = padding, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt b/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt index 0f84d8978..000e6c7d3 100644 --- a/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt @@ -97,6 +97,7 @@ fun HomeScreen( postActionBarMode: PostActionBarMode, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, padding: PaddingValues, ) { Log.d("jerboa", "got to home screen") @@ -181,6 +182,7 @@ fun HomeScreen( postActionBarMode = postActionBarMode, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } }, @@ -232,6 +234,7 @@ fun MainPostListingsContent( postActionBarMode: PostActionBarMode, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { val ctx = LocalContext.current val resources = LocalResources.current @@ -467,6 +470,7 @@ fun MainPostListingsContent( showPostAppendRetry = homeViewModel.postsRes is ApiState.AppendingFailure, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/person/PersonProfileScreen.kt b/app/src/main/java/com/jerboa/ui/components/person/PersonProfileScreen.kt index 2d9a21da2..66aa2ba27 100644 --- a/app/src/main/java/com/jerboa/ui/components/person/PersonProfileScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/person/PersonProfileScreen.kt @@ -126,6 +126,7 @@ fun PersonProfileScreen( onBack: (() -> Unit)?, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, padding: PaddingValues? = null, ) { Log.d("jerboa", "got to person screen") @@ -317,6 +318,7 @@ fun PersonProfileScreen( postActionBarMode = postActionBarMode, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } }, @@ -355,6 +357,7 @@ fun UserTabs( postActionBarMode: PostActionBarMode, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { val tabTitles = if (savedMode) { @@ -677,6 +680,7 @@ fun UserTabs( showPostAppendRetry = personProfileViewModel.personDetailsRes is ApiState.AppendingFailure, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt index cf699bb40..c1057197c 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt @@ -116,6 +116,7 @@ fun PostListing( postActionBarMode: PostActionBarMode, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { val ctx = LocalContext.current val resources = LocalResources.current @@ -213,6 +214,7 @@ fun PostListing( voteDisplayMode = voteDisplayMode, postActionBarMode = postActionBarMode, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } @@ -259,6 +261,7 @@ fun PostListing( voteDisplayMode = voteDisplayMode, postActionBarMode = postActionBarMode, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } @@ -279,6 +282,7 @@ fun PostListing( showIfRead = showIfRead, enableDownVotes = enableDownVotes, voteDisplayMode = voteDisplayMode, + lowBandwidthMode = lowBandwidthMode, ) } } @@ -308,91 +312,131 @@ fun ThumbnailTile( usePrivateTabs: Boolean, blurEnabled: Boolean, appState: JerboaAppState, + lowBandwidthMode: Boolean, ) { - if (post.url == null) { - return - } + val postUrl = post.url ?: return // no URL means no thumbnail to render - EmbeddedDataLoader(post, imageDetails, { - AsyncImage( - model = null, - contentDescription = null, - placeholder = rememberAsyncImagePainter(R.drawable.ic_launcher_mono), - error = rememberAsyncImagePainter(R.drawable.ic_launcher_mono), - modifier = Modifier.size(POST_LINK_PIC_SIZE).clip(Shapes.large), + if (lowBandwidthMode) { + // Skip EmbeddedDataLoader (which would fetch metadata over the + // network) and render the same link placeholder the normal path + // falls back to when no thumbnail is available. + val postLinkType = PostLinkType.fromURL(postUrl) + ThumbnailBox( + thumbnailUrl = null, + targetUrl = postUrl, + postLinkType = postLinkType, + blurEnabled = blurEnabled, + altText = post.alt_text, + appState = appState, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, ) - }) { - if (it.isFailure) { - Log.e("EmbeddedData", "Data failed loading", it.exceptionOrNull()) - return@EmbeddedDataLoader - } - val embeddedData = it.getOrThrow() - val targetUrl = embeddedData.videoUrl ?: post.url ?: return@EmbeddedDataLoader - val postLinkType = PostLinkType.fromURL(targetUrl) - val thumbnailUrl = embeddedData.thumbnailUrl ?: if (postLinkType == PostLinkType.Image) post.url else null + } else { + EmbeddedDataLoader(post, imageDetails, { + AsyncImage( + model = null, + contentDescription = null, + placeholder = rememberAsyncImagePainter(R.drawable.ic_launcher_mono), + error = rememberAsyncImagePainter(R.drawable.ic_launcher_mono), + modifier = Modifier.size(POST_LINK_PIC_SIZE).clip(Shapes.large), + ) + }) { + if (it.isFailure) { + Log.e("EmbeddedData", "Data failed loading", it.exceptionOrNull()) + return@EmbeddedDataLoader + } + val embeddedData = it.getOrThrow() + val targetUrl = embeddedData.videoUrl ?: postUrl + val postLinkType = PostLinkType.fromURL(targetUrl) + val thumbnailUrl = embeddedData.thumbnailUrl ?: if (postLinkType == PostLinkType.Image) postUrl else null - val postLinkPicMod = Modifier - .size(POST_LINK_PIC_SIZE) - .combinedClickable( - onClick = { - if (postLinkType != PostLinkType.Link) { - appState.openMediaViewer(targetUrl, postLinkType) - } else { - appState.openLink( - targetUrl, - useCustomTabs, - usePrivateTabs, - ) - } - }, - onLongClick = { - appState.showLinkPopup(targetUrl) - }, + ThumbnailBox( + thumbnailUrl = thumbnailUrl, + targetUrl = targetUrl, + postLinkType = postLinkType, + blurEnabled = blurEnabled, + altText = post.alt_text, + appState = appState, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, ) + } + } +} - Box { - if (thumbnailUrl != null) { - PictrsThumbnailImage( - thumbnail = thumbnailUrl, - blur = blurEnabled, - roundBottomEndCorner = postLinkType != PostLinkType.Link, - contentDescription = post.alt_text, - modifier = postLinkPicMod, - ) - } else { - Card( - modifier = postLinkPicMod, - shape = MaterialTheme.shapes.large, +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ThumbnailBox( + thumbnailUrl: String?, + targetUrl: String, + postLinkType: PostLinkType, + blurEnabled: Boolean, + altText: String?, + appState: JerboaAppState, + useCustomTabs: Boolean, + usePrivateTabs: Boolean, +) { + val postLinkPicMod = Modifier + .size(POST_LINK_PIC_SIZE) + .combinedClickable( + onClick = { + if (postLinkType != PostLinkType.Link) { + appState.openMediaViewer(targetUrl, postLinkType) + } else { + appState.openLink( + targetUrl, + useCustomTabs, + usePrivateTabs, + ) + } + }, + onLongClick = { + appState.showLinkPopup(targetUrl) + }, + ) + + Box { + if (thumbnailUrl != null) { + PictrsThumbnailImage( + thumbnail = thumbnailUrl, + blur = blurEnabled, + roundBottomEndCorner = postLinkType != PostLinkType.Link, + contentDescription = altText, + modifier = postLinkPicMod, + ) + } else { + Card( + modifier = postLinkPicMod, + shape = MaterialTheme.shapes.large, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - Icon( - imageVector = Icons.Outlined.Link, - contentDescription = null, - modifier = Modifier.size(LINK_ICON_SIZE), - ) - } + Icon( + imageVector = Icons.Outlined.Link, + contentDescription = null, + modifier = Modifier.size(LINK_ICON_SIZE), + ) } } + } - // Display a caret in the bottom right corner to denote this as an image/video - if (postLinkType != PostLinkType.Link) { - Icon( - painter = painterResource(id = R.drawable.triangle), - contentDescription = null, - modifier = - Modifier - .size(THUMBNAIL_CARET_SIZE) - .align(Alignment.BottomEnd), - tint = - when (postLinkType) { - PostLinkType.Video -> MaterialTheme.jerboaColorScheme.videoHighlight - else -> MaterialTheme.jerboaColorScheme.imageHighlight - }, - ) - } + // Display a caret in the bottom right corner to denote this as an image/video + if (postLinkType != PostLinkType.Link) { + Icon( + painter = painterResource(id = R.drawable.triangle), + contentDescription = null, + modifier = + Modifier + .size(THUMBNAIL_CARET_SIZE) + .align(Alignment.BottomEnd), + tint = + when (postLinkType) { + PostLinkType.Video -> MaterialTheme.jerboaColorScheme.videoHighlight + else -> MaterialTheme.jerboaColorScheme.imageHighlight + }, + ) } } } diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListingCard.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListingCard.kt index 8c0727ee2..54dc9322d 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListingCard.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListingCard.kt @@ -4,12 +4,14 @@ import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -22,11 +24,14 @@ import androidx.compose.material.icons.outlined.ChatBubbleOutline import androidx.compose.material.icons.outlined.CommentsDisabled import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Gavel +import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.PlayCircle import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -44,6 +49,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.jerboa.JerboaAppState import com.jerboa.PostLinkType import com.jerboa.R @@ -95,6 +101,7 @@ import com.jerboa.ui.components.videoviewer.VideoHostComposer import com.jerboa.ui.components.videoviewer.hosts.DirectFileVideoHost import com.jerboa.ui.theme.ACTION_BAR_ICON_SIZE import com.jerboa.ui.theme.LARGE_PADDING +import com.jerboa.ui.theme.LINK_ICON_SIZE import com.jerboa.ui.theme.MEDIUM_PADDING import com.jerboa.ui.theme.SMALLER_PADDING import com.jerboa.ui.theme.SMALL_PADDING @@ -151,6 +158,7 @@ fun PostListingCard( voteDisplayMode: LocalUserVoteDisplayMode, postActionBarMode: PostActionBarMode, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { Column( modifier = @@ -180,6 +188,7 @@ fun PostListingCard( showAvatar = showAvatar, showCommunityName = showCommunityName, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) Spacer(modifier = Modifier.padding(vertical = VERTICAL_SPACING)) @@ -262,6 +271,7 @@ fun PreviewPostListingCard() { viewSource = false, expandedImage = false, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } @@ -307,6 +317,7 @@ fun PreviewLinkPostListing() { viewSource = false, expandedImage = false, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } @@ -352,6 +363,7 @@ fun PreviewImagePostListingCard() { viewSource = false, expandedImage = true, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } @@ -397,6 +409,7 @@ fun PreviewImagePostListingSmallCard() { viewSource = false, expandedImage = false, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } @@ -442,6 +455,7 @@ fun PreviewLinkNoThumbnailPostListing() { viewSource = false, expandedImage = false, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } @@ -826,6 +840,7 @@ fun PostTitleAttributionBody( showCommunityName: Boolean, showAvatar: Boolean, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { Column( verticalArrangement = Arrangement.spacedBy(VERTICAL_SPACING), @@ -844,6 +859,7 @@ fun PostTitleAttributionBody( PostTitleBlock( postView = postView, expandedImage = expandedImage, + fullBody = fullBody, account = account, useCustomTabs = useCustomTabs, usePrivateTabs = usePrivateTabs, @@ -851,6 +867,7 @@ fun PostTitleAttributionBody( showIfRead = showIfRead, blurNSFW = blurNSFW, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) // The metadata card @@ -941,6 +958,7 @@ fun PreviewStoryTitleAndMetadata() { showAvatar = true, showCommunityName = true, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } @@ -966,6 +984,7 @@ fun PreviewSourcePost() { showAvatar = true, showCommunityName = true, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } @@ -1001,6 +1020,7 @@ fun SavedButton( fun PostTitleBlock( postView: PostView, expandedImage: Boolean, + fullBody: Boolean, account: Account, useCustomTabs: Boolean, usePrivateTabs: Boolean, @@ -1008,6 +1028,7 @@ fun PostTitleBlock( showIfRead: Boolean, blurNSFW: BlurNSFW, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { val postUrl = postView.post.url val postType = postUrl?.let { PostLinkType.fromURL(it) } @@ -1018,7 +1039,7 @@ fun PostTitleBlock( (postUrl != null && VideoHostComposer.isVideo(postUrl)) when { - videoPost && expandedImage -> { + videoPost && expandedImage && !lowBandwidthMode -> { PostTitleAndVideoLink( postView = postView, appState = appState, @@ -1028,7 +1049,7 @@ fun PostTitleBlock( ) } - imagePost && expandedImage -> { + imagePost && expandedImage && !lowBandwidthMode -> { PostTitleAndImageLink( postView = postView, appState = appState, @@ -1037,6 +1058,18 @@ fun PostTitleBlock( ) } + // Show the full-width placeholder whenever the card would otherwise + // expand the media (Card mode) or when we're in the post detail view + // (SmallCard with fullBody), where the usual thumbnail is too small. + (videoPost || imagePost) && lowBandwidthMode && (expandedImage || fullBody) -> { + PostTitleAndMediaPlaceholder( + postView = postView, + appState = appState, + showIfRead = showIfRead, + linkType = if (videoPost) PostLinkType.Video else PostLinkType.Image, + ) + } + else -> { PostTitleAndThumbnail( postView = postView, @@ -1046,6 +1079,7 @@ fun PostTitleBlock( appState = appState, showIfRead = showIfRead, blurNSFW = blurNSFW, + lowBandwidthMode = lowBandwidthMode, ) } } @@ -1126,6 +1160,49 @@ fun PostTitleAndVideoLink( } } +@Composable +fun PostTitleAndMediaPlaceholder( + postView: PostView, + appState: JerboaAppState, + showIfRead: Boolean, + linkType: PostLinkType, +) { + val url = postView.post.url ?: return + + PostName( + post = postView.post, + read = postView.read, + showIfRead = showIfRead, + modifier = Modifier.padding(horizontal = MEDIUM_PADDING), + ) + + OutlinedCard( + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16f / 9f) + .combinedClickable( + onClick = { appState.openMediaViewer(url, linkType) }, + onLongClick = { appState.showLinkPopup(url) }, + ), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + Icon( + imageVector = when (linkType) { + PostLinkType.Video -> Icons.Outlined.PlayCircle + else -> Icons.Outlined.Image + }, + contentDescription = null, + modifier = Modifier.size(LINK_ICON_SIZE), + tint = MaterialTheme.colorScheme.outline, + ) + } + } +} + @Composable fun PostTitleAndThumbnail( postView: PostView, @@ -1135,6 +1212,7 @@ fun PostTitleAndThumbnail( appState: JerboaAppState, showIfRead: Boolean, blurNSFW: BlurNSFW, + lowBandwidthMode: Boolean, ) { Row( horizontalArrangement = Arrangement.spacedBy(SMALL_PADDING), @@ -1159,6 +1237,7 @@ fun PostTitleAndThumbnail( usePrivateTabs = usePrivateTabs, blurEnabled = blurNSFW.needBlur(postView), appState = appState, + lowBandwidthMode = lowBandwidthMode, ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListingList.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListingList.kt index 9a05d1b13..8b39e9f98 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListingList.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListingList.kt @@ -71,6 +71,7 @@ fun PostListingList( showIfRead: Boolean, enableDownVotes: Boolean, voteDisplayMode: LocalUserVoteDisplayMode, + lowBandwidthMode: Boolean, ) { Column( modifier = @@ -188,6 +189,7 @@ fun PostListingList( usePrivateTabs = usePrivateTabs, blurEnabled = blurNSFW.needBlur(postView), appState = appState, + lowBandwidthMode = lowBandwidthMode, ) } } @@ -211,6 +213,7 @@ fun PostListingListPreview() { showIfRead = true, enableDownVotes = false, voteDisplayMode = LocalUserVoteDisplayMode.default(), + lowBandwidthMode = false, ) } @@ -232,6 +235,7 @@ fun PostListingListWithThumbPreview() { showIfRead = true, enableDownVotes = false, voteDisplayMode = LocalUserVoteDisplayMode.default(), + lowBandwidthMode = false, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt index 9fb3983ff..81900c983 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt @@ -83,6 +83,7 @@ fun PostListings( showPostAppendRetry: Boolean, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { LazyColumn( state = listState, @@ -136,6 +137,7 @@ fun PostListings( postActionBarMode = postActionBarMode, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ).let { if (!postView.read && markAsReadOnScroll) { DisposableEffect(key1 = postView.post.id) { @@ -207,5 +209,6 @@ fun PreviewPostListings() { swipeToActionPreset = SwipeToActionPreset.TwoSides, onReplyClick = {}, disableVideoAutoplay = false, + lowBandwidthMode = false, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt b/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt index 794763b83..da61b728d 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt @@ -135,6 +135,7 @@ fun PostScreen( postActionBarMode: PostActionBarMode, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { Log.d("jerboa", "got to post screen") @@ -282,6 +283,7 @@ fun PostScreen( postActionBarMode = postActionBarMode, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } }, @@ -308,6 +310,7 @@ fun MainPostScreenBody( postActionBarMode: PostActionBarMode, swipeToActionPreset: SwipeToActionPreset, disableVideoAutoplay: Boolean, + lowBandwidthMode: Boolean, ) { val ctx = LocalContext.current val resources = LocalResources.current @@ -520,6 +523,7 @@ fun MainPostScreenBody( postActionBarMode = postActionBarMode, swipeToActionPreset = swipeToActionPreset, disableVideoAutoplay = disableVideoAutoplay, + lowBandwidthMode = lowBandwidthMode, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelScreen.kt b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelScreen.kt index 9420e0975..7c15ed50d 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelScreen.kt @@ -41,6 +41,7 @@ import com.jerboa.db.APP_SETTINGS_DEFAULT import com.jerboa.db.entity.AppSettings import com.jerboa.feat.BackConfirmationMode import com.jerboa.feat.BlurNSFW +import com.jerboa.feat.LowBandwidthMode import com.jerboa.feat.PostActionBarMode import com.jerboa.feat.PostNavigationGestureMode import com.jerboa.feat.SwipeToActionPreset @@ -99,6 +100,7 @@ fun LookAndFeelScreen( var markAsReadOnScrollState by remember { mutableStateOf(settings.markAsReadOnScroll) } var autoPlayGifsState by remember { mutableStateOf(settings.autoPlayGifs) } var disableVideoAutoplayState by remember { mutableStateOf(settings.disableVideoAutoplay == 1) } + var lowBandwidthModeState by remember { mutableStateOf(LowBandwidthMode.entries[settings.lowBandwidthMode]) } val snackbarHostState = remember { SnackbarHostState() } @@ -132,6 +134,7 @@ fun LookAndFeelScreen( postNavigationGestureMode = postNavigationGestureModeState.ordinal, swipeToActionPreset = swipeToActionPresetState.ordinal, disableVideoAutoplay = if (disableVideoAutoplayState) 1 else 0, + lowBandwidthMode = lowBandwidthModeState.ordinal, ), ) } @@ -535,6 +538,24 @@ fun LookAndFeelScreen( Text(stringResource(id = R.string.settings_disable_video_autoplay)) }, ) + ListPreference( + type = ListPreferenceType.DROPDOWN_MENU, + value = lowBandwidthModeState, + onValueChange = { + lowBandwidthModeState = it + updateAppSettings() + }, + values = LowBandwidthMode.entries, + valueToText = { + AnnotatedString(resources.getString(it.resId)) + }, + title = { + Text(stringResource(id = R.string.settings_low_bandwidth_mode)) + }, + summary = { + Text(stringResource(lowBandwidthModeState.resId)) + }, + ) } } }, diff --git a/app/src/main/java/com/jerboa/util/markwon/LinkOnlyImagesPlugin.kt b/app/src/main/java/com/jerboa/util/markwon/LinkOnlyImagesPlugin.kt new file mode 100644 index 000000000..7458cba65 --- /dev/null +++ b/app/src/main/java/com/jerboa/util/markwon/LinkOnlyImagesPlugin.kt @@ -0,0 +1,21 @@ +package com.jerboa.util.markwon + +import android.text.style.URLSpan +import com.jerboa.toHttps +import io.noties.markwon.AbstractMarkwonPlugin +import io.noties.markwon.MarkwonSpansFactory +import io.noties.markwon.image.ImageProps +import org.commonmark.node.Image + +/** + * Renders markdown `![alt](url)` images as plain clickable links using the + * alt text as the visible label. Use this instead of a real image-loading + * plugin to avoid network traffic while keeping the link reachable. + */ +class LinkOnlyImagesPlugin : AbstractMarkwonPlugin() { + override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { + builder.setFactory(Image::class.java) { _, props -> + URLSpan(ImageProps.DESTINATION.require(props).toHttps()) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 952a1b894..898334e81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -369,6 +369,10 @@ Post action bar mode Autoplay GIFs Disable video autoplay + Low bandwidth mode + Auto (metered or Data Saver) + Always on + Off Unblock community %1$s is blocked %1$s is unblocked diff --git a/app/src/test/java/com/jerboa/LowBandwidthModeTest.kt b/app/src/test/java/com/jerboa/LowBandwidthModeTest.kt new file mode 100644 index 000000000..5c3a1f3b6 --- /dev/null +++ b/app/src/test/java/com/jerboa/LowBandwidthModeTest.kt @@ -0,0 +1,19 @@ +package com.jerboa + +import com.jerboa.db.APP_SETTINGS_DEFAULT +import com.jerboa.db.DEFAULT_LOW_BANDWIDTH_MODE +import com.jerboa.feat.LowBandwidthMode +import org.junit.Assert.assertEquals +import org.junit.Test + +class LowBandwidthModeTest { + @Test + fun `default low bandwidth mode constant is auto`() { + assertEquals(LowBandwidthMode.Auto.ordinal, DEFAULT_LOW_BANDWIDTH_MODE) + } + + @Test + fun `default app settings uses auto low bandwidth mode`() { + assertEquals(LowBandwidthMode.Auto.ordinal, APP_SETTINGS_DEFAULT.lowBandwidthMode) + } +}