diff --git a/.editorconfig b/.editorconfig index 903340637..fb119c398 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,7 +17,8 @@ ij_wrap_on_typing = false max_line_length = 200 ktlint_code_style = ktlint_official ktlint_function_naming_ignore_when_annotated_with=Composable -twitter_compose_allowed_composition_locals = LocalTypographySettings,LocalDimens,LocalWindowSize,LocalFoldableHinge +compose_allowed_composition_locals = LocalTypographySettings,LocalDI,LocalDimens,LocalWindowSize,LocalFoldableHinge +compose_allowed_forwarding = .*Screen [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.opml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul,rss_kuketz,rss_morningpaper}] indent_size = 2 diff --git a/.github/workflows/android_pr_build.yml b/.github/workflows/android_pr_build.yml index ac394e5b7..a037a8a4d 100644 --- a/.github/workflows/android_pr_build.yml +++ b/.github/workflows/android_pr_build.yml @@ -29,4 +29,24 @@ jobs: - name: gradle build uses: gradle/gradle-build-action@v2 with: - arguments: build packageDebugAndroidTest :app:lint check + arguments: assemble bundle packageDebugAndroidTest :app:lint test + + # Lint + ktlint: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: perform the checkout + uses: actions/checkout@v3 + + - name: setup JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: ktlint check + uses: gradle/gradle-build-action@v2 + with: + arguments: ktlintCheck diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md deleted file mode 100644 index 9cd8257c4..000000000 --- a/.gitlab/issue_templates/Bug.md +++ /dev/null @@ -1,16 +0,0 @@ - - -## Description - - - -## URL to affected feed - - diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md deleted file mode 100644 index 90da64edc..000000000 --- a/.gitlab/issue_templates/Feature.md +++ /dev/null @@ -1 +0,0 @@ -FEATURE REQUESTS ARE NOT ACCEPTED AT THIS TIME diff --git a/.gitlab/merge_request_templates/patch.md b/.gitlab/merge_request_templates/patch.md deleted file mode 100644 index 742841161..000000000 --- a/.gitlab/merge_request_templates/patch.md +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5f8775d41..564f7d940 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.kotlin.ksp) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ktlint.gradle) } android { @@ -45,16 +46,16 @@ android { } if (project.hasProperty("STORE_FILE")) { create("release") { - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "ktlint:standard:property-naming") val STORE_FILE: String by project.properties - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "ktlint:standard:property-naming") val STORE_PASSWORD: String by project.properties - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "ktlint:standard:property-naming") val KEY_ALIAS: String by project.properties - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "ktlint:standard:property-naming") val KEY_PASSWORD: String by project.properties storeFile = file(STORE_FILE) storePassword = STORE_PASSWORD @@ -183,14 +184,16 @@ android { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) { kotlinOptions { if (project.findProperty("myapp.enableComposeCompilerReports") == "true") { - freeCompilerArgs += listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.resolve("compose_metrics").canonicalPath}" - ) - freeCompilerArgs += listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir.resolve("compose_metrics").canonicalPath}" - ) + freeCompilerArgs += + listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.resolve("compose_metrics").canonicalPath}", + ) + freeCompilerArgs += + listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir.resolve("compose_metrics").canonicalPath}", + ) } } } @@ -202,6 +205,7 @@ configurations.all { } dependencies { + ktlintRuleset(libs.ktlint.compose) ksp(libs.room) // For java time coreLibraryDesugaring(libs.desugar) diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt index 869ba09f7..69fd8356e 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt @@ -27,7 +27,7 @@ class Feeds { * All items are genuine - except the first which is taken from the feed at 2021-10-06T21:06:00 */ @Language("xml") - const val rssWithDuplicateGuids = """ + const val RSS_WITH_DUPLICATE_GUIDS = """ Weather Warnings for Victoria. Issued by the Australian Bureau of Meteorology diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt index 018754b09..687f8a85a 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt @@ -24,10 +24,10 @@ import com.nononsenseapps.feeder.db.room.ReadStatusSyncedDao import com.nononsenseapps.feeder.db.room.RemoteReadMarkDao import com.nononsenseapps.feeder.db.room.SyncRemoteDao import com.nononsenseapps.feeder.di.networkModule +import com.nononsenseapps.feeder.model.Feeds.Companion.RSS_WITH_DUPLICATE_GUIDS import com.nononsenseapps.feeder.model.Feeds.Companion.cowboyAtom import com.nononsenseapps.feeder.model.Feeds.Companion.cowboyJson import com.nononsenseapps.feeder.model.Feeds.Companion.nixosRss -import com.nononsenseapps.feeder.model.Feeds.Companion.rssWithDuplicateGuids import com.nononsenseapps.feeder.ui.TestDatabaseRule import com.nononsenseapps.feeder.util.DoNotUseInProd import com.nononsenseapps.feeder.util.FilePathProvider @@ -210,7 +210,7 @@ class RssLocalSyncKtTest : DIAware { insertFeed( "aussieWeather", server.url("/IDZ00059.warnings_vic.xml").toUrl(), - rssWithDuplicateGuids, + RSS_WITH_DUPLICATE_GUIDS, isJson = false, useAlternateId = true, ) @@ -686,27 +686,26 @@ class RssLocalSyncKtTest : DIAware { } private fun fooRss(itemsCount: Int = 1): String { + val items = + (1..itemsCount).joinToString("\n") { + """ + + Foo Item $it + https://foo.bar/$it + Woop woop $it + + """.trimIndent() + } + return """ - - - - Foo Feed - https://foo.bar - ${ - (1..itemsCount).map { - """ - - Foo Item $it - https://foo.bar/$it - Woop woop $it - + + + + Foo Feed + https://foo.bar + $items + + """.trimIndent() - }.fold("") { acc, s -> - "$acc\n$s" - } - } - - - """.trimIndent() } } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt index 237d54c82..795323894 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt @@ -57,6 +57,7 @@ class RssNotificationsKtTest { @Test fun queryParameterDoesntGetGarbled() { + @Suppress("ktlint:standard:max-line-length") val magnetLink = "magnet:?xt=urn:btih:82B1726F2D1B22F383A2B2CD6977B00F908FB315&dn=Crazy+Ex+Girlfriend+S04E10+720p+HDTV+x264+LucidTV&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce" val enclosureIntent = getOpenInDefaultActivityIntent(getInstrumentation().context, 5, link = magnetLink) diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/OpenLinkInDefaultActivityTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/OpenLinkInDefaultActivityTest.kt deleted file mode 100644 index b01c39ce3..000000000 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/OpenLinkInDefaultActivityTest.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.nononsenseapps.feeder.ui - -// @Ignore("Activity rewritten in compose") -// class OpenLinkInDefaultActivityTest { -// @get:Rule -// val activityTestRule = ActivityTestRule(OpenLinkInDefaultActivity::class.java, false, false) -// -// @get:Rule -// val testDb = TestDatabaseRule(getApplicationContext()) -// -// private lateinit var feedItem: FeedItem -// -// @Before -// fun setup() = runBlocking { -// val db = testDb.db -// -// val feedId = db.feedDao().insertFeed( -// Feed( -// title = "foo", -// url = URL("http://foo") -// ) -// ) -// -// val item = FeedItem( -// feedId = feedId, -// guid = "foobar", -// title = "bla", -// link = "http://foo", -// notified = false, -// unread = true -// ) -// -// val feedItemId = db.feedItemDao().insertFeedItem(item) -// -// feedItem = item.copy(id = feedItemId) -// } -// -// @After -// fun pressHome() { -// UiDevice.getInstance(getInstrumentation()).pressBack() -// } -// -// @Test -// fun noIntentDoesNothing() { -// activityTestRule.launchActivity(null) -// -// runBlocking { -// val item = withContext(Dispatchers.Default) { -// untilEq(feedItem) { -// testDb.db.feedItemDao().loadFeedItem(feedItem.id) -// } -// } -// assertEquals(feedItem, item) -// } -// } -// -// @Test -// fun faultyLinkDoesntCrash() { -// activityTestRule.launchActivity( -// getOpenInDefaultActivityIntent( -// getApplicationContext(), -// feedItemId = -252, -// link = "bob" -// ) -// ) -// -// runBlocking { -// val item = withContext(Dispatchers.Default) { -// untilEq(feedItem) { -// testDb.db.feedItemDao().loadFeedItem(feedItem.id) -// } -// } -// assertEquals(feedItem, item) -// } -// } -// -// @Test -// fun withIntentItemIsMarkedAsReadAndNotified() { -// activityTestRule.launchActivity( -// getOpenInDefaultActivityIntent( -// getApplicationContext(), -// feedItemId = feedItem.id, -// link = feedItem.link!! -// ) -// ) -// -// val expected = feedItem.copy( -// unread = false, -// notified = true -// ) -// -// runBlocking { -// val item = withContext(Dispatchers.Default) { -// untilEq(expected) { -// testDb.db.feedItemDao().loadFeedItem(feedItem.id) -// } -// } -// assertEquals(expected, item) -// } -// } -// -// @Test -// fun noLinkButItemIsMarkedAsReadAndNotified() { -// activityTestRule.launchActivity( -// getOpenInDefaultActivityIntent( -// getApplicationContext(), -// feedItemId = feedItem.id -// ) -// ) -// -// val expected = feedItem.copy( -// unread = false, -// notified = true -// ) -// -// runBlocking { -// val item = withContext(Dispatchers.Default) { -// untilEq(expected) { -// testDb.db.feedItemDao().loadFeedItem(feedItem.id) -// } -// } -// assertEquals(expected, item) -// } -// } -// } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/HtmlRecompositionTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/HtmlRecompositionTest.kt deleted file mode 100644 index f099d07dc..000000000 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/HtmlRecompositionTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.nononsenseapps.feeder.ui.compose - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import com.nononsenseapps.feeder.archmodel.Enclosure -import com.nononsenseapps.feeder.ui.compose.feedarticle.DefaultArticleItemKeyHolder -import com.nononsenseapps.feeder.ui.compose.feedarticle.ReaderView -import com.nononsenseapps.feeder.ui.compose.text.htmlFormattedText -import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme -import com.nononsenseapps.feeder.ui.compose.utils.ScreenType -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.junit.Rule -import org.junit.Test - -class HtmlRecompositionTest { - @get:Rule - val composeTestRule = createComposeRule() - - @Test - fun testReaderViewDoesNotMixArticleContents() { - var content: Int by mutableStateOf(0) - - composeTestRule.setContent { - FeederTheme { - ReaderView( - screenType = ScreenType.SINGLE, - wordCount = 1, - onEnclosureClick = { }, - onFeedTitleClick = { }, - enclosure = Enclosure(), - articleTitle = "title", - feedTitle = "feed", - authorDate = null, - image = null, - isFeedText = false, - articleBody = { - if (content == 1) { - "

one

".byteInputStream().use { - htmlFormattedText( - inputStream = it, - baseUrl = "http://example.com", - keyHolder = DefaultArticleItemKeyHolder(1), - onLinkClick = {}, - ) - } - } else { - "

zero

ze-ro

".byteInputStream().use { - htmlFormattedText( - inputStream = it, - baseUrl = "http://example.com", - keyHolder = DefaultArticleItemKeyHolder(0), - onLinkClick = {}, - ) - } - } - }, - ) - } - } - - runBlocking { - assertWithin(1000) { - composeTestRule.awaitIdle() - composeTestRule.onNodeWithText("zero").assertIsDisplayed() - composeTestRule.onNodeWithText("ze-ro").assertIsDisplayed() - } - - content = 1 - - assertWithin(1000) { - composeTestRule.awaitIdle() - composeTestRule.onNodeWithText("one").assertIsDisplayed() - composeTestRule.onNodeWithText("zero").assertDoesNotExist() - composeTestRule.onNodeWithText("ze-ro").assertDoesNotExist() - } - } - } -} - -suspend fun assertWithin( - timeoutMs: Long, - block: suspend () -> Unit, -) { - val start = System.currentTimeMillis() - var lastError: Throwable? = null - while (System.currentTimeMillis() - start < timeoutMs) { - try { - block() - return - } catch (e: AssertionError) { - lastError = e - delay(50) - } - } - throw lastError ?: Exception("Timed out") -} diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/ReaderScreenScrollingTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/ReaderScreenScrollingTest.kt deleted file mode 100644 index dbf460d7d..000000000 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/ReaderScreenScrollingTest.kt +++ /dev/null @@ -1,254 +0,0 @@ -package com.nononsenseapps.feeder.ui.compose - -import androidx.compose.ui.test.junit4.createComposeRule -import org.junit.Ignore -import org.junit.Rule - -@Ignore -class ReaderScreenScrollingTest { - @get:Rule - val composeTestRule = createComposeRule() - // createAndroidComposeRule() - - /** - * This test doesn't really test anything - it's just good documentation - * of how to test scrolling - */ -// @OptIn(ExperimentalTestApi::class) -// @Test -// fun scrollingWorks() { -// composeTestRule.setContent { -// FeederTheme { -// withDI { -// ReaderScreen( -// articleTitle = "Title", -// feedDisplayTitle = "Feed", -// author = "Author", -// pubDate = null, -// enclosure = null, -// onFetchFullText = {}, -// onMarkAsUnread = {}, -// onShare = {}, -// onOpenInCustomTab = {}, -// onNavigateUp = {}, -// ttsPlayer = {}, -// onTTSStart = {}, -// onFeedTitleClick = {}, -// articleBody = { -// dummyHtml.byteInputStream().use { -// htmlFormattedText( -// it, -// baseUrl = "http://google.com", -// imagePlaceholder = R.drawable.placeholder_image_article_day, -// onLinkClick = {}, -// ) -// } -// } -// ) -// } -// } -// } -// -// // composeTestRule.onRoot(useUnmergedTree = false).printToLog("FeederTest") -// -// // Does not exist until it is rendered -// composeTestRule -// .onNodeWithText( -// "16 paragraph", -// substring = true, -// ignoreCase = true, -// useUnmergedTree = true -// ) -// .assertDoesNotExist() -// -// composeTestRule -// .onNode(hasScrollToIndexAction()) -// .performGesture { -// val start = Offset(centerX, top) -// val end = Offset(centerX, -20_000f) -// swipe(start, end) -// } -// -// -// // composeTestRule.onRoot(useUnmergedTree = false).printToLog("FeederTest") -// -// // If we scrolled, then this node should exist now -// composeTestRule -// .onNodeWithText( -// "16 paragraph", -// substring = true, -// ignoreCase = true, -// useUnmergedTree = true -// ) -// .assertExists() -// } -} - -private const val dummyHtml = """ -

First paragraph

-

First paragraph

-

First paragraph

-

First paragraph

-

First paragraph

-

First paragraph

-

First paragraph

-

First paragraph

- -
First paragraph
- -

Second paragraph

-

Second paragraph

-

Second paragraph

-

Second paragraph

-

Second paragraph

-

Second paragraph

-

Second paragraph

-

Second paragraph

- -
Second paragraph
- -

Third paragraph

-

Third paragraph

-

Third paragraph

-

Third paragraph

-

Third paragraph

-

Third paragraph

-

Third paragraph

-

Third paragraph

- -
Third paragraph
- -

Fourth paragraph

-

Fourth paragraph

-

Fourth paragraph

-

Fourth paragraph

-

Fourth paragraph

-

Fourth paragraph

-

Fourth paragraph

-

Fourth paragraph

- -
Fourth paragraph
- -

5 paragraph

