diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts deleted file mode 100644 index f980f01b..00000000 --- a/desktop/build.gradle.kts +++ /dev/null @@ -1,50 +0,0 @@ -@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") - -import org.jetbrains.compose.compose -import org.jetbrains.compose.desktop.application.dsl.TargetFormat - -plugins { - kotlin("multiplatform") - alias(libs.plugins.compose) - id("dev.msfjarvis.claw.kotlin-common") -} - -group = "dev.msfjarvis.claw" - -version = "1.0" - -kotlin { - jvm { compilations.all { kotlinOptions.jvmTarget = "11" } } - sourceSets["jvmMain"].apply { - dependencies { - implementation(compose.desktop.currentOs) - implementation(projects.api) - implementation(projects.common) - implementation(libs.aurora.component) - implementation(libs.aurora.theming) - implementation(libs.aurora.window) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) - implementation(libs.ktor.client.java) - implementation(libs.multiplatform.paging) - implementation(libs.retrofit.lib) - implementation(libs.retrofit.kotlinxSerializationConverter) - } - } -} - -compose.desktop { - application { - mainClass = "MainKt" - jvmArgs += listOf("-Xmx1G") - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "Claw" - packageVersion = "1.0.0" - description = "Desktop client for lobste.rs link aggregation site" - copyright = "© 2021 Harsh Shandilya. All rights reserved." - vendor = "Harsh Shandilya" - includeAllModules = false - } - } -} diff --git a/desktop/src/jvmMain/kotlin/Api.kt b/desktop/src/jvmMain/kotlin/Api.kt deleted file mode 100644 index 31f6ffa2..00000000 --- a/desktop/src/jvmMain/kotlin/Api.kt +++ /dev/null @@ -1,37 +0,0 @@ -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import dev.msfjarvis.claw.api.LobstersApi -import io.github.aakira.napier.Napier -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import okhttp3.MediaType -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.create - -@OptIn(ExperimentalSerializationApi::class) -class Api { - private val json = Json { ignoreUnknownKeys = true } - - private fun getOkHttpClient(): OkHttpClient { - return OkHttpClient.Builder() - .addNetworkInterceptor { chain -> - val request = chain.request() - Napier.d(tag = "LobstersApi") { "${request.method()}: ${request.url()}" } - chain.proceed(request) - } - .build() - } - - private fun getRetrofit( - okHttpClient: OkHttpClient, - ): Retrofit { - val contentType = MediaType.get("application/json") - return Retrofit.Builder() - .client(okHttpClient) - .baseUrl(LobstersApi.BASE_URL) - .addConverterFactory(json.asConverterFactory(contentType)) - .build() - } - - val api: LobstersApi = getRetrofit(getOkHttpClient()).create() -} diff --git a/desktop/src/jvmMain/kotlin/LazyPagingItems.kt b/desktop/src/jvmMain/kotlin/LazyPagingItems.kt deleted file mode 100644 index ba2488b5..00000000 --- a/desktop/src/jvmMain/kotlin/LazyPagingItems.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.paging.CombinedLoadStates -import androidx.paging.DifferCallback -import androidx.paging.ItemSnapshotList -import androidx.paging.LoadState -import androidx.paging.LoadStates -import androidx.paging.NullPaddedList -import androidx.paging.PagingData -import androidx.paging.PagingDataDiffer -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.collectLatest - -/** - * The class responsible for accessing the data from a [Flow] of [PagingData]. In order to obtain an - * instance of [LazyPagingItems] use the [collectAsLazyPagingItems] extension method of [Flow] with - * [PagingData]. This instance can be used by the [items] and [itemsIndexed] methods inside - * [LazyListScope] to display data received from the [Flow] of [PagingData]. - * - * @param T the type of value used by [PagingData]. - */ -class LazyPagingItems -internal constructor( - /** the [Flow] object which contains a stream of [PagingData] elements. */ - private val flow: Flow> -) { - private val mainDispatcher = Dispatchers.Default - - /** - * Contains the immutable [ItemSnapshotList] of currently presented items, including any - * placeholders if they are enabled. Note that similarly to [peek] accessing the items in a list - * will not trigger any loads. Use [get] to achieve such behavior. - */ - var itemSnapshotList by mutableStateOf(ItemSnapshotList(0, 0, emptyList())) - private set - - /** The number of items which can be accessed. */ - val itemCount: Int - get() = itemSnapshotList.size - - private val differCallback: DifferCallback = - object : DifferCallback { - override fun onChanged(position: Int, count: Int) { - if (count > 0) { - updateItemSnapshotList() - } - } - - override fun onInserted(position: Int, count: Int) { - if (count > 0) { - updateItemSnapshotList() - } - } - - override fun onRemoved(position: Int, count: Int) { - if (count > 0) { - updateItemSnapshotList() - } - } - } - - private val pagingDataDiffer = - object : PagingDataDiffer(differCallback = differCallback, mainDispatcher = mainDispatcher) { - override suspend fun presentNewList( - previousList: NullPaddedList, - newList: NullPaddedList, - lastAccessedIndex: Int, - onListPresentable: () -> Unit - ): Int? { - onListPresentable() - updateItemSnapshotList() - return null - } - } - - private fun updateItemSnapshotList() { - itemSnapshotList = pagingDataDiffer.snapshot() - } - - /** - * Returns the presented item at the specified position, notifying Paging of the item access to - * trigger any loads necessary to fulfill prefetchDistance. - * - * @see peek - */ - operator fun get(index: Int): T? { - pagingDataDiffer[index] // this registers the value load - return itemSnapshotList[index] - } - - /** - * Returns the presented item at the specified position, without notifying Paging of the item - * access that would normally trigger page loads. - * - * @param index Index of the presented item to return, including placeholders. - * @return The presented item at position [index], `null` if it is a placeholder - */ - fun peek(index: Int): T? { - return itemSnapshotList[index] - } - - /** - * Retry any failed load requests that would result in a [LoadState.Error] update to this - * [LazyPagingItems]. - * - * Unlike [refresh], this does not invalidate [PagingSource], it only retries failed loads within - * the same generation of [PagingData]. - * - * [LoadState.Error] can be generated from two types of load requests: - * * [PagingSource.load] returning [PagingSource.LoadResult.Error] - * * [RemoteMediator.load] returning [RemoteMediator.MediatorResult.Error] - */ - fun retry() { - pagingDataDiffer.retry() - } - - /** - * Refresh the data presented by this [LazyPagingItems]. - * - * [refresh] triggers the creation of a new [PagingData] with a new instance of [PagingSource] to - * represent an updated snapshot of the backing dataset. If a [RemoteMediator] is set, calling - * [refresh] will also trigger a call to [RemoteMediator.load] with [LoadType] [REFRESH] to allow - * [RemoteMediator] to check for updates to the dataset backing [PagingSource]. - * - * Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh. - * Invalidation due repository-layer signals, such as DB-updates, should instead use - * [PagingSource.invalidate]. - * - * @see PagingSource.invalidate - */ - fun refresh() { - pagingDataDiffer.refresh() - } - - /** A [CombinedLoadStates] object which represents the current loading state. */ - var loadState: CombinedLoadStates by - mutableStateOf( - CombinedLoadStates( - refresh = InitialLoadStates.refresh, - prepend = InitialLoadStates.prepend, - append = InitialLoadStates.append, - source = InitialLoadStates - ) - ) - private set - - internal suspend fun collectLoadState() { - pagingDataDiffer.loadStateFlow.collect { loadState = it } - } - - internal suspend fun collectPagingData() { - flow.collectLatest { pagingDataDiffer.collectFrom(it) } - } -} - -private val IncompleteLoadState = LoadState.NotLoading(false) -private val InitialLoadStates = - LoadStates(IncompleteLoadState, IncompleteLoadState, IncompleteLoadState) - -/** - * Collects values from this [Flow] of [PagingData] and represents them inside a [LazyPagingItems] - * instance. The [LazyPagingItems] instance can be used by the [items] and [itemsIndexed] methods - * from [LazyListScope] in order to display the data obtained from a [Flow] of [PagingData]. - * - * @sample androidx.paging.compose.samples.PagingBackendSample - */ -@Composable -fun Flow>.collectAsLazyPagingItems(): LazyPagingItems { - val lazyPagingItems = remember(this) { LazyPagingItems(this) } - - LaunchedEffect(lazyPagingItems) { lazyPagingItems.collectPagingData() } - LaunchedEffect(lazyPagingItems) { lazyPagingItems.collectLoadState() } - - return lazyPagingItems -} - -/** - * Adds the [LazyPagingItems] and their content to the scope. The range from 0 (inclusive) to - * [LazyPagingItems.itemCount] (exclusive) always represents the full range of presentable items, - * because every event from [PagingDataDiffer] will trigger a recomposition. - * - * @sample androidx.paging.compose.samples.ItemsDemo - * - * @param items the items received from a [Flow] of [PagingData]. - * @param key a factory of stable and unique keys representing the item. Using the same key for - * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on - * Android. If null is passed the position in the list will represent the key. When you specify the - * key the scroll position will be maintained based on the key, which means if you add/remove items - * before the current visible item the item with the given key will be kept as the first visible - * one. - * @param itemContent the content displayed by a single item. In case the item is `null`, the - * [itemContent] method should handle the logic of displaying a placeholder instead of the main - * content displayed by an item which is not `null`. - */ -fun LazyListScope.items( - items: LazyPagingItems, - key: ((item: T) -> Any)? = null, - itemContent: @Composable LazyItemScope.(value: T?) -> Unit -) { - items( - count = items.itemCount, - key = - if (key == null) null - else - { index -> - val item = items.peek(index) - if (item == null) { - PagingPlaceholderKey(index) - } else { - key(item) - } - } - ) { index -> itemContent(items[index]) } -} - -/** - * Adds the [LazyPagingItems] and their content to the scope where the content of an item is aware - * of its local index. The range from 0 (inclusive) to [LazyPagingItems.itemCount] (exclusive) - * always represents the full range of presentable items, because every event from - * [PagingDataDiffer] will trigger a recomposition. - * - * @sample androidx.paging.compose.samples.ItemsIndexedDemo - * - * @param items the items received from a [Flow] of [PagingData]. - * @param key a factory of stable and unique keys representing the item. Using the same key for - * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on - * Android. If null is passed the position in the list will represent the key. When you specify the - * key the scroll position will be maintained based on the key, which means if you add/remove items - * before the current visible item the item with the given key will be kept as the first visible - * one. - * @param itemContent the content displayed by a single item. In case the item is `null`, the - * [itemContent] method should handle the logic of displaying a placeholder instead of the main - * content displayed by an item which is not `null`. - */ -fun LazyListScope.itemsIndexed( - items: LazyPagingItems, - key: ((index: Int, item: T) -> Any)? = null, - itemContent: @Composable LazyItemScope.(index: Int, value: T?) -> Unit -) { - items( - count = items.itemCount, - key = - if (key == null) null - else - { index -> - val item = items.peek(index) - if (item == null) { - PagingPlaceholderKey(index) - } else { - key(index, item) - } - } - ) { index -> itemContent(index, items[index]) } -} - -private data class PagingPlaceholderKey(private val index: Int) diff --git a/desktop/src/jvmMain/kotlin/Paging.kt b/desktop/src/jvmMain/kotlin/Paging.kt deleted file mode 100644 index 069e0144..00000000 --- a/desktop/src/jvmMain/kotlin/Paging.kt +++ /dev/null @@ -1,31 +0,0 @@ -import androidx.paging.PagingConfig -import androidx.paging.cachedIn -import com.kuuurt.paging.multiplatform.Pager -import com.kuuurt.paging.multiplatform.PagingResult -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview - -@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) -class Paging( - private val coroutineScope: CoroutineScope, -) { - private val api = Api() - private val pager = - Pager( - clientScope = coroutineScope, - config = PagingConfig(20), - initialKey = 1, - getItems = { currentKey, _ -> - val items = api.api.getHottestPosts(currentKey) - PagingResult( - items = items, - currentKey = currentKey, - prevKey = { if (currentKey == 1) null else currentKey - 1 }, - nextKey = { currentKey + 1 }, - ) - } - ) - val pagingData - get() = pager.pagingData.cachedIn(coroutineScope) -} diff --git a/desktop/src/jvmMain/kotlin/main.kt b/desktop/src/jvmMain/kotlin/main.kt deleted file mode 100644 index d68703de..00000000 --- a/desktop/src/jvmMain/kotlin/main.kt +++ /dev/null @@ -1,104 +0,0 @@ -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.WindowPlacement -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.rememberWindowState -import dev.msfjarvis.claw.api.LobstersApi -import dev.msfjarvis.claw.common.posts.LobstersCard -import dev.msfjarvis.claw.common.posts.PostActions -import dev.msfjarvis.claw.common.posts.toDbModel -import dev.msfjarvis.claw.common.theme.DarkThemeColors -import dev.msfjarvis.claw.common.theme.LightThemeColors -import dev.msfjarvis.claw.common.theme.LobstersTheme -import dev.msfjarvis.claw.common.urllauncher.UrlLauncher -import dev.msfjarvis.claw.database.local.SavedPost -import org.pushingpixels.aurora.component.AuroraVerticalScrollbar -import org.pushingpixels.aurora.theming.ceruleanSkin -import org.pushingpixels.aurora.window.AuroraWindow -import org.pushingpixels.aurora.window.auroraApplication - -fun main() = auroraApplication { - val paging = Paging(rememberCoroutineScope()) - val items = paging.pagingData.collectAsLazyPagingItems() - val urlLauncher = UrlLauncher() - val state = - rememberWindowState( - placement = WindowPlacement.Floating, - position = WindowPosition.Aligned(Alignment.Center), - ) - val postActions = remember { - object : PostActions { - override fun viewPost(postUrl: String, commentsUrl: String) { - urlLauncher.openUri(postUrl.ifEmpty { commentsUrl }) - } - - override fun viewComments(postId: String) { - urlLauncher.openUri("${LobstersApi.BASE_URL}/s/${postId}") - } - - override fun viewCommentsPage(commentsUrl: String) { - urlLauncher.openUri(commentsUrl) - } - - override fun toggleSave(post: SavedPost) {} - } - } - AuroraWindow( - skin = ceruleanSkin(), - title = "Claw", - state = state, - undecorated = true, - onCloseRequest = ::exitApplication, - ) { - val colorScheme = - if (isSystemInDarkTheme()) { - DarkThemeColors - } else { - LightThemeColors - } - LobstersTheme( - colorScheme = colorScheme, - providedValues = arrayOf(LocalUriHandler provides urlLauncher), - ) { - Box( - modifier = Modifier.fillMaxSize(), - ) { - val listState = rememberLazyListState() - if (items.itemCount == 0) { - Box(modifier = Modifier.fillMaxSize()) - } else { - LazyColumn( - state = listState, - ) { - items(items) { item -> - if (item != null) { - LobstersCard( - post = item.toDbModel(), - isSaved = false, - postActions = postActions, - modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp), - ) - } - } - } - } - AuroraVerticalScrollbar( - adapter = rememberScrollbarAdapter(listState), - modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), - ) - } - } - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3e0bbbb4..3872af86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,6 @@ accompanist = "0.26.0-alpha" agp = "7.2.1" androidXTest = "1.4.0" -aurora = "1.2-SNAPSHOT" coil = "2.1.0" # @keep This is used by paparazzi-tests and Renovate composeCompiler = "1.3.0-rc01" @@ -47,9 +46,6 @@ androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidXT androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidXTest" } androidx-test-uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0" androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workmanager" } -aurora-component = { module = "org.pushing-pixels:aurora-component", version.ref = "aurora" } -aurora-theming = { module = "org.pushing-pixels:aurora-theming", version.ref = "aurora" } -aurora-window = { module = "org.pushing-pixels:aurora-window", version.ref = "aurora" } build-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } build-cachefix = "org.gradle.android.cache-fix:org.gradle.android.cache-fix.gradle.plugin:2.5.5" build-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -66,15 +62,12 @@ copydown = "io.github.furstenheim:copy_down:1.0" dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger" } dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger" } javapoet = "com.squareup:javapoet:1.13.0" -kamel-image = "com.alialbaali.kamel:kamel-image:0.4.1" kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } -ktor-client-java = "io.ktor:ktor-client-java:2.0.3" material_motion-core = { module = "io.github.fornewid:material-motion-compose-core", version.ref = "material_motion" } material_motion-navigation = { module = "io.github.fornewid:material-motion-compose-navigation", version.ref = "material_motion" } -multiplatform-paging = "io.github.kuuuurt:multiplatform-paging:0.4.7" napier = "io.github.aakira:napier:2.6.1" okhttp-loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:3.14.9" retrofit-kotlinxSerializationConverter = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" @@ -88,8 +81,7 @@ testparameterinjector = "com.google.testparameterinjector:test-parameter-injecto [plugins] android-test = { id = "com.android.test", version.ref = "agp" } -aurora-svg-transcoder = { id = "org.pushing-pixels.aurora.tools.svgtranscoder.gradle", version.ref = "aurora" } -compose = "org.jetbrains.compose:1.2.0-alpha01-dev753" +aurora-svg-transcoder = "org.pushing-pixels.aurora.tools.svgtranscoder.gradle:1.2-SNAPSHOT" hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" } paparazzi = "app.cash.paparazzi:1.0.0" sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }