Skip to content

Commit

Permalink
Added display of article image inside reader
Browse files Browse the repository at this point in the history
  • Loading branch information
spacecowboy committed Nov 26, 2023
1 parent 8433be8 commit 5e16a05
Show file tree
Hide file tree
Showing 14 changed files with 976 additions and 95 deletions.
755 changes: 755 additions & 0 deletions app/schemas/com.nononsenseapps.feeder.db.room.AppDatabase/32.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.nononsenseapps.feeder.db.room

import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import com.nononsenseapps.feeder.FeederApplication
import kotlin.test.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI

@RunWith(AndroidJUnit4::class)
@LargeTest
class TestMigrationFrom31To32 : DIAware {
private val dbName = "testDb"
private val feederApplication: FeederApplication = ApplicationProvider.getApplicationContext()
override val di: DI by closestDI(feederApplication)

@Rule
@JvmField
val testHelper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
emptyList(),
FrameworkSQLiteOpenHelperFactory(),
)

@Test
fun migrate() {
@Suppress("SimpleRedundantLet")
testHelper.createDatabase(dbName, FROM_VERSION).let { oldDB ->
oldDB.execSQL(
"""
INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default, open_articles_with, alternate_id, currently_syncing, when_modified, site_fetched)
VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0, '', 0, 0, 0, 0)
""".trimIndent(),
)
oldDB.execSQL(
"""
INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded, read_time, unread, word_count, word_count_full)
VALUES(8, 'http://item1', 'title', 'ptitle', 'psnippet', 0, 1, 0, 0, 1, 0, 0, 0, 1, 5, 900)
""".trimIndent(),
)
}
val db = testHelper.runMigrationsAndValidate(
dbName,
TO_VERSION,
true,
MigrationFrom31To32(di),
)

db.query(
"""
select image_from_body from feed_items
""".trimIndent(),
).use {
assert(it.count == 1)
assert(it.moveToFirst())
assertEquals(0, it.getInt(0))
}
}