-

5 paragraph

-

5 paragraph

-

5 paragraph

-

5 paragraph

-

5 paragraph

-

5 paragraph

-

5 paragraph

- -
5 paragraph
- -

6 paragraph

-

6 paragraph

-

6 paragraph

-

6 paragraph

-

6 paragraph

-

6 paragraph

-

6 paragraph

-

6 paragraph

- -
6 paragraph
- -

7 paragraph

-

7 paragraph

-

7 paragraph

-

7 paragraph

-

7 paragraph

-

7 paragraph

-

7 paragraph

-

7 paragraph

- -
7 paragraph
- -

8 paragraph

-

8 paragraph

-

8 paragraph

-

8 paragraph

-

8 paragraph

-

8 paragraph

-

8 paragraph

-

8 paragraph

- -
8 paragraph
- -

9 paragraph

-

9 paragraph

-

9 paragraph

-

9 paragraph

-

9 paragraph

-

9 paragraph

-

9 paragraph

-

9 paragraph

- -
9 paragraph
- -

10 paragraph

-

10 paragraph

-

10 paragraph

-

10 paragraph

-

10 paragraph

-

10 paragraph

-

10 paragraph

-

10 paragraph

- -
10 paragraph
- -

11 paragraph

-

11 paragraph

-

11 paragraph

-

11 paragraph

-

11 paragraph

-

11 paragraph

-

11 paragraph

-

11 paragraph

- -
11 paragraph
- -

12 paragraph

-

12 paragraph

-

12 paragraph

-

12 paragraph

-

12 paragraph

-

12 paragraph

-

12 paragraph

-

12 paragraph

- -
12 paragraph
- -

13 paragraph

-

13 paragraph

-

13 paragraph

-

13 paragraph

-

13 paragraph

-

13 paragraph

-

13 paragraph

-

13 paragraph

- -
13 paragraph
- -

14 paragraph

-

14 paragraph

-

14 paragraph

-

14 paragraph

-

14 paragraph

-

14 paragraph

-

14 paragraph

-

14 paragraph

- -
14 paragraph
- -

15 paragraph

-

15 paragraph

-

15 paragraph

-

15 paragraph

-

15 paragraph

-

15 paragraph

-

15 paragraph

-

15 paragraph