companion object {
private const val FROM_VERSION = 31
private const val TO_VERSION = 32
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,8 @@ data class Article(
val bookmarked: Boolean = item?.bookmarked ?: false
val wordCount: Int = item?.wordCount ?: 0
val wordCountFull: Int = item?.wordCountFull ?: 0
val image: String? = item?.imageUrl
val imageFromBody: Boolean = item?.imageFromBody ?: false
}

enum class TextToDisplay {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const val COL_GUID = "guid"
const val COL_PLAINTITLE = "plain_title"
const val COL_PLAINSNIPPET = "plain_snippet"
const val COL_IMAGEURL = "image_url"
const val COL_IMAGE_FROM_BODY = "image_from_body"
const val COL_ENCLOSURELINK = "enclosure_link"
const val COL_ENCLOSURE_TYPE = "enclosure_type"
const val COL_LINK = "link"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private const val LOG_TAG = "FEEDER_APPDB"
RemoteFeed::class,
SyncDevice::class,
],
version = 31,
version = 32,
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
Expand Down Expand Up @@ -118,12 +118,23 @@ fun getAllMigrations(di: DI) = arrayOf(
MigrationFrom28To29(di),
MigrationFrom29To30(di),
MigrationFrom30To31(di),
MigrationFrom31To32(di),
)

/*
* 6 represents legacy database
* 7 represents new Room database
*/
class MigrationFrom31To32(override val di: DI) : Migration(31, 32), DIAware {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
alter table feed_items add column image_from_body integer not null default 0
""".trimIndent(),
)
}
}

class MigrationFrom30To31(override val di: DI) : Migration(30, 31), DIAware {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
Expand Down
15 changes: 5 additions & 10 deletions app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.nononsenseapps.feeder.db.COL_FULLTEXT_DOWNLOADED
import com.nononsenseapps.feeder.db.COL_GUID
import com.nononsenseapps.feeder.db.COL_ID
import com.nononsenseapps.feeder.db.COL_IMAGEURL
import com.nononsenseapps.feeder.db.COL_IMAGE_FROM_BODY
import com.nononsenseapps.feeder.db.COL_LINK
import com.nononsenseapps.feeder.db.COL_NOTIFIED
import com.nononsenseapps.feeder.db.COL_PLAINSNIPPET
Expand Down Expand Up @@ -73,6 +74,7 @@ data class FeedItem @Ignore constructor(
@ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "",
@ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "",
@ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null,
@ColumnInfo(name = COL_IMAGE_FROM_BODY) var imageFromBody: Boolean = false,
@ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null,
@ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null,
@ColumnInfo(name = COL_AUTHOR) var author: String? = null,
Expand Down Expand Up @@ -137,25 +139,18 @@ data class FeedItem @Ignore constructor(

// Make double sure no base64 images are used as thumbnails
val safeImage = when {
entry.image?.startsWith("data") == true -> null
entry.image?.url?.startsWith("data") == true -> null
else -> entry.image
}

val absoluteImage = when {
feed.feed_url != null && safeImage != null -> {
relativeLinkIntoAbsolute(sloppyLinkToStrictURL(feed.feed_url), safeImage)
}

else -> safeImage
}

this.guid = entryGuid
entry.title?.let { this.plainTitle = it.take(MAX_TITLE_LENGTH) }
@Suppress("DEPRECATION")
this.title = this.plainTitle
this.plainSnippet = summary

this.imageUrl = absoluteImage
this.imageUrl = safeImage?.url
this.imageFromBody = safeImage?.fromBody ?: false
val firstEnclosure = entry.attachments?.firstOrNull()
this.enclosureLink = firstEnclosure?.url
this.enclosureType = firstEnclosure?.mime_type?.lowercase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.nononsenseapps.feeder.db.COL_FULLTEXT_BY_DEFAULT
import com.nononsenseapps.feeder.db.COL_GUID
import com.nononsenseapps.feeder.db.COL_ID
import com.nononsenseapps.feeder.db.COL_IMAGEURL
import com.nononsenseapps.feeder.db.COL_IMAGE_FROM_BODY
import com.nononsenseapps.feeder.db.COL_LINK
import com.nononsenseapps.feeder.db.COL_PLAINSNIPPET
import com.nononsenseapps.feeder.db.COL_PLAINTITLE
Expand Down Expand Up @@ -44,7 +45,8 @@ const val feedItemColumnsWithFeed = """
$FEEDS_TABLE_NAME.$COL_FULLTEXT_BY_DEFAULT AS $COL_FULLTEXT_BY_DEFAULT,
$COL_BOOKMARKED,
$COL_WORD_COUNT,
$COL_WORD_COUNT_FULL
$COL_WORD_COUNT_FULL,
$COL_IMAGE_FROM_BODY
"""

data class FeedItemWithFeed @Ignore constructor(
Expand All @@ -55,6 +57,7 @@ data class FeedItemWithFeed @Ignore constructor(
@ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "",
@ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "",
@ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null,
@ColumnInfo(name = COL_IMAGE_FROM_BODY) var imageFromBody: Boolean = false,
@ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null,
@ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null,
var author: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ fun ArticleContent(

else -> null
},
image = viewState.image,
imageFromBody = viewState.imageFromBody,
) {
// Can take a composition or two before viewstate is set to its actual values
if (viewState.articleId > ID_UNSET) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ class FeedArticleViewModel(
TextToDisplay.FAILED_NOT_HTML,
-> 0
},
image = article.image,
imageFromBody = article.imageFromBody,
)
}
.stateIn(
Expand Down Expand Up @@ -534,6 +536,8 @@ interface ArticleScreenViewState {
val isBookmarked: Boolean
val keyHolder: ArticleItemKeyHolder
val wordCount: Int
val image: String?
val imageFromBody: Boolean
}

interface ArticleItemKeyHolder {
Expand Down Expand Up @@ -618,6 +622,8 @@ data class FeedArticleScreenViewState(
override val filter: FeedListFilter = emptyFeedListFilter,
val isArticleOpen: Boolean = false,
override val wordCount: Int = 0,
override val image: String? = null,
override val imageFromBody: Boolean = false,
) : FeedScreenViewState, ArticleScreenViewState

sealed class TSSError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ fun ReaderView(
wordCount: Int,
onEnclosureClick: () -> Unit,
onFeedTitleClick: () -> Unit,
enclosure: Enclosure ,
articleTitle: String ,
feedTitle: String,
authorDate: String?,
image: String?,
imageFromBody: Boolean,
modifier: Modifier = Modifier,
articleListState: LazyListState = rememberLazyListState(),
enclosure: Enclosure = Enclosure(),
articleTitle: String = "Article title on top",
feedTitle: String = "Feed Title is here",
authorDate: String? = "2018-01-02",
articleBody: LazyListScope.() -> Unit,
) {
val dimens = LocalDimens.current
Expand Down Expand Up @@ -136,16 +138,18 @@ fun ReaderView(
}
},
) {
WithBidiDeterminedLayoutDirection(paragraph = articleTitle) {
val interactionSource = remember { MutableInteractionSource() }
Text(
text = articleTitle,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier
.indication(interactionSource, LocalIndication.current)
.focusableInNonTouchMode(interactionSource = interactionSource)
.width(dimens.maxReaderWidth),
)
if (articleTitle.isNotBlank()) {
WithBidiDeterminedLayoutDirection(paragraph = articleTitle) {
val interactionSource = remember { MutableInteractionSource() }
Text(
text = articleTitle,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier
.indication(interactionSource, LocalIndication.current)
.focusableInNonTouchMode(interactionSource = interactionSource)
.width(dimens.maxReaderWidth),
)
}
}
ProvideScaledText(
style = MaterialTheme.typography.titleMedium.merge(
Expand Down Expand Up @@ -195,7 +199,10 @@ fun ReaderView(
contentDescription = null,
)
val readTimeText =
pluralStringResource(id = R.plurals.n_minutes, count = readTimeSecs / 60)
pluralStringResource(
id = R.plurals.n_minutes,
count = readTimeSecs / 60,
)
.format(
"${readTimeSecs / 60}:${readTimeSecs % 60}",
)
Expand All @@ -222,42 +229,8 @@ fun ReaderView(

if (enclosure.present) {
item {
if (enclosure.isImage) {
BoxWithConstraints(
modifier = Modifier
.clip(RectangleShape)
.fillMaxWidth(),
) {
WithTooltipIfNotBlank(tooltip = enclosure.name) { innerModifier ->
val imageWidth by rememberMaxImageWidth()
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(enclosure.link)
.scale(Scale.FIT)
.size(imageWidth)
.precision(Precision.INEXACT)
.build(),
contentDescription = enclosure.name,
placeholder = rememberTintedVectorPainter(
Icons.Outlined.Terrain,
),
error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline),
contentScale = if (dimens.hasImageAspectRatioInReader) {
ContentScale.Fit
} else {
ContentScale.FillWidth
},
modifier = innerModifier
.fillMaxWidth()
.run {
dimens.imageAspectRatioInReader?.let { ratio ->
aspectRatio(ratio)
} ?: this
},
)
}
}
} else {
// Image will be shown in block below
if (!enclosure.isImage) {
val openLabel = if (enclosure.name.isBlank()) {
stringResource(R.string.open_enclosed_media)
} else {
Expand Down Expand Up @@ -299,6 +272,45 @@ fun ReaderView(
}
}

if (!imageFromBody && image != null) {
item {
BoxWithConstraints(
modifier = Modifier
.clip(RectangleShape)
.fillMaxWidth(),
) {
WithTooltipIfNotBlank(tooltip = stringResource(id = R.string.article_image)) { innerModifier ->
val imageWidth by rememberMaxImageWidth()
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(image)
.scale(Scale.FIT)
.size(imageWidth)
.precision(Precision.INEXACT)
.build(),
contentDescription = enclosure.name,
placeholder = rememberTintedVectorPainter(
Icons.Outlined.Terrain,
),
error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline),
contentScale = if (dimens.hasImageAspectRatioInReader) {
ContentScale.Fit
} else {
ContentScale.FillWidth
},
modifier = innerModifier
.fillMaxWidth()
.run {
dimens.imageAspectRatioInReader?.let { ratio ->
aspectRatio(ratio)
} ?: this
},
)
}
}
}
}

articleBody()
}
}
Expand All @@ -314,6 +326,12 @@ fun ReaderPreview() {
wordCount = 9831,
onEnclosureClick = {},
onFeedTitleClick = {},
enclosure = Enclosure(),
articleTitle = "Article title on top",
feedTitle = "Feed Title is here",
authorDate = "2018-01-02",
image = "https://cowboyprogrammer.org/images/2017/10/gimp_image_mode_index.png",
imageFromBody = false,
) {}
}
}
Expand Down
Loading

0 comments on commit 5e16a05

Please sign in to comment.