- -
15 paragraph
- - 16 paragraph -""" diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/EditFeedScreenRobot.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/EditFeedScreenRobot.kt index 7c736e0f1..6a25e5615 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/EditFeedScreenRobot.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/EditFeedScreenRobot.kt @@ -1,7 +1,12 @@ package com.nononsenseapps.feeder.ui.robots -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasScrollAction import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeUp class EditFeedScreenRobot( private val testRule: ComposeTestRule, diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt index ec69b515e..6c55b57a4 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:property-naming") + package com.nononsenseapps.feeder.archmodel import android.content.SharedPreferences diff --git a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt index 04f870d1c..93842d884 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt @@ -24,15 +24,15 @@ class RSSContentProvider : ContentProvider(), DIAware { private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { - addURI(AUTHORITY, RssContentProviderContract.feedsUriPathList, URI_FEED_LIST) + addURI(AUTHORITY, RssContentProviderContract.FEEDS_URI_PATH_LIST, URI_FEED_LIST) addURI( AUTHORITY, - RssContentProviderContract.articlesUriPathList, + RssContentProviderContract.ARTICLES_URI_PATH_LIST, URI_ARTICLE_LIST, ) addURI( AUTHORITY, - RssContentProviderContract.articlesUriPathItem, + RssContentProviderContract.ARTICLES_URI_PATH_ITEM, URI_ARTICLE_IN_FEED_LIST, ) } @@ -51,9 +51,9 @@ class RSSContentProvider : ContentProvider(), DIAware { override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) { - URI_FEED_LIST -> RssContentProviderContract.feedsMimeTypeList - URI_ARTICLE_LIST -> RssContentProviderContract.articlesMimeTypeList - URI_ARTICLE_IN_FEED_LIST -> RssContentProviderContract.articlesMimeTypeItem + URI_FEED_LIST -> RssContentProviderContract.FEEDS_MIME_TYPE_LIST + URI_ARTICLE_LIST -> RssContentProviderContract.ARTICLES_MIME_TYPE_LIST + URI_ARTICLE_IN_FEED_LIST -> RssContentProviderContract.ARTICLES_MIME_TYPE_ITEM else -> null } diff --git a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt index 5111717d8..ce5893008 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt @@ -1,26 +1,28 @@ package com.nononsenseapps.feeder.contentprovider object RssContentProviderContract { - const val feedsMimeTypeList = "vnd.android.cursor.dir/vnd.rssprovider.feeds" - const val feedsUriPathList = "feeds" + const val FEEDS_MIME_TYPE_LIST = "vnd.android.cursor.dir/vnd.rssprovider.feeds" + const val FEEDS_URI_PATH_LIST = "feeds" /** * Columns available via the content provider */ + @Suppress("unused") val feedsColumns = listOf( "id", "title", ) - const val articlesMimeTypeList = "vnd.android.cursor.dir/vnd.rssprovider.items" - const val articlesUriPathList = "articles" - const val articlesMimeTypeItem = "vnd.android.cursor.item/vnd.rssprovider.item" - const val articlesUriPathItem = "articles/#" + const val ARTICLES_MIME_TYPE_LIST = "vnd.android.cursor.dir/vnd.rssprovider.items" + const val ARTICLES_URI_PATH_LIST = "articles" + const val ARTICLES_MIME_TYPE_ITEM = "vnd.android.cursor.item/vnd.rssprovider.item" + const val ARTICLES_URI_PATH_ITEM = "articles/#" /** * Columns available via the content provider */ + @Suppress("unused") val articlesColumns = listOf( "id", diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt index 2f784569c..c5db55564 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt @@ -150,6 +150,7 @@ class MigrationFrom34To35(override val di: DI) : Migration(34, 35), DIAware { database.execSQL("create index index_feed_items_block_time on feed_items (block_time)") // Room schema is anal about whitespace + @Suppress("ktlint:standard:max-line-length") val sql = "CREATE VIEW `feeds_with_items_for_nav_drawer` AS select feeds.id as feed_id, item_id, case when custom_title is '' then title else custom_title end as display_title, tag, image_url, unread, bookmarked\n from feeds\n left join (\n select id as item_id, feed_id, read_time is null as unread, bookmarked\n from feed_items\n where block_time is null\n )\n ON feeds.id = feed_id" database.execSQL(sql) @@ -160,6 +161,7 @@ class MigrationFrom34To35(override val di: DI) : Migration(34, 35), DIAware { class MigrationFrom33To34(override val di: DI) : Migration(33, 34), DIAware { override fun migrate(database: SupportSQLiteDatabase) { // Room schema is anal about whitespace + @Suppress("ktlint:standard:max-line-length") val sql = "CREATE VIEW `feeds_with_items_for_nav_drawer` AS select feeds.id as feed_id, item_id, case when custom_title is '' then title else custom_title end as display_title, tag, image_url, unread, bookmarked\n from feeds\n left join (\n select id as item_id, feed_id, read_time is null as unread, bookmarked\n from feed_items\n where not exists(select 1 from blocklist where lower(feed_items.plain_title) glob blocklist.glob_pattern)\n )\n ON feeds.id = feed_id" database.execSQL(sql) } @@ -659,6 +661,7 @@ object MIGRATION_9_10 : Migration(9, 10) { } } +@Suppress("ktlint:standard:property-naming", "ClassName") object MIGRATION_8_9 : Migration(8, 9) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( @@ -669,6 +672,7 @@ object MIGRATION_8_9 : Migration(8, 9) { } } +@Suppress("ktlint:standard:property-naming", "ClassName") object MIGRATION_7_8 : Migration(7, 8) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( @@ -679,12 +683,14 @@ object MIGRATION_7_8 : Migration(7, 8) { } } +@Suppress("ktlint:standard:property-naming", "ClassName") object MIGRATION_6_7 : Migration(6, 7) { override fun migrate(database: SupportSQLiteDatabase) { legacyMigration(database, 6) } } +@Suppress("ktlint:standard:property-naming", "ClassName") object MIGRATION_5_7 : Migration(5, 7) { override fun migrate(database: SupportSQLiteDatabase) { legacyMigration(database, 5) diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt b/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt index b06b83b9f..f8a332d38 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt @@ -54,7 +54,7 @@ class FeedParser(override val di: DI) : DIAware { @VisibleForTesting internal fun getSiteMetaDataInHtml( url: URL, - html: String + html: String, ): Either { if (!html.contains("", ignoreCase = true)) { // Probably a a feed URL and not a page @@ -66,12 +66,12 @@ class FeedParser(override val di: DI) : DIAware { MetaDataParseError(url = url.toString(), throwable = t).also { Log.w(LOG_TAG, "Error when fetching site metadata", t) } - } + }, ) { SiteMetaData( url = url, alternateFeedLinks = getAlternateFeedLinksInHtml(html, baseUrl = url), - feedImage = getFeedIconInHtml(html, baseUrl = url) + feedImage = getFeedIconInHtml(html, baseUrl = url), ) } } @@ -79,7 +79,7 @@ class FeedParser(override val di: DI) : DIAware { @VisibleForTesting internal fun getFeedIconInHtml( html: String, - baseUrl: URL? = null + baseUrl: URL? = null, ): String? { val doc = html.byteInputStream().use { @@ -90,14 +90,14 @@ class FeedParser(override val di: DI) : DIAware { doc.getElementsByAttributeValue("rel", "apple-touch-icon") + doc.getElementsByAttributeValue("rel", "icon") + doc.getElementsByAttributeValue("rel", "shortcut icon") - ) + ) .filter { it.hasAttr("href") } .firstNotNullOfOrNull { e -> when { baseUrl != null -> relativeLinkIntoAbsolute( base = baseUrl, - link = e.attr("href") + link = e.attr("href"), ) else -> sloppyLinkToStrictURLOrNull(e.attr("href"))?.toString() @@ -110,7 +110,7 @@ class FeedParser(override val di: DI) : DIAware { */ private fun getAlternateFeedLinksInHtml( html: String, - baseUrl: URL? = null + baseUrl: URL? = null, ): List { val doc = html.byteInputStream().use { @@ -150,10 +150,10 @@ class FeedParser(override val di: DI) : DIAware { AlternateLink( type = e.attr("type"), link = - relativeLinkIntoAbsoluteOrThrow( - base = baseUrl, - link = e.attr("href") - ) + relativeLinkIntoAbsoluteOrThrow( + base = baseUrl, + link = e.attr("href"), + ), ) } catch (e: Exception) { null @@ -164,7 +164,7 @@ class FeedParser(override val di: DI) : DIAware { sloppyLinkToStrictURLOrNull(e.attr("href"))?.let { l -> AlternateLink( type = e.attr("type"), - link = l + link = l, ) } } @@ -174,7 +174,7 @@ class FeedParser(override val di: DI) : DIAware { feeds.isNotEmpty() -> feeds baseUrl?.host == "www.youtube.com" || baseUrl?.host == "youtube.com" -> findFeedLinksForYoutube( - doc + doc, ) else -> emptyList() @@ -193,8 +193,8 @@ class FeedParser(override val di: DI) : DIAware { listOf( AlternateLink( type = "atom", - link = URL("https://www.youtube.com/feeds/videos.xml?channel_id=$channelId") - ) + link = URL("https://www.youtube.com/feeds/videos.xml?channel_id=$channelId"), + ), ) } } @@ -220,14 +220,14 @@ class FeedParser(override val di: DI) : DIAware { // OkHttp string method handles BOM and Content-Type header in request parseFeedResponse( response.request.url.toUrl(), - it + it, ) } ?: Either.Left(NoBody(url = response.request.url.toString())) } private fun parseFeedBytes( url: URL, - body: ByteArray + body: ByteArray, ): ParsedFeed? { return goFeedAdapter.parseBody(body)?.asFeed(url) } @@ -237,7 +237,7 @@ class FeedParser(override val di: DI) : DIAware { */ fun parseFeedResponse( url: URL, - responseBody: ResponseBody + responseBody: ResponseBody, ): Either { val primaryType = responseBody.contentType()?.type val subType = responseBody.contentType()?.subtype ?: "" @@ -246,7 +246,7 @@ class FeedParser(override val di: DI) : DIAware { Either.catching( onCatch = { t -> RSSParseError(url = url.toString(), throwable = t) - } + }, ) { responseBody.byteStream().use { bs -> parseFeedBytes(url, bs.readBytes()) @@ -257,8 +257,8 @@ class FeedParser(override val di: DI) : DIAware { else -> return Either.Left( UnsupportedContentType( url = url.toString(), - mimeType = responseBody.contentType().toString() - ) + mimeType = responseBody.contentType().toString(), + ), ) } } @@ -269,12 +269,12 @@ class FeedParser(override val di: DI) : DIAware { @VisibleForTesting internal fun parseFeedResponse( url: URL, - body: String + body: String, ): Either { return Either.catching( onCatch = { t -> RSSParseError(url = url.toString(), throwable = t) - } + }, ) { parseFeedBytes(url, body.toByteArray()) ?: throw NullPointerException("Parsed feed is null") @@ -299,7 +299,7 @@ private fun GoFeed.asFeed(url: URL): ParsedFeed = favicon = null, author = author?.asParsedAuthor(), expired = null, - items = items?.mapNotNull { it?.let { FeederGoItem(it, author, url).asParsedArticle() } } + items = items?.mapNotNull { it?.let { FeederGoItem(it, author, url).asParsedArticle() } }, ) private fun FeederGoItem.asParsedArticle() = @@ -316,7 +316,7 @@ private fun FeederGoItem.asParsedArticle() = date_modified = updated, author = author?.asParsedAuthor(), tags = categories, - attachments = enclosures?.map { it.asParsedEnclosure() } + attachments = enclosures?.map { it.asParsedEnclosure() }, ) private fun GoEnclosure.asParsedEnclosure() = @@ -325,19 +325,19 @@ private fun GoEnclosure.asParsedEnclosure() = title = null, mime_type = type, size_in_bytes = length?.toLongOrNull(), - duration_in_seconds = null + duration_in_seconds = null, ) private fun GoPerson.asParsedAuthor() = ParsedAuthor( name = name, url = null, - avatar = null + avatar = null, ) suspend fun OkHttpClient.getResponse( url: URL, - forceNetwork: Boolean = false + forceNetwork: Boolean = false, ): Response { val request = Request.Builder() @@ -347,7 +347,7 @@ suspend fun OkHttpClient.getResponse( cacheControl( CacheControl.Builder() .maxAge(1, TimeUnit.MINUTES) - .build() + .build(), ) } else { this @@ -418,16 +418,16 @@ suspend fun OkHttpClient.curl(url: URL): Either { onCatch = { throwable -> FetchError( throwable = throwable, - url = url.toString() + url = url.toString(), ) - } + }, ) { body.string() } } ?: Either.Left( NoBody( - url = url.toString() - ) + url = url.toString(), + ), ) } @@ -435,8 +435,8 @@ suspend fun OkHttpClient.curl(url: URL): Either { Either.Left( UnsupportedContentType( url = url.toString(), - mimeType = contentType.toString() - ) + mimeType = contentType.toString(), + ), ) } } @@ -445,8 +445,8 @@ suspend fun OkHttpClient.curl(url: URL): Either { Either.Left( UnsupportedContentType( url = url.toString(), - mimeType = contentType.toString() - ) + mimeType = contentType.toString(), + ), ) } } @@ -454,12 +454,12 @@ suspend fun OkHttpClient.curl(url: URL): Either { suspend fun OkHttpClient.curlAndOnResponse( url: URL, - block: (suspend (Response) -> Either) + block: (suspend (Response) -> Either), ): Either { return Either.catching( onCatch = { t -> FetchError(url = url.toString(), throwable = t) - } + }, ) { getResponse(url) }.flatMap { response -> @@ -472,8 +472,8 @@ suspend fun OkHttpClient.curlAndOnResponse( HttpError( url = url.toString(), code = response.code, - message = response.message - ) + message = response.message, + ), ) } } @@ -493,49 +493,49 @@ sealed class FeedParserError : Parcelable { data class NotInitializedYet( override val url: String = "", override val description: String = "", - override val throwable: Throwable? = null + override val throwable: Throwable? = null, ) : FeedParserError() @Parcelize data class FetchError( override val url: String, override val throwable: Throwable?, - override val description: String = throwable?.message ?: "" + override val description: String = throwable?.message ?: "", ) : FeedParserError() @Parcelize data class NotHTML( override val url: String, override val description: String = "", - override val throwable: Throwable? = null + override val throwable: Throwable? = null, ) : FeedParserError() @Parcelize data class MetaDataParseError( override val url: String, override val throwable: Throwable?, - override val description: String = throwable?.message ?: "" + override val description: String = throwable?.message ?: "", ) : FeedParserError() @Parcelize data class RSSParseError( override val throwable: Throwable?, override val url: String, - override val description: String = throwable?.message ?: "" + override val description: String = throwable?.message ?: "", ) : FeedParserError() @Parcelize data class JsonFeedParseError( override val throwable: Throwable?, override val url: String, - override val description: String = throwable?.message ?: "" + override val description: String = throwable?.message ?: "", ) : FeedParserError() @Parcelize data class NoAlternateFeeds( override val url: String, override val description: String = "", - override val throwable: Throwable? = null + override val throwable: Throwable? = null, ) : FeedParserError() @Parcelize @@ -544,7 +544,7 @@ data class HttpError( val code: Int, val message: String, override val description: String = "$code: $message", - override val throwable: Throwable? = null + override val throwable: Throwable? = null, ) : FeedParserError() @Parcelize @@ -552,26 +552,26 @@ data class UnsupportedContentType( override val url: String, val mimeType: String, override val description: String = mimeType, - override val throwable: Throwable? = null + override val throwable: Throwable? = null, ) : FeedParserError() @Parcelize data class NoBody( override val url: String, override val description: String = "", - override val throwable: Throwable? = null + override val throwable: Throwable? = null, ) : FeedParserError() @Parcelize data class NoUrl( override val description: String = "", override val url: String = "", - override val throwable: Throwable? = null + override val throwable: Throwable? = null, ) : FeedParserError() @Parcelize data class FullTextDecodingFailure( override val url: String, override val throwable: Throwable?, - override val description: String = throwable?.message ?: "" + override val description: String = throwable?.message ?: "", ) : FeedParserError() diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt b/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt index 2b325b161..455ee8735 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt @@ -86,6 +86,7 @@ class TTSStateHolder( private val _ttsState = MutableStateFlow(PlaybackStatus.STOPPED) val ttsState: StateFlow = _ttsState.asStateFlow() + @Suppress("ktlint:standard:property-naming") private val _lang = MutableStateFlow(AppSetting) val language: StateFlow = _lang.asStateFlow() diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt b/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt index 596e5ac1f..59951e2aa 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt @@ -46,7 +46,7 @@ import org.kodein.di.DI import org.kodein.di.android.closestDI import org.kodein.di.instance -const val summaryNotificationId = 2_147_483_646 +const val SUMMARY_NOTIFICATION_ID = 2_147_483_646 private const val CHANNEL_ID = "feederNotifications" private const val ARTICLE_NOTIFICATION_GROUP = "com.nononsenseapps.feeder.ARTICLE" @@ -85,7 +85,7 @@ suspend fun notify( } } // Shown on API Level < 24 - nm.notify(summaryNotificationId, inboxNotification(appContext, feedItems)) + nm.notify(SUMMARY_NOTIFICATION_ID, inboxNotification(appContext, feedItems)) } } catch (e: TransactionTooLargeException) { // This can happen if there are too many notifications @@ -317,7 +317,8 @@ private fun inboxNotification( Intent.ACTION_VIEW, deepLinkUri.toUri(), context, - OpenLinkInDefaultActivity::class.java, // Proxy activity to mark as read + // Proxy activity to mark as read + OpenLinkInDefaultActivity::class.java, ).apply { putExtra( EXTRA_FEEDITEMS_TO_MARK_AS_NOTIFIED, @@ -329,7 +330,7 @@ private fun inboxNotification( val pendingIntent = PendingIntent.getActivity( context, - summaryNotificationId, + SUMMARY_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE, ) @@ -414,5 +415,4 @@ private suspend fun getItemsToNotify(di: DI): List { } } -fun NavDeepLinkBuilder.createPendingIntent(requestCode: Int): PendingIntent? = - this.createTaskStackBuilder().getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT) +fun NavDeepLinkBuilder.createPendingIntent(requestCode: Int): PendingIntent? = this.createTaskStackBuilder().getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT) diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt b/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt index cff22649b..641647ee2 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt @@ -33,7 +33,7 @@ const val UNIQUE_PERIODIC_NAME = "feeder_periodic_3" val oldPeriodics = listOf( "feeder_periodic", - "feeder_periodic_2" + "feeder_periodic_2", ) private const val UNIQUE_FEEDSYNC_NAME = "feeder_sync_onetime" private const val MIN_FEED_AGE_MINUTES = "min_feed_age_minutes" @@ -63,7 +63,7 @@ class FeedSyncer(val context: Context, workerParams: WorkerParameters) : feedId = feedId, feedTag = feedTag, forceNetwork = forceNetwork, - minFeedAgeMinutes = minFeedAgeMinutes + minFeedAgeMinutes = minFeedAgeMinutes, ) } catch (e: Exception) { success = false @@ -84,7 +84,7 @@ fun requestFeedSync( di: DI, feedId: Long = ID_UNSET, feedTag: String = "", - forceNetwork: Boolean = false + forceNetwork: Boolean = false, ) { val repository: Repository by di.instance() val constraints = Constraints.Builder() @@ -106,7 +106,7 @@ fun requestFeedSync( workDataOf( ARG_FEED_ID to feedId, ARG_FEED_TAG to feedTag, - ARG_FORCE_NETWORK to forceNetwork + ARG_FORCE_NETWORK to forceNetwork, ) workRequest.setInputData(data) @@ -114,6 +114,6 @@ fun requestFeedSync( workManager.enqueueUniqueWork( UNIQUE_FEEDSYNC_NAME, ExistingWorkPolicy.KEEP, - workRequest.build() + workRequest.build(), ) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt b/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt index a96946e44..4317a653e 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt @@ -4,9 +4,9 @@ import android.app.Application import android.util.Log import com.nononsenseapps.feeder.ApplicationCoroutineScope import com.nononsenseapps.feeder.archmodel.Repository +import com.nononsenseapps.feeder.model.SUMMARY_NOTIFICATION_ID import com.nononsenseapps.feeder.model.cancelNotification import com.nononsenseapps.feeder.model.notify -import com.nononsenseapps.feeder.model.summaryNotificationId import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.delay @@ -60,7 +60,7 @@ class NotificationsWorker(override val di: DI) : DIAware { current: List, ) { if (current.isEmpty()) { - cancelNotification(summaryNotificationId.toLong()) + cancelNotification(SUMMARY_NOTIFICATION_ID.toLong()) } prev.filter { it !in current diff --git a/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt b/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt index e4d03ad7c..c4bf6f7bf 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt @@ -75,9 +75,7 @@ class SyncRestClient(override val di: DI) : DIAware { } } - private suspend fun safeBlock( - block: (suspend (SyncRemote, FeederSync, SecretKeys) -> Either)?, - ): Either { + private suspend fun safeBlock(block: (suspend (SyncRemote, FeederSync, SecretKeys) -> Either)?): Either { if (block != null) { repository.getSyncRemote().let { syncRemote -> if (syncRemote.hasSyncChain()) { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt index 663604346..ce640bb9e 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt @@ -29,7 +29,7 @@ import com.nononsenseapps.feeder.ui.compose.utils.immutableListHolderOf fun AutoCompleteResults( displaySuggestions: Boolean, suggestions: ImmutableHolder>, - onSuggestionClicked: (String) -> Unit, + onSuggestionClick: (String) -> Unit, suggestionContent: @Composable (String) -> Unit, modifier: Modifier = Modifier, maxHeight: Dp = TextFieldDefaults.MinHeight * 3, @@ -58,7 +58,7 @@ fun AutoCompleteResults( Box( modifier = Modifier - .clickable { onSuggestionClicked(item) }, + .clickable { onSuggestionClick(item) }, ) { suggestionContent(item) } @@ -74,7 +74,7 @@ private fun PreviewAutoCompleteOutlinedText() { AutoCompleteResults( displaySuggestions = true, suggestions = immutableListHolderOf("One", "Two", "Three"), - onSuggestionClicked = {}, + onSuggestionClick = {}, suggestionContent = { Text(text = it) }, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt index 4507d1124..3f11bcc62 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt @@ -99,7 +99,7 @@ fun CreateFeedScreen( } } - @Suppress("ktlint:twitter-compose:vm-forwarding-check") + @Suppress("ktlint:compose:vm-forwarding-check") EditFeedScreen( onNavigateUp = onNavigateUp, viewState = createFeedScreenViewModel, @@ -129,7 +129,7 @@ fun EditFeedScreen( } } - @Suppress("ktlint:twitter-compose:vm-forwarding-check") + @Suppress("ktlint:compose:vm-forwarding-check") EditFeedScreen( onNavigateUp = onNavigateUp, viewState = editFeedScreenViewModel, @@ -322,7 +322,7 @@ fun EditFeedView( } } -@Suppress("ktlint:twitter-compose:modifier-missing-check") +@Suppress("ktlint:compose:modifier-missing-check") @OptIn( ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class, @@ -425,7 +425,7 @@ fun ColumnScope.LeftContent( }, displaySuggestions = showTagSuggestions, suggestions = filteredTags, - onSuggestionClicked = { tag -> + onSuggestionClick = { tag -> viewState.feedTag = tag rightFocusRequester.requestFocus() showTagSuggestions = false @@ -479,7 +479,7 @@ fun ColumnScope.LeftContent( } } -@Suppress("ktlint:twitter-compose:modifier-missing-check", "UnusedReceiverParameter") +@Suppress("ktlint:compose:modifier-missing-check", "UnusedReceiverParameter") @Composable fun ColumnScope.RightContent( viewState: EditFeedScreenState, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt index 942c50677..b8c6b67ea 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt @@ -28,7 +28,7 @@ import com.nononsenseapps.feeder.ui.compose.deletefeed.DeletableFeed import com.nononsenseapps.feeder.ui.compose.minimumTouchSize import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme import com.nononsenseapps.feeder.ui.compose.utils.ImmutableHolder -import com.nononsenseapps.feeder.ui.compose.utils.ThemePreviews +import com.nononsenseapps.feeder.ui.compose.utils.PreviewThemes import com.nononsenseapps.feeder.ui.compose.utils.immutableListHolderOf @Composable @@ -105,7 +105,7 @@ fun EditFeedDialog( } @Composable -@ThemePreviews +@PreviewThemes private fun Preview() { FeederTheme { EditFeedDialog( diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt index c59d75115..a53cbfadf 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt @@ -63,7 +63,7 @@ import com.nononsenseapps.feeder.ui.compose.theme.FeedListItemSnippetTextStyle import com.nononsenseapps.feeder.ui.compose.theme.FeedListItemTitleTextStyle import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme import com.nononsenseapps.feeder.ui.compose.theme.titleFontWeight -import com.nononsenseapps.feeder.ui.compose.utils.ThemePreviews +import com.nononsenseapps.feeder.ui.compose.utils.PreviewThemes import com.nononsenseapps.feeder.ui.compose.utils.onKeyEventLikeEscape import java.net.URL import java.time.Instant @@ -75,7 +75,7 @@ fun FeedItemCard( onMarkAboveAsRead: () -> Unit, onMarkBelowAsRead: () -> Unit, onShareItem: () -> Unit, - onToggleBookmarked: () -> Unit, + onToggleBookmark: () -> Unit, dropDownMenuExpanded: Boolean, onDismissDropdown: () -> Unit, bookmarkIndicator: Boolean, @@ -165,7 +165,7 @@ fun FeedItemCard( onMarkAboveAsRead = onMarkAboveAsRead, onMarkBelowAsRead = onMarkBelowAsRead, onShareItem = onShareItem, - onToggleBookmarked = onToggleBookmarked, + onToggleBookmark = onToggleBookmark, dropDownMenuExpanded = dropDownMenuExpanded, onDismissDropdown = onDismissDropdown, maxLines = maxLines, @@ -183,7 +183,7 @@ fun RowScope.FeedItemText( onMarkAboveAsRead: () -> Unit, onMarkBelowAsRead: () -> Unit, onShareItem: () -> Unit, - onToggleBookmarked: () -> Unit, + onToggleBookmark: () -> Unit, dropDownMenuExpanded: Boolean, onDismissDropdown: () -> Unit, maxLines: Int, @@ -267,7 +267,7 @@ fun RowScope.FeedItemText( DropdownMenuItem( onClick = { onDismissDropdown() - onToggleBookmarked() + onToggleBookmark() }, text = { Text( @@ -376,7 +376,7 @@ fun RowScope.FeedItemText( } @Composable -@ThemePreviews +@PreviewThemes @Suppress("ktlint:standard:max-line-length") private fun Preview() { FeederTheme { @@ -401,7 +401,7 @@ private fun Preview() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, @@ -413,7 +413,7 @@ private fun Preview() { } @Composable -@ThemePreviews +@PreviewThemes @Suppress("ktlint:standard:max-line-length") private fun PreviewWithImageUnread() { FeederTheme { @@ -441,7 +441,7 @@ private fun PreviewWithImageUnread() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, @@ -454,7 +454,7 @@ private fun PreviewWithImageUnread() { } @Composable -@ThemePreviews +@PreviewThemes @Suppress("ktlint:standard:max-line-length") private fun PreviewWithImageRead() { FeederTheme { @@ -482,7 +482,7 @@ private fun PreviewWithImageRead() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt index a85c6bd6f..5de00181b 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.outlined.Terrain import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -52,7 +53,7 @@ fun FeedItemCompact( onMarkAboveAsRead: () -> Unit, onMarkBelowAsRead: () -> Unit, onShareItem: () -> Unit, - onToggleBookmarked: () -> Unit, + onToggleBookmark: () -> Unit, dropDownMenuExpanded: Boolean, onDismissDropdown: () -> Unit, bookmarkIndicator: Boolean, @@ -74,7 +75,7 @@ fun FeedItemCompact( onMarkAboveAsRead = onMarkAboveAsRead, onMarkBelowAsRead = onMarkBelowAsRead, onShareItem = onShareItem, - onToggleBookmarked = onToggleBookmarked, + onToggleBookmark = onToggleBookmark, dropDownMenuExpanded = dropDownMenuExpanded, onDismissDropdown = onDismissDropdown, maxLines = maxLines, @@ -211,7 +212,7 @@ private fun PreviewRead() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, @@ -251,7 +252,7 @@ private fun PreviewUnread() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, @@ -294,7 +295,7 @@ private fun PreviewWithImage() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompactCard.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompactCard.kt index 256b18e40..f1b86894f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompactCard.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompactCard.kt @@ -43,17 +43,21 @@ import com.nononsenseapps.feeder.model.EnclosureImage import com.nononsenseapps.feeder.ui.compose.coil.rememberTintedVectorPainter import com.nononsenseapps.feeder.ui.compose.minimumTouchSize import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme -import com.nononsenseapps.feeder.ui.compose.utils.ThemePreviews +import com.nononsenseapps.feeder.ui.compose.utils.PreviewThemes +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.net.URL import java.time.Instant -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull sealed interface FeedItemEvent { - data object MarkAboveAsRead: FeedItemEvent - data object MarkBelowAsRead: FeedItemEvent - data object ShareItem: FeedItemEvent - data object ToggleBookmarked: FeedItemEvent - data object DismissDropdown: FeedItemEvent + data object MarkAboveAsRead : FeedItemEvent + + data object MarkBelowAsRead : FeedItemEvent + + data object ShareItem : FeedItemEvent + + data object ToggleBookmarked : FeedItemEvent + + data object DismissDropdown : FeedItemEvent } @Immutable @@ -63,7 +67,7 @@ data class FeedItemState( val bookmarkIndicator: Boolean = true, val dropDownMenuExpanded: Boolean = false, val showReadingTime: Boolean = false, - val maxLines: Int = 2 + val maxLines: Int = 2, ) private val iconSize = 24.dp @@ -72,64 +76,74 @@ private val gradientColors = listOf(Color.Black.copy(alpha = 0.8f), Color.Black. @Composable fun FeedItemCompactCard( state: FeedItemState, - onEvent: (FeedItemEvent) -> Unit = { }, modifier: Modifier = Modifier, + onEvent: (FeedItemEvent) -> Unit = { }, ) { ElevatedCard( modifier = modifier, - shape = MaterialTheme.shapes.medium + shape = MaterialTheme.shapes.medium, ) { BoxWithConstraints( - modifier = Modifier - .requiredHeightIn(min = minimumTouchSize) - .fillMaxWidth() + modifier = + Modifier + .requiredHeightIn(min = minimumTouchSize) + .fillMaxWidth(), ) { if (state.showThumbnail) { - val sizePx = with(LocalDensity.current) { - val width = maxWidth.roundToPx() - Size(width, (width * 9) / 16) - } - val gradient = Brush.verticalGradient( - colors = gradientColors, - startY = 0f, - endY = sizePx.height.pxOrElse { 0 }.toFloat() - ) + val sizePx = + with(LocalDensity.current) { + val width = maxWidth.roundToPx() + Size(width, (width * 9) / 16) + } + val gradient = + Brush.verticalGradient( + colors = gradientColors, + startY = 0f, + endY = sizePx.height.pxOrElse { 0 }.toFloat(), + ) val imageUrl = state.item.image?.url if (imageUrl != null) { FeedItemThumbnail( imageUrl = imageUrl, - sizePx = sizePx + sizePx = sizePx, ) } - Box(modifier = Modifier - .matchParentSize() - .background(gradient)) + Box( + modifier = + Modifier + .matchParentSize() + .background(gradient), + ) } - Row(modifier = Modifier - .height(iconSize) - .padding(top = 8.dp, end = 8.dp, start = 8.dp) - .align(Alignment.TopEnd) + Row( + modifier = + Modifier + .height(iconSize) + .padding(top = 8.dp, end = 8.dp, start = 8.dp) + .align(Alignment.TopEnd), ) { if (state.item.bookmarked && state.bookmarkIndicator) { - FeedItemSavedIndicator(size = iconSize, modifier = modifier) + FeedItemSavedIndicator(size = iconSize, modifier = Modifier) } if (state.item.unread) { - FeedItemNewIndicator(size = iconSize, modifier = modifier) + FeedItemNewIndicator(size = iconSize, modifier = Modifier) } state.item.feedImageUrl?.toHttpUrlOrNull()?.also { FeedItemFeedIconIndicator( feedImageUrl = it.toString(), size = iconSize, - modifier = modifier, + modifier = Modifier, ) } } - Box(modifier = Modifier - .width(maxWidth) - .padding(top = 48.dp) - .align(Alignment.BottomCenter) + Box( + modifier = + Modifier + .width(maxWidth) + .padding(top = 48.dp) + .align(Alignment.BottomCenter), ) { FeedItemTitle( state = state, @@ -143,145 +157,180 @@ fun FeedItemCompactCard( @Composable private fun FeedItemTitle( state: FeedItemState, - onEvent: (FeedItemEvent) -> Unit + onEvent: (FeedItemEvent) -> Unit, ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp, horizontal = 8.dp), ) { - val textColor = when { - !state.showThumbnail -> LocalContentColor.current - state.item.unread -> Color.White - else -> Color.White.copy(alpha = 0.74f) - } + val textColor = + when { + !state.showThumbnail -> LocalContentColor.current + state.item.unread -> Color.White + else -> Color.White.copy(alpha = 0.74f) + } CompositionLocalProvider(LocalContentColor provides textColor) { FeedItemText( item = state.item, onMarkAboveAsRead = { onEvent(FeedItemEvent.MarkAboveAsRead) }, onMarkBelowAsRead = { onEvent(FeedItemEvent.MarkBelowAsRead) }, onShareItem = { onEvent(FeedItemEvent.ShareItem) }, - onToggleBookmarked = { onEvent(FeedItemEvent.ToggleBookmarked) }, + onToggleBookmark = { onEvent(FeedItemEvent.ToggleBookmarked) }, dropDownMenuExpanded = state.dropDownMenuExpanded, onDismissDropdown = { onEvent(FeedItemEvent.DismissDropdown) }, maxLines = state.maxLines, showOnlyTitle = true, - showReadingTime = state.showReadingTime + showReadingTime = state.showReadingTime, ) } } } @Composable -private fun FeedItemThumbnail(imageUrl: String?, sizePx: Size) { +private fun FeedItemThumbnail( + imageUrl: String?, + sizePx: Size, +) { if (imageUrl != null) { AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .listener( - onError = { a, b -> - Log.e("FEEDER_CARD", "error ${a.data}", b.throwable) - }, - ) - .scale(Scale.FILL) - .size(sizePx) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .listener( + onError = { a, b -> + Log.e("FEEDER_CARD", "error ${a.data}", b.throwable) + }, + ) + .scale(Scale.FILL) + .size(sizePx) + .precision(Precision.INEXACT) + .build(), placeholder = rememberTintedVectorPainter(Icons.Outlined.Terrain), error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), contentDescription = stringResource(id = R.string.article_image), contentScale = ContentScale.Crop, alignment = Alignment.Center, - modifier = Modifier - .clip(MaterialTheme.shapes.medium) - .fillMaxWidth() - .aspectRatio(16.0f / 9.0f) - .alpha(0.74f), + modifier = + Modifier + .clip(MaterialTheme.shapes.medium) + .fillMaxWidth() + .aspectRatio(16.0f / 9.0f) + .alpha(0.74f), ) } } @Composable -@ThemePreviews +@PreviewThemes private fun Preview() { FeederTheme { FeedItemCompactCard( - state = FeedItemState( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf dsaf asd fsa dfasdf", - pubDate = "Jun 9, 2021", - unread = true, - image = null, - link = null, - id = ID_UNSET, - bookmarked = true, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 0 - ) - ) + state = + FeedItemState( + item = + FeedListItem( + title = "title", + snippet = + "snippet which is quite long as you might expect from a snipper of a story." + + " It keeps going and going and going and going and going and going and going" + + " and going and going and going and going and going and going and going" + + " and going and going and going and going and going and going and going" + + " and going and going and going and going and going and going and going" + + " and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf dsaf asd fsa dfasdf", + pubDate = "Jun 9, 2021", + unread = true, + image = null, + link = null, + id = ID_UNSET, + bookmarked = true, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 0, + ), + ), ) } } @Composable -@ThemePreviews +@PreviewThemes private fun PreviewWithImageUnread() { FeederTheme { Box( modifier = Modifier.width((300 - 2 * 16).dp), ) { FeedItemCompactCard( - state = FeedItemState( - item = FeedListItem( - title = "title can be one line", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Feed", - pubDate = "Jun 9, 2021", - unread = true, - image = EnclosureImage(url = "blabal", length = 0), - link = null, - id = ID_UNSET, - bookmarked = false, - feedImageUrl = URL("https://foo/bar.png"), - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 0 - ) - ) + state = + FeedItemState( + item = + FeedListItem( + title = "title can be one line", + snippet = + "snippet which is quite long as you might expect from a" + + " snipper of a story. It keeps going and going and going" + + " and going and going and going and going and going and" + + " going and going and going and going and going and going" + + " and going and going and going and going and going and" + + " going and going and going and going and going and going" + + " and going and going and going and going and going and" + + " going and going and going and going and snowing", + feedTitle = "Super Feed", + pubDate = "Jun 9, 2021", + unread = true, + image = EnclosureImage(url = "blabal", length = 0), + link = null, + id = ID_UNSET, + bookmarked = false, + feedImageUrl = URL("https://foo/bar.png"), + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 0, + ), + ), ) } } } @Composable -@ThemePreviews +@PreviewThemes private fun PreviewWithImageRead() { FeederTheme { Box( modifier = Modifier.width((300 - 2 * 16).dp), ) { FeedItemCompactCard( - state = FeedItemState( - item = FeedListItem( - title = "title can be one line", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed", - pubDate = "Jun 9, 2021", - unread = false, - image = EnclosureImage(url = "blabal", length = 0), - link = null, - id = ID_UNSET, - bookmarked = true, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 0 - ) - ) + state = + FeedItemState( + item = + FeedListItem( + title = "title can be one line", + snippet = + "snippet which is quite long as you might expect from a" + + " snipper of a story. It keeps going and going and going" + + " and going and going and going and going and going and" + + " going and going and going and going and going and going" + + " and going and going and going and going and going and" + + " going and going and going and going and going and going" + + " and going and going and going and going and going and" + + " going and going and going and going and snowing", + feedTitle = "Super Duper Feed", + pubDate = "Jun 9, 2021", + unread = false, + image = EnclosureImage(url = "blabal", length = 0), + link = null, + id = ID_UNSET, + bookmarked = true, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 0, + ), + ), ) } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt index f7defe39f..e63a12780 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt @@ -15,7 +15,6 @@ import com.nononsenseapps.feeder.model.MediaImage import com.nononsenseapps.feeder.ui.compose.minimumTouchSize import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme import com.nononsenseapps.feeder.ui.compose.theme.LocalDimens -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.net.URL import java.time.Instant @@ -25,7 +24,7 @@ fun FeedItemSuperCompact( onMarkAboveAsRead: () -> Unit, onMarkBelowAsRead: () -> Unit, onShareItem: () -> Unit, - onToggleBookmarked: () -> Unit, + onToggleBookmark: () -> Unit, dropDownMenuExpanded: Boolean, onDismissDropdown: () -> Unit, bookmarkIndicator: Boolean, @@ -53,7 +52,7 @@ fun FeedItemSuperCompact( onMarkAboveAsRead = onMarkAboveAsRead, onMarkBelowAsRead = onMarkBelowAsRead, onShareItem = onShareItem, - onToggleBookmarked = onToggleBookmarked, + onToggleBookmark = onToggleBookmark, dropDownMenuExpanded = dropDownMenuExpanded, onDismissDropdown = onDismissDropdown, maxLines = maxLines, @@ -89,7 +88,7 @@ private fun PreviewRead() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, @@ -127,7 +126,7 @@ private fun PreviewUnread() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, @@ -165,7 +164,7 @@ private fun PreviewWithImage() { onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, - onToggleBookmarked = {}, + onToggleBookmark = {}, dropDownMenuExpanded = false, onDismissDropdown = {}, bookmarkIndicator = true, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt index 222ea7a59..6d37a21fe 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt @@ -79,6 +79,7 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -265,7 +266,7 @@ fun FeedScreen( onToggleTagExpansion = { tag -> viewModel.toggleTagExpansion(tag) }, - onDrawerItemSelected = { feedId, tag -> + onDrawerItemSelect = { feedId, tag -> FeedDestination.navigate(navController, feedId = feedId, tag = tag) }, focusRequester = focusNavDrawer, @@ -417,7 +418,7 @@ fun FeedScreen( }, ) }, - onSetBookmarked = { itemId, value -> + onSetBookmark = { itemId, value -> viewModel.setBookmarked(itemId, value) }, onShowFilterMenu = viewModel::setFilterMenuVisible, @@ -460,7 +461,7 @@ fun FeedScreen( markBeforeAsRead: (FeedItemCursor) -> Unit, markAfterAsRead: (FeedItemCursor) -> Unit, onOpenFeedItem: (Long) -> Unit, - onSetBookmarked: (Long, Boolean) -> Unit, + onSetBookmark: (Long, Boolean) -> Unit, onShowFilterMenu: (Boolean) -> Unit, filterCallback: FeedListFilterCallback, feedListState: LazyListState, @@ -871,7 +872,7 @@ fun FeedScreen( markBeforeAsRead = markBeforeAsRead, markAfterAsRead = markAfterAsRead, onItemClick = onOpenFeedItem, - onSetBookmarked = onSetBookmarked, + onSetBookmark = onSetBookmark, gridState = feedGridState, pagedFeedItems = pagedFeedItems, modifier = innerModifier, @@ -895,7 +896,7 @@ fun FeedScreen( markBeforeAsRead = markBeforeAsRead, markAfterAsRead = markAfterAsRead, onItemClick = onOpenFeedItem, - onSetBookmarked = onSetBookmarked, + onSetBookmark = onSetBookmark, listState = feedListState, pagedFeedItems = pagedFeedItems, modifier = innerModifier, @@ -1108,7 +1109,7 @@ fun FeedListContent( markBeforeAsRead: (FeedItemCursor) -> Unit, markAfterAsRead: (FeedItemCursor) -> Unit, onItemClick: (Long) -> Unit, - onSetBookmarked: (Long, Boolean) -> Unit, + onSetBookmark: (Long, Boolean) -> Unit, listState: LazyListState, pagedFeedItems: LazyPagingItems, modifier: Modifier = Modifier, @@ -1241,8 +1242,8 @@ fun FeedListContent( onMarkBelowAsRead = { markAfterAsRead(previewItem.cursor) }, - onToggleBookmarked = { - onSetBookmarked(previewItem.id, !previewItem.bookmarked) + onToggleBookmark = { + onSetBookmark(previewItem.id, !previewItem.bookmarked) }, onShareItem = { val intent = @@ -1321,7 +1322,7 @@ fun FeedGridContent( markBeforeAsRead: (FeedItemCursor) -> Unit, markAfterAsRead: (FeedItemCursor) -> Unit, onItemClick: (Long) -> Unit, - onSetBookmarked: (Long, Boolean) -> Unit, + onSetBookmark: (Long, Boolean) -> Unit, gridState: LazyStaggeredGridState, pagedFeedItems: LazyPagingItems, modifier: Modifier = Modifier, @@ -1441,8 +1442,8 @@ fun FeedGridContent( onMarkBelowAsRead = { markAfterAsRead(previewItem.cursor) }, - onToggleBookmarked = { - onSetBookmarked(previewItem.id, !previewItem.bookmarked) + onToggleBookmark = { + onSetBookmark(previewItem.id, !previewItem.bookmarked) }, onShareItem = { val intent = @@ -1520,6 +1521,8 @@ fun MarkItemAsReadOnScroll( coroutineScope: CoroutineScope, markAsRead: (Long, Boolean, FeedOrTag?) -> Unit, ) { + // Lambda parameters in a @Composable that are referenced directly inside of restarting effects can cause issues or unpredictable behavior. + val markAsReadCallback by rememberUpdatedState(newValue = markAsRead) var visibleTime by remember { mutableStateOf(FAR_FUTURE) } @@ -1544,7 +1547,7 @@ fun MarkItemAsReadOnScroll( @Suppress("UNNECESSARYVARIABLE") val feedOrTag = currentFeedOrTag delay(100) - markAsRead( + markAsReadCallback( itemId, false, feedOrTag, @@ -1578,6 +1581,7 @@ private val PLACEHOLDER_ITEM = @Composable fun PlainTooltipBox( tooltip: @Composable () -> Unit, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { TooltipBox( @@ -1588,6 +1592,7 @@ fun PlainTooltipBox( tooltip() } }, + modifier = modifier, content = content, ) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt index 4199cf638..5c3c0a798 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -84,7 +85,7 @@ fun SwipeableFeedItemPreview( showReadingTime: Boolean, onMarkAboveAsRead: () -> Unit, onMarkBelowAsRead: () -> Unit, - onToggleBookmarked: () -> Unit, + onToggleBookmark: () -> Unit, onShareItem: () -> Unit, modifier: Modifier = Modifier, onItemClick: () -> Unit, @@ -92,6 +93,7 @@ fun SwipeableFeedItemPreview( val coroutineScope = rememberCoroutineScope() val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val swipeableState = rememberSwipeableState(initialValue = FeedItemSwipeState.NONE) + val onSwipeCallback by rememberUpdatedState(newValue = onSwipe) val color by animateColorAsState( targetValue = @@ -111,7 +113,7 @@ fun SwipeableFeedItemPreview( LaunchedEffect(swipeableState.currentValue, swipeableState.isAnimationRunning) { if (!swipeableState.isAnimationRunning && swipeableState.currentValue != FeedItemSwipeState.NONE) { logDebug(LOG_TAG, "onSwipe ${item.unread}") - onSwipe(item.unread) + onSwipeCallback(item.unread) } } @@ -176,7 +178,7 @@ fun SwipeableFeedItemPreview( false -> saveArticleLabel }, ) { - onToggleBookmarked() + onToggleBookmark() true }, CustomAccessibilityAction(markAboveAsReadLabel) { @@ -243,7 +245,7 @@ fun SwipeableFeedItemPreview( onMarkAboveAsRead = onMarkAboveAsRead, onMarkBelowAsRead = onMarkBelowAsRead, onShareItem = onShareItem, - onToggleBookmarked = onToggleBookmarked, + onToggleBookmark = onToggleBookmark, dropDownMenuExpanded = dropDownMenuExpanded, onDismissDropdown = { dropDownMenuExpanded = false }, bookmarkIndicator = bookmarkIndicator, @@ -259,26 +261,30 @@ fun SwipeableFeedItemPreview( FeedItemStyle.COMPACT_CARD -> { FeedItemCompactCard( - state = FeedItemState( - item = item, - showThumbnail = showThumbnail && !compactLandscape, - dropDownMenuExpanded = dropDownMenuExpanded, - bookmarkIndicator = bookmarkIndicator, - maxLines = maxLines, - showReadingTime = showReadingTime - ), + state = + FeedItemState( + item = item, + showThumbnail = showThumbnail && !compactLandscape, + dropDownMenuExpanded = dropDownMenuExpanded, + bookmarkIndicator = bookmarkIndicator, + maxLines = maxLines, + showReadingTime = showReadingTime, + ), + modifier = + Modifier + .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } + .graphicsLayer(alpha = itemAlpha), onEvent = { event -> when (event) { - FeedItemEvent.DismissDropdown -> { dropDownMenuExpanded = false } + FeedItemEvent.DismissDropdown -> { + dropDownMenuExpanded = false + } FeedItemEvent.MarkAboveAsRead -> onMarkAboveAsRead() FeedItemEvent.MarkBelowAsRead -> onMarkBelowAsRead() FeedItemEvent.ShareItem -> onShareItem() - FeedItemEvent.ToggleBookmarked -> onToggleBookmarked() + FeedItemEvent.ToggleBookmarked -> onToggleBookmark() } }, - modifier = Modifier - .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } - .graphicsLayer(alpha = itemAlpha), ) } @@ -289,7 +295,7 @@ fun SwipeableFeedItemPreview( onMarkAboveAsRead = onMarkAboveAsRead, onMarkBelowAsRead = onMarkBelowAsRead, onShareItem = onShareItem, - onToggleBookmarked = onToggleBookmarked, + onToggleBookmark = onToggleBookmark, dropDownMenuExpanded = dropDownMenuExpanded, onDismissDropdown = { dropDownMenuExpanded = false }, bookmarkIndicator = bookmarkIndicator, @@ -314,7 +320,7 @@ fun SwipeableFeedItemPreview( onMarkAboveAsRead = onMarkAboveAsRead, onMarkBelowAsRead = onMarkBelowAsRead, onShareItem = onShareItem, - onToggleBookmarked = onToggleBookmarked, + onToggleBookmark = onToggleBookmark, dropDownMenuExpanded = dropDownMenuExpanded, onDismissDropdown = { dropDownMenuExpanded = false }, bookmarkIndicator = bookmarkIndicator, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt index a875655fc..b71a1e5e0 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Article -import androidx.compose.material.icons.filled.Article import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material.icons.filled.Share @@ -42,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties @@ -155,7 +155,7 @@ fun ArticleScreen( ttsOnStop = viewModel::ttsStop, ttsOnSkipNext = viewModel::ttsSkipNext, ttsOnSelectLanguage = viewModel::ttsOnSelectLanguage, - onToggleBookmarked = { + onToggleBookmark = { viewModel.setBookmarked(viewState.articleId, !viewState.isBookmarked) }, articleListState = articleListState, @@ -182,7 +182,7 @@ fun ArticleScreen( ttsOnStop: () -> Unit, ttsOnSkipNext: () -> Unit, ttsOnSelectLanguage: (LocaleOverride) -> Unit, - onToggleBookmarked: () -> Unit, + onToggleBookmark: () -> Unit, articleListState: LazyListState, modifier: Modifier = Modifier, onNavigateUp: () -> Unit, @@ -302,7 +302,7 @@ fun ArticleScreen( DropdownMenuItem( onClick = { onShowToolbarMenu(false) - onToggleBookmarked() + onToggleBookmark() }, leadingIcon = { Icon( @@ -414,9 +414,11 @@ fun ArticleContent( viewState.textToDisplay == TextToDisplay.FULLTEXT && !blobFullFile(viewState.articleId, filePathProvider.fullArticleDir).isFile ) { + // Lambda parameters in a @Composable that are referenced directly inside of restarting effects can cause issues or unpredictable behavior. + val callback by rememberUpdatedState(newValue = displayFullText) LaunchedEffect(viewState.articleId, viewState.textToDisplay) { // Trigger parse and fetch - displayFullText() + callback() } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/icons/TextToSpeech.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/icons/TextToSpeech.kt index 307b9927b..5225f0205 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/icons/TextToSpeech.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/icons/TextToSpeech.kt @@ -4,7 +4,7 @@ import androidx.compose.material.icons.materialIcon import androidx.compose.material.icons.materialPath import androidx.compose.ui.graphics.vector.ImageVector -@Suppress("ObjectPropertyName") +@Suppress("ObjectPropertyName", "ktlint:standard:property-naming") private var _textToSpeech: ImageVector? = null @Suppress("UnusedReceiverParameter") diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt index ca2a1d322..bbf650f3f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt @@ -225,7 +225,7 @@ class DrawerState( /** * The default [Saver] implementation for [DrawerState]. */ - fun Saver(confirmStateChange: (DrawerValue) -> Boolean) = + fun saver(confirmStateChange: (DrawerValue) -> Boolean) = Saver( save = { it.currentValue }, restore = { DrawerState(it, confirmStateChange) }, @@ -245,7 +245,7 @@ fun rememberDrawerState( initialValue: DrawerValue, confirmStateChange: (DrawerValue) -> Boolean = { true }, ): DrawerState { - return rememberSaveable(saver = DrawerState.Saver(confirmStateChange)) { + return rememberSaveable(saver = DrawerState.saver(confirmStateChange)) { DrawerState(initialValue, confirmStateChange) } } @@ -641,7 +641,7 @@ object DrawerDefaults { /** Default color of the scrim that obscures content when the drawer is open */ val scrimColor: Color - @Composable get() = ScrimTokens.ContainerColor.toColor().copy(ScrimTokens.ContainerOpacity) + @Composable get() = ScrimTokens.ContainerColor.toColor().copy(ScrimTokens.CONTAINER_OPACITY) /** Default container color for a navigation drawer */ val containerColor: Color @Composable get() = NavigationDrawerTokens.ContainerColor.toColor() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt index 1a4a05e6f..112b57e53 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt @@ -31,25 +31,25 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.AnimationSpec -import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.StandardResistanceFactor +import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.STANDARD_RESISTANCE_FACTOR import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.VelocityThreshold import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.resistanceConfig import kotlinx.coroutines.CancellationException @@ -110,12 +110,12 @@ internal open class SwipeableState( val overflow: State get() = overflowState // Use `Float.NaN` as a placeholder while the state is uninitialised. - private val offsetState = mutableStateOf(0f) - private val overflowState = mutableStateOf(0f) + private val offsetState = mutableFloatStateOf(0f) + private val overflowState = mutableFloatStateOf(0f) // the source of truth for the "real"(non ui) position // basically position in bounds + overflow - private val absoluteOffset = mutableStateOf(0f) + private val absoluteOffset = mutableFloatStateOf(0f) // current animation target, if animating, otherwise null private val animationTarget = mutableStateOf(null) @@ -194,7 +194,7 @@ internal open class SwipeableState( internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f }) - internal var velocityThreshold by mutableStateOf(0f) + internal var velocityThreshold by mutableFloatStateOf(0f) internal var resistance: ResistanceConfig? by mutableStateOf(null) @@ -276,11 +276,13 @@ internal open class SwipeableState( to = currentValue fraction = 1f } + 1 -> { from = anchors.getValue(bounds[0]) to = anchors.getValue(bounds[0]) fraction = 1f } + else -> { val (a, b) = if (direction > 0f) { @@ -381,8 +383,8 @@ internal open class SwipeableState( val targetState = anchors[targetValue] if (targetState != null && confirmStateChange(targetState)) { animateTo(targetState) - } // If the user vetoed the state change, rollback to the previous state. - else { + } else { + // If the user vetoed the state change, rollback to the previous state. animateInternalToOffset(lastAnchor, animationSpec) } } @@ -418,7 +420,7 @@ internal open class SwipeableState( /** * The default [Saver] implementation for [SwipeableState]. */ - fun Saver( + fun saver( animationSpec: AnimationSpec, confirmStateChange: (T) -> Boolean, ) = Saver, T>( @@ -485,7 +487,7 @@ internal fun rememberSwipeableState( ): SwipeableState { return rememberSaveable( saver = - SwipeableState.Saver( + SwipeableState.saver( animationSpec = animationSpec, confirmStateChange = confirmStateChange, ), @@ -527,9 +529,10 @@ internal fun rememberSwipeableStateFor( swipeableState.animateTo(value) } } + val onValueChangCallback by rememberUpdatedState(newValue = onValueChange) DisposableEffect(swipeableState.currentValue) { if (value != swipeableState.currentValue) { - onValueChange(swipeableState.currentValue) + onValueChangCallback(swipeableState.currentValue) forceAnimationCheck.value = !forceAnimationCheck.value } onDispose { } @@ -571,6 +574,8 @@ internal fun rememberSwipeableStateFor( * in order to animate to the next state, even if the positional [thresholds] have not been reached. */ @ExperimentalMaterial3Api +@Composable +@Suppress("ktlint:compose:modifier-composable-check") internal fun Modifier.swipeable( state: SwipeableState, anchors: Map, @@ -581,27 +586,14 @@ internal fun Modifier.swipeable( thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) }, resistance: ResistanceConfig? = resistanceConfig(anchors.keys), velocityThreshold: Dp = VelocityThreshold, -) = composed( - inspectorInfo = - debugInspectorInfo { - name = "swipeable" - properties["state"] = state - properties["anchors"] = anchors - properties["orientation"] = orientation - properties["enabled"] = enabled - properties["reverseDirection"] = reverseDirection - properties["interactionSource"] = interactionSource - properties["thresholds"] = thresholds - properties["resistance"] = resistance - properties["velocityThreshold"] = velocityThreshold - }, -) { +): Modifier { require(anchors.isNotEmpty()) { "You must have at least one anchor." } require(anchors.values.distinct().count() == anchors.size) { "You cannot have two anchors mapped to the same state." } + val thresholdsCallback by rememberUpdatedState(thresholds) val density = LocalDensity.current state.ensureInit(anchors) LaunchedEffect(anchors, state) { @@ -611,7 +603,7 @@ internal fun Modifier.swipeable( state.thresholds = { a, b -> val from = anchors.getValue(a) val to = anchors.getValue(b) - with(thresholds(from, to)) { density.computeThreshold(a, b) } + with(thresholdsCallback(from, to)) { density.computeThreshold(a, b) } } with(density) { state.velocityThreshold = velocityThreshold.toPx() @@ -619,14 +611,16 @@ internal fun Modifier.swipeable( state.processNewAnchors(oldAnchors, anchors) } - Modifier.draggable( - orientation = orientation, - enabled = enabled, - reverseDirection = reverseDirection, - interactionSource = interactionSource, - startDragImmediately = state.isAnimationRunning, - onDragStopped = { velocity -> launch { state.performFling(velocity) } }, - state = state.draggableState, + return this.then( + Modifier.draggable( + orientation = orientation, + enabled = enabled, + reverseDirection = reverseDirection, + interactionSource = interactionSource, + startDragImmediately = state.isAnimationRunning, + onDragStopped = { velocity -> launch { state.performFling(velocity) } }, + state = state.draggableState, + ), ) } @@ -709,9 +703,9 @@ internal class ResistanceConfig( // @FloatRange(from = 0.0, fromInclusive = false) val basis: Float, // @FloatRange(from = 0.0) - val factorAtMin: Float = StandardResistanceFactor, + val factorAtMin: Float = STANDARD_RESISTANCE_FACTOR, // @FloatRange(from = 0.0) - val factorAtMax: Float = StandardResistanceFactor, + val factorAtMax: Float = STANDARD_RESISTANCE_FACTOR, ) { fun computeResistance(overflow: Float): Float { val factor = if (overflow < 0) factorAtMin else factorAtMax @@ -764,14 +758,17 @@ private fun findBounds( a == null -> // case 1 or 3 listOfNotNull(b) + b == null -> // case 4 listOf(a) + a == b -> // case 2 // Can't return offset itself here since it might not be exactly equal // to the anchor, despite being considered an exact match. listOf(a) + else -> // case 5 listOf(a, b) @@ -835,12 +832,12 @@ internal object SwipeableDefaults { /** * A stiff resistance factor which indicates that swiping isn't available right now. */ - const val StiffResistanceFactor = 20f + const val STIFF_RESISTANCE_FACTOR = 20f /** * A standard resistance factor which indicates that the user has run out of things to see. */ - const val StandardResistanceFactor = 10f + const val STANDARD_RESISTANCE_FACTOR = 10f /** * The default resistance config used by [swipeable]. @@ -850,8 +847,8 @@ internal object SwipeableDefaults { */ internal fun resistanceConfig( anchors: Set, - factorAtMin: Float = StandardResistanceFactor, - factorAtMax: Float = StandardResistanceFactor, + factorAtMin: Float = STANDARD_RESISTANCE_FACTOR, + factorAtMax: Float = STANDARD_RESISTANCE_FACTOR, ): ResistanceConfig? { return if (anchors.size <= 1) { null diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/NavigationDrawerTokens.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/NavigationDrawerTokens.kt index 43b2752c9..c19904304 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/NavigationDrawerTokens.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/NavigationDrawerTokens.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:property-naming") + package com.nononsenseapps.feeder.ui.compose.material3.tokens import androidx.compose.runtime.Composable diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/ScrimTokens.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/ScrimTokens.kt index 68b4a3851..b7be7d05c 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/ScrimTokens.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/tokens/ScrimTokens.kt @@ -2,5 +2,5 @@ package com.nononsenseapps.feeder.ui.compose.material3.tokens internal object ScrimTokens { val ContainerColor = ColorSchemeKeyTokens.Scrim - const val ContainerOpacity = 0.32f + const val CONTAINER_OPACITY = 0.32f } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt index 9bf037a12..abd50f6a5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt @@ -75,7 +75,7 @@ import kotlinx.coroutines.launch fun ScreenWithNavDrawer( feedsAndTags: LazyPagingItems, onToggleTagExpansion: (String) -> Unit, - onDrawerItemSelected: (Long, String) -> Unit, + onDrawerItemSelect: (Long, String) -> Unit, drawerState: DrawerState, focusRequester: FocusRequester, navDrawerListState: LazyListState, @@ -110,7 +110,7 @@ fun ScreenWithNavDrawer( onToggleTagExpansion = onToggleTagExpansion, ) { item -> coroutineScope.launch { - onDrawerItemSelected(item.id, item.tag) + onDrawerItemSelect(item.id, item.tag) drawerState.close() } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt index 3b6ad4732..9956660f5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt @@ -52,9 +52,9 @@ import com.nononsenseapps.feeder.ui.compose.theme.LocalDimens import com.nononsenseapps.feeder.ui.compose.theme.SensibleTopAppBar import com.nononsenseapps.feeder.ui.compose.theme.SetStatusBarColorToMatchScrollableTopAppBar import com.nononsenseapps.feeder.ui.compose.utils.LocalWindowSize +import com.nononsenseapps.feeder.ui.compose.utils.PreviewThemes import com.nononsenseapps.feeder.ui.compose.utils.ProvideScaledText import com.nononsenseapps.feeder.ui.compose.utils.ScreenType -import com.nononsenseapps.feeder.ui.compose.utils.ThemePreviews import com.nononsenseapps.feeder.ui.compose.utils.getScreenType import kotlinx.coroutines.launch import org.kodein.di.compose.LocalDI @@ -296,7 +296,7 @@ data class ViewState( val tagCount: Int = feeds.values.asSequence().map { it.tag }.filterNot { it.isBlank() }.count() } -@ThemePreviews +@PreviewThemes @Composable private fun PreviewOpmlImportScreenSingle() { FeederTheme { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/preview/FakeArticle.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/preview/FakeArticle.kt deleted file mode 100644 index bca2f4857..000000000 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/preview/FakeArticle.kt +++ /dev/null @@ -1 +0,0 @@ -package com.nononsenseapps.feeder.ui.compose.preview diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/preview/FakeFeed.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/preview/FakeFeed.kt deleted file mode 100644 index bca2f4857..000000000 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/preview/FakeFeed.kt +++ /dev/null @@ -1 +0,0 @@ -package com.nononsenseapps.feeder.ui.compose.preview diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt index be7da767e..667ec112d 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt @@ -15,12 +15,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.center @@ -33,7 +32,6 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp @@ -71,7 +69,7 @@ fun PullRefreshIndicator( Crossfade( targetState = refreshing, label = "PullToRefresh", - animationSpec = tween(durationMillis = CrossfadeDurationMs), + animationSpec = tween(durationMillis = CROSSFADE_DURATION_MS), ) { refreshing -> Box( modifier = Modifier.fillMaxSize(), @@ -153,7 +151,7 @@ private fun ArrowValues(progress: Float): ArrowValues { // Calculations based on SwipeRefreshLayout specification. val alpha = progress.coerceIn(0f, 1f) - val endTrim = adjustedPercent * MaxProgressArc + val endTrim = adjustedPercent * MAX_PROGRESS_ARC val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f val startAngle = rotation * 360 val endAngle = (rotation + endTrim) * 360 @@ -192,8 +190,8 @@ private fun DrawScope.drawArrow( } } -private const val CrossfadeDurationMs = 100 -private const val MaxProgressArc = 0.8f +private const val CROSSFADE_DURATION_MS = 100 +private const val MAX_PROGRESS_ARC = 0.8f private val IndicatorSize = 40.dp private val SpinnerShape = CircleShape @@ -203,31 +201,28 @@ private val ArrowWidth = 10.dp private val ArrowHeight = 5.dp private val Elevation = 6.dp +@Composable +@Suppress("ktlint:compose:modifier-composable-check") fun Modifier.pullRefreshIndicatorTransform( state: PullRefreshState, scale: Boolean = false, -) = composed( - inspectorInfo = - debugInspectorInfo { - name = "pullRefreshIndicatorTransform" - properties["state"] = state - properties["scale"] = scale - }, -) { - var height by remember { mutableStateOf(0) } - - Modifier - .onSizeChanged { height = it.height } - .graphicsLayer { - translationY = state.position - height - - if (scale && !state.refreshing) { - val scaleFraction = - LinearOutSlowInEasing - .transform(state.position / state.threshold) - .coerceIn(0f, 1f) - scaleX = scaleFraction - scaleY = scaleFraction - } - } +): Modifier { + var height by remember { mutableIntStateOf(0) } + + return this.then( + Modifier + .onSizeChanged { height = it.height } + .graphicsLayer { + translationY = state.position - height + + if (scale && !state.refreshing) { + val scaleFraction = + LinearOutSlowInEasing + .transform(state.position / state.threshold) + .coerceIn(0f, 1f) + scaleX = scaleFraction + scaleY = scaleFraction + } + }, + ) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt index 4723ef6f9..334a23bda 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -102,11 +103,11 @@ class PullRefreshState internal constructor( internal val refreshing get() = _refreshing internal val position get() = _position - private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier } + private val adjustedDistancePulled by derivedStateOf { distancePulled * DRAG_MULTIPLIER } private var _refreshing by mutableStateOf(false) - private var _position by mutableStateOf(0f) - private var distancePulled by mutableStateOf(0f) + private var _position by mutableFloatStateOf(0f) + private var distancePulled by mutableFloatStateOf(0f) internal fun onPull(pullDelta: Float): Float { if (this._refreshing) return 0f // Already refreshing, do nothing. @@ -185,4 +186,4 @@ object PullRefreshDefaults { * the refresh threshold, it is the indicator position, otherwise the indicator position is * derived from the progress). */ -private const val DragMultiplier = 0.5f +private const val DRAG_MULTIPLIER = 0.5f diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt index 12b6806da..4f28f9046 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt @@ -44,10 +44,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -176,7 +176,7 @@ fun SearchFeedView( SearchFeedView( screenType = screenType, - onUrlChanged = { + onUrlChange = { feedUrl = it }, onSearch = { url -> @@ -206,11 +206,10 @@ fun SearchFeedView( ) } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun SearchFeedView( screenType: ScreenType, - onUrlChanged: (String) -> Unit, + onUrlChange: (String) -> Unit, onSearch: (URL) -> Unit, results: StableHolder>, errors: StableHolder>, @@ -223,6 +222,8 @@ fun SearchFeedView( val dimens = LocalDimens.current val keyboardController = LocalSoftwareKeyboardController.current + val onSearchCallback by rememberUpdatedState(newValue = onSearch) + // If screen is opened from intent with pre-filled URL, trigger search directly LaunchedEffect(Unit) { if (results.item.isEmpty() && errors.item.isEmpty() && feedUrl.isNotBlank() && @@ -230,7 +231,7 @@ fun SearchFeedView( feedUrl, ) ) { - onSearch(sloppyLinkToStrictURLNoThrows(feedUrl)) + onSearchCallback(sloppyLinkToStrictURLNoThrows(feedUrl)) } } @@ -239,9 +240,10 @@ fun SearchFeedView( Box( contentAlignment = Alignment.TopCenter, modifier = - modifier + Modifier .fillMaxWidth() - .verticalScroll(scrollState), + .verticalScroll(scrollState) + .then(modifier), ) { if (screenType == ScreenType.DUAL) { Row( @@ -262,7 +264,7 @@ fun SearchFeedView( }, dimens = dimens, keyboardController = keyboardController, - onUrlChanged = onUrlChanged, + onUrlChange = onUrlChange, onSearch = onSearch, ) } @@ -294,7 +296,7 @@ fun SearchFeedView( ) { leftContent( feedUrl = feedUrl, - onUrlChanged = onUrlChanged, + onUrlChange = onUrlChange, onSearch = onSearch, clearFocus = { focusManager.clearFocus() @@ -314,11 +316,10 @@ fun SearchFeedView( } @Suppress("UnusedReceiverParameter") -@OptIn(ExperimentalComposeUiApi::class) @Composable fun ColumnScope.leftContent( feedUrl: String, - onUrlChanged: (String) -> Unit, + onUrlChange: (String) -> Unit, onSearch: (URL) -> Unit, clearFocus: () -> Unit, dimens: Dimensions, @@ -337,7 +338,7 @@ fun ColumnScope.leftContent( } TextField( value = feedUrl, - onValueChange = onUrlChanged, + onValueChange = onUrlChange, label = { Text(stringResource(id = R.string.add_feed_search_hint)) }, @@ -563,7 +564,7 @@ private fun SearchPreview() { Surface { SearchFeedView( screenType = ScreenType.SINGLE, - onUrlChanged = {}, + onUrlChange = {}, onSearch = {}, results = stableListHolderOf( @@ -601,7 +602,7 @@ private fun ErrorPreview() { Surface { SearchFeedView( screenType = ScreenType.SINGLE, - onUrlChanged = {}, + onUrlChange = {}, onSearch = {}, results = stableListHolderOf(), errors = @@ -637,7 +638,7 @@ private fun SearchPreviewLarge() { Surface { SearchFeedView( screenType = ScreenType.DUAL, - onUrlChanged = {}, + onUrlChange = {}, onSearch = {}, results = stableListHolderOf( diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt index 06eeb1ca8..3202f2d52 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt @@ -95,11 +95,11 @@ import com.nononsenseapps.feeder.ui.compose.theme.LocalDimens import com.nononsenseapps.feeder.ui.compose.theme.SensibleTopAppBar import com.nononsenseapps.feeder.ui.compose.theme.SetStatusBarColorToMatchScrollableTopAppBar import com.nononsenseapps.feeder.ui.compose.utils.ImmutableHolder +import com.nononsenseapps.feeder.ui.compose.utils.WithAllPreviewProviders import com.nononsenseapps.feeder.ui.compose.utils.immutableListHolderOf import com.nononsenseapps.feeder.ui.compose.utils.isCompactDevice import com.nononsenseapps.feeder.ui.compose.utils.onKeyEventLikeEscape import com.nononsenseapps.feeder.ui.compose.utils.rememberApiPermissionState -import com.nononsenseapps.feeder.ui.compose.utils.withAllPreviewProviders import com.nononsenseapps.feeder.util.ActivityLauncher import org.kodein.di.compose.LocalDI import org.kodein.di.instance @@ -143,44 +143,44 @@ fun SettingsScreen( ) { padding -> SettingsList( currentThemeValue = viewState.currentTheme.asThemeOption(), - onThemeChanged = { value -> + onThemeChange = { value -> settingsViewModel.setCurrentTheme(value.currentTheme) }, currentDarkThemePreference = viewState.darkThemePreference.asDarkThemeOption(), - onDarkThemePreferenceChanged = { value -> + onDarkThemePreferenceChange = { value -> settingsViewModel.setPreferredDarkTheme(value.darkThemePreferences) }, currentSortingValue = viewState.currentSorting.asSortOption(), - onSortingChanged = { value -> + onSortingChange = { value -> settingsViewModel.setCurrentSorting(value.currentSorting) }, showFabValue = viewState.showFab, - onShowFabChanged = settingsViewModel::setShowFab, + onShowFabChange = settingsViewModel::setShowFab, feedItemStyleValue = viewState.feedItemStyle, - onFeedItemStyleChanged = settingsViewModel::setFeedItemStyle, + onFeedItemStyleChange = settingsViewModel::setFeedItemStyle, blockListValue = ImmutableHolder(viewState.blockList.sorted()), swipeAsReadValue = viewState.swipeAsRead, - onSwipeAsReadOptionChanged = settingsViewModel::setSwipeAsRead, + onSwipeAsReadOptionChange = settingsViewModel::setSwipeAsRead, syncOnStartupValue = viewState.syncOnResume, - onSyncOnStartupChanged = settingsViewModel::setSyncOnResume, + onSyncOnStartupChange = settingsViewModel::setSyncOnResume, syncOnlyOnWifiValue = viewState.syncOnlyOnWifi, - onSyncOnlyOnWifiChanged = settingsViewModel::setSyncOnlyOnWifi, + onSyncOnlyOnWifiChange = settingsViewModel::setSyncOnlyOnWifi, syncOnlyWhenChargingValue = viewState.syncOnlyWhenCharging, - onSyncOnlyWhenChargingChanged = settingsViewModel::setSyncOnlyWhenCharging, + onSyncOnlyWhenChargingChange = settingsViewModel::setSyncOnlyWhenCharging, loadImageOnlyOnWifiValue = viewState.loadImageOnlyOnWifi, - onLoadImageOnlyOnWifiChanged = settingsViewModel::setLoadImageOnlyOnWifi, + onLoadImageOnlyOnWifiChange = settingsViewModel::setLoadImageOnlyOnWifi, showThumbnailsValue = viewState.showThumbnails, - onShowThumbnailsChanged = settingsViewModel::setShowThumbnails, + onShowThumbnailsChange = settingsViewModel::setShowThumbnails, useDetectLanguage = viewState.useDetectLanguage, - onUseDetectLanguageChanged = settingsViewModel::setUseDetectLanguage, + onUseDetectLanguageChange = settingsViewModel::setUseDetectLanguage, maxItemsPerFeedValue = viewState.maximumCountPerFeed, - onMaxItemsPerFeedChanged = settingsViewModel::setMaxCountPerFeed, + onMaxItemsPerFeedChange = settingsViewModel::setMaxCountPerFeed, currentItemOpenerValue = viewState.itemOpener, - onItemOpenerChanged = settingsViewModel::setItemOpener, + onItemOpenerChange = settingsViewModel::setItemOpener, currentLinkOpenerValue = viewState.linkOpener, - onLinkOpenerChanged = settingsViewModel::setLinkOpener, + onLinkOpenerChange = settingsViewModel::setLinkOpener, currentSyncFrequencyValue = viewState.syncFrequency, - onSyncFrequencyChanged = settingsViewModel::setSyncFrequency, + onSyncFrequencyChange = settingsViewModel::setSyncFrequency, batteryOptimizationIgnoredValue = viewState.batteryOptimizationIgnored, onOpenSyncSettings = onNavigateToSyncScreen, useDynamicTheme = viewState.useDynamicTheme, @@ -200,7 +200,7 @@ fun SettingsScreen( isOpenAdjacent = viewState.isOpenAdjacent, onOpenAdjacent = settingsViewModel::setIsOpenAdjacent, showReadingTime = viewState.showReadingTime, - onShowReadingTimeChanged = settingsViewModel::setShowReadingTime, + onShowReadingTimeChange = settingsViewModel::setShowReadingTime, onStartActivity = { intent -> activityLauncher.startActivity(false, intent) }, @@ -213,41 +213,41 @@ fun SettingsScreen( @Preview(showBackground = true, device = PIXEL_C) @Preview(showBackground = true, device = NEXUS_5) private fun SettingsScreenPreview() { - withAllPreviewProviders { + WithAllPreviewProviders { SettingsList( currentThemeValue = ThemeOptions.SYSTEM.asThemeOption(), - onThemeChanged = {}, + onThemeChange = {}, currentDarkThemePreference = DarkThemePreferences.BLACK.asDarkThemeOption(), - onDarkThemePreferenceChanged = {}, + onDarkThemePreferenceChange = {}, currentSortingValue = SortingOptions.NEWEST_FIRST.asSortOption(), - onSortingChanged = {}, + onSortingChange = {}, showFabValue = true, - onShowFabChanged = {}, + onShowFabChange = {}, feedItemStyleValue = FeedItemStyle.CARD, - onFeedItemStyleChanged = {}, + onFeedItemStyleChange = {}, blockListValue = ImmutableHolder(emptyList()), swipeAsReadValue = SwipeAsRead.ONLY_FROM_END, - onSwipeAsReadOptionChanged = {}, + onSwipeAsReadOptionChange = {}, syncOnStartupValue = true, - onSyncOnStartupChanged = {}, + onSyncOnStartupChange = {}, syncOnlyOnWifiValue = true, - onSyncOnlyOnWifiChanged = {}, + onSyncOnlyOnWifiChange = {}, syncOnlyWhenChargingValue = true, - onSyncOnlyWhenChargingChanged = {}, + onSyncOnlyWhenChargingChange = {}, loadImageOnlyOnWifiValue = true, - onLoadImageOnlyOnWifiChanged = {}, + onLoadImageOnlyOnWifiChange = {}, showThumbnailsValue = true, - onShowThumbnailsChanged = {}, + onShowThumbnailsChange = {}, useDetectLanguage = true, - onUseDetectLanguageChanged = {}, + onUseDetectLanguageChange = {}, maxItemsPerFeedValue = 101, - onMaxItemsPerFeedChanged = {}, + onMaxItemsPerFeedChange = {}, currentItemOpenerValue = ItemOpener.CUSTOM_TAB, - onItemOpenerChanged = {}, + onItemOpenerChange = {}, currentLinkOpenerValue = LinkOpener.DEFAULT_BROWSER, - onLinkOpenerChanged = {}, + onLinkOpenerChange = {}, currentSyncFrequencyValue = SyncFrequency.EVERY_12_HOURS, - onSyncFrequencyChanged = {}, + onSyncFrequencyChange = {}, batteryOptimizationIgnoredValue = false, onOpenSyncSettings = {}, useDynamicTheme = true, @@ -267,7 +267,7 @@ private fun SettingsScreenPreview() { isOpenAdjacent = false, onOpenAdjacent = {}, showReadingTime = false, - onShowReadingTimeChanged = {}, + onShowReadingTimeChange = {}, onStartActivity = {}, modifier = Modifier, ) @@ -277,38 +277,38 @@ private fun SettingsScreenPreview() { @Composable fun SettingsList( currentThemeValue: ThemeOption, - onThemeChanged: (ThemeOption) -> Unit, + onThemeChange: (ThemeOption) -> Unit, currentDarkThemePreference: DarkThemeOption, - onDarkThemePreferenceChanged: (DarkThemeOption) -> Unit, + onDarkThemePreferenceChange: (DarkThemeOption) -> Unit, currentSortingValue: SortOption, - onSortingChanged: (SortOption) -> Unit, + onSortingChange: (SortOption) -> Unit, showFabValue: Boolean, - onShowFabChanged: (Boolean) -> Unit, + onShowFabChange: (Boolean) -> Unit, feedItemStyleValue: FeedItemStyle, - onFeedItemStyleChanged: (FeedItemStyle) -> Unit, + onFeedItemStyleChange: (FeedItemStyle) -> Unit, blockListValue: ImmutableHolder>, swipeAsReadValue: SwipeAsRead, - onSwipeAsReadOptionChanged: (SwipeAsRead) -> Unit, + onSwipeAsReadOptionChange: (SwipeAsRead) -> Unit, syncOnStartupValue: Boolean, - onSyncOnStartupChanged: (Boolean) -> Unit, + onSyncOnStartupChange: (Boolean) -> Unit, syncOnlyOnWifiValue: Boolean, - onSyncOnlyOnWifiChanged: (Boolean) -> Unit, + onSyncOnlyOnWifiChange: (Boolean) -> Unit, syncOnlyWhenChargingValue: Boolean, - onSyncOnlyWhenChargingChanged: (Boolean) -> Unit, + onSyncOnlyWhenChargingChange: (Boolean) -> Unit, loadImageOnlyOnWifiValue: Boolean, - onLoadImageOnlyOnWifiChanged: (Boolean) -> Unit, + onLoadImageOnlyOnWifiChange: (Boolean) -> Unit, showThumbnailsValue: Boolean, - onShowThumbnailsChanged: (Boolean) -> Unit, + onShowThumbnailsChange: (Boolean) -> Unit, useDetectLanguage: Boolean, - onUseDetectLanguageChanged: (Boolean) -> Unit, + onUseDetectLanguageChange: (Boolean) -> Unit, maxItemsPerFeedValue: Int, - onMaxItemsPerFeedChanged: (Int) -> Unit, + onMaxItemsPerFeedChange: (Int) -> Unit, currentItemOpenerValue: ItemOpener, - onItemOpenerChanged: (ItemOpener) -> Unit, + onItemOpenerChange: (ItemOpener) -> Unit, currentLinkOpenerValue: LinkOpener, - onLinkOpenerChanged: (LinkOpener) -> Unit, + onLinkOpenerChange: (LinkOpener) -> Unit, currentSyncFrequencyValue: SyncFrequency, - onSyncFrequencyChanged: (SyncFrequency) -> Unit, + onSyncFrequencyChange: (SyncFrequency) -> Unit, batteryOptimizationIgnoredValue: Boolean, onOpenSyncSettings: () -> Unit, useDynamicTheme: Boolean, @@ -328,7 +328,7 @@ fun SettingsList( isOpenAdjacent: Boolean, onOpenAdjacent: (Boolean) -> Unit, showReadingTime: Boolean, - onShowReadingTimeChanged: (Boolean) -> Unit, + onShowReadingTimeChange: (Boolean) -> Unit, onStartActivity: (intent: Intent) -> Unit, modifier: Modifier = Modifier, ) { @@ -361,7 +361,7 @@ fun SettingsList( ThemeOptions.E_INK.asThemeOption(), ), title = stringResource(id = R.string.theme), - onSelection = onThemeChanged, + onSelection = onThemeChange, ) SwitchSetting( @@ -381,7 +381,7 @@ fun SettingsList( } }, enabled = isAndroidSAndAbove, - onCheckedChanged = onUseDynamicTheme, + onCheckedChange = onUseDynamicTheme, ) MenuSetting( @@ -392,7 +392,7 @@ fun SettingsList( DarkThemePreferences.BLACK.asDarkThemeOption(), DarkThemePreferences.DARK.asDarkThemeOption(), ), - onSelection = onDarkThemePreferenceChanged, + onSelection = onDarkThemePreferenceChange, ) ListDialogSetting( @@ -460,26 +460,26 @@ fun SettingsList( ), title = stringResource(id = R.string.check_for_updates), onSelection = { - onSyncFrequencyChanged(it.syncFrequency) + onSyncFrequencyChange(it.syncFrequency) }, ) SwitchSetting( title = stringResource(id = R.string.on_startup), checked = syncOnStartupValue, - onCheckedChanged = onSyncOnStartupChanged, + onCheckedChange = onSyncOnStartupChange, ) SwitchSetting( title = stringResource(id = R.string.only_on_wifi), checked = syncOnlyOnWifiValue, - onCheckedChanged = onSyncOnlyOnWifiChanged, + onCheckedChange = onSyncOnlyOnWifiChange, ) SwitchSetting( title = stringResource(id = R.string.only_when_charging), checked = syncOnlyWhenChargingValue, - onCheckedChanged = onSyncOnlyWhenChargingChanged, + onCheckedChange = onSyncOnlyWhenChargingChange, ) MenuSetting( @@ -493,7 +493,7 @@ fun SettingsList( 1000, ), title = stringResource(id = R.string.max_feed_items), - onSelection = onMaxItemsPerFeedChanged, + onSelection = onMaxItemsPerFeedChange, ) ExternalSetting( @@ -531,13 +531,13 @@ fun SettingsList( SortingOptions.OLDEST_FIRST.asSortOption(), ), title = stringResource(id = R.string.sort), - onSelection = onSortingChanged, + onSelection = onSortingChange, ) SwitchSetting( title = stringResource(id = R.string.show_fab), checked = showFabValue, - onCheckedChanged = onShowFabChanged, + onCheckedChange = onShowFabChange, ) if (isCompactDevice()) { @@ -546,7 +546,7 @@ fun SettingsList( currentValue = feedItemStyleValue.asFeedItemStyleOption(), values = ImmutableHolder(FeedItemStyle.values().map { it.asFeedItemStyleOption() }), onSelection = { - onFeedItemStyleChanged(it.feedItemStyle) + onFeedItemStyleChange(it.feedItemStyle) }, ) } @@ -561,7 +561,7 @@ fun SettingsList( SwitchSetting( title = stringResource(id = R.string.show_only_title), checked = showOnlyTitle, - onCheckedChanged = onShowOnlyTitle, + onCheckedChange = onShowOnlyTitle, ) MenuSetting( @@ -569,26 +569,26 @@ fun SettingsList( currentValue = swipeAsReadValue.asSwipeAsReadOption(), values = ImmutableHolder(SwipeAsRead.values().map { it.asSwipeAsReadOption() }), onSelection = { - onSwipeAsReadOptionChanged(it.swipeAsRead) + onSwipeAsReadOptionChange(it.swipeAsRead) }, ) SwitchSetting( title = stringResource(id = R.string.mark_as_read_on_scroll), checked = isMarkAsReadOnScroll, - onCheckedChanged = onMarkAsReadOnScroll, + onCheckedChange = onMarkAsReadOnScroll, ) SwitchSetting( title = stringResource(id = R.string.show_thumbnails), checked = showThumbnailsValue, - onCheckedChanged = onShowThumbnailsChanged, + onCheckedChange = onShowThumbnailsChange, ) SwitchSetting( title = stringResource(id = R.string.show_reading_time), checked = showReadingTime, - onCheckedChanged = onShowReadingTimeChanged, + onCheckedChange = onShowReadingTimeChange, ) HorizontalDivider(modifier = Modifier.width(dimens.maxContentWidth)) @@ -610,7 +610,7 @@ fun SettingsList( ), title = stringResource(id = R.string.open_item_by_default_with), onSelection = { - onItemOpenerChanged(it.itemOpener) + onItemOpenerChange(it.itemOpener) }, ) @@ -623,7 +623,7 @@ fun SettingsList( ), title = stringResource(id = R.string.open_links_with), onSelection = { - onLinkOpenerChanged(it.linkOpener) + onLinkOpenerChange(it.linkOpener) }, ) @@ -633,7 +633,7 @@ fun SettingsList( SwitchSetting( title = stringResource(id = R.string.open_browser_in_split_screen), checked = isOpenAdjacent, - onCheckedChanged = onOpenAdjacent, + onCheckedChange = onOpenAdjacent, ) } @@ -649,7 +649,7 @@ fun SettingsList( SwitchSetting( title = stringResource(id = R.string.only_on_wifi), checked = loadImageOnlyOnWifiValue, - onCheckedChanged = onLoadImageOnlyOnWifiChanged, + onCheckedChange = onLoadImageOnlyOnWifiChange, ) HorizontalDivider(modifier = Modifier.width(dimens.maxContentWidth)) @@ -674,7 +674,7 @@ fun SettingsList( ) }, enabled = isAndroidQAndAbove, - onCheckedChanged = onUseDetectLanguageChanged, + onCheckedChange = onUseDetectLanguageChange, ) Spacer(modifier = Modifier.navigationBarsPadding()) @@ -1088,7 +1088,7 @@ fun SwitchSetting( description: String? = null, icon: @Composable (() -> Unit)? = {}, enabled: Boolean = true, - onCheckedChanged: (Boolean) -> Unit, + onCheckedChange: (Boolean) -> Unit, ) { val context = LocalContext.current val dimens = LocalDimens.current @@ -1099,7 +1099,7 @@ fun SwitchSetting( .heightIn(min = 64.dp) .clickable( enabled = enabled, - onClick = { onCheckedChanged(!checked) }, + onClick = { onCheckedChange(!checked) }, ) .safeSemantics(mergeDescendants = true) { stateDescription = @@ -1136,7 +1136,7 @@ fun SwitchSetting( Switch( checked = checked, - onCheckedChange = onCheckedChanged, + onCheckedChange = onCheckedChange, modifier = Modifier.clearAndSetSemantics { }, enabled = enabled, ) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt index 73f2d82cb..d18337bd4 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt @@ -17,7 +17,7 @@ import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -44,15 +44,17 @@ fun SliderWithLabel( Column { BoxWithConstraints( modifier = - modifier - .fillMaxWidth(), + Modifier + .fillMaxWidth() + .then(modifier), ) { val offset = getSliderOffset( value = value, valueRange = valueRange, boxWidth = maxWidth, - labelWidth = labelMinWidth + 8.dp, // Since we use a padding of 4.dp on either sides of the SliderLabel, we need to account for this in our calculation + // Since we use a padding of 4.dp on either sides of the SliderLabel, we need to account for this in our calculation + labelWidth = labelMinWidth + 8.dp, ) SliderLabel( @@ -159,7 +161,7 @@ private fun PreviewSliderWithLabel() { FeederTheme { Surface { var value by remember { - mutableStateOf(1f) + mutableFloatStateOf(1f) } SliderWithLabel( value = value, @@ -178,7 +180,7 @@ private fun PreviewSliderWithEndLabels() { FeederTheme { Surface { var value by remember { - mutableStateOf(1f) + mutableFloatStateOf(1f) } SliderWithEndLabels( value = value, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt index cd4092a1e..bd4caa585 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt @@ -33,16 +33,19 @@ class SyncScreenViewModel(di: DI, private val state: SavedStateHandle) : DIAware private val applicationCoroutineScope: ApplicationCoroutineScope by instance() + @Suppress("ktlint:standard:property-naming") private val _syncCode: MutableStateFlow = MutableStateFlow( state["syncCode"] ?: "", ) + @Suppress("ktlint:standard:property-naming") private val _secretKey: MutableStateFlow = MutableStateFlow( state["secretKey"] ?: "", ) + @Suppress("ktlint:standard:property-naming") private val _screenToShow: MutableStateFlow = MutableStateFlow( state["syncScreen"] ?: SyncScreenToShow.SETUP, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt index b78eea8eb..b046fa3f5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt @@ -28,13 +28,11 @@ import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.Terrain -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -1503,23 +1501,17 @@ fun stripHtml(html: String): String { return result.toString() } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun WithTooltipIfNotBlank( tooltip: String, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { if (tooltip.isNotBlank()) { - PlainTooltipBox(tooltip = { Text(tooltip) }) { + PlainTooltipBox(modifier = modifier, tooltip = { Text(tooltip) }) { content() } } else { content() } } - -@Immutable -data class ImageSize( - val width: Int, - val height: Int, -) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt index a363c739e..ba32e5894 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt @@ -29,7 +29,7 @@ fun DIAwareComponentActivity.withAllProviders(content: @Composable () -> Unit) { FeederTheme( currentTheme = currentTheme, darkThemePreference = darkThemePreference, - dynamicColors = dynamicColors + dynamicColors = dynamicColors, ) { withWindowSize { ProvideFontScale(fontScale = textScale) { @@ -43,19 +43,21 @@ fun DIAwareComponentActivity.withAllProviders(content: @Composable () -> Unit) { @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable -fun withAllPreviewProviders( +@Suppress("ktlint:compose:modifier-missing-check") +fun WithAllPreviewProviders( currentTheme: ThemeOptions = ThemeOptions.DAY, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { FeederTheme(currentTheme = currentTheme) { val dm = LocalContext.current.resources.displayMetrics - val dpSize = with(LocalDensity.current) { - DpSize( - dm.widthPixels.toDp(), - dm.heightPixels.toDp() - ) - } - withPreviewWindowSize(WindowSizeClass.calculateFromSize(dpSize)) { + val dpSize = + with(LocalDensity.current) { + DpSize( + dm.widthPixels.toDp(), + dm.heightPixels.toDp(), + ) + } + WithPreviewWindowSize(WindowSizeClass.calculateFromSize(dpSize)) { Surface { content() } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt index 0843b7ead..b1bd66d82 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt @@ -2,27 +2,20 @@ package com.nononsenseapps.feeder.ui.compose.utils import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalInputModeManager -import androidx.compose.ui.platform.debugInspectorInfo -@OptIn(ExperimentalComposeUiApi::class) -fun Modifier.onKeyEventLikeEscape(action: () -> Unit) = - composed( - inspectorInfo = - debugInspectorInfo { - name = "onEscapeLikeKeyPress" - properties["action"] = action - }, - ) { - onKeyEvent { +@Composable +@Suppress("ktlint:compose:modifier-composable-check") +fun Modifier.onKeyEventLikeEscape(action: () -> Unit): Modifier { + return this.then( + Modifier.onKeyEvent { when (it.key) { Key.Escape, Key.Back, Key.NavigateOut -> { action() @@ -31,22 +24,20 @@ fun Modifier.onKeyEventLikeEscape(action: () -> Unit) = else -> false } - } - } + }, + ) +} +@Composable +@Suppress("ktlint:compose:modifier-composable-check") fun Modifier.focusableInNonTouchMode( enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, -) = composed( - inspectorInfo = - debugInspectorInfo { - name = "focusableInNonTouchMode" - properties["enabled"] = enabled - properties["interactionSource"] = interactionSource - }, -) { +): Modifier { val inputModeManager = LocalInputModeManager.current - Modifier - .focusProperties { canFocus = inputModeManager.inputMode != InputMode.Touch } - .focusable(enabled, interactionSource) + return this.then( + Modifier + .focusProperties { canFocus = inputModeManager.inputMode != InputMode.Touch } + .focusable(enabled, interactionSource), + ) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Permissions.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Permissions.kt index b95e55ee2..9b8167d3f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Permissions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Permissions.kt @@ -14,7 +14,7 @@ import com.google.accompanist.permissions.rememberPermissionState fun rememberApiPermissionState( permission: String, minimumApiLevel: Int = 1, - onPermissionResult: (Boolean) -> Unit = {} + onPermissionResult: (Boolean) -> Unit = {}, ): PermissionState = when { // When layout is being previewed diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Preview.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Preview.kt deleted file mode 100644 index bfa3622f1..000000000 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Preview.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.nononsenseapps.feeder.ui.compose.utils - -import android.content.res.Configuration -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview - -@Preview( - name = "Phone portrait", - device = Devices.PIXEL_2, - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO, -) -@Preview( - name = "Foldable light", - device = Devices.FOLDABLE, - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO, -) -@Preview( - name = "Tablet light", - device = Devices.PIXEL_C, - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO, -) -annotation class DevicePreviews() - -@Preview( - name = "Light", - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO, -) -@Preview( - name = "Dark", - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES, -) -annotation class ThemePreviews() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/PreviewThemes.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/PreviewThemes.kt new file mode 100644 index 000000000..243f59f9e --- /dev/null +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/PreviewThemes.kt @@ -0,0 +1,16 @@ +package com.nononsenseapps.feeder.ui.compose.utils + +import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Preview + +@Preview( + name = "Light", + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_NO, +) +@Preview( + name = "Dark", + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +annotation class PreviewThemes diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowSize.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowSize.kt index b781caae9..9b8505f85 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowSize.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowSize.kt @@ -30,7 +30,10 @@ fun Activity.withWindowSize(content: @Composable () -> Unit) { } @Composable -fun withPreviewWindowSize(windowSizeclass: WindowSizeClass, content: @Composable () -> Unit) { +fun WithPreviewWindowSize( + windowSizeclass: WindowSizeClass, + content: @Composable () -> Unit, +) { CompositionLocalProvider(LocalWindowSize provides windowSizeclass) { content() } @@ -51,7 +54,7 @@ fun isCompactDevice(): Boolean { enum class ScreenType { DUAL, - SINGLE + SINGLE, } fun getScreenType(windowSize: WindowSizeClass) = diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt b/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt index b3bb3df53..a43cff6b3 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt @@ -26,17 +26,17 @@ private fun bugBody(): String = I'd like to report an issue: """.trimIndent() -private const val emailReportAddress: String = "feeder@nononsenseapps.com" +private const val EMAIL_REPORT_ADDRESS: String = "feeder@nononsenseapps.com" fun emailBugReportIntent(): Intent = Intent(ACTION_SENDTO).apply { - data = Uri.parse("mailto:$emailReportAddress") + data = Uri.parse("mailto:$EMAIL_REPORT_ADDRESS") putExtra(EXTRA_SUBJECT, "Bug report for Feeder") - putExtra(EXTRA_EMAIL, emailReportAddress) + putExtra(EXTRA_EMAIL, EMAIL_REPORT_ADDRESS) putExtra(Intent.EXTRA_TEXT, bugBody()) } -private const val crashReportAddress: String = "crashes@nononsenseapps.com" +private const val CRASH_REPORT_ADDRESS: String = "crashes@nononsenseapps.com" private fun crashBody(throwable: Throwable): String = """ @@ -49,9 +49,9 @@ private fun crashBody(throwable: Throwable): String = fun emailCrashReportIntent(throwable: Throwable): Intent = Intent(ACTION_SENDTO).apply { - data = Uri.parse("mailto:$crashReportAddress") + data = Uri.parse("mailto:$CRASH_REPORT_ADDRESS") putExtra(EXTRA_SUBJECT, "Crash report for Feeder") - putExtra(EXTRA_EMAIL, crashReportAddress) + putExtra(EXTRA_EMAIL, CRASH_REPORT_ADDRESS) putExtra(Intent.EXTRA_TEXT, crashBody(throwable)) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } diff --git a/app/src/main/java/org/kodein/di/compose/CompositionLocal.kt b/app/src/main/java/org/kodein/di/compose/LocalDI.kt similarity index 62% rename from app/src/main/java/org/kodein/di/compose/CompositionLocal.kt rename to app/src/main/java/org/kodein/di/compose/LocalDI.kt index e1ceb6532..106febfeb 100644 --- a/app/src/main/java/org/kodein/di/compose/CompositionLocal.kt +++ b/app/src/main/java/org/kodein/di/compose/LocalDI.kt @@ -1,6 +1,5 @@ package org.kodein.di.compose -import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf @@ -10,16 +9,8 @@ import org.kodein.di.DI * DI container holder that can be shared and accessed across the Composable tree * * Note that the current container can be different depending on the Composable node - * see [CompositionLocalProvider] / [withDI] / [subDI] + * see [CompositionLocalProvider] / [withDI] / [SubDI] * * @throws [IllegalStateException] if no DI container is attached to the Composable tree */ val LocalDI: ProvidableCompositionLocal = compositionLocalOf { error("Missing DI container!") } - -/** - * Composable helper function to access the current DI container from the Composable tree (if there is one!) - * - * @throws [IllegalStateException] if no DI container is attached to the Composable tree - */ -@Composable -fun localDI(): DI = LocalDI.current diff --git a/app/src/main/java/org/kodein/di/compose/Retreiving.kt b/app/src/main/java/org/kodein/di/compose/Retreiving.kt index a01aa62a2..603b8b497 100644 --- a/app/src/main/java/org/kodein/di/compose/Retreiving.kt +++ b/app/src/main/java/org/kodein/di/compose/Retreiving.kt @@ -2,7 +2,10 @@ package org.kodein.di.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import org.kodein.di.* +import org.kodein.di.DIProperty +import org.kodein.di.factory +import org.kodein.di.instance +import org.kodein.di.provider /** * Gets an instance of `T` for the given type and tag. @@ -18,7 +21,7 @@ import org.kodein.di.* @Composable inline fun instance(tag: Any? = null): DIProperty = with(LocalDI.current) { - remember { instance(tag) } + remember { instance(tag = tag) } } /** @@ -36,11 +39,11 @@ inline fun instance(tag: Any? = null): DIProperty = */ @Composable inline fun instance( - tag: Any? = null, arg: A, + tag: Any? = null, ): DIProperty = with(LocalDI.current) { - remember { instance(tag, arg) } + remember { instance(tag = tag, arg = arg) } } /** @@ -58,7 +61,7 @@ inline fun instance( @Composable inline fun factory(tag: Any? = null): DIProperty<(A) -> T> = with(LocalDI.current) { - remember { factory(tag) } + remember { factory(tag = tag) } } /** @@ -75,7 +78,7 @@ inline fun factory(tag: Any? = null): DIPrope @Composable inline fun provider(tag: Any? = null): DIProperty<() -> T> = with(LocalDI.current) { - remember { provider(tag) } + remember { provider(tag = tag) } } /** @@ -93,11 +96,11 @@ inline fun provider(tag: Any? = null): DIProp */ @Composable inline fun provider( - tag: Any? = null, arg: A, + tag: Any? = null, ): DIProperty<() -> T> = with(LocalDI.current) { - remember { provider(tag, arg) } + remember { provider(tag = tag, arg = arg) } } /** @@ -115,9 +118,9 @@ inline fun provider( */ @Composable inline fun provider( - tag: Any? = null, noinline fArg: () -> A, + tag: Any? = null, ): DIProperty<() -> T> = with(LocalDI.current) { - remember { provider(tag, fArg) } + remember { provider(tag = tag, arg = fArg) } } diff --git a/app/src/main/java/org/kodein/di/compose/SubDI.kt b/app/src/main/java/org/kodein/di/compose/SubDI.kt index cda9d772d..e7269f379 100644 --- a/app/src/main/java/org/kodein/di/compose/SubDI.kt +++ b/app/src/main/java/org/kodein/di/compose/SubDI.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:compose:param-order-check", "ktlint:compose:modifier-composed-check") + package org.kodein.di.compose import androidx.compose.runtime.Composable @@ -18,7 +20,7 @@ import org.kodein.di.DI * @param content underlying [Composable] tree that will be able to consume the [DI] container */ @Composable -fun subDI( +fun SubDI( parentDI: DI, allowSilentOverride: Boolean = false, copy: Copy = Copy.NonCached, @@ -43,11 +45,11 @@ fun subDI( * @param content underlying [Composable] tree that will be able to consume the [DI] container */ @Composable -fun subDI( +fun SubDI( allowSilentOverride: Boolean = false, copy: Copy = Copy.NonCached, diBuilder: DI.MainBuilder.() -> Unit, content: @Composable() () -> Unit, -) = subDI(LocalDI.current, allowSilentOverride, copy, diBuilder, content) +) = SubDI(LocalDI.current, allowSilentOverride, copy, diBuilder, content) diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SessionStoreTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SessionStoreTest.kt index 69711bf63..c940258f6 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SessionStoreTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SessionStoreTest.kt @@ -1,7 +1,7 @@ package com.nononsenseapps.feeder.archmodel -import org.junit.Assert.* import org.junit.Test +import kotlin.test.assertEquals class SessionStoreTest { private val store = SessionStore() diff --git a/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt b/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt index 61b594cd6..20106a79f 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt @@ -34,6 +34,7 @@ class FeedItemTest { } @Test + @Suppress("ktlint:standard:max-line-length") fun magnetLinkGivesNullFilename() { val fi = FeedItem( diff --git a/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt b/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt index 381ab63a3..1b7084586 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt @@ -1,9 +1,11 @@ package com.nononsenseapps.feeder.sync import org.intellij.lang.annotations.Language -import org.junit.Assert.* import org.junit.Test import java.net.URL +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class EncryptedFeedTest { private val moshi = getMoshi() diff --git a/build.gradle.kts b/build.gradle.kts index baf65e74f..bcc33fd76 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ plugins { alias(libs.plugins.kotlin.parcelize).apply(false) alias(libs.plugins.kotlin.jvm).apply(false) alias(libs.plugins.kotlin.serialization).apply(false) + alias(libs.plugins.ktlint.gradle).apply(false) } allprojects { diff --git a/settings.gradle.kts b/settings.gradle.kts index 264cdac8f..ff7ae12bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,8 @@ dependencyResolutionManagement { // END Unison // Rest + version("ktlint-gradle", "12.1.1") + version("ktlint-compose", "0.4.3") version("kodein", "7.5.0") version("coroutines", "1.7.3") version("gofeed", "0.1.2") @@ -78,6 +80,7 @@ dependencyResolutionManagement { plugin("kotlin-ksp", "com.google.devtools.ksp").versionRef("ksp") plugin("kotlin-parcelize", "org.jetbrains.kotlin.plugin.parcelize").versionRef("kotlin") plugin("kotlin-serialization", "org.jetbrains.kotlin.plugin.serialization").versionRef("kotlin") + plugin("ktlint-gradle", "org.jlleitschuh.gradle.ktlint").versionRef("ktlint-gradle") // BOMS library("okhttp-bom", "com.squareup.okhttp3", "okhttp-bom").versionRef("okhttp") @@ -85,6 +88,7 @@ dependencyResolutionManagement { library("compose-bom", "androidx.compose", "compose-bom").versionRef("compose") // Libraries + library("ktlint-compose", "io.nlopez.compose.rules", "ktlint").versionRef("ktlint-compose") library("room", "androidx.room", "room-compiler").versionRef("room") library("room-ktx", "androidx.room", "room-ktx").versionRef("room") library("room-paging", "androidx.room", "room-paging").versionRef("room")