diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 217e5c51..f8467b45 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 4788961c..f2e1e462 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Хабы 🇷🇺 | [English version 🇬🇧](https://github.com/Garneg/Hubs/blob/master/README_EN.md)
+# Хабы
Неофициальный мобильный клиент для Хабра. Создан для изучения Jetpack Compose и Android.
### Скриншоты
diff --git a/app/build.gradle b/app/build.gradle
index 123ad760..b3121b96 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,6 +5,12 @@ plugins {
id "kotlin-kapt"
}
+def getDate() {
+ def date = new Date()
+ def formattedDate = date.format('HHmmddMMyy')
+ return formattedDate
+}
+
android {
namespace "com.garnegsoft.hubs"
compileSdk 34
@@ -13,8 +19,8 @@ android {
applicationId "com.garnegsoft.hubs"
minSdk 23
targetSdk 34
- versionCode 12
- versionName "2.3.4"
+ versionCode 13
+ versionName "2.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -26,6 +32,7 @@ android {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+
}
benchmark {
proguardFiles "proguard-rules.pro"
@@ -34,9 +41,9 @@ android {
debuggable false
}
debug {
-
debuggable true
- applicationIdSuffix ".debug"
+// applicationIdSuffix ".debug"
+ versionNameSuffix "-build" + getDate()
}
}
@@ -51,7 +58,7 @@ android {
compose true
}
composeOptions {
- kotlinCompilerExtensionVersion "1.4.7"
+ kotlinCompilerExtensionVersion "1.5.3"
}
packagingOptions {
resources {
@@ -64,27 +71,29 @@ kapt {
}
dependencies {
- var m3version = "1.1.1"
+ var m3version = "1.1.2"
implementation "androidx.compose.material3:material3:$m3version"
+ implementation "androidx.work:work-runtime-ktx:2.9.0"
+
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
- implementation "androidx.compose.runtime:runtime-livedata:1.5.0"
+ implementation "androidx.compose.runtime:runtime-livedata:1.5.4"
implementation "me.saket.telephoto:zoomable-image-coil:0.4.0"
implementation "org.jsoup:jsoup:1.16.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.5.1"
implementation "androidx.appcompat:appcompat:1.6.1"
- implementation "androidx.core:core-ktx:1.10.1"
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
- implementation "androidx.activity:activity-compose:1.7.2"
+ implementation "androidx.core:core-ktx:1.12.0"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
+ implementation "androidx.activity:activity-compose:1.8.1"
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
- implementation "androidx.compose.material:material:1.5.0"
+ implementation "androidx.compose.material:material:1.5.4"
implementation "com.squareup.okhttp3:okhttp:4.11.0"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
@@ -98,14 +107,15 @@ dependencies {
implementation "io.coil-kt:coil-svg:$coil_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.0"
- implementation "androidx.browser:browser:1.6.0"
- implementation "com.google.android.material:material:1.9.0"
+ implementation "androidx.browser:browser:1.7.0"
+ implementation "com.google.android.material:material:1.10.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
- implementation "androidx.core:core-ktx:1.10.1"
+ implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.room:room-ktx:$room_version"
+ implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
testImplementation "junit:junit:4.13.2"
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 9ffdfe2b..a32be191 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -46,4 +46,5 @@
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
--keepattributes RuntimeVisibleAnnotations,AnnotationDefault
\ No newline at end of file
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+-dontobfuscate
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/garnegsoft/hubs/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/garnegsoft/hubs/ExampleInstrumentedTest.kt
index 0efe9e19..1c0fc09c 100644
--- a/app/src/androidTest/java/com/garnegsoft/hubs/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/garnegsoft/hubs/ExampleInstrumentedTest.kt
@@ -20,5 +20,7 @@ class ExampleInstrumentedTest {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.garnegsoft.hubs", appContext.packageName)
+
+
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 36e7bb56..eb3bcc81 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -33,6 +33,10 @@
android:name="android.app.lib_name"
android:value="" />
+
+
@@ -50,6 +54,17 @@
android:name=".AuthActivity"
android:exported="false"
android:theme="@style/Theme.Hubs.Auth" />
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/MainActivity.kt b/app/src/main/java/com/garnegsoft/hubs/MainActivity.kt
index cda08d2e..4d1772eb 100644
--- a/app/src/main/java/com/garnegsoft/hubs/MainActivity.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/MainActivity.kt
@@ -1,50 +1,53 @@
package com.garnegsoft.hubs
-
-import android.content.Context
+import android.app.ActivityManager
+import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.webkit.CookieManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
-import androidx.compose.animation.EnterExitState
import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.animateColor
import androidx.compose.animation.core.EaseIn
+import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.EaseOut
-import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
+import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
import androidx.core.view.WindowCompat
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.coroutineScope
+import androidx.navigation.NavDeepLink
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.garnegsoft.hubs.api.HabrApi
+import com.garnegsoft.hubs.api.dataStore.AuthDataController
import com.garnegsoft.hubs.api.dataStore.HubsDataStore
-import com.garnegsoft.hubs.api.dataStore.settingsDataStoreFlowWithDefault
+import com.garnegsoft.hubs.api.dataStore.LastReadArticleController
import com.garnegsoft.hubs.api.me.Me
import com.garnegsoft.hubs.api.me.MeController
import com.garnegsoft.hubs.ui.screens.AboutScreen
@@ -53,47 +56,51 @@ import com.garnegsoft.hubs.ui.screens.article.ArticleScreen
import com.garnegsoft.hubs.ui.screens.comments.CommentsScreen
import com.garnegsoft.hubs.ui.screens.comments.CommentsThreadScreen
import com.garnegsoft.hubs.ui.screens.company.CompanyScreen
+import com.garnegsoft.hubs.ui.screens.history.HistoryScreen
import com.garnegsoft.hubs.ui.screens.hub.HubScreen
-import com.garnegsoft.hubs.ui.screens.main.ArticlesScreen
import com.garnegsoft.hubs.ui.screens.main.AuthorizedMenu
+import com.garnegsoft.hubs.ui.screens.main.MainScreen
import com.garnegsoft.hubs.ui.screens.main.UnauthorizedMenu
import com.garnegsoft.hubs.ui.screens.offline.OfflineArticlesScreen
import com.garnegsoft.hubs.ui.screens.search.SearchScreen
import com.garnegsoft.hubs.ui.screens.settings.ArticleScreenSettingsScreen
+import com.garnegsoft.hubs.ui.screens.settings.FeedSettingsScreen
import com.garnegsoft.hubs.ui.screens.settings.SettingsScreen
+import com.garnegsoft.hubs.ui.screens.user.LogoutConfirmDialog
import com.garnegsoft.hubs.ui.screens.user.UserScreen
import com.garnegsoft.hubs.ui.screens.user.UserScreenPages
import com.garnegsoft.hubs.ui.theme.HubsTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
// TODO: shouldn't be singleton
-var cookies: String = ""
+var cookies: String by mutableStateOf("")
var authorized: Boolean = false
-val Context.authDataStore by preferencesDataStore(HubsDataStore.Auth.DataStoreName)
-val Context.settingsDataStore by preferencesDataStore(HubsDataStore.Settings.DataStoreName)
-val Context.lastReadDataStore by preferencesDataStore(HubsDataStore.LastRead.DataStoreName)
-
class MainActivity : ComponentActivity() {
- @OptIn(ExperimentalAnimationApi::class, InternalAnimationApi::class)
+
+
+ @OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
+// (this.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
+// .clearApplicationUserData()
+
var authStatus: Boolean? by mutableStateOf(null)
- val cookiesFlow = authDataStore.data.map { it.get(HubsDataStore.Auth.Keys.Cookies) ?: "" }
- val isAuthorizedFlow =
- authDataStore.data.map { it[HubsDataStore.Auth.Keys.Authorized] ?: false }
+ val cookiesFlow = HubsDataStore.Auth.getValueFlow(this, HubsDataStore.Auth.Cookies)
+ val isAuthorizedFlow = HubsDataStore.Auth.getValueFlow(this, HubsDataStore.Auth.Authorized)
+
lifecycle.coroutineScope.launch {
cookiesFlow.collect {
cookies = it
+
}
}
@@ -104,409 +111,507 @@ class MainActivity : ComponentActivity() {
}
val authActivityLauncher =
- registerForActivityResult(AuthActivityResultContract()) { result ->
+ registerForActivityResult(AuthActivityResultContract()) { it ->
CookieManager.getInstance().removeAllCookies(null)
lifecycle.coroutineScope.launch {
- result?.let { res ->
- authDataStore.edit {
- it[HubsDataStore.Auth.Keys.Cookies] = res.split("; ")
- .find { it.startsWith("connect_sid") }!! + "; hl=ru; fl=ru"
- it[HubsDataStore.Auth.Keys.Authorized] = true
- authorized = true
- //cookies = result
+ it?.let { result ->
+ HubsDataStore.Auth.edit(
+ context = this@MainActivity,
+ pref = HubsDataStore.Auth.Cookies,
+ value = result.split("; ").find { it.startsWith("connect_sid") }!!
+ )
+
+ HubsDataStore.Auth.edit(
+ context = this@MainActivity,
+ pref = HubsDataStore.Auth.Authorized,
+ value = true
+ )
+ authorized = true
+
+ launch(Dispatchers.IO) {
+ MeController.getMe()?.let {
+ val shortcut = ShortcutInfoCompat.Builder(this@MainActivity, "bookmarks_shortcut")
+ .setIntent(
+ Intent(Intent.ACTION_VIEW).apply {
+ `package` = BuildConfig.APPLICATION_ID
+ data = Uri.parse("https://habr.com/users/${it.alias}/bookmarks")
+ }
+ )
+ .setIcon(IconCompat.createWithResource(this@MainActivity, R.drawable.bookmarks_shortcut_icon))
+ .setShortLabel("Закладки")
+ .setLongLabel("Закладки")
+ .build()
+ ShortcutManagerCompat.pushDynamicShortcut(this@MainActivity, shortcut)
+
+ }
+
}
}
}
}
-
intent.dataString?.let { Log.e("intentData", it) }
HabrApi.initialize(this)
+
+
setContent {
- val themeMode by settingsDataStoreFlowWithDefault(
- HubsDataStore.Settings.Keys.Theme,
- HubsDataStore.Settings.Keys.ThemeModes.Undetermined.ordinal
- ).collectAsState(initial = null)
- val theme by remember(themeMode) {
- mutableStateOf(
- themeMode?.let {
- HubsDataStore.Settings.Keys.ThemeModes.values()[it]
- } ?: HubsDataStore.Settings.Keys.ThemeModes.Undetermined
+ key(cookies) {
+ val themeMode by HubsDataStore.Settings
+ .getValueFlow(this, HubsDataStore.Settings.Theme.ColorSchemeMode)
+ .run { HubsDataStore.Settings.Theme.ColorSchemeMode.mapValues(this) }
+ .collectAsState(initial = null)
- )
- }
-
- var userInfo: Me? by remember { mutableStateOf(null) }
- val userInfoUpdateBlock = remember {
- {
- userInfo = MeController.getMe()
- Log.e("userInfoUpdateBlock", userInfo.toString())
- }
- }
- LaunchedEffect(
- key1 = isAuthorizedFlow.collectAsState(initial = false).value,
- block = {
- launch(Dispatchers.IO, block = { userInfoUpdateBlock() })
- })
- if (themeMode != null && authStatus != null) {
- HubsTheme(
- darkTheme = when (theme) {
- HubsDataStore.Settings.Keys.ThemeModes.Undetermined -> isSystemInDarkTheme()
- HubsDataStore.Settings.Keys.ThemeModes.SystemDefined -> isSystemInDarkTheme()
- HubsDataStore.Settings.Keys.ThemeModes.Dark -> true
- else -> false
+ var userInfo: Me? by remember { mutableStateOf(null) }
+ val userInfoUpdateBlock = remember {
+ {
+ userInfo = MeController.getMe()
+ Log.e("userInfoUpdateBlock", userInfo.toString())
}
- ) {
- val navController = rememberNavController()
-
- NavHost(
- modifier = Modifier
- .statusBarsPadding()
- .navigationBarsPadding(),
- navController = navController,
- startDestination = "articles",
- builder = {
-
- composable(
- "articles",
- exitTransition = { ExitTransition.None },
- popEnterTransition = { EnterTransition.None }
- ) {
+ }
+ LaunchedEffect(
+ key1 = isAuthorizedFlow.collectAsState(initial = false).value,
+ block = {
+ launch(Dispatchers.IO, block = { userInfoUpdateBlock() })
+ })
+ if (themeMode != null && authStatus != null) {
+ HubsTheme(
+ darkTheme = when (themeMode) {
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.SystemDefined -> isSystemInDarkTheme()
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Undetermined -> isSystemInDarkTheme()
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Dark -> true
+ else -> false
+ }
+ ) {
+ val navController = rememberNavController()
+
+ NavHost(
+ modifier = Modifier
+ .statusBarsPadding()
+ .navigationBarsPadding(),
+ navController = navController,
+ startDestination = "articles",
+ builder = {
- ArticlesScreen(
- viewModelStoreOwner = it,
- onSearchClicked = { navController.navigate("search") },
- onArticleClicked = {
- navController.navigate("article/$it")
- },
- onCommentsClicked = {
- navController.navigate("comments/$it")
- },
- onUserClicked = {
- navController.navigate("user/$it")
- },
- onCompanyClicked = {
- navController.navigate("company/$it")
- },
- onHubClicked = {
- navController.navigate("hub/$it")
- },
- menu = {
- val authorizedMenu by isAuthorizedFlow.collectAsState(
- initial = false
- )
- if (authorizedMenu && userInfo != null) {
- AuthorizedMenu(
- userAlias = userInfo!!.alias,
- avatarUrl = userInfo!!.avatarUrl,
- onProfileClick = { navController.navigate("user/${userInfo!!.alias}") },
- onArticlesClick = { navController.navigate("user/${userInfo!!.alias}?page=${UserScreenPages.Articles}") },
- onCommentsClick = { navController.navigate("user/${userInfo!!.alias}?page=${UserScreenPages.Comments}") },
- onBookmarksClick = { navController.navigate("user/${userInfo!!.alias}?page=${UserScreenPages.Bookmarks}") },
- onSavedArticlesClick = { navController.navigate("savedArticles") },
- onSettingsClick = { navController.navigate("settings") },
- onAboutClick = { navController.navigate("about") }
- )
- } else {
- UnauthorizedMenu(
- onLoginClick = {
- authActivityLauncher.launch(Unit)
- lifecycle.coroutineScope.launch(Dispatchers.IO) { userInfoUpdateBlock() }
- },
- onSavedArticlesClick = { navController.navigate("savedArticles") },
- onSettingsClick = { navController.navigate("settings") },
- onAboutClick = { navController.navigate("about") }
+ composable(
+ "articles",
+
+ popEnterTransition = { EnterTransition.None }
+ ) {
+
+ MainScreen(
+ viewModelStoreOwner = it,
+ onSearchClicked = { navController.navigate("search") },
+ onArticleClicked = {
+ navController.navigate("article/$it")
+ },
+ onCommentsClicked = {
+ navController.navigate("comments/$it")
+ },
+ onUserClicked = {
+ navController.navigate("user/$it")
+ },
+ onCompanyClicked = {
+ navController.navigate("company/$it")
+ },
+ onHubClicked = {
+ navController.navigate("hub/$it")
+ },
+ menu = {
+ val authorizedMenu by isAuthorizedFlow.collectAsState(
+ initial = false
)
+ if (authorizedMenu && userInfo != null) {
+ AuthorizedMenu(
+ userAlias = userInfo!!.alias,
+ avatarUrl = userInfo!!.avatarUrl,
+ onProfileClick = { navController.navigate("user/${userInfo!!.alias}") },
+ onArticlesClick = { navController.navigate("user/${userInfo!!.alias}?page=${UserScreenPages.Articles}") },
+ onCommentsClick = { navController.navigate("user/${userInfo!!.alias}?page=${UserScreenPages.Comments}") },
+ onBookmarksClick = {
+ navController.navigate(
+ "user/${userInfo!!.alias}?page=${UserScreenPages.Bookmarks}"
+ )
+ },
+ onSavedArticlesClick = {
+ navController.navigate(
+ "savedArticles"
+ )
+ },
+ onHistoryClick = { navController.navigate("history")},
+ onSettingsClick = { navController.navigate("settings") },
+ onAboutClick = { navController.navigate("about") }
+ )
+ } else {
+ UnauthorizedMenu(
+ onLoginClick = {
+ authActivityLauncher.launch(Unit)
+ lifecycle.coroutineScope.launch(
+ Dispatchers.IO
+ ) { userInfoUpdateBlock() }
+ },
+ onSavedArticlesClick = {
+ navController.navigate(
+ "savedArticles"
+ )
+ },
+ onHistoryClick = { navController.navigate("history")},
+ onSettingsClick = { navController.navigate("settings") },
+ onAboutClick = { navController.navigate("about") }
+ )
+ }
}
- }
- )
- }
-
- composable(
- route = "article/{id}?offline={offline}",
- deepLinks = ArticleNavDeepLinks,
- enterTransition = {
- scaleIn(
- tween(200, easing = EaseOut),
- 0.9f
- ) + fadeIn(
- tween(durationMillis = 200, easing = EaseIn)
)
- },
- exitTransition = {
- ExitTransition.None
- },
- popExitTransition = {
- scaleOut(
- tween(200, easing = EaseOut),
- 0.9f
- ) + fadeOut(
- tween(200, easing = EaseOut)
- )
-
- },
- popEnterTransition = {
- EnterTransition.None
}
- ) {
-
- val id = it.arguments?.getString("id")?.toIntOrNull()
- val offline = it.arguments?.getString("offline")?.toBooleanStrict()
- val clearLastArticle = remember {
- {
- lifecycle.coroutineScope.launch(Dispatchers.IO) {
- lastReadDataStore.edit {
- it[HubsDataStore.LastRead.Keys.LastArticleRead] = 0
- it[HubsDataStore.LastRead.Keys.LastArticleReadPosition] =
- 0
+ composable(
+ route = "article/{id}?offline={offline}",
+ deepLinks = ArticleNavDeepLinks,
+ enterTransition = {
+ scaleIn(
+ tween(150, easing = EaseInOut),
+ 0.9f
+ ) + fadeIn(
+ tween(durationMillis = 150, easing = EaseIn)
+ ) + slideInVertically(
+ tween(durationMillis = 150, easing = EaseIn),
+ initialOffsetY = { it / 9 }
+ )
+ },
+ popEnterTransition = {
+ fadeIn(
+ tween(durationMillis = 50, easing = EaseIn)
+ )
+ },
+ exitTransition = {
+ scaleOut(
+ tween(150, easing = EaseIn),
+ 0.9f
+ ) + fadeOut(
+ tween(150, easing = EaseOut)
+ )
+
+ },
+ popExitTransition = {
+ scaleOut(
+ tween(150, easing = EaseOut),
+ 0.9f
+ ) + fadeOut(
+ tween(150, easing = EaseOut)
+ )
+
+ },
+ ) {
+
+ val id = it.arguments?.getString("id")?.toIntOrNull()
+ val offline =
+ it.arguments?.getString("offline")?.toBooleanStrict()
+
+ val clearLastArticle = remember {
+ {
+ lifecycle.coroutineScope.launch(Dispatchers.IO) {
+ LastReadArticleController.clearLastArticle(this@MainActivity)
}
}
}
- }
-
- BackHandler(enabled = true) {
- clearLastArticle()
- if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
- this@MainActivity.finish()
- } else {
- navController.popBackStack()
- }
- }
-
-
- ArticleScreen(
- articleId = id!!,
- isOffline = offline ?: false,
- onBackButtonClicked = {
+
+ BackHandler(enabled = true) {
clearLastArticle()
if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
this@MainActivity.finish()
} else {
navController.popBackStack()
}
- },
- onCommentsClicked = {
- clearLastArticle()
- navController.navigate("comments/${id}")
- },
- onAuthorClicked = {
- clearLastArticle()
- navController.navigate("user/${it}")
- },
- onHubClicked = {
- clearLastArticle()
- navController.navigate("hub/$it")
- },
- onCompanyClick = {
- clearLastArticle()
- navController.navigate("company/$it")
- },
- onViewImageRequest = {
- navController.navigate(route = "imageViewer?imageUrl=$it")
- },
- onArticleClick = {
- navController.navigate("article/$it")
- },
- viewModelStoreOwner = it
- )
+ }
+
+ ArticleScreen(
+ articleId = id!!,
+ isOffline = offline ?: false,
+ onBackButtonClicked = {
+ clearLastArticle()
+ if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
+ this@MainActivity.finish()
+ } else {
+ navController.popBackStack()
+ }
+ },
+ onCommentsClicked = {
+ clearLastArticle()
+ navController.navigate("comments/${id}")
+ },
+ onAuthorClicked = {
+ clearLastArticle()
+ navController.navigate("user/${it}")
+ },
+ onHubClicked = {
+ clearLastArticle()
+ navController.navigate("hub/$it")
+ },
+ onCompanyClick = {
+ clearLastArticle()
+ navController.navigate("company/$it")
+ },
+ onViewImageRequest = {
+ navController.navigate(route = "imageViewer?imageUrl=$it")
+ },
+ onArticleClick = {
+ navController.navigate("article/$it")
+ },
+ viewModelStoreOwner = it
+ )
+
+
+ }
+ composable(route = "search") {
+ SearchScreen(
+ viewModelStoreOwner = it,
+ onArticleClicked = { navController.navigate("article/$it") },
+ onUserClicked = { navController.navigate("user/$it") },
+ onHubClicked = { navController.navigate("hub/$it") },
+ onCompanyClicked = { navController.navigate("company/$it") },
+ onCommentsClicked = { navController.navigate("comments/$it") },
+ onBackClicked = { navController.navigateUp() }
+ )
+ }
- }
-
- composable(route = "search") {
- SearchScreen(
- viewModelStoreOwner = it,
- onArticleClicked = { navController.navigate("article/$it") },
- onUserClicked = { navController.navigate("user/$it") },
- onHubClicked = { navController.navigate("hub/$it") },
- onCompanyClicked = { navController.navigate("company/$it") },
- onCommentsClicked = { navController.navigate("comments/$it") },
- onBackClicked = { navController.navigateUp() }
- )
- }
-
- composable(route = "settings") {
- SettingsScreen(
- onBack = {
- navController.popBackStack()
- },
- onArticleScreenSettings = {
- navController.navigate("article_settings")
- }
- )
- }
-
- composable(route = "article_settings") {
- ArticleScreenSettingsScreen(
- onBack = { navController.popBackStack() }
- )
- }
-
- composable(
- route = "comments/{postId}?commentId={commentId}",
- deepLinks = CommentsScreenNavDeepLinks
- ) {
- val postId = it.arguments!!.getString("postId")!!
- val commentId = it.arguments?.getString("commentId")
- CommentsScreen(
- viewModelStoreOwner = it,
- parentPostId = postId.toInt(),
- commentId = commentId?.toInt(),
- onBackClicked = {
- if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
- this@MainActivity.finish()
- } else {
+ composable(route = "settings") {
+ SettingsScreen(
+ onBack = {
navController.popBackStack()
+ },
+ onArticleScreenSettings = {
+ navController.navigate("article_settings")
+ },
+ onFeedSettings = {
+ navController.navigate("feed_settings")
}
- },
- onArticleClicked = { navController.navigate("article/$postId") },
- onUserClicked = { navController.navigate("user/$it") },
- onImageClick = { navController.navigate(route = "imageViewer?imageUrl=$it") },
- onThreadClick = { navController.navigate("thread/$postId/$it") }
- )
+ )
+ }
- }
-
- composable("thread/{articleId}/{threadId}") {
- val articleId = it.arguments!!.getString("articleId")?.toInt()
- val threadId = it.arguments!!.getString("threadId")?.toInt()
+ composable(route = "article_settings") {
+ ArticleScreenSettingsScreen(
+ onBack = { navController.popBackStack() }
+ )
+ }
- CommentsThreadScreen(
- articleId = articleId!!,
- threadId = threadId!!,
- onAuthor = { navController.navigate("user/$it") },
- onImageClick = { navController.navigate("image/$it") },
- onBack = { navController.popBackStack() }
- )
- }
-
- composable(
- "user/{alias}?page={page}",
- deepLinks = UserScreenNavDeepLinks
- ) {
+ composable(route = "feed_settings") {
+ FeedSettingsScreen(
+ onBack = { navController.popBackStack()}
+ )
+ }
- val page =
- it.arguments?.getString("page")
- ?.let { UserScreenPages.valueOf(it) }
- ?: UserScreenPages.Profile
- val deepLinkPage =
- it.arguments?.getString("deepLinkPage")?.let {
- when (it) {
- "posts" -> UserScreenPages.Articles
- "comments" -> UserScreenPages.Comments
- "bookmarks" -> UserScreenPages.Bookmarks
-
- else -> null
- }
- }
- val alias = it.arguments!!.getString("alias")!!
+ composable(
+ route = "comments/{postId}?commentId={commentId}",
+ deepLinks = CommentsScreenNavDeepLinks
+ ) {
+ val postId = it.arguments!!.getString("postId")!!
+ val commentId = it.arguments?.getString("commentId")
+ CommentsScreen(
+ viewModelStoreOwner = it,
+ parentPostId = postId.toInt(),
+ commentId = commentId?.toInt(),
+ onBackClicked = {
+ if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
+ this@MainActivity.finish()
+ } else {
+ navController.popBackStack()
+ }
+ },
+ onArticleClicked = { navController.navigate("article/$postId") },
+ onUserClicked = { navController.navigate("user/$it") },
+ onImageClick = { navController.navigate(route = "imageViewer?imageUrl=$it") },
+ onThreadClick = { navController.navigate("thread/$postId/$it") }
+ )
+
+ }
- val logoutCoroutineScope = rememberCoroutineScope()
- UserScreen(
- isAppUser = alias == userInfo?.alias,
- initialPage = deepLinkPage ?: page,
- alias = alias,
- onBack = {
- if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
- this@MainActivity.finish()
- } else {
- navController.popBackStack()
+ composable("thread/{articleId}/{threadId}") {
+ val articleId =
+ it.arguments!!.getString("articleId")?.toInt()
+ val threadId = it.arguments!!.getString("threadId")?.toInt()
+
+ CommentsThreadScreen(
+ articleId = articleId!!,
+ threadId = threadId!!,
+ onAuthor = { navController.navigate("user/$it") },
+ onImageClick = { navController.navigate("image/$it") },
+ onBack = { navController.popBackStack() }
+ )
+ }
+
+ composable(
+ route = "user/{alias}?page={page}",
+ deepLinks = UserScreenNavDeepLinks,
+ popExitTransition = { fadeOut(tween(50)) },
+
+ ) {
+
+ val page =
+ it.arguments?.getString("page")
+ ?.let { UserScreenPages.valueOf(it) }
+ ?: UserScreenPages.Profile
+ val deepLinkPage =
+ it.arguments?.getString("deepLinkPage")?.let {
+ when (it) {
+ "posts" -> UserScreenPages.Articles
+ "comments" -> UserScreenPages.Comments
+ "bookmarks" -> UserScreenPages.Bookmarks
+
+ else -> null
+ }
}
- },
- onArticleClicked = { navController.navigate("article/$it") },
- onUserClicked = { navController.navigate("user/$it") },
- onCommentsClicked = { navController.navigate("comments/$it") },
- onCommentClicked = { postId, commentId ->
- navController.navigate(
- "comments/$postId?commentId=$commentId"
- )
- },
- onCompanyClick = { navController.navigate("company/$it") },
- viewModelStoreOwner = it,
- onLogout = {
- logoutCoroutineScope.launch {
- authDataStore.edit {
- it[HubsDataStore.Auth.Keys.Authorized] = false
- it[HubsDataStore.Auth.Keys.Cookies] = ""
+ val alias = it.arguments!!.getString("alias")!!
+
+ val logoutCoroutineScope = rememberCoroutineScope()
+ var showLogoutConfirmationDialog by remember { mutableStateOf(false) }
+ LogoutConfirmDialog(
+ show = showLogoutConfirmationDialog,
+ onDismiss = { showLogoutConfirmationDialog = false },
+ onProceed = {
+ logoutCoroutineScope.launch {
+ val shortcuts = ShortcutManagerCompat.getDynamicShortcuts(this@MainActivity).map { it.id }
+ ShortcutManagerCompat.disableShortcuts(this@MainActivity, shortcuts, "Вы вышли из приложения!")
+
+ AuthDataController.clearAuthData(this@MainActivity)
+
+ authorized = false
+ navController.popBackStack(
+ "articles",
+ inclusive = false
+ )
+ showLogoutConfirmationDialog = false
}
- //cookies = ""
- authorized = false
- navController.popBackStack(
- "articles",
- inclusive = false
+ })
+ UserScreen(
+ isAppUser = alias == userInfo?.alias,
+ initialPage = deepLinkPage ?: page,
+ alias = alias,
+ onBack = {
+ if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
+ this@MainActivity.finish()
+ } else {
+ navController.popBackStack()
+ }
+ },
+ onArticleClicked = { navController.navigate("article/$it") },
+ onUserClicked = { navController.navigate("user/$it") },
+ onCommentsClicked = { navController.navigate("comments/$it") },
+ onCommentClicked = { postId, commentId ->
+ navController.navigate(
+ "comments/$postId?commentId=$commentId"
)
+ },
+ onCompanyClick = { navController.navigate("company/$it") },
+ viewModelStoreOwner = it,
+ onLogout = {
+ showLogoutConfirmationDialog = true
+ },
+ onHubClicked = {
+ navController.navigate("hub/$it")
}
- },
- onHubClicked = {
- navController.navigate("hub/$it")
+ )
+ if (this.transition.isRunning) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .pointerInput(this.transition.isRunning) {})
}
- )
- }
-
- composable(
- "hub/{alias}",
- deepLinks = HubScreenNavDeepLinks
- ) {
- val alias = it.arguments?.getString("alias")
- HubScreen(alias = alias!!, viewModelStoreOwner = it,
- onBackClick = {
- if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
- this@MainActivity.finish()
- } else {
- navController.popBackStack()
- }
- },
- onArticleClick = { navController.navigate("article/$it") },
- onCompanyClick = { navController.navigate("company/$it") },
- onUserClick = { navController.navigate("user/$it") },
- onCommentsClick = { navController.navigate("comments/$it") }
- )
- }
- composable(
- "company/{alias}",
- deepLinks = CompanyScreenNavDeepLinks,
- ) {
- val alias = it.arguments?.getString("alias")!!
- CompanyScreen(
- viewModelStoreOwner = it,
- alias = alias,
- onBack = {
- if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
- this@MainActivity.finish()
- } else {
- navController.popBackStack()
- }
- },
- onArticleClick = { navController.navigate("article/$it") },
- onCommentsClick = { navController.navigate("comments/$it") },
- onUserClick = { navController.navigate("user/$it") }
- )
- }
-
- composable("about") {
- AboutScreen {
- navController.popBackStack()
}
- }
-
- composable("savedArticles") {
- OfflineArticlesScreen(
- onBack = { navController.popBackStack() },
- onArticleClick = { navController.navigate("article/$it?offline=true") }
- )
- }
-
- composable("imageViewer?imageUrl={imageUrl}") {
- val url = it.arguments?.getString("imageUrl")
- ImageViewScreen(
- model = url!!,
- onBack = { navController.popBackStack() })
- }
- })
-
+
+ composable(
+ "hub/{alias}",
+ deepLinks = HubScreenNavDeepLinks,
+ popExitTransition = { fadeOut(tween(50)) }
+ ) {
+ val alias = it.arguments?.getString("alias")
+ HubScreen(alias = alias!!, viewModelStoreOwner = it,
+ onBackClick = {
+ if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
+ this@MainActivity.finish()
+ } else {
+ navController.popBackStack()
+ }
+ },
+ onArticleClick = { navController.navigate("article/$it") },
+ onCompanyClick = { navController.navigate("company/$it") },
+ onUserClick = { navController.navigate("user/$it") },
+ onCommentsClick = { navController.navigate("comments/$it") }
+ )
+
+ if (this.transition.isRunning) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .pointerInput(this.transition.isRunning) {})
+ }
+ }
+ composable(
+ "company/{alias}",
+ deepLinks = CompanyScreenNavDeepLinks,
+ popExitTransition = { fadeOut(tween(50)) }
+ ) {
+ val alias = it.arguments?.getString("alias")!!
+ CompanyScreen(
+ viewModelStoreOwner = it,
+ alias = alias,
+ onBack = {
+ if (this@MainActivity.intent.data != null && navController.previousBackStackEntry == null) {
+ this@MainActivity.finish()
+ } else {
+ navController.popBackStack()
+ }
+ },
+ onArticleClick = { navController.navigate("article/$it") },
+ onCommentsClick = { navController.navigate("comments/$it") },
+ onUserClick = { navController.navigate("user/$it") }
+ )
+
+ if (this.transition.isRunning) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .pointerInput(this.transition.isRunning) {})
+ }
+ }
+
+ composable("about") {
+ AboutScreen {
+ navController.popBackStack()
+ }
+ }
+
+ composable(
+ route = "savedArticles",
+ deepLinks = listOf(NavDeepLink("hubs://saved-articles"))
+ ) {
+
+ OfflineArticlesScreen(
+ onBack = { navController.popBackStack() },
+ onArticleClick = { navController.navigate("article/$it?offline=true") }
+ )
+ }
+
+ composable("imageViewer?imageUrl={imageUrl}") {
+ val url = it.arguments?.getString("imageUrl")
+ ImageViewScreen(
+ model = url!!,
+ onBack = { navController.popBackStack() })
+ }
+
+ composable("history"){
+ HistoryScreen(
+ onBack = { navController.popBackStack() },
+ onArticleClick = { navController.navigate("article/$it")},
+ onUserClick = { navController.navigate("user/$it")},
+ onHubClick = { navController.navigate("hub/$it")},
+ onCompanyClick = { navController.navigate("company/$it")}
+ )
+ }
+ })
+
+ }
}
}
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/NavDeepLinks.kt b/app/src/main/java/com/garnegsoft/hubs/NavDeepLinks.kt
index 4c9ed9d9..75b17b89 100644
--- a/app/src/main/java/com/garnegsoft/hubs/NavDeepLinks.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/NavDeepLinks.kt
@@ -19,6 +19,9 @@ val UserScreenNavDeepLinks = listOf(
NavDeepLink("https://habr.com/{lang}/users/{alias}/{deepLinkPage}/{subPage}"),
NavDeepLink("https://habr.com/users/{alias}"),
NavDeepLink("https://habr.com/users/{alias}/"),
+ NavDeepLink("https://habr.com/users/{alias}/{deepLinkPage}"),
+ NavDeepLink("https://habr.com/users/{alias}/{deepLinkPage}/")
+
)
val HubScreenNavDeepLinks = listOf(
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/BaseHabrSnippetListModel.kt b/app/src/main/java/com/garnegsoft/hubs/api/BaseHabrSnippetListModel.kt
index 5122b282..78cb7348 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/BaseHabrSnippetListModel.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/BaseHabrSnippetListModel.kt
@@ -11,8 +11,8 @@ import kotlinx.coroutines.launch
abstract class AbstractSnippetListModel(
- override val path: String,
- override val baseArgs: Map,
+ open val path: String,
+ open val baseArgs: Map,
initialFilter: Filter? = null,
open val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default),
) : HabrSnippetListModel where S : HabrSnippet {
@@ -43,7 +43,7 @@ abstract class AbstractSnippetListModel(
override val isRefreshing: LiveData get() = _isRefreshing
private val _isLoadingNextPage = MutableLiveData(false)
- val isLoadingNextPage: LiveData get() = _isLoadingNextPage
+ override val isLoadingNextPage: LiveData get() = _isLoadingNextPage
private val _lastLoadedPage = MutableLiveData()
override val lastLoadedPage: LiveData
@@ -98,8 +98,6 @@ abstract class AbstractSnippetListModel(
interface HabrSnippetListModel where T : HabrSnippet {
- val path: String
- val baseArgs: Map
val data: LiveData?>
val lastLoadedPage: LiveData
@@ -107,6 +105,8 @@ interface HabrSnippetListModel where T : HabrSnippet {
val isLoading: LiveData
val isRefreshing: LiveData
+
+ val isLoadingNextPage: LiveData
fun load(args: Map): HabrList?
@@ -119,4 +119,3 @@ interface HabrSnippetListModel where T : HabrSnippet {
}
-
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/Enums.kt b/app/src/main/java/com/garnegsoft/hubs/api/Enums.kt
index 09614f62..98dba79e 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/Enums.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/Enums.kt
@@ -11,17 +11,14 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withAnnotation
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.sp
import com.garnegsoft.hubs.ui.theme.HubsTheme
@@ -97,14 +94,14 @@ enum class PostType {
}
}
-enum class PostComplexity {
+enum class PublicationComplexity {
Low,
Medium,
High,
None;
companion object{
- fun fromString(complexity: String?): PostComplexity {
+ fun fromString(complexity: String?): PublicationComplexity {
// if (complexity == null)
// return PostComplexity.None
return when(complexity){
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/HabrApi.kt b/app/src/main/java/com/garnegsoft/hubs/api/HabrApi.kt
index c4f5a897..417de5e4 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/HabrApi.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/HabrApi.kt
@@ -36,6 +36,11 @@ class HabrApi {
.build()
}
+
+ // Shouldn't be used in production. Use only in tests
+ fun setHttpClient(client: OkHttpClient) {
+ HttpClient = client
+ }
fun get(path: String, args: Map? = null, version: Int = 2): Response? {
val finalArgs = mutableMapOf("hl" to "ru", "fl" to "ru")
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/Article.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/Article.kt
index e638e9a8..b59efa9d 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/article/Article.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/article/Article.kt
@@ -2,50 +2,50 @@ package com.garnegsoft.hubs.api.article
import com.garnegsoft.hubs.api.ArticleFormat
import com.garnegsoft.hubs.api.EditorVersion
-import com.garnegsoft.hubs.api.PostComplexity
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.api.PostType
/**
* Represents article
*/
-class Article(
-
- val id: Int,
-
- val title: String,
-
- val timePublished: String,
-
- val author: Author?,
-
- val contentHtml: String,
-
- val isCorporative: Boolean,
-
- val editorVersion: EditorVersion,
-
- val hubs: List,
-
- val tags: List,
-
- val statistics: Statistics,
-
- val format: ArticleFormat?,
-
- val postType: PostType,
-
- val metadata: Metadata?,
-
- val readingTime: Int,
-
- val complexity: PostComplexity,
-
- val relatedData: RelatedData?,
-
- val translationData: TranslationData,
-
- val polls: List
+data class Article(
+
+ val id: Int,
+
+ val title: String,
+
+ val timePublished: String,
+
+ val author: Author?,
+
+ val contentHtml: String,
+
+ val isCorporative: Boolean,
+
+ val editorVersion: EditorVersion,
+
+ val hubs: List,
+
+ val tags: List,
+
+ val statistics: Statistics,
+
+ val format: ArticleFormat?,
+
+ val postType: PostType,
+
+ val metadata: Metadata?,
+
+ val readingTime: Int,
+
+ val complexity: PublicationComplexity,
+
+ val relatedData: RelatedData?,
+
+ val translationData: TranslationData,
+
+ val polls: List
) {
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/ArticleController.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/ArticleController.kt
index 65704eb2..a4463f23 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/article/ArticleController.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/article/ArticleController.kt
@@ -152,7 +152,7 @@ class ArticleController {
metadata = if (it.metadata != null) com.garnegsoft.hubs.api.article.Article.Metadata(
it.metadata!!.mainImageUrl
) else null,
- complexity = PostComplexity.fromString(it.complexity),
+ complexity = PublicationComplexity.fromString(it.complexity),
readingTime = it.readingTime,
relatedData = it.relatedData?.let {
com.garnegsoft.hubs.api.article.Article.RelatedData(
@@ -255,7 +255,7 @@ class ArticleController {
)
},
- complexity = PostComplexity.fromString(formatted.complexity),
+ complexity = PublicationComplexity.fromString(formatted.complexity),
readingTime = formatted.readingTime,
relatedData = formatted.relatedData?.let {
com.garnegsoft.hubs.api.article.Article.RelatedData(
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticleSnippet.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticleSnippet.kt
index 28c3ecb2..1fe5b320 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticleSnippet.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticleSnippet.kt
@@ -9,92 +9,87 @@ import com.garnegsoft.hubs.api.article.Article
*/
@Immutable
data class ArticleSnippet(
-
- /**
+
+ /**
* Id of the post
*/
override val id: Int,
-
- /**
+
+ /**
* Formatted time of publishing of the post
*/
val timePublished: String,
-
- /**
+
+ /**
* Is corporative post or not
*/
val isCorporative: Boolean,
-
- /**
+
+ /**
* Title of the post
*/
val title: String,
-
- /**
+
+ /**
* Version of editor used for writing the post
*/
val editorVersion: EditorVersion,
-
- /**
+
+ /**
* Type of the post
*/
val type: PostType,
-
- /**
+
+ /**
* Labels of the post (e.g. translate)
*/
val labels: List?,
-
- /**
+
+ /**
* Author of the post
*/
val author: Article.Author? = null,
-
- /**
+
+ /**
* Unformatted statistics data, better use formatted statistics field
*/
val statistics: Article.Statistics,
-
- /**
+
+ /**
* Hubs that include the post
*/
val hubs: List?,
-
- /**
+
+ /**
* Text snippet of the post, max length is 3500 characters
*/
val textSnippet: String,
-
- /**
+
+ /**
* Url of image to draw attention (a.k.a. КДПВ)
*/
val imageUrl: String?,
-
- /**
- * Tags of the post
- */
- val tags: List? = null,
-
- /**
+
+ /**
* Format of the post
*/
val format: ArticleFormat?,
-
- /**
+
+ /**
* Time to read article
*/
val readingTime: Int,
-
- /**
+
+ /**
* Complexity of the article
*/
- val complexity: PostComplexity,
-
- /**
+ val complexity: PublicationComplexity,
+
+ /**
* Data related to app user
*/
val relatedData: Article.RelatedData?,
-
- val isTranslation: Boolean
+
+ val isTranslation: Boolean
) : HabrSnippet
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticlesListController.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticlesListController.kt
index 9bc6ef2f..d76f08d4 100755
--- a/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticlesListController.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/article/list/ArticlesListController.kt
@@ -36,8 +36,6 @@ class ArticlesListController {
var articleIdsfinal = mutableListOf()
val articlesRefs = mutableMapOf()
-
-
articlesIds.forEach {
try {
articlesRefs += mapOf(
@@ -140,7 +138,7 @@ class ArticlesListController {
),
imageUrl = it.leadData.imageUrl,
textSnippet = it.leadData.textHtml,
- complexity = PostComplexity.fromString(it.complexity),
+ complexity = PublicationComplexity.fromString(it.complexity),
readingTime = it.readingTime,
relatedData = it.relatedData?.let {
com.garnegsoft.hubs.api.article.Article.RelatedData(
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/offline/InArticleImageEntity.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/offline/InArticleImageEntity.kt
deleted file mode 100644
index 1fb25121..00000000
--- a/app/src/main/java/com/garnegsoft/hubs/api/article/offline/InArticleImageEntity.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.garnegsoft.hubs.api.article.offline
-
-import androidx.room.*
-
-
-@Entity(
- tableName = "in_article_images",
-)
-data class InArticleImageEntity(
-
- @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
- val data: ByteArray,
-
- @ColumnInfo(name = "original_url", index = true)
- val originalUrl: String,
-
- @ColumnInfo(name = "parent_article_id", index = true)
- val parentArticleId: Int,
-
- @PrimaryKey(autoGenerate = true)
- val id: Long = 0,
-)
-
-@Dao
-interface InArticleImagesDao {
-
- @Upsert
- fun upsert(entity: InArticleImageEntity)
-
- @Query("DELETE FROM in_article_images WHERE parent_article_id = :articleId")
- fun deleteAllBelongsToArticle(articleId: Int)
-
- @Delete
- fun delete(entity: InArticleImageEntity)
-
- @Query("SELECT * FROM in_article_images WHERE parent_article_id = :articleId")
- fun getAllBelongsToArticle(articleId: Int): List
-
- @Query("SELECT * FROM in_article_images WHERE original_url = :url LIMIT 1")
- fun getByUrl(url: String): InArticleImageEntity
-
-}
-
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticle.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticle.kt
new file mode 100644
index 00000000..e9711fd9
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticle.kt
@@ -0,0 +1,184 @@
+package com.garnegsoft.hubs.api.article.offline
+
+import android.content.Context
+import androidx.room.*
+import androidx.room.migration.Migration
+import kotlinx.coroutines.flow.Flow
+
+
+private const val SNIPPETS_TABLE_NAME = "offline_articles_snippets"
+private const val ARTICLES_TABLE_NAME = "offline_articles"
+private const val DATABASE_NAME = "offline_db"
+
+@Entity(
+ tableName = SNIPPETS_TABLE_NAME,
+ indices = [Index("article_id", unique = true)]
+)
+data class OfflineArticleSnippet(
+
+ @ColumnInfo("article_id")
+ val articleId: Int,
+
+ @ColumnInfo("author_name")
+ val authorName: String?,
+
+ @ColumnInfo("author_avatar_url")
+ val authorAvatarUrl: String?,
+
+ @ColumnInfo("time_published")
+ val timePublished: String,
+
+ val title: String,
+
+ @ColumnInfo("reading_time")
+ val readingTime: Int,
+
+ @ColumnInfo("is_translation")
+ val isTranslation: Boolean,
+
+ @ColumnInfo("text_snippet")
+ val textSnippet: String,
+
+ @ColumnInfo("thumbnail_url")
+ val thumbnailUrl: String?,
+
+ @TypeConverters(HubsConverter::class)
+ val hubs: HubsList,
+
+ @ColumnInfo
+ @PrimaryKey(autoGenerate = true)
+ val id: Int = 0,
+)
+
+@Entity(
+ tableName = ARTICLES_TABLE_NAME,
+)
+data class OfflineArticle(
+ @ColumnInfo("article_id", index = true)
+ val articleId: Int,
+
+ @ColumnInfo("author_name")
+ val authorName: String?,
+
+ @ColumnInfo("author_avatar_url")
+ val authorAvatarUrl: String?,
+
+ @ColumnInfo("time_published")
+ val timePublished: String,
+
+ val title: String,
+
+ @ColumnInfo("reading_time")
+ val readingTime: Int,
+
+ @ColumnInfo("is_translation")
+ val isTranslation: Boolean,
+
+
+ @ColumnInfo("content_html")
+ val contentHtml: String,
+
+ @TypeConverters(HubsConverter::class)
+ val hubs: HubsList,
+
+ @PrimaryKey(autoGenerate = true)
+ val id: Int = 0,
+
+ )
+
+@ProvidedTypeConverter
+class HubsConverter {
+
+ @TypeConverter
+ fun fromHubs(hubs: HubsList): String {
+ return hubs.hubsList.joinToString(",")
+ }
+
+ @TypeConverter
+ fun fromString(hubs: String): HubsList {
+ return HubsList(hubs.split(","))
+ }
+
+
+}
+
+class HubsList(
+ val hubsList: List
+)
+
+val Context.offlineArticlesDatabase: OfflineArticlesDatabase
+ get() = OfflineArticlesDatabase.getDb(this)
+
+
+@Dao
+interface OfflineArticlesDao {
+
+ /**
+ * @return article entity by **article_id**, not by room table id
+ */
+ @Query("SELECT * FROM $ARTICLES_TABLE_NAME WHERE article_id = :articleId")
+ fun getArticleById(articleId: Int): OfflineArticle
+
+ @Query("SELECT EXISTS (SELECT * FROM $ARTICLES_TABLE_NAME WHERE article_id = :articleId)")
+ fun exists(articleId: Int): Boolean
+
+ @Query("SELECT EXISTS (SELECT * FROM $ARTICLES_TABLE_NAME WHERE article_id = :articleId)")
+ fun existsFlow(articleId: Int): Flow
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertSnippet(entity: OfflineArticleSnippet)
+
+ @Delete
+ fun deleteSnippet(entity: OfflineArticleSnippet)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(entity: OfflineArticle)
+
+ @Query("DELETE FROM $ARTICLES_TABLE_NAME WHERE article_id = :id")
+ fun delete(id: Int)
+
+ @Query("DELETE FROM $SNIPPETS_TABLE_NAME WHERE article_id = :id")
+ fun deleteSnippet(id: Int)
+
+ /**
+ * @return list of article entities from older to newer
+ */
+ @Query("SELECT * FROM $SNIPPETS_TABLE_NAME ORDER BY id ASC")
+ fun getAllSnippetsSortedByIdAsc(): Flow>
+
+ /**
+ * @return list of article entities from newer to older
+ */
+ @Query("SELECT * FROM $SNIPPETS_TABLE_NAME ORDER BY id DESC")
+ fun getAllSnippetsSortedByIdDesc(): Flow>
+
+}
+
+@TypeConverters(HubsConverter::class)
+@Database(
+ entities = [OfflineArticleSnippet::class, OfflineArticle::class],
+ version = 2
+)
+abstract class OfflineArticlesDatabase : RoomDatabase() {
+
+ abstract fun articlesDao(): OfflineArticlesDao
+
+ companion object {
+ @Volatile
+ private var instance: OfflineArticlesDatabase? = null
+ fun getDb(context: Context): OfflineArticlesDatabase {
+ return instance ?: synchronized(this) {
+ val inst = Room.databaseBuilder(
+ context = context,
+ klass = OfflineArticlesDatabase::class.java,
+ name = DATABASE_NAME
+ )
+ .addTypeConverter(HubsConverter())
+ .fallbackToDestructiveMigration()
+ .build()
+ instance = inst
+ inst
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticleSnippet.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticleSnippet.kt
deleted file mode 100644
index 476623c2..00000000
--- a/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticleSnippet.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-package com.garnegsoft.hubs.api.article.offline
-
-import android.content.Context
-import androidx.room.*
-import kotlinx.coroutines.flow.Flow
-
-
-private const val SNIPPETS_TABLE_NAME = "offline_articles_snippets"
-private const val ARTICLES_TABLE_NAME = "offline_articles"
-private const val DATABASE_NAME = "offline_db"
-
-@Entity(
- tableName = SNIPPETS_TABLE_NAME,
- indices = [Index("article_id", unique = true),]
-)
-data class OfflineArticleSnippet(
-
- @ColumnInfo("article_id")
- val articleId: Int,
-
- @ColumnInfo("author_name")
- val authorName: String?,
-
- @ColumnInfo("author_avatar_url")
- val authorAvatarUrl: String?,
-
- @ColumnInfo("time_published")
- val timePublished: String,
-
- val title: String,
-
- @ColumnInfo("reading_time")
- val readingTime: Int,
-
- @ColumnInfo("is_translation")
- val isTranslation: Boolean,
-
- @ColumnInfo("text_snippet")
- val textSnippet: String,
-
- @ColumnInfo("thumbnail_url")
- val thumbnailUrl: String?,
-
- @TypeConverters(HubsConverter::class)
- val hubs: HubsList,
-
- @ColumnInfo
- @PrimaryKey(autoGenerate = true)
- val id: Int = 0,
-)
-
-@Entity(
- tableName = ARTICLES_TABLE_NAME,
-)
-data class OfflineArticle(
- @ColumnInfo("article_id", index = true)
- val articleId: Int,
-
- @ColumnInfo("author_name")
- val authorName: String?,
-
- @ColumnInfo("author_avatar_url")
- val authorAvatarUrl: String?,
-
- @ColumnInfo("time_published")
- val timePublished: String,
-
- val title: String,
-
- @ColumnInfo("reading_time")
- val readingTime: Int,
-
- @ColumnInfo("is_translation")
- val isTranslation: Boolean,
-
-
- @ColumnInfo("content_html")
- val contentHtml: String,
-
- @TypeConverters(HubsConverter::class)
- val hubs: HubsList,
-
- @PrimaryKey(autoGenerate = true)
- val id: Int = 0,
-
- )
-
-@ProvidedTypeConverter
-class HubsConverter {
-
- @TypeConverter
- fun fromHubs(hubs: HubsList): String {
- return hubs.hubsList.joinToString(",")
- }
-
- @TypeConverter
- fun fromString(hubs: String): HubsList {
- return HubsList(hubs.split(","))
- }
-
-
-}
-
-class HubsList(
- val hubsList: List
-)
-
-val Context.offlineArticlesDatabase: OfflineArticlesDatabase
- get() = OfflineArticlesDatabase.getDb(this)
-
-
-@Dao
-interface OfflineArticlesDao{
-
- /**
- * @return article entity by **article_id**, not just by id
- */
- @Query("SELECT * FROM $ARTICLES_TABLE_NAME WHERE article_id = :articleId")
- suspend fun _getArticleById(articleId: Int): OfflineArticle
-
- @Query("SELECT EXISTS (SELECT * FROM $ARTICLES_TABLE_NAME WHERE article_id = :articleId)")
- suspend fun exists(articleId: Int): Boolean
-
- @Query("SELECT EXISTS (SELECT * FROM $ARTICLES_TABLE_NAME WHERE article_id = :articleId)")
- fun existsFlow(articleId: Int): Flow
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertSnippet(entity: OfflineArticleSnippet)
-
- @Delete
- suspend fun deleteSnippet(entity: OfflineArticleSnippet)
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insert(entity: OfflineArticle)
-
- @Query("DELETE FROM $ARTICLES_TABLE_NAME WHERE article_id = :id")
- suspend fun delete(id: Int)
-
- @Query("DELETE FROM $SNIPPETS_TABLE_NAME WHERE article_id = :id")
- suspend fun deleteSnippet(id: Int)
-
- /**
- * @return list of article entities from older to newer
- */
- @Query("SELECT * FROM $SNIPPETS_TABLE_NAME ORDER BY id ASC")
- fun getAllSnippetsSortedByIdAsc(): Flow>
-
- /**
- * @return list of article entities from newer to older
- */
- @Query("SELECT * FROM $SNIPPETS_TABLE_NAME ORDER BY id DESC")
- fun getAllSnippetsSortedByIdDesc(): Flow>
-
-}
-
-@TypeConverters(HubsConverter::class)
-@Database(entities = [OfflineArticleSnippet::class, InArticleImageEntity::class, OfflineArticle::class], version = 1)
-abstract class OfflineArticlesDatabase : RoomDatabase() {
-
- abstract fun articlesDao(): OfflineArticlesDao
- abstract fun imagesDao(): InArticleImagesDao
-
- companion object {
- @Volatile
- private var instance: OfflineArticlesDatabase? = null
- fun getDb(context: Context): OfflineArticlesDatabase {
- return instance ?: synchronized(this){
- val inst = Room.databaseBuilder(
- context = context,
- klass = OfflineArticlesDatabase::class.java,
- name = DATABASE_NAME
- )
- .addTypeConverter(HubsConverter()).build()
- instance = inst
- inst
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticlesController.kt b/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticlesController.kt
index db35ab39..d8d7cf45 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticlesController.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/article/offline/OfflineArticlesController.kt
@@ -1,7 +1,28 @@
package com.garnegsoft.hubs.api.article.offline
import ArticleController
+import android.app.Notification
import android.content.Context
+import android.os.Build
+import android.os.Looper
+import android.widget.Toast
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.ForegroundInfo
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import com.garnegsoft.hubs.R
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.io.File
class OfflineArticlesController {
@@ -10,19 +31,26 @@ class OfflineArticlesController {
private var _dao: OfflineArticlesDao? = null
private fun getDao(context: Context): OfflineArticlesDao {
- if (_dao == null){
- synchronized(this){
+
+ if (_dao == null) {
+ synchronized(this) {
_dao = OfflineArticlesDatabase.getDb(context).articlesDao()
return _dao!!
}
} else {
return _dao!!
-
}
}
- suspend fun downloadArticle(articleId: Int, context: Context): Boolean {
+ fun downloadArticle(articleId: Int, context: Context) {
+ val request = OneTimeWorkRequestBuilder()
+ .setInputData(Data.Builder().putInt("ARTICLE_ID", articleId).build())
+ .build()
+ WorkManager.getInstance(context).enqueue(request)
+ }
+
+ private fun _downloadArticle(articleId: Int, context: Context): Boolean {
val dao = getDao(context)
val article = ArticleController.getOfflineArticle(articleId)
@@ -30,19 +58,215 @@ class OfflineArticlesController {
if (article == null || articleSnippet == null) {
return false
}
- dao.insert(article)
- dao.insertSnippet(articleSnippet)
+
+ val newArticle = Downloader.downloadArticleResources(article, context)
+ val newArticleSnippet = Downloader.downloadArticleSnippetResources(articleSnippet, context)
+
+ dao.insert(newArticle)
+ dao.insertSnippet(newArticleSnippet)
return true
}
- suspend fun deleteArticle(articleId: Int, context: Context): Boolean {
+ fun deleteArticle(articleId: Int, context: Context): Boolean {
+ val request = OneTimeWorkRequestBuilder()
+ .setInputData(Data.Builder().putInt("ARTICLE_ID", articleId).build())
+ .build()
+ WorkManager.getInstance(context).enqueue(request)
+
+ return false
+ }
+
+ private fun _deleteArticle(articleId: Int, context: Context): Boolean {
+
+ val dir = File(context.filesDir, "offline_resources")
+ if (!dir.exists())
+ return false
+
+ val thisArticleDir = File(dir, articleId.toString())
+
+ if (!thisArticleDir.exists() || !thisArticleDir.deleteRecursively())
+ return false
+
val dao = getDao(context)
- if (dao.exists(articleId)){
+ if (dao.exists(articleId)) {
dao.delete(articleId)
dao.deleteSnippet(articleId)
return true
}
return false
}
+
+ class DownloadOfflineArticleResourcesWorker(
+ appContext: Context,
+ params: WorkerParameters
+ ) : CoroutineWorker(appContext, params) {
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val notification = if (Build.VERSION.SDK_INT > 26) {
+ Notification.Builder(applicationContext, "download_article_worker_channel")
+ .setSmallIcon(R.drawable.download).build()
+ } else {
+ Notification()
+ }
+ return ForegroundInfo(0, notification)
+
+ }
+ override suspend fun doWork(): Result {
+ setForeground(getForegroundInfo())
+ val articleId = inputData.getInt("ARTICLE_ID", -1)
+ if (articleId < 1) {
+ Looper.prepare()
+ Toast.makeText(
+ applicationContext,
+ "Invalid article id was passed to the worker!",
+ Toast.LENGTH_SHORT
+ ).show()
+ return Result.failure()
+ }
+ withContext(Dispatchers.IO) {
+ _downloadArticle(articleId, applicationContext)
+ Looper.prepare()
+ Toast.makeText(
+ applicationContext,
+ "Статья скачана!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ return Result.success()
+ }
+
+ }
+
+
+ class DeleteOfflineArticleResourcesWorker(
+ appContext: Context,
+ params: WorkerParameters
+ ) : CoroutineWorker(appContext, params) {
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val notification = if (Build.VERSION.SDK_INT > 26) {
+ Notification.Builder(applicationContext, "delete_article_worker_channel")
+ .setSmallIcon(R.drawable.offline).build()
+ } else {
+ Notification()
+ }
+ return ForegroundInfo(0, notification)
+
+ }
+ override suspend fun doWork(): Result {
+ setForeground(getForegroundInfo())
+ val articleId = inputData.getInt("ARTICLE_ID", -1)
+ if (articleId < 1) {
+ Looper.prepare()
+ Toast.makeText(
+ applicationContext,
+ "Invalid article id was passed to the worker!",
+ Toast.LENGTH_SHORT
+ ).show()
+ return Result.failure()
+ }
+ withContext(Dispatchers.IO) {
+ _deleteArticle(articleId, applicationContext)
+ Looper.prepare()
+ Toast.makeText(
+ applicationContext,
+ "Статья удалена из сохраненных!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ return Result.success()
+ }
+
+ }
+
+ object Downloader {
+ val client: OkHttpClient = OkHttpClient()
+
+ fun downloadArticleResources(article: OfflineArticle, context: Context): OfflineArticle {
+ var articleResult = article
+ var urls = mutableListOf()
+
+ var imageCounter = 0
+ val result = Jsoup.parse(article.contentHtml).forEachNode {
+ if(it is Element && it.tagName() == "img"){
+ var url = if (it.hasAttr("data-src")){
+ it.attr("data-src")
+ } else {
+ it.attr("src")
+ }
+
+ urls.add(url)
+ it.attr("data-src", "offline-article:${article.articleId}/img$imageCounter.${url.split(".").last()}")
+
+ imageCounter++
+ }
+
+ } as Document
+
+ downloadImages(urls, article.articleId, context)
+
+ article.authorAvatarUrl?.let { url ->
+ val request = Request.Builder().get().url(url).build()
+ client.newCall(request).execute().body?.let {
+ val dir = File(context.filesDir, "offline_resources")
+ if (!dir.exists())
+ dir.mkdir()
+ val thisArticleDir = File(dir, article.articleId.toString())
+ val filename = "authorAvatar.png"
+ val avatarImageFile = File(thisArticleDir, filename)
+ avatarImageFile.writeBytes(it.bytes())
+ }
+ articleResult = articleResult.copy(authorAvatarUrl = "offline-article:${article.articleId}/authorAvatar.png")
+ }
+
+ return articleResult.copy(contentHtml = result.body().html())
+ }
+
+ private fun downloadImages(urls: Collection, articleId: Int, context: Context) {
+ val dir = File(context.filesDir, "offline_resources")
+ if (!dir.exists())
+ dir.mkdir()
+ val thisArticleDir = File(dir, articleId.toString())
+ thisArticleDir.mkdir()
+ runBlocking(Dispatchers.IO) {
+ urls.forEachIndexed { index, url ->
+ launch {
+ val request = Request.Builder().get().url(url).build()
+ val bytes = client.newCall(request).execute().body?.bytes()
+ bytes?.let {
+ val resourceFile =
+ File(thisArticleDir, "img$index.${url.split(".").last()}")
+ resourceFile.createNewFile()
+ resourceFile.writeBytes(it)
+ }
+ }
+ }
+ }
+ }
+
+ // Downloads only thumbnail image because avatar of user should be downloaded by downloadArticleResources method
+ fun downloadArticleSnippetResources(snippet: OfflineArticleSnippet, context: Context): OfflineArticleSnippet {
+ var articleSnippet = snippet
+ snippet.thumbnailUrl?.let { url ->
+ val request = Request.Builder().get().url(url).build()
+ client.newCall(request).execute().body?.let {
+ val dir = File(context.filesDir, "offline_resources")
+ if (!dir.exists())
+ dir.mkdir()
+ val thisArticleDir = File(dir, snippet.articleId.toString())
+ if (!thisArticleDir.exists())
+ thisArticleDir.mkdir()
+ val filename = "thumbnail.${url.split(".").last()}"
+ val thumbnailImageFile = File(thisArticleDir, filename)
+ thumbnailImageFile.createNewFile()
+ thumbnailImageFile.writeBytes(it.bytes())
+ articleSnippet = articleSnippet.copy(thumbnailUrl = "offline-article:${snippet.articleId}/$filename")
+ }
+
+ }
+ articleSnippet = articleSnippet.copy(authorAvatarUrl = "offline-article:${snippet.articleId}/authorAvatar.png")
+ return articleSnippet
+ }
+ }
}
+
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/dataStore/AuthDataController.kt b/app/src/main/java/com/garnegsoft/hubs/api/dataStore/AuthDataController.kt
new file mode 100644
index 00000000..4f55538a
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/api/dataStore/AuthDataController.kt
@@ -0,0 +1,28 @@
+package com.garnegsoft.hubs.api.dataStore
+
+import android.content.Context
+import kotlinx.coroutines.flow.Flow
+
+class AuthDataController {
+ companion object {
+ suspend fun clearAuthData(context: Context) {
+ HubsDataStore.Auth.edit(
+ context = context,
+ pref = HubsDataStore.Auth.Authorized,
+ value = false
+ )
+ HubsDataStore.Auth.edit(
+ context = context,
+ pref = HubsDataStore.Auth.Cookies,
+ value = ""
+ )
+ }
+
+ fun isAuthorizedFlow(context: Context): Flow {
+ return HubsDataStore.Auth.getValueFlow(
+ context = context,
+ pref = HubsDataStore.Auth.Authorized)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/dataStore/DataStoreMethods.kt b/app/src/main/java/com/garnegsoft/hubs/api/dataStore/DataStoreMethods.kt
deleted file mode 100644
index 8846dfb3..00000000
--- a/app/src/main/java/com/garnegsoft/hubs/api/dataStore/DataStoreMethods.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.garnegsoft.hubs.api.dataStore
-
-import android.content.Context
-import androidx.datastore.preferences.core.Preferences
-import com.garnegsoft.hubs.authDataStore
-import com.garnegsoft.hubs.lastReadDataStore
-import com.garnegsoft.hubs.settingsDataStore
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-fun Context.lastReadDataStoreFlow(key: Preferences.Key): Flow {
- return lastReadDataStore.data.map { it[key] }
-}
-
-fun Context.settingsDataStoreFlow(key: Preferences.Key): Flow {
- return settingsDataStore.data.map { it[key] }
-}
-
-fun Context.settingsDataStoreFlowWithDefault(
- key: Preferences.Key,
- defaultValue: T
-): Flow {
- return settingsDataStore.data.map { it[key] ?: defaultValue }
-}
-
-fun Context.authDataStoreFlowWithDefault(key: Preferences.Key, defaultValue: T): Flow {
- return authDataStore.data.map { it[key] ?: defaultValue }
-}
-
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/dataStore/HubsDataStore.kt b/app/src/main/java/com/garnegsoft/hubs/api/dataStore/HubsDataStore.kt
index 914a3cc6..6f62c70b 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/dataStore/HubsDataStore.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/dataStore/HubsDataStore.kt
@@ -1,61 +1,139 @@
package com.garnegsoft.hubs.api.dataStore
+import android.content.Context
+import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
object HubsDataStore {
- object Settings {
- const val DataStoreName = "settings"
- object Keys {
- /**
- * Theme of app.
- * - 0 - undetermined
- * - 1 - light
- * - 2 - dark
- * - 3 - defined by system
- */
- val Theme = intPreferencesKey("theme_mode")
- enum class ThemeModes {
- Undetermined,
- Light,
- Dark,
- SystemDefined
- }
- object ArticleScreen {
- val FontSize = floatPreferencesKey("article_font_size")
- val LineHeightFactor = floatPreferencesKey("line_height_factor")
- val TextWrapMode = intPreferencesKey("article_text_wrap")
- val Indent = intPreferencesKey("article_indent")
- }
- object Comments {
- val CommentsDisplayMode = intPreferencesKey("comment_display_mode")
- enum class CommentsDisplayModes {
- Default,
- Threads,
- }
- }
+ object Settings : SingleDataStore(name = "settings") {
+ object Theme {
+ /**
+ * Theme of app.
+ * - 0 - undetermined
+ * - 1 - light
+ * - 2 - dark
+ * - 3 - defined by system
+ *
+ * You also have **mapValues** method for mapping int to enum values
+ */
+ object ColorSchemeMode : DataStorePreference {
+ override val key = intPreferencesKey("theme_mode")
+ override val defaultValue = ColorScheme.SystemDefined.ordinal
+
+ fun mapValues(flow: Flow): Flow {
+ return flow.map { ColorScheme.values()[it] }
+ }
+
+ enum class ColorScheme {
+ Undetermined,
+ Light,
+ Dark,
+ SystemDefined
+ }
+ }
+
+ }
+
+ object ArticleScreen {
+ val FontSize = DataStorePreference.FloatPreference("article_font_size", 16f)
+// val LineHeightFactor = floatPreferencesKey("line_height_factor")
+// val TextWrapMode = intPreferencesKey("article_text_wrap")
+// val Indent = intPreferencesKey("article_indent")
+ }
+
+ object ArticleCard {
+ val TextSnippetFontSize = DataStorePreference.FloatPreference("article_card_snippet_font_size", 16f)
+ val ShowTextSnippet = DataStorePreference.BooleanPreference("article_card_show_snippet", true)
+ val ShowImage = DataStorePreference.BooleanPreference("article_card_show_image", true)
+ val TitleFontSize = DataStorePreference.FloatPreference("article_card_title_font_size", 20f)
+ val TextSnippetMaxLines = DataStorePreference.IntPreference("article_card_snippet_max_lines", 4)
+ }
+
+ object CommentsDisplayMode : DataStorePreference {
+
+ override val key = intPreferencesKey("comment_display_mode")
+
+ override val defaultValue = CommentsDisplayModes.Default.ordinal
+
+ enum class CommentsDisplayModes {
+ Default,
+ Threads,
+ }
+ }
+ }
+
+ object Auth : SingleDataStore(name = "auth") {
+ val Authorized = DataStorePreference.BooleanPreference("authorized", false)
+ val Cookies = DataStorePreference.StringPreference("cookies", "")
+ }
+
+ object LastRead : SingleDataStore(name = "last_read") {
+
+ val LastArticleRead = DataStorePreference.IntPreference("last_article", 0)
+ //val LastArticleReadPosition = intPreferencesKey("last_article_position")
+
+ }
+
+}
+interface DataStorePreference {
+ val key: Preferences.Key
+ val defaultValue: T
+
+ class FloatPreference(
+ name: String,
+ override val defaultValue: Float
+ ) : DataStorePreference {
+ override val key = floatPreferencesKey(name)
+ }
+
+ class BooleanPreference(
+ name: String,
+ override val defaultValue: Boolean
+ ) : DataStorePreference {
+ override val key = booleanPreferencesKey(name)
+ }
+
+ class StringPreference(
+ name: String,
+ override val defaultValue: String
+ ) : DataStorePreference {
+ override val key = stringPreferencesKey(name)
+ }
+
+ class IntPreference(
+ name: String,
+ override val defaultValue: Int
+ ) : DataStorePreference {
+ override val key = intPreferencesKey(name)
+ }
+
+}
- }
- }
- object Auth {
- const val DataStoreName = "auth"
- object Keys {
- val Authorized = booleanPreferencesKey("authorized")
- val Cookies = stringPreferencesKey("cookies")
- }
+abstract class SingleDataStore(
+ private val name: String
+) {
+
+ private val Context.store by preferencesDataStore(name)
+
+ fun getValueFlow(
+ context: Context,
+ pref: DataStorePreference,
+ defaultValue: T = pref.defaultValue
+ ): Flow {
+ return context.store.data.map { it.get(pref.key) ?: defaultValue }
+ }
+
+ suspend fun edit(context: Context, pref: DataStorePreference, value: T) {
+ context.store.edit { it.set(pref.key, value) }
+ }
+}
- }
-
- object LastRead {
- const val DataStoreName = "last_read"
- object Keys{
- val LastArticleRead = intPreferencesKey("last_article")
- val LastArticleReadPosition = intPreferencesKey("last_article_position")
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/dataStore/LastReadArticleController.kt b/app/src/main/java/com/garnegsoft/hubs/api/dataStore/LastReadArticleController.kt
new file mode 100644
index 00000000..160cac6c
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/api/dataStore/LastReadArticleController.kt
@@ -0,0 +1,30 @@
+package com.garnegsoft.hubs.api.dataStore
+
+import android.content.Context
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+
+class LastReadArticleController {
+ companion object {
+ suspend fun clearLastArticle(context: Context) {
+ HubsDataStore.LastRead.edit(
+ context,
+ HubsDataStore.LastRead.LastArticleRead,
+ 0
+ )
+ }
+
+ suspend fun setLastArticle(context: Context, articleId: Int) {
+ HubsDataStore.LastRead.edit(
+ context,
+ HubsDataStore.LastRead.LastArticleRead,
+ articleId
+ )
+ }
+
+ fun getLastArticleFlow(context: Context): Flow {
+ return HubsDataStore.LastRead.getValueFlow(context, HubsDataStore.LastRead.LastArticleRead)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryController.kt b/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryController.kt
new file mode 100644
index 00000000..7e37d589
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryController.kt
@@ -0,0 +1,22 @@
+package com.garnegsoft.hubs.api.history
+
+import ArticleController
+import android.content.Context
+
+class HistoryController {
+ companion object {
+ fun insertArticle(articleId: Int, context: Context) {
+ val dao = HistoryDatabase.getDb(context).dao()
+ dao.getEventsPaged(0, 1).firstOrNull()?.let {
+ if (it.actionType == HistoryActionType.Article && it.getArticle().articleId == articleId)
+ return
+ }
+
+ ArticleController.getSnippet(articleId)?.let {
+ val data = HistoryArticle(articleId, it.title, it.author?.alias ?: "", it.author?.avatarUrl ?: "", it.imageUrl)
+ dao.insertEvent(data.toHistoryEntity())
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryEntity.kt b/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryEntity.kt
new file mode 100644
index 00000000..d45e8c8b
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryEntity.kt
@@ -0,0 +1,170 @@
+package com.garnegsoft.hubs.api.history
+
+import android.content.Context
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Delete
+import androidx.room.Entity
+import androidx.room.Ignore
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.Upsert
+import com.garnegsoft.hubs.api.HabrSnippet
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import java.util.Calendar
+import kotlin.math.ceil
+
+@Entity(tableName = "history")
+data class HistoryEntity(
+
+ /**
+ * Data in json about the event depending on type.
+ */
+ @ColumnInfo("data")
+ val data: String,
+
+ @ColumnInfo("action_type")
+ val actionType: HistoryActionType,
+
+ @ColumnInfo("timestamp")
+ val timestamp: Long,
+
+ @PrimaryKey(autoGenerate = true)
+ override val id: Int = 0
+) : HabrSnippet
+
+enum class HistoryActionType {
+ Undefined,
+ Article,
+ UserProfile,
+ HubProfile,
+ CompanyProfile,
+ Comments
+}
+
+
+@Dao
+interface HistoryDao {
+
+ @Upsert
+ fun insertEvent(event: HistoryEntity): Long
+
+ @Query("SELECT * FROM history ORDER BY timestamp DESC LIMIT :eventsPerPage OFFSET :pageIndex * :eventsPerPage")
+ fun getEventsPaged(pageIndex: Int, eventsPerPage: Int = 20): List
+
+ @Delete
+ fun deleteEvent(event: HistoryEntity)
+
+ @Query("DELETE FROM history")
+ fun clearAll()
+
+ @Query("SELECT COUNT(id) FROM history")
+ fun eventsCount(): Int
+
+ fun pagesCount(eventsPerPage: Int = 20): Int {
+ return ceil(eventsCount().toFloat() / eventsPerPage).toInt()
+ }
+}
+
+@Serializable
+sealed class HistoryType() {
+ @get:Ignore
+ abstract val actionType: HistoryActionType
+ fun toHistoryEntity(timestamp: Long = Calendar.getInstance().time.time) : HistoryEntity {
+ val data = Json.encodeToString(this)
+ return HistoryEntity(data, actionType, timestamp)
+ }
+}
+
+@Serializable
+data class HistoryArticle(
+ val articleId: Int,
+ val title: String,
+ val authorAlias: String,
+ val authorAvatarUrl: String,
+ val thumbnailUrl: String?,
+) : HistoryType() {
+ override val actionType: HistoryActionType = HistoryActionType.Article
+}
+
+@Serializable
+data class HistoryUser(
+ val alias: String,
+ val avatarUrl: String,
+) : HistoryType() {
+ override val actionType = HistoryActionType.UserProfile
+}
+
+@Serializable
+data class HistoryHub(
+ val alias: String,
+ val avatarUrl: String,
+) : HistoryType() {
+ override val actionType = HistoryActionType.HubProfile
+}
+
+@Serializable
+data class HistoryCompany(
+ val alias: String,
+ val avatarUrl: String?,
+) : HistoryType() {
+ override val actionType = HistoryActionType.CompanyProfile
+}
+
+@Serializable
+data class HistoryComments(
+ val parentArticle: HistoryArticle,
+) : HistoryType() {
+ override val actionType = HistoryActionType.Comments
+}
+
+
+private val json = Json { ignoreUnknownKeys = true }
+
+fun HistoryEntity.getArticle() : HistoryArticle {
+ return json.decodeFromString(data)
+}
+
+fun HistoryEntity.getUser() : HistoryUser {
+ return json.decodeFromString(data)
+}
+
+fun HistoryEntity.getHub() : HistoryHub {
+ return json.decodeFromString(data)
+}
+
+fun HistoryEntity.getCompany() : HistoryCompany {
+ return json.decodeFromString(data)
+}
+
+fun HistoryEntity.getComments() : HistoryComments {
+ return json.decodeFromString(data)
+}
+
+
+@Database(entities = [HistoryEntity::class], version = 1)
+abstract class HistoryDatabase : RoomDatabase() {
+
+ abstract fun dao(): HistoryDao
+
+ companion object {
+ @Volatile
+ private var instance: HistoryDatabase? = null
+ fun getDb(context: Context): HistoryDatabase {
+ return instance ?: synchronized(this){
+ val inst = Room.databaseBuilder(
+ context = context,
+ klass = HistoryDatabase::class.java,
+ name = "history"
+ ).build()
+ instance = inst
+ inst
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryEntityListModel.kt b/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryEntityListModel.kt
new file mode 100644
index 00000000..01abdf00
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/api/history/HistoryEntityListModel.kt
@@ -0,0 +1,88 @@
+package com.garnegsoft.hubs.api.history
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.garnegsoft.hubs.api.HabrList
+import com.garnegsoft.hubs.api.article.HabrSnippetListModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+class HistoryEntityListModel(
+ val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default),
+ val dao: HistoryDao
+) : HabrSnippetListModel {
+
+ private val _data = MutableLiveData?>()
+
+ override val data: LiveData?>
+ get() = _data
+
+ private val _isLoading = MutableLiveData(false)
+ override val isLoading: LiveData
+ get() = _isLoading
+
+ private val _isRefreshing = MutableLiveData(false)
+ override val isRefreshing: LiveData get() = _isRefreshing
+
+ private val _isLoadingNextPage = MutableLiveData(false)
+ override val isLoadingNextPage: LiveData get() = _isLoadingNextPage
+
+ override fun load(args: Map): HabrList =
+ HabrList(dao.getEventsPaged(pageNumber), pagesCount = dao.pagesCount())
+
+
+ private val _lastLoadedPage = MutableLiveData()
+ override val lastLoadedPage: LiveData
+ get() = _lastLoadedPage
+
+ private var pageNumber = 0
+
+ private fun _load(): HabrList? {
+ _isLoading.postValue(true)
+ val result = load(emptyMap())
+ _isLoading.postValue(false)
+ return result
+ }
+
+ override fun refresh() {
+ coroutineScope.launch(Dispatchers.IO) {
+ _isRefreshing.postValue(true)
+ pageNumber = 0
+ _lastLoadedPage.postValue(pageNumber)
+ _data.postValue(_load())
+ _isRefreshing.postValue(false)
+ }
+ }
+
+ override fun loadNextPage() {
+ coroutineScope.launch(Dispatchers.IO) {
+ if (_data.value!!.pagesCount > pageNumber) {
+ _isLoadingNextPage.postValue(true)
+ pageNumber++
+ var doRetry = true
+ while (doRetry) {
+ _load()?.let { nextPage ->
+ _data.value?.let {
+ delay(1000)
+ _data.postValue(it + nextPage)
+ doRetry = false
+ _lastLoadedPage.postValue(pageNumber)
+ }
+ } ?: delay(500)
+ }
+ _isLoadingNextPage.postValue(false)
+ }
+ }
+ }
+
+ override fun loadFirstPage() {
+ coroutineScope.launch(Dispatchers.IO) {
+ _data.postValue(_load())
+ _lastLoadedPage.postValue(0)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/history/search/SearchHistoryEntity.kt b/app/src/main/java/com/garnegsoft/hubs/api/history/search/SearchHistoryEntity.kt
new file mode 100644
index 00000000..45431446
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/api/history/search/SearchHistoryEntity.kt
@@ -0,0 +1,5 @@
+package com.garnegsoft.hubs.api.history.search
+
+
+class SearchHistoryEntity {
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/hub/HubController.kt b/app/src/main/java/com/garnegsoft/hubs/api/hub/HubController.kt
index 4d9e0668..cebd12b3 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/hub/HubController.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/hub/HubController.kt
@@ -30,7 +30,7 @@ class HubController {
return get(path = "hubs/$alias/profile")
}
- fun get(path: String, args: Map? = null): Hub? {
+ private fun get(path: String, args: Map? = null): Hub? {
var raw = getProfile(path, args)
var result: Hub? = null
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/user/UserController.kt b/app/src/main/java/com/garnegsoft/hubs/api/user/UserController.kt
index bf7ce264..8f2c1bd7 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/user/UserController.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/user/UserController.kt
@@ -10,271 +10,283 @@ import kotlinx.serialization.json.*
import org.jsoup.Jsoup
class UserController {
-
- companion object {
- private fun _get(path: String, args: Map? = null): UserProfileData? {
- val response = HabrApi.get(path, args)
-
- var result: UserProfileData? = null
-
- if (response?.code != 200)
- return null
-
- response.body?.let {
- var customJson = Json { ignoreUnknownKeys = true }
-
- result =
- customJson.decodeFromJsonElement(customJson.parseToJsonElement(it.string()))
-
- result?.let {
- if (it.avatarUrl != null) {
- it.avatarUrl = "https:" + it.avatarUrl!!.replace("habrastorage", "hsto")
- }
- else {
- it.avatarUrl = placeholderAvatarUrl(it.alias)
- }
- it.registerDateTime = formatTime(it.registerDateTime).split(' ')
- .run { "${this[0]} ${this[1]} ${this[2]}" }
-
- if (it.birthday != null)
- it.birthday = formatBirthdate(it.birthday!!)
-
- it.lastActivityDateTime =
- it.lastActivityDateTime?.let { it1 -> formatTime(it1) }
- }
- result
- }
-
- return result
- }
-
- private fun _whoIs(path: String, args: Map? = null): WhoIs? {
- val response = HabrApi.get(path, args)
- val responseBody = response?.body?.string()
- if (response?.code != 200 || responseBody == null){
- return null
- }
- responseBody.let {
- val result = HabrDataParser.parseJson(it)
- result.invitedBy?.let {
- result.invitedBy!!.timeCreated = formatTime(it.timeCreated)
- }
-
- return result
- }
- return null
- }
-
- private fun _note(path: String, args: Map? = null): Note? {
- val response = HabrApi.get(path, args)
-
- if (response?.code != 200)
- return null
-
- response.body?.string()?.let {
- val result = HabrDataParser.parseJson(it)
- result.text?.let {
- result.text = Jsoup.parse(it).text()
- }
- return result
- }
- return null
- }
-
- fun get(
- alias: String,
- args: Map? = null,
- ): User? {
- val raw = _get("users/$alias/card", args)
-
- var result: User? = null
-
- raw?.let {
-
- result = User(
- alias = it.alias,
- fullname = if (it.fullname?.isEmpty() == true) null else it.fullname,
- avatarUrl = it.avatarUrl,
- speciality = if (it.speciality?.isEmpty() == true) null else it.speciality,
- rating = it.rating,
- ratingPosition = it.ratingPos,
- score = it.scoreStats.score,
- followersCount = it.followStats.followersCount,
- subscriptionsCount = it.followStats.followCount,
- isReadonly = it.isReadonly,
- registrationDate = it.registerDateTime,
- lastActivityDate = it.lastActivityDateTime,
- birthday = it.birthday,
- canBeInvited = it.canBeInvited,
- location = it.location?.let {
- val buffer = StringBuilder()
- it.city?.title?.let {
- buffer.append("$it, ")
- }
- it.region?.title?.let {
- //buffer.append("$it, ")
- }
- it.country?.title?.let{
- buffer.append(it)
- }
-
- buffer.toString()
- },
- articlesCount = it.counterStats.postCount,
- commentsCount = it.counterStats.commentCount,
- bookmarksCount = it.counterStats.favoriteCount,
- workPlaces = it.workplace.map { User.WorkPlace(it.title, it.alias) },
- relatedData = it.relatedData?.let{ User.RelatedData(it.isSubscribed)}
- )
- }
-
- return result
- }
-
- fun whoIs(
- alias: String,
- args: Map? = null,
- ): User.WhoIs? {
- val raw = _whoIs("users/$alias/whois", args)
-
- raw?.let {
- return User.WhoIs(
- aboutHtml = it.aboutHtml,
- badges = it.badgets.map { User.WhoIs.Badge(it.title, it.description) },
- invite = it.invitedBy?.let { User.WhoIs.Invite(it.issuerLogin, it.timeCreated) },
- contacts = it.contacts.map { User.WhoIs.Contact(it.title, it.url, it.favicon) }
- )
- }
- return null
- }
-
- fun note(
- alias: String,
- args: Map? = null
- ): User.Note? {
- val raw = _note("users/$alias/note", args)
-
- raw?.let {
- return User.Note(it.text)
- }
- return null
- }
-
- /**
- * Subscribe/unsubscribe to user.
- * @return subscription status
- * @throws UnsupportedOperationException
- */
- fun subscription(alias: String): Boolean {
- val response = HabrApi.post("users/$alias/following/toggle")
-
- response.body?.string()?.let {
- return Json.parseToJsonElement(it).jsonObject["isSubscribed"]?.jsonPrimitive!!.boolean
- }
- throw UnsupportedOperationException("User is not authorized")
-
- }
- }
-
-
- @Serializable
- data class Note(
- var text: String?
- )
-
- @Serializable
- data class WhoIs(
- var alias: String,
- var badgets: List,
- var aboutHtml: String,
- var contacts: List,
- var invitedBy: InvitedBy? = null
- )
-
- @Serializable
- data class Badget(
- var title: String,
- var description: String,
+
+ companion object {
+ private fun _get(path: String, args: Map? = null): UserProfileData? {
+ val response = HabrApi.get(path, args)
+
+ var result: UserProfileData? = null
+
+ if (response?.code != 200)
+ return null
+
+ response.body?.let {
+ var customJson = Json { ignoreUnknownKeys = true }
+
+ result =
+ customJson.decodeFromJsonElement(customJson.parseToJsonElement(it.string()))
+
+ result?.let {
+ if (it.avatarUrl != null) {
+ it.avatarUrl = "https:" + it.avatarUrl!!.replace("habrastorage", "hsto")
+ } else {
+ it.avatarUrl = placeholderAvatarUrl(it.alias)
+ }
+ it.registerDateTime = formatTime(it.registerDateTime).split(' ')
+ .run { "${this[0]} ${this[1]} ${this[2]}" }
+
+ if (it.birthday != null)
+ it.birthday = formatBirthdate(it.birthday!!)
+
+ it.lastActivityDateTime =
+ it.lastActivityDateTime?.let { it1 -> formatTime(it1) }
+ }
+ result
+ }
+
+ return result
+ }
+
+ private fun _whoIs(path: String, args: Map? = null): WhoIs? {
+ val response = HabrApi.get(path, args)
+ val responseBody = response?.body?.string()
+ if (response?.code != 200 || responseBody == null) {
+ return null
+ }
+ responseBody.let {
+ val result = HabrDataParser.parseJson(it)
+ result.invitedBy?.let {
+ result.invitedBy!!.timeCreated = formatTime(it.timeCreated)
+ }
+
+
+ return result
+ }
+ }
+
+ private fun _note(path: String, args: Map? = null): Note? {
+ val response = HabrApi.get(path, args)
+
+ if (response?.code != 200)
+ return null
+
+ response.body?.string()?.let {
+ val result = HabrDataParser.parseJson(it)
+ result.text?.let {
+ result.text = Jsoup.parse(it).text()
+ }
+ return result
+ }
+ return null
+ }
+
+ fun get(
+ alias: String,
+ args: Map? = null,
+ ): User? {
+ val raw = _get("users/$alias/card", args)
+
+ var result: User? = null
+
+ raw?.let {
+
+ result = User(
+ alias = it.alias,
+ fullname = if (it.fullname?.isEmpty() == true) null else it.fullname,
+ avatarUrl = it.avatarUrl,
+ speciality = if (it.speciality?.isEmpty() == true) null else it.speciality,
+ rating = it.rating,
+ ratingPosition = it.ratingPos,
+ score = it.scoreStats.score,
+ followersCount = it.followStats.followersCount,
+ subscriptionsCount = it.followStats.followCount,
+ isReadonly = it.isReadonly,
+ registrationDate = it.registerDateTime,
+ lastActivityDate = it.lastActivityDateTime,
+ birthday = it.birthday,
+ canBeInvited = it.canBeInvited,
+ location = it.location?.let {
+ val buffer = StringBuilder()
+ it.city?.title?.let {
+ buffer.append("$it, ")
+ }
+ it.region?.title?.let {
+ //buffer.append("$it, ")
+ }
+ it.country?.title?.let {
+ buffer.append(it)
+ }
+
+ buffer.toString()
+ },
+ articlesCount = it.counterStats.postCount,
+ commentsCount = it.counterStats.commentCount,
+ bookmarksCount = it.counterStats.favoriteCount,
+ workPlaces = it.workplace.map { User.WorkPlace(it.title, it.alias) },
+ relatedData = it.relatedData?.let { User.RelatedData(it.isSubscribed) }
+ )
+ }
+
+ return result
+ }
+
+ fun whoIs(
+ alias: String,
+ args: Map? = null,
+ ): User.WhoIs? {
+ val raw = _whoIs("users/$alias/whois", args)
+
+ raw?.let {
+ return User.WhoIs(
+ aboutHtml = it.aboutHtml,
+ badges = it.badgets.map { User.WhoIs.Badge(it.title, it.description) },
+ invite = it.invitedBy?.let {
+ User.WhoIs.Invite(
+ it.issuerLogin,
+ it.timeCreated
+ )
+ },
+ contacts = it.contacts.map {
+ var title = it.title
+ if (it.title == "Сайт" || it.title == "Site") {
+ title += " • ${it.siteTitle}"
+ } else {
+ title += " • ${it.value}"
+ }
+
+ User.WhoIs.Contact(title, it.url, it.favicon)
+ }
+ )
+ }
+ return null
+ }
+
+ fun note(
+ alias: String,
+ args: Map? = null
+ ): User.Note? {
+ val raw = _note("users/$alias/note", args)
+
+ raw?.let {
+ return User.Note(it.text)
+ }
+ return null
+ }
+
+ /**
+ * Subscribe/unsubscribe to user.
+ * @return subscription status
+ * @throws UnsupportedOperationException
+ */
+ fun subscription(alias: String): Boolean {
+ val response = HabrApi.post("users/$alias/following/toggle")
+
+ response.body?.string()?.let {
+ return Json.parseToJsonElement(it).jsonObject["isSubscribed"]?.jsonPrimitive!!.boolean
+ }
+ throw UnsupportedOperationException("User is not authorized")
+
+ }
+ }
+
+ @Serializable
+ data class Note(
+ var text: String?
+ )
+
+ @Serializable
+ data class WhoIs(
+ var alias: String,
+ var badgets: List,
+ var aboutHtml: String,
+ var contacts: List,
+ var invitedBy: InvitedBy? = null
+ )
+
+ @Serializable
+ data class Badget(
+ var title: String,
+ var description: String,
// var url: String? = null,
// var isRemovable: Boolean
- )
-
- @Serializable
- data class Contact(
- val title: String,
- val url: String,
- val value: String?,
- val siteTitle: String? = null,
- val favicon: String? = null
- )
-
- @Serializable
- data class InvitedBy(
- val issuerLogin: String?,
- var timeCreated: String
- )
-
+ )
+
+ @Serializable
+ data class Contact(
+ val title: String,
+ val url: String,
+ val value: String?,
+ val siteTitle: String? = null,
+ val favicon: String? = null
+ )
+
+ @Serializable
+ data class InvitedBy(
+ val issuerLogin: String?,
+ var timeCreated: String
+ )
+
}
@Serializable
data class UserProfileData(
- var alias: String,
- var fullname: String?,
- var avatarUrl: String? = null,
- var speciality: String?,
- var gender: String,
- var rating: Float,
- var ratingPos: Int? = null,
- var scoreStats: ScoreStats,
- var relatedData: RelatedData? = null,
- var followStats: FollowStats,
- var lastActivityDateTime: String?,
- var registerDateTime: String,
- var birthday: String? = null,
- var location: Location?,
- var workplace: List,
- var counterStats: CounterStats,
- var isReadonly: Boolean,
- var canBeInvited: Boolean
+ var alias: String,
+ var fullname: String?,
+ var avatarUrl: String? = null,
+ var speciality: String?,
+ var gender: String,
+ var rating: Float,
+ var ratingPos: Int? = null,
+ var scoreStats: ScoreStats,
+ var relatedData: RelatedData? = null,
+ var followStats: FollowStats,
+ var lastActivityDateTime: String?,
+ var registerDateTime: String,
+ var birthday: String? = null,
+ var location: Location?,
+ var workplace: List,
+ var counterStats: CounterStats,
+ var isReadonly: Boolean,
+ var canBeInvited: Boolean
) {
- @Serializable
- data class RelatedData(var isSubscribed: Boolean)
+ @Serializable
+ data class RelatedData(var isSubscribed: Boolean)
}
@Serializable
data class CounterStats(
- var postCount: Int,
- var commentCount: Int,
- var favoriteCount: Int
+ var postCount: Int,
+ var commentCount: Int,
+ var favoriteCount: Int
)
@Serializable
data class FollowStats(
- val followCount: Int,
- val followersCount: Int
+ val followCount: Int,
+ val followersCount: Int
)
@Serializable
data class Location(
- val city: LocationItem?,
- val region: LocationItem?,
- val country: LocationItem?
+ val city: LocationItem?,
+ val region: LocationItem?,
+ val country: LocationItem?
)
@Serializable
data class LocationItem(
- val id: String,
- val title: String
+ val id: String,
+ val title: String
)
@Serializable
data class ScoreStats(
- val score: Int,
- val votesCount: Int
+ val score: Int,
+ val votesCount: Int
)
@Serializable
data class WorkPlace(
- val title: String,
- val alias: String
+ val title: String,
+ val alias: String
)
diff --git a/app/src/main/java/com/garnegsoft/hubs/api/utils/dateParsing.kt b/app/src/main/java/com/garnegsoft/hubs/api/utils/dateParsing.kt
index dd7d5df1..0428ba63 100644
--- a/app/src/main/java/com/garnegsoft/hubs/api/utils/dateParsing.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/api/utils/dateParsing.kt
@@ -38,7 +38,7 @@ fun formatTime(time: String): String {
var result = String()
val localedTime = defaultInputFormatter.parse(time.split('+')[0].replace('T', ' '))!!
- localedTime.time = localedTime.time + TimeZone.getDefault().rawOffset + TimeZone.getDefault().dstSavings
+ localedTime.time = localedTime.time + TimeZone.getDefault().getOffset(Date().time)
var todayCallendar = Calendar.getInstance()
var publishCalendar = Calendar.getInstance()
@@ -94,4 +94,9 @@ fun formatFoundationDate(day: String?, month: String?, year: String?): String? {
result += it.toInt().toString()
}
return result.ifEmpty { null }
+}
+
+fun formatTimestamp(timestamp: Long): String {
+ val date = Date(timestamp)
+ return customOutputFormatter.format(date)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncGifImage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncGifImage.kt
index e09bf1f6..e20f866e 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncGifImage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncGifImage.kt
@@ -1,6 +1,7 @@
package com.garnegsoft.hubs.api
import android.os.Build
+import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
@@ -8,13 +9,70 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.ImageLoader
-import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import coil.size.Size
+import okhttp3.OkHttpClient
+import android.content.Context
+import android.graphics.BitmapFactory
+import android.net.Uri
+import androidx.core.graphics.drawable.toDrawable
+import coil.decode.DataSource
+import coil.fetch.DrawableResult
+import coil.fetch.FetchResult
+import coil.fetch.Fetcher
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+import java.io.File
+
+
+private var loader: ImageLoader? = null
+var offlineResourcesDir: File? = null
+private val Context.CommonImageLoader: ImageLoader
+ get() {
+ if (loader == null) {
+ loader = ImageLoader.Builder(this)
+ .components {
+ if (Build.VERSION.SDK_INT >= 28) {
+ add(ImageDecoderDecoder.Factory())
+ } else {
+ add(GifDecoder.Factory())
+ }
+ }
+
+ .okHttpClient {
+ OkHttpClient.Builder()
+ .addInterceptor {
+ val urlString = it.request().url.toString()
+ if (it.request().url.toString().startsWith("offline-article")){
+ val fileUri = urlString.split(":").last()
+ if (offlineResourcesDir == null){
+ offlineResourcesDir = File(filesDir, "offline_resources")
+ }
+ val file = File(offlineResourcesDir, fileUri)
+
+ if (file.exists()){
+ file.readBytes().let {
+ return@addInterceptor Response.Builder().body(it.toResponseBody("image/${fileUri.split(".").last()}".toMediaType())).build()
+ }
+ }
+ return@addInterceptor Response.Builder().build()
+ } else {
+ it.proceed(it.request())
+ }
+ }.build()
+ }
+ .crossfade(true)
+ .build()
+ }
+ return loader!!
+ }
+
+
@Composable
fun AsyncGifImage(
@@ -25,28 +83,44 @@ fun AsyncGifImage(
onState: (AsyncImagePainter.State) -> Unit = {}
) {
val context = LocalContext.current
- val imageLoader = ImageLoader.Builder(context)
- .components {
- if (Build.VERSION.SDK_INT >= 28) {
- add(ImageDecoderDecoder.Factory())
- } else {
- add(GifDecoder.Factory())
- }
- }
- .crossfade(true)
- .build()
+
Image(
painter = rememberAsyncImagePainter(
ImageRequest.Builder(context)
.data(model)
.size(Size.ORIGINAL)
-
+ .fetcherFactory(
+ Fetcher.Factory { data, options, imageLoader ->
+ CommonImageRequestFetcher(data, context)
+ }
+ )
.build(),
- imageLoader = imageLoader,
+ imageLoader = context.CommonImageLoader,
onState = onState
),
contentDescription = contentDescription,
modifier = modifier.fillMaxWidth(),
contentScale = contentScale,
)
+}
+
+class CommonImageRequestFetcher(val data: Any, val context: Context) : Fetcher {
+ override suspend fun fetch(): FetchResult? {
+ if (data is String || data is Uri){
+ val url = data.toString()
+ val fileUri = url.split(":").last()
+ if (offlineResourcesDir == null){
+ offlineResourcesDir = File(context.filesDir, "offline_resources")
+ }
+ val file = File(offlineResourcesDir, fileUri)
+
+ if (file.exists()){
+ file.readBytes().let {
+ val image = BitmapFactory.decodeByteArray(it, 0, it.size).toDrawable(context.resources)
+ return DrawableResult(image, false, DataSource.DISK)
+ }
+ }
+ }
+ return null
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncSvgImage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncSvgImage.kt
index a43e7a4d..bf4ab289 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncSvgImage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/AsyncSvgImage.kt
@@ -1,14 +1,30 @@
package com.garnegsoft.hubs.ui.common
+import android.graphics.BitmapFactory
+import android.net.Uri
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.core.graphics.drawable.toDrawable
+import androidx.work.WorkManager
import coil.compose.AsyncImage
+import coil.decode.DataSource
+import coil.decode.ImageSource
import coil.decode.SvgDecoder
+import coil.fetch.DrawableResult
+import coil.fetch.FetchResult
+import coil.fetch.Fetcher
+import coil.fetch.SourceResult
import coil.request.ImageRequest
+import com.garnegsoft.hubs.api.CommonImageRequestFetcher
+import com.garnegsoft.hubs.api.offlineResourcesDir
+import okio.BufferedSource
+import okio.Path
+import okio.Path.Companion.toPath
+import java.io.File
private val colorMatrix = floatArrayOf(
-0.7f, 0f, 0f, 0f, 255f,
@@ -25,12 +41,34 @@ fun AsyncSvgImage(
revertColorsOnDarkTheme: Boolean = true,
contentDescription: String? = null
) {
+
val context = LocalContext.current
AsyncImage(
modifier = modifier,
model = ImageRequest.Builder(context)
.data(data)
.decoderFactory(SvgDecoder.Factory())
+ .fetcherFactory(
+ Fetcher.Factory { factoryData, _, _ ->
+ object : Fetcher {
+ override suspend fun fetch(): FetchResult? {
+ if (factoryData is String || factoryData is Uri){
+ val url = factoryData.toString()
+ val fileUri = url.split(":").last()
+ if (offlineResourcesDir == null){
+ offlineResourcesDir = File(context.filesDir, "offline_resources")
+ }
+ val file = File(offlineResourcesDir, fileUri)
+
+ if (file.exists()){
+ return SourceResult(ImageSource(file.absolutePath.toPath()), "image/svg", DataSource.DISK)
+ }
+ }
+ return null
+ }
+ }
+ }
+ )
.build(),
contentDescription = contentDescription,
colorFilter = if (MaterialTheme.colors.isLight || !revertColorsOnDarkTheme) null else ColorFilter
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/HabrScrollableTabRow.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/HabrScrollableTabRow.kt
index d0843e16..7af6fd30 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/HabrScrollableTabRow.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/HabrScrollableTabRow.kt
@@ -22,73 +22,75 @@ import kotlin.math.absoluteValue
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HabrScrollableTabRow(
- pagerState: PagerState,
- tabs: List,
- onCurrentPositionTabClick: suspend CoroutineScope.(index: Int, title: String) -> Unit = { i, t -> }
+ pagerState: PagerState,
+ tabs: List,
+ onCurrentPositionTabClick: suspend CoroutineScope.(index: Int, title: String) -> Unit = { i, t -> }
) {
- val pagerStateCoroutineScope = rememberCoroutineScope()
-
- ScrollableTabRow(
- selectedTabIndex = pagerState.currentPage,
- edgePadding = 8.dp,
- divider = {
- Divider()
- },
- backgroundColor = MaterialTheme.colors.surface,
- indicator = {
- TabRowDefaults.Indicator(
- modifier = Modifier.customTabIndicatorOffset(
- currentTabPosition = it[pagerState.currentPage],
- offset = pagerState.currentPageOffsetFraction,
- nextTabPosition =
- when {
- pagerState.currentPageOffsetFraction < 0 -> it[pagerState.currentPage - 1]
- pagerState.currentPageOffsetFraction > 0 -> it[pagerState.currentPage + 1]
- else -> it[pagerState.currentPage]
- }
-
- )
- .padding(horizontal = 8.dp)
- .clip(CircleShape)
- )
- },
- contentColor = if (MaterialTheme.colors.isLight) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
- ) {
- tabs.forEachIndexed { index, title ->
- Tab(
- selected = index == pagerState.currentPage,
- onClick = {
- pagerStateCoroutineScope.launch {
- if (pagerState.currentPage == index)
- onCurrentPositionTabClick(this, index, title)
- else
- pagerState.animateScrollToPage(index)
-
- }
- },
- ) {
- Text(
- title,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
- fontWeight = FontWeight.W500,
- )
- }
- }
- }
+ val pagerStateCoroutineScope = rememberCoroutineScope()
+
+ ScrollableTabRow(
+ modifier = Modifier.fillMaxWidth(),
+ selectedTabIndex = pagerState.currentPage,
+ edgePadding = 8.dp,
+ divider = {
+ Divider()
+ },
+ backgroundColor = MaterialTheme.colors.surface,
+ indicator = {
+ TabRowDefaults.Indicator(
+ modifier = Modifier
+ .customTabIndicatorOffset(
+ currentTabPosition = it[pagerState.currentPage],
+ offset = pagerState.currentPageOffsetFraction,
+ nextTabPosition =
+ when {
+ pagerState.currentPageOffsetFraction < 0 -> it[pagerState.currentPage - 1]
+ pagerState.currentPageOffsetFraction > 0 -> it[pagerState.currentPage + 1]
+ else -> it[pagerState.currentPage]
+ }
+
+ )
+ .padding(horizontal = 8.dp)
+ .clip(CircleShape)
+ )
+ },
+ contentColor = if (MaterialTheme.colors.isLight) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
+ ) {
+ tabs.forEachIndexed { index, title ->
+ Tab(
+ selected = index == pagerState.currentPage,
+ onClick = {
+ pagerStateCoroutineScope.launch {
+ if (pagerState.currentPage == index)
+ onCurrentPositionTabClick(this, index, title)
+ else
+ pagerState.animateScrollToPage(index)
+
+ }
+ },
+ ) {
+ Text(
+ title,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
+ fontWeight = FontWeight.W500,
+ )
+ }
+ }
+ }
}
fun Modifier.customTabIndicatorOffset(
- currentTabPosition: TabPosition,
- offset: Float,
- nextTabPosition: TabPosition,
+ currentTabPosition: TabPosition,
+ offset: Float,
+ nextTabPosition: TabPosition,
): Modifier {
- val indicatorWidth =
- currentTabPosition.width + (nextTabPosition.width - currentTabPosition.width) * offset.absoluteValue
- val indicatorOffset =
- currentTabPosition.left + (nextTabPosition.left - currentTabPosition.left) * offset.absoluteValue
-
- return fillMaxWidth()
- .wrapContentSize(Alignment.BottomStart)
- .offset(x = indicatorOffset)
- .width(indicatorWidth)
+ val indicatorWidth =
+ currentTabPosition.width + (nextTabPosition.width - currentTabPosition.width) * offset.absoluteValue
+ val indicatorOffset =
+ currentTabPosition.left + (nextTabPosition.left - currentTabPosition.left) * offset.absoluteValue
+
+ return fillMaxWidth()
+ .wrapContentSize(Alignment.BottomStart)
+ .offset(x = indicatorOffset)
+ .width(indicatorWidth)
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/HubChip.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/HubChip.kt
similarity index 96%
rename from app/src/main/java/com/garnegsoft/hubs/ui/screens/user/HubChip.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/common/HubChip.kt
index b525c83f..4f4a83da 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/HubChip.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/HubChip.kt
@@ -1,4 +1,4 @@
-package com.garnegsoft.hubs.ui.screens.user
+package com.garnegsoft.hubs.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -60,6 +60,7 @@ fun HubChip(
@Composable
fun HubChip(
title: String,
+ isSubscribed: Boolean = false,
onClick: () -> Unit
) {
Text(
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/LazyHabrSnippetsColumn.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/LazyHabrSnippetsColumn.kt
index 9e74ecbf..48f80df0 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/LazyHabrSnippetsColumn.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/LazyHabrSnippetsColumn.kt
@@ -1,14 +1,11 @@
package com.garnegsoft.hubs.ui.common
import android.annotation.SuppressLint
-import android.util.Log
import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.animate
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@@ -23,10 +20,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.HabrList
import com.garnegsoft.hubs.api.HabrSnippet
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlin.math.abs
@Composable
fun LazyHabrSnippetsColumn(
@@ -43,12 +36,70 @@ fun LazyHabrSnippetsColumn(
}
},
snippet: @Composable (T) -> Unit,
+) {
+ BaseHubsLazyColumn(
+ data = data,
+ onScrollEnd = onScrollEnd,
+ lazyList = {
+ val density = LocalDensity.current
+ LazyColumn(
+ modifier = modifier,
+ state = it,
+ flingBehavior = object : FlingBehavior {
+ override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+ var lastValue = 0f
+ var lastVelocity = 0f
+ AnimationState(
+ initialValue = 0f,
+ initialVelocity = initialVelocity * 1.3f
+ ).animateDecay(splineBasedDecay(density)) {
+ val delta = value - lastValue
+ val consumed = scrollBy(delta)
+ lastValue = value
+ lastVelocity = velocity
+
+ if (consumed == 0f)
+ cancelAnimation()
+ }
+
+ return lastVelocity
+ }
+ },
+ contentPadding = contentPadding,
+ verticalArrangement = verticalArrangement,
+ horizontalAlignment = horizontalAlignment
+ ) {
+ items(
+ items = data.list,
+ key = { it.id },
+ ) {
+ snippet(it)
+ }
+ nextPageLoadingIndicator?.let {
+ item {
+ nextPageLoadingIndicator()
+ }
+ }
+ }
+ },
+ lazyListState = lazyListState)
+
+}
+
+// Most simple lazy column that notifies about scroll end
+@Composable
+fun BaseHubsLazyColumn(
+ data: HabrList,
+ onScrollEnd: () -> Unit,
+ lazyList: @Composable (state: LazyListState) -> Unit,
+ lazyListState: LazyListState,
) {
val derivedItemsCount by remember { derivedStateOf { lazyListState.layoutInfo.totalItemsCount } }
val isLastDerived by remember {
derivedStateOf {
- if (data.list.size > 0 && lazyListState.layoutInfo.totalItemsCount > 0)
- lazyListState.layoutInfo.totalItemsCount - 1 == lazyListState.layoutInfo.visibleItemsInfo.last().index
+ // will work only if controller loads more than 8 snippets per page
+ if (data.list.size > 8 && lazyListState.layoutInfo.totalItemsCount > 8)
+ lazyListState.layoutInfo.totalItemsCount - 7 <= lazyListState.layoutInfo.visibleItemsInfo.last().index
else
false
}
@@ -64,49 +115,9 @@ fun LazyHabrSnippetsColumn(
doPageLoad = true
}
- val density = LocalDensity.current
- LazyColumn(
- modifier = modifier,
- state = lazyListState,
- flingBehavior = object : FlingBehavior {
- override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
- var lastValue = 0f
- var lastVelocity = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = initialVelocity * 1.3f
- ).animateDecay(splineBasedDecay(density)) {
- val delta = value - lastValue
- val consumed = scrollBy(delta)
- lastValue = value
- lastVelocity = velocity
-
- if (consumed == 0f)
- cancelAnimation()
- }
-
- return lastVelocity
- }
- },
- contentPadding = contentPadding,
- verticalArrangement = verticalArrangement,
- horizontalAlignment = horizontalAlignment
- ) {
- items(
- items = data.list,
- key = { it.id },
- ) {
- snippet(it)
- }
- nextPageLoadingIndicator?.let {
- item {
- nextPageLoadingIndicator()
- }
- }
- }
+ lazyList(lazyListState)
}
-
@Composable
@SuppressLint("ModifierParameter")
fun PagedHabrSnippetsColumn(
@@ -127,7 +138,6 @@ fun PagedHabrSnippetsColumn(
},
snippet: @Composable (T) -> Unit,
) {
- val scrollEndCoroutineScope = rememberCoroutineScope()
LazyHabrSnippetsColumn(
data = data,
modifier = modifier,
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/ScrollUpMethods.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/ScrollUpMethods.kt
index c4707c20..9fc8ca42 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/ScrollUpMethods.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/ScrollUpMethods.kt
@@ -31,10 +31,12 @@ import org.jsoup.select.Elements
sealed interface ScrollUpMethods {
companion object {
suspend fun scrollLazyList(lazyListState: LazyListState){
- lazyListState.scrollToItem(
- 0,
- lazyListState.firstVisibleItemScrollOffset
- )
+ if (lazyListState.firstVisibleItemIndex > 3) {
+ lazyListState.scrollToItem(
+ 2,
+ lazyListState.firstVisibleItemScrollOffset
+ )
+ }
lazyListState.animateScrollToItem(0)
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/TitledColumn.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/TitledColumn.kt
index 571ed111..174d2a9a 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/TitledColumn.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/TitledColumn.kt
@@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -39,6 +40,7 @@ fun TitledColumn(
modifier: Modifier = Modifier,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+ spaceAfterTitle: Dp = 6.dp,
titleStyle: TextStyle = MaterialTheme.typography.subtitle2.copy(color = MaterialTheme.colors.onSurface),
content: @Composable ColumnScope.() -> Unit
) {
@@ -47,7 +49,7 @@ fun TitledColumn(
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
title = { Text(text = title, style = titleStyle) },
- divider = { Spacer(modifier = Modifier.height(6.dp)) },
+ divider = { Spacer(modifier = Modifier.height(spaceAfterTitle)) },
content = content
)
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/ArticlesUI.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCard.kt
similarity index 66%
rename from app/src/main/java/com/garnegsoft/hubs/ui/common/ArticlesUI.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCard.kt
index 08ff7df1..4d79c620 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/ArticlesUI.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCard.kt
@@ -1,146 +1,44 @@
-package com.garnegsoft.hubs.ui.common
+package com.garnegsoft.hubs.ui.common.feedCards.article
import ArticleController
-import android.widget.Toast
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
-import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.garnegsoft.hubs.R
-import com.garnegsoft.hubs.api.PostComplexity
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.api.PostType
import com.garnegsoft.hubs.api.article.list.ArticleSnippet
import com.garnegsoft.hubs.api.article.offline.OfflineArticlesController
-import com.garnegsoft.hubs.api.article.offline.OfflineArticlesDao
-import com.garnegsoft.hubs.api.article.offline.OfflineArticlesDatabase
import com.garnegsoft.hubs.api.utils.formatLongNumbers
-import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
-import com.garnegsoft.hubs.ui.theme.RatingNegative
-import com.garnegsoft.hubs.ui.theme.RatingPositive
-import com.garnegsoft.hubs.ui.theme.SecondaryColor
-import kotlinx.coroutines.CoroutineScope
+import com.garnegsoft.hubs.ui.theme.HubSubscribedColor
+import com.garnegsoft.hubs.ui.theme.RatingNegativeColor
+import com.garnegsoft.hubs.ui.theme.RatingPositiveColor
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.future.future
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-/**
- * Style of the [ArticleCard]
- */
-@Immutable
-data class ArticleCardStyle(
- val innerPadding: Dp = 16.dp,
- val innerElementsShape: Shape = RoundedCornerShape(10.dp),
- val cardShape: Shape = RoundedCornerShape(26.dp),
-
- val showImage: Boolean = true,
-
- val showTextSnippet: Boolean = true,
-
- val showHubsList: Boolean = true,
-
- val commentsButtonEnabled: Boolean = true,
-
- val addToBookmarksButtonEnabled: Boolean = false,
-
- val backgroundColor: Color = Color.White,
-
- val textColor: Color = Color.Black,
-
- val authorAvatarSize: Dp = 34.dp,
-
- val snippetMaxLines: Int = 4,
-
- val rippleColor: Color = textColor,
-
- val imageLoadingIndicatorColor: Color = SecondaryColor,
-
- val titleTextStyle: TextStyle = TextStyle(
- color = textColor,
- fontSize = 20.sp,
- fontWeight = FontWeight.W700,
- ),
-
- val snippetTextStyle: TextStyle = TextStyle(
- color = textColor.copy(alpha = 0.75f),
- fontSize = 16.sp,
- fontWeight = FontWeight.W400,
- lineHeight = 16.sp.times(1.25f)
- ),
-
- val authorTextStyle: TextStyle = TextStyle(
- color = textColor,
- fontSize = 14.sp,
- fontWeight = FontWeight.W600
- ),
-
- val publishedTimeTextStyle: TextStyle = TextStyle(
- color = textColor.copy(alpha = 0.5f),
- fontSize = 12.sp,
- fontWeight = FontWeight.W400
- ),
-
- /**
- * Text style of statistics row, note that text color for score indicator won't apply if it is non-zero value (will be red or green)
- */
- val statisticsColor: Color = textColor.copy(alpha = 0.5f),
-
- val statisticsTextStyle: TextStyle = TextStyle(
- color = statisticsColor,
- fontSize = 15.sp,
- fontWeight = FontWeight.W400
- ),
-
- val hubsTextStyle: TextStyle = TextStyle(
- color = textColor.copy(alpha = 0.5f),
- fontSize = 12.sp,
- fontWeight = FontWeight.W600
- )
-
-)
-
-@Composable
-@ReadOnlyComposable
-fun defaultArticleCardStyle(): ArticleCardStyle {
- return ArticleCardStyle(
- backgroundColor = MaterialTheme.colors.surface,
- textColor = MaterialTheme.colors.onSurface,
- statisticsColor = MaterialTheme.colors.onSurface
- .copy(
- alpha = if (MaterialTheme.colors.isLight) {
- 0.75f
- } else {
- 0.5f
- }
-
- ),
- )
-}
@OptIn(ExperimentalFoundationApi::class)
@@ -150,7 +48,7 @@ fun ArticleCard(
onClick: () -> Unit,
onAuthorClick: () -> Unit,
onCommentsClick: () -> Unit,
- style: ArticleCardStyle = defaultArticleCardStyle().copy(addToBookmarksButtonEnabled = article.relatedData != null)
+ style: ArticleCardStyle
) {
Column(
@@ -191,46 +89,30 @@ fun ArticleCard(
onClick = onAuthorClick
)
) {
- if (it.avatarUrl.isNullOrBlank()) {
- Icon(
- modifier = Modifier
- .size(style.authorAvatarSize)
- .clip(style.innerElementsShape)
- .background(Color.White)
- .border(
- BorderStroke(
- 2.dp,
- placeholderColorLegacy(article.author.alias)
- ),
- shape = style.innerElementsShape
- )
- .padding(2.dp),
- painter = painterResource(id = R.drawable.user_avatar_placeholder),
- contentDescription = "",
- tint = placeholderColorLegacy(article.author.alias)
- )
- } else {
- AsyncImage(
- modifier = Modifier
- .size(style.authorAvatarSize)
- .clip(style.innerElementsShape)
- .background(Color.White),
- model = it.avatarUrl,
- contentDescription = "avatar",
- onState = { })
- }
- Spacer(modifier = Modifier.width(4.dp))
+
+ AsyncImage(
+ modifier = Modifier
+ .size(style.authorAvatarSize)
+ .clip(style.innerElementsShape)
+ .background(Color.White),
+ model = it.avatarUrl,
+ contentDescription = "avatar",
+ onState = { })
+
+ Spacer(modifier = Modifier.width(8.dp))
Text(
- it.alias,
+ modifier = Modifier.weight(1f),
+ text = it.alias,
style = style.authorTextStyle,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
- Spacer(modifier = Modifier.width(4.dp))
}
+
}
}
+ Spacer(modifier = Modifier.width(style.innerPadding))
// Published time
Text(
@@ -249,15 +131,15 @@ fun ArticleCard(
)
Spacer(modifier = Modifier.height(0.dp))
Row(
- modifier = Modifier.padding(horizontal = style.innerPadding),
+ modifier = Modifier.padding(horizontal = style.innerPadding, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- if (article.complexity != PostComplexity.None) {
- val postComplexityColor = remember {
+ if (article.complexity != PublicationComplexity.None) {
+ val publicationComplexityColor = remember {
when (article.complexity) {
- PostComplexity.Low -> Color(0xFF4CBE51)
- PostComplexity.Medium -> Color(0xFFEEBC25)
- PostComplexity.High -> Color(0xFFEB3B2E)
+ PublicationComplexity.Low -> Color(0xFF4CBE51)
+ PublicationComplexity.Medium -> Color(0xFFEEBC25)
+ PublicationComplexity.High -> Color(0xFFEB3B2E)
else -> style.statisticsColor
}
}
@@ -265,17 +147,17 @@ fun ArticleCard(
modifier = Modifier.size(height = 10.dp, width = 20.dp),
painter = painterResource(id = R.drawable.speedmeter_hard),
contentDescription = "",
- tint = postComplexityColor
+ tint = publicationComplexityColor
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = when (article.complexity) {
- PostComplexity.Low -> "Простой"
- PostComplexity.Medium -> "Средний"
- PostComplexity.High -> "Сложный"
+ PublicationComplexity.Low -> "Простой"
+ PublicationComplexity.Medium -> "Средний"
+ PublicationComplexity.High -> "Сложный"
else -> ""
},
- color = postComplexityColor,
+ color = publicationComplexityColor,
fontWeight = FontWeight.W500,
fontSize = 14.sp
@@ -291,7 +173,7 @@ fun ArticleCard(
)
Spacer(modifier = Modifier.width(4.dp))
Text(
- text = "${article.readingTime} мин",
+ text = remember { "${article.readingTime} мин" },
color = style.statisticsColor,
fontWeight = FontWeight.W500,
fontSize = 14.sp
@@ -314,15 +196,28 @@ fun ArticleCard(
}
}
- var hubsText by remember { mutableStateOf("") }
+ var hubsText by remember { mutableStateOf(buildAnnotatedString { }) }
LaunchedEffect(key1 = Unit, block = {
- if (hubsText == "") {
- hubsText = article.hubs!!.joinToString(separator = ", ") {
- if (it.isProfiled)
- (it.title + "*").replace(" ", "\u00A0")
- else
- it.title.replace(" ", "\u00A0")
+ if (hubsText.text == "") {
+ hubsText = buildAnnotatedString {
+ article.hubs!!.forEachIndexed { index, it ->
+ val textFunc = if (it.isProfiled) {
+ { append((it.title + "*").replace(" ", "\u00A0")) }
+ } else {
+ { append(it.title.replace(" ", "\u00A0")) }
+ }
+ if (it.relatedData != null && it.relatedData.isSubscribed) {
+ withStyle(SpanStyle(color = HubSubscribedColor)) {
+ textFunc()
+ }
+ } else {
+ textFunc()
+ }
+ if (index < article.hubs.size - 1) {
+ append(", ")
+ }
+ }
}
}
})
@@ -335,7 +230,8 @@ fun ArticleCard(
// Snippet
- if (style.showTextSnippet)
+ if (style.showTextSnippet) {
+ Spacer(modifier = Modifier.height(2.dp))
Text(
modifier = Modifier.padding(horizontal = style.innerPadding),
text = article.textSnippet,
@@ -343,10 +239,10 @@ fun ArticleCard(
overflow = TextOverflow.Ellipsis,
style = style.snippetTextStyle
)
-
+ }
// Image to draw attention (a.k.a. KDPV)
if (style.showImage && !article.imageUrl.isNullOrBlank()) {
- Spacer(modifier = Modifier.height(4.dp))
+ Spacer(modifier = Modifier.height(6.dp))
AsyncImage(
modifier = Modifier
.padding(horizontal = style.innerPadding)
@@ -394,14 +290,14 @@ fun ArticleCard(
Text(
text = '+' + article.statistics.score.toString(),
style = style.statisticsTextStyle,
- color = RatingPositive
+ color = RatingPositiveColor
)
} else
if (article.statistics.score < 0) {
Text(
text = article.statistics.score.toString(),
style = style.statisticsTextStyle,
- color = RatingNegative
+ color = RatingNegativeColor
)
} else {
Text(
@@ -451,7 +347,11 @@ fun ArticleCard(
addedToBookmarksCount--
addedToBookmarksCount =
addedToBookmarksCount.coerceAtLeast(0)
- if (!ArticleController.removeFromBookmarks(article.id, article.type == PostType.News)) {
+ if (!ArticleController.removeFromBookmarks(
+ article.id,
+ article.type == PostType.News
+ )
+ ) {
addedToBookmarks = true
addedToBookmarksCount++
addedToBookmarksCount =
@@ -461,7 +361,11 @@ fun ArticleCard(
} else {
addedToBookmarks = true
addedToBookmarksCount++
- if (!ArticleController.addToBookmarks(article.id, article.type == PostType.News)) {
+ if (!ArticleController.addToBookmarks(
+ article.id,
+ article.type == PostType.News
+ )
+ ) {
addedToBookmarks = false
addedToBookmarksCount--
addedToBookmarksCount =
@@ -481,6 +385,8 @@ fun ArticleCard(
}
val hapticFeedback = LocalHapticFeedback.current
+ val addToBookmarksButtonEnabled =
+ remember { style.bookmarksButtonAllowedBeEnabled && article.relatedData != null }
//Added to bookmarks
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -497,7 +403,7 @@ fun ArticleCard(
HapticFeedbackType.LongPress
)
},
- enabled = style.addToBookmarksButtonEnabled,
+ enabled = addToBookmarksButtonEnabled,
)
.onGloballyPositioned {
bounds = it.size
@@ -527,26 +433,11 @@ fun ArticleCard(
bounds = bounds,
cardStyle = style,
onSaveClick = {
- coroutineScope.launch(Dispatchers.IO) {
- val downloaded = OfflineArticlesController.downloadArticle(article.id, context)
- if (downloaded) {
- withContext(Dispatchers.Main) {
- Toast.makeText(context, "Статья скачана!", Toast.LENGTH_SHORT).show()
- }
- }
- }
+ OfflineArticlesController.downloadArticle(article.id, context)
showPopup = false
},
onDeleteClick = {
- coroutineScope.launch(Dispatchers.IO) {
- val deleted = OfflineArticlesController.deleteArticle(article.id, context)
- if (deleted) {
- withContext(Dispatchers.Main) {
- Toast.makeText(context, "Статья удалена!", Toast.LENGTH_SHORT)
- .show()
- }
- }
- }
+ OfflineArticlesController.deleteArticle(article.id, context)
showPopup = false
},
onDismissRequest = { showPopup = false },
@@ -586,7 +477,7 @@ fun ArticleCard(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
- .background(RatingPositive)
+ .background(RatingPositiveColor)
)
}
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/ArticleCardSavePopup.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCardSavePopup.kt
similarity index 81%
rename from app/src/main/java/com/garnegsoft/hubs/ui/common/ArticleCardSavePopup.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCardSavePopup.kt
index a3829c9d..044b7a15 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/ArticleCardSavePopup.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCardSavePopup.kt
@@ -1,43 +1,27 @@
-package com.garnegsoft.hubs.ui.common
+package com.garnegsoft.hubs.ui.common.feedCards.article
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
@@ -48,7 +32,6 @@ import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.garnegsoft.hubs.R
import com.garnegsoft.hubs.api.article.offline.OfflineArticlesDatabase
-import kotlinx.coroutines.delay
@Composable
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCardStyle.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCardStyle.kt
new file mode 100644
index 00000000..f2f5fc7e
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/article/ArticleCardStyle.kt
@@ -0,0 +1,179 @@
+package com.garnegsoft.hubs.ui.common.feedCards.article
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.size.Size
+import com.garnegsoft.hubs.api.dataStore.HubsDataStore
+import com.garnegsoft.hubs.ui.theme.SecondaryColor
+
+/**
+ * Style of the [ArticleCard]
+ */
+@Immutable
+data class ArticleCardStyle(
+ val innerPadding: Dp = 16.dp,
+ val innerElementsShape: Shape = RoundedCornerShape(10.dp),
+ val cardShape: Shape = RoundedCornerShape(26.dp),
+
+ val showImage: Boolean = true,
+
+ val showTextSnippet: Boolean = true,
+
+ val showHubsList: Boolean = true,
+
+ val commentsButtonEnabled: Boolean = true,
+
+ /**
+ * Sets whether corresponding button will be clickable or not.
+ * Note that, even if this parameter set to true,
+ * button can be disabled if user haven't logged in yet.
+ *
+ * All you can do with this parameter is to restrict use of bookmarks button
+ */
+ val bookmarksButtonAllowedBeEnabled: Boolean = true,
+
+ val backgroundColor: Color = Color.White,
+
+ val textColor: Color = Color.Black,
+
+ val authorAvatarSize: Dp = 34.dp,
+
+ val snippetMaxLines: Int = 4,
+
+ val rippleColor: Color = textColor,
+
+ val imageLoadingIndicatorColor: Color = SecondaryColor,
+
+ val titleTextStyle: TextStyle = TextStyle(
+ color = textColor,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.W700,
+ ),
+
+ val snippetTextStyle: TextStyle = TextStyle(
+ color = textColor.copy(alpha = 0.75f),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.W400,
+ ),
+
+ val authorTextStyle: TextStyle = TextStyle(
+ color = textColor,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.W600
+ ),
+
+ val publishedTimeTextStyle: TextStyle = TextStyle(
+ color = textColor.copy(alpha = 0.5f),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.W400
+ ),
+
+ /**
+ * Text style of statistics row,
+ * note that statistics color can be overridden for score indicator
+ */
+ val statisticsColor: Color = textColor.copy(alpha = 0.75f),
+
+ val statisticsTextStyle: TextStyle = TextStyle(
+ color = statisticsColor,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.W400
+ ),
+
+ val hubsTextStyle: TextStyle = TextStyle(
+ color = textColor.copy(alpha = 0.5f),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.W500
+ )
+
+) {
+ companion object {
+
+ @Composable
+ fun defaultArticleCardStyle(): ArticleCardStyle? {
+ val showImage by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.ShowImage
+ ).collectAsState(initial = null)
+
+ val showTextSnippet by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.ShowTextSnippet
+ ).collectAsState(initial = null)
+
+ val textSnippetFontSize by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.TextSnippetFontSize
+ ).collectAsState(initial = null)
+
+ val textSnippetMaxLines by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.TextSnippetMaxLines
+ ).collectAsState(initial = null)
+
+ val titleFontSize by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.TitleFontSize
+ ).collectAsState(initial = null)
+
+ if (showImage == null || showTextSnippet == null
+ || textSnippetFontSize == null || textSnippetMaxLines == null
+ || titleFontSize == null)
+ return null
+
+ val colors = MaterialTheme.colors
+ val defaultCardStyle = remember {
+ ArticleCardStyle(textColor = colors.onSurface,
+ statisticsColor = colors.onSurface.copy(
+ alpha = if (colors.isLight) {
+ 0.75f
+ } else {
+ 0.5f
+ }
+
+ ))
+ }
+
+ return defaultCardStyle.copy(
+ backgroundColor = MaterialTheme.colors.surface,
+ textColor = MaterialTheme.colors.onSurface,
+ statisticsColor = MaterialTheme.colors.onSurface
+ .copy(
+ alpha = if (MaterialTheme.colors.isLight) {
+ 0.75f
+ } else {
+ 0.5f
+ }
+
+ ),
+ snippetTextStyle = defaultCardStyle.snippetTextStyle.copy(
+ fontSize = textSnippetFontSize!!.sp
+ ),
+ titleTextStyle = defaultCardStyle.titleTextStyle.copy(fontSize = titleFontSize!!.sp),
+ showImage = showImage!!,
+ showTextSnippet = showTextSnippet!!,
+ snippetMaxLines = textSnippetMaxLines!!
+ )
+ }
+ }
+}
+
+class lol {
+ var aboba: Int = 0
+
+}
+
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/CommentCard.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/comment/CommentCard.kt
similarity index 92%
rename from app/src/main/java/com/garnegsoft/hubs/ui/common/CommentCard.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/comment/CommentCard.kt
index bf684204..5725ee75 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/CommentCard.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/comment/CommentCard.kt
@@ -1,4 +1,4 @@
-package com.garnegsoft.hubs.ui.common
+package com.garnegsoft.hubs.ui.common.feedCards.comment
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -30,8 +30,8 @@ import com.garnegsoft.hubs.api.comment.list.CommentSnippet
import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
import com.garnegsoft.hubs.ui.screens.article.ElementSettings
import com.garnegsoft.hubs.ui.screens.article.parseElement
-import com.garnegsoft.hubs.ui.theme.RatingNegative
-import com.garnegsoft.hubs.ui.theme.RatingPositive
+import com.garnegsoft.hubs.ui.theme.RatingNegativeColor
+import com.garnegsoft.hubs.ui.theme.RatingPositiveColor
@Composable
@@ -49,11 +49,11 @@ private fun defaultCommentCardStyle(): CommentCardStyle {
@Composable
fun CommentCard(
- comment: CommentSnippet,
- style: CommentCardStyle = defaultCommentCardStyle(),
- onCommentClick: () -> Unit,
- onAuthorClick: () -> Unit,
- onParentPostClick: () -> Unit
+ comment: CommentSnippet,
+ style: CommentCardStyle = defaultCommentCardStyle(),
+ onCommentClick: () -> Unit,
+ onAuthorClick: () -> Unit,
+ onParentPostClick: () -> Unit
) {
Column(
modifier = Modifier
@@ -85,7 +85,6 @@ fun CommentCard(
)
Column(
modifier = Modifier
- .clickable(onClick = onCommentClick)
.padding(
top = style.padding
.calculateTopPadding()
@@ -99,13 +98,14 @@ fun CommentCard(
Box(
modifier = Modifier
.weight(1f)
+ .clip(style.avatarShape)
+ .clickable(onClick = onAuthorClick)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(style.avatarSize)
- .clip(style.avatarShape)
- .clickable(onClick = onAuthorClick)
+
) {
if (comment.author.avatarUrl != null) {
AsyncImage(
@@ -132,12 +132,12 @@ fun CommentCard(
tint = placeholderColorLegacy(comment.author.alias)
)
}
- Spacer(modifier = Modifier.width(4.dp))
+ Spacer(modifier = Modifier.width(6.dp))
Text(
text = comment.author.alias,
style = style.authorAliasTextStyle
)
- Spacer(modifier = Modifier.width(4.dp))
+
}
}
@@ -180,8 +180,8 @@ fun CommentCard(
""
} + it,
color = when {
- it > 0 -> RatingPositive
- it < 0 -> RatingNegative
+ it > 0 -> RatingPositiveColor
+ it < 0 -> RatingNegativeColor
else -> style.textColor
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/CompanyUi.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/company/CompanyCard.kt
similarity index 88%
rename from app/src/main/java/com/garnegsoft/hubs/ui/common/CompanyUi.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/company/CompanyCard.kt
index ca187e98..03e6b123 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/CompanyUi.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/company/CompanyCard.kt
@@ -1,4 +1,4 @@
-package com.garnegsoft.hubs.ui.common
+package com.garnegsoft.hubs.ui.common.feedCards.company
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -44,16 +44,10 @@ private fun defaultCompanyCardStyle(): CompanyCardStyle {
@Composable
fun CompanyCard(
- company: CompanySnippet,
- style: CompanyCardStyle = defaultCompanyCardStyle(),
- indicator: @Composable () -> Unit = {
- Text(
- company.statistics.rating.toString(),
- fontWeight = FontWeight.W400,
- color = DefaultRatingIndicatorColor
- )
- },
- onClick: () -> Unit
+ company: CompanySnippet,
+ style: CompanyCardStyle = defaultCompanyCardStyle(),
+ indicator: @Composable () -> Unit = { DefaultCompanyIndicator(company = company) },
+ onClick: () -> Unit
) {
Row(
modifier = Modifier
@@ -129,4 +123,18 @@ data class CompanyCardStyle(
val indicatorValueTextStyle: TextStyle = TextStyle.Default.copy(color = Color.DarkGray),
val showDescription: Boolean = false,
val descriptionMaxLines: Int = 1
-)
\ No newline at end of file
+)
+
+@Composable
+fun DefaultCompanyIndicator(
+ company: CompanySnippet
+) {
+ val rating = remember {
+ String.format("%.1f", company.statistics.rating).replace(',', '.')
+ }
+ Text(
+ text = rating,
+ fontWeight = FontWeight.W400,
+ color = DefaultRatingIndicatorColor
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/HubUi.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/hub/HubCard.kt
similarity index 95%
rename from app/src/main/java/com/garnegsoft/hubs/ui/common/HubUi.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/hub/HubCard.kt
index 42f93bde..9488ad4f 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/HubUi.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/hub/HubCard.kt
@@ -1,4 +1,4 @@
-package com.garnegsoft.hubs.ui.common
+package com.garnegsoft.hubs.ui.common.feedCards.hub
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -39,10 +39,10 @@ fun defaultHubCardStyle(): HubCardStyle {
@Composable
fun HubCard(
- hub: HubSnippet,
- style: HubCardStyle = defaultHubCardStyle(),
- onClick: () -> Unit,
- indicator: @Composable (hub: HubSnippet) -> Unit = {
+ hub: HubSnippet,
+ style: HubCardStyle = defaultHubCardStyle(),
+ onClick: () -> Unit,
+ indicator: @Composable (hub: HubSnippet) -> Unit = {
Text(
text = String.format("%.1f", hub.statistics.rating).replace(',', '.'),
fontWeight = FontWeight.W400,
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/UserUi.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/user/UserCard.kt
similarity index 96%
rename from app/src/main/java/com/garnegsoft/hubs/ui/common/UserUi.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/user/UserCard.kt
index a8366a16..b70616f8 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/UserUi.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/feedCards/user/UserCard.kt
@@ -1,4 +1,4 @@
-package com.garnegsoft.hubs.ui.common
+package com.garnegsoft.hubs.ui.common.feedCards.user
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -51,12 +51,12 @@ private fun defaultUserCardStyle(): UserCardStyle {
@Composable
fun UserCard(
- user: UserSnippet,
- style: UserCardStyle = defaultUserCardStyle(),
- indicator: @Composable () -> Unit = {
+ user: UserSnippet,
+ style: UserCardStyle = defaultUserCardStyle(),
+ indicator: @Composable () -> Unit = {
Text(text = user.rating.toString(), fontWeight = FontWeight.W400, color = DefaultRatingIndicatorColor)
},
- onClick: () -> Unit
+ onClick: () -> Unit
) {
Row(
modifier = Modifier
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/ArticlesListPage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/ArticlesListPage.kt
index 17d04df7..8b1eabed 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/ArticlesListPage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/ArticlesListPage.kt
@@ -5,11 +5,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.CollapsingContentState
import com.garnegsoft.hubs.api.Filter
import com.garnegsoft.hubs.api.article.ArticlesListModel
import com.garnegsoft.hubs.api.rememberCollapsingContentState
-import com.garnegsoft.hubs.ui.common.ArticleCard
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCard
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
import com.garnegsoft.hubs.ui.common.FilterElement
import kotlinx.coroutines.launch
@@ -22,20 +25,25 @@ fun ArticlesListPage(
onArticleSnippetClick: (articleId: Int) -> Unit,
onArticleAuthorClick: (authorAlias: String) -> Unit,
onArticleCommentsClick: (articleId: Int) -> Unit,
- doInitialLoading: Boolean = true
+ doInitialLoading: Boolean = true,
) {
- CommonPage(
- listModel = listModel,
- lazyListState = lazyListState,
- collapsingBar = filterIndicator,
- doInitialLoading = doInitialLoading
- ) {
- ArticleCard(
- article = it,
- onClick = { onArticleSnippetClick(it.id) },
- onAuthorClick = { it.author?.alias?.let { onArticleAuthorClick(it) } },
- onCommentsClick = { onArticleCommentsClick(it.id) }
- )
+ val cardsStyle = ArticleCardStyle.defaultArticleCardStyle()
+
+ cardsStyle?.let { articleCardStyle ->
+ CommonPage(
+ listModel = listModel,
+ lazyListState = lazyListState,
+ collapsingBar = filterIndicator,
+ doInitialLoading = doInitialLoading
+ ) {
+ ArticleCard(
+ article = it,
+ onClick = { onArticleSnippetClick(it.id) },
+ onAuthorClick = { it.author?.alias?.let { onArticleAuthorClick(it) } },
+ onCommentsClick = { onArticleCommentsClick(it.id) },
+ style = articleCardStyle
+ )
+ }
}
}
@@ -61,18 +69,24 @@ fun ArticlesListPageWithFilter(
doInitialLoading: Boolean = true,
filterDialog: @Composable (defaultValues: F, onDismiss: () -> Unit, onDone: (F) -> Unit) -> Unit,
) {
- CommonPageWithFilter(
- listModel = listModel, filterDialog = filterDialog,
- filter = filter,
- doInitialLoading = doInitialLoading,
- lazyListState = lazyListState,
- collapsingContentState = collapsingContentState
- ) {
- ArticleCard(
- article = it,
- onClick = { onArticleSnippetClick(it.id) },
- onAuthorClick = { it.author?.alias?.let { onArticleAuthorClick(it) } },
- onCommentsClick = { onArticleCommentsClick(it.id) }
- )
+ val cardsStyle = ArticleCardStyle.defaultArticleCardStyle()
+
+ cardsStyle?.let { style ->
+ CommonPageWithFilter(
+ listModel = listModel, filterDialog = filterDialog,
+ filter = filter,
+ doInitialLoading = doInitialLoading,
+ lazyListState = lazyListState,
+ collapsingContentState = collapsingContentState
+ ) {
+ ArticleCard(
+ article = it,
+ onClick = { onArticleSnippetClick(it.id) },
+ onAuthorClick = { it.author?.alias?.let { onArticleAuthorClick(it) } },
+ onCommentsClick = { onArticleCommentsClick(it.id) },
+ style = style
+ )
+ }
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommentsListPage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommentsListPage.kt
index 6a9d5153..52e39ee8 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommentsListPage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommentsListPage.kt
@@ -4,7 +4,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import com.garnegsoft.hubs.api.comment.CommentsListModel
-import com.garnegsoft.hubs.ui.common.CommentCard
+import com.garnegsoft.hubs.ui.common.feedCards.comment.CommentCard
@Composable
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommonPage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommonPage.kt
index bb592fd7..075acc5b 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommonPage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CommonPage.kt
@@ -1,6 +1,8 @@
package com.garnegsoft.hubs.ui.common.snippetsPages
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyListState
@@ -11,11 +13,13 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.CollapsingContent
import com.garnegsoft.hubs.api.CollapsingContentState
import com.garnegsoft.hubs.api.Filter
import com.garnegsoft.hubs.api.HabrSnippet
import com.garnegsoft.hubs.api.article.AbstractSnippetListModel
+import com.garnegsoft.hubs.api.article.HabrSnippetListModel
import com.garnegsoft.hubs.api.rememberCollapsingContentState
import com.garnegsoft.hubs.ui.common.FilterElement
import com.garnegsoft.hubs.ui.common.LazyHabrSnippetsColumn
@@ -24,7 +28,7 @@ import kotlinx.coroutines.launch
@Composable
fun CommonPage(
- listModel: AbstractSnippetListModel,
+ listModel: HabrSnippetListModel,
lazyListState: LazyListState = rememberLazyListState(),
collapsingBar: (@Composable () -> Unit)? = null,
doInitialLoading: Boolean = true,
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CompaniesListPage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CompaniesListPage.kt
index d5678d1a..857d383e 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CompaniesListPage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/CompaniesListPage.kt
@@ -2,17 +2,13 @@ package com.garnegsoft.hubs.ui.common.snippetsPages
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
import com.garnegsoft.hubs.api.CollapsingContentState
-import com.garnegsoft.hubs.api.article.AbstractSnippetListModel
import com.garnegsoft.hubs.api.company.CompaniesListModel
import com.garnegsoft.hubs.api.company.list.CompanySnippet
import com.garnegsoft.hubs.api.rememberCollapsingContentState
-import com.garnegsoft.hubs.ui.common.CompanyCard
-import com.garnegsoft.hubs.ui.theme.DefaultRatingIndicatorColor
+import com.garnegsoft.hubs.ui.common.feedCards.company.CompanyCard
+import com.garnegsoft.hubs.ui.common.feedCards.company.DefaultCompanyIndicator
@Composable
@@ -23,13 +19,7 @@ fun CompaniesListPage(
doInitialLoading: Boolean = true,
collapsingContentState: CollapsingContentState = rememberCollapsingContentState(),
onCompanyClick: (alias: String) -> Unit,
- cardIndicator: @Composable (CompanySnippet) -> Unit = {
- Text(
- it.statistics.rating.toString(),
- fontWeight = FontWeight.W400,
- color = DefaultRatingIndicatorColor
- )
- }
+ cardIndicator: @Composable (CompanySnippet) -> Unit = { DefaultCompanyIndicator(company = it) }
) {
CommonPage(
listModel = listModel,
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/HubsListPage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/HubsListPage.kt
index 6d6a768f..5127931a 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/HubsListPage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/HubsListPage.kt
@@ -8,7 +8,7 @@ import com.garnegsoft.hubs.api.article.AbstractSnippetListModel
import com.garnegsoft.hubs.api.hub.HubsListModel
import com.garnegsoft.hubs.api.hub.list.HubSnippet
import com.garnegsoft.hubs.api.rememberCollapsingContentState
-import com.garnegsoft.hubs.ui.common.HubCard
+import com.garnegsoft.hubs.ui.common.feedCards.hub.HubCard
@Composable
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/UserListPage.kt b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/UserListPage.kt
index 6229289b..48bc3dde 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/UserListPage.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/common/snippetsPages/UserListPage.kt
@@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import com.garnegsoft.hubs.api.user.UsersListModel
import com.garnegsoft.hubs.api.user.list.UserSnippet
-import com.garnegsoft.hubs.ui.common.UserCard
+import com.garnegsoft.hubs.ui.common.feedCards.user.UserCard
import com.garnegsoft.hubs.ui.theme.DefaultRatingIndicatorColor
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/ImageViewerScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/ImageViewerScreen.kt
index e73b76a9..eb85670e 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/ImageViewerScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/ImageViewerScreen.kt
@@ -1,8 +1,14 @@
package com.garnegsoft.hubs.ui.screens
+import android.graphics.BitmapFactory
+import android.net.Uri
import android.os.Build
import android.util.Log
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.*
@@ -24,15 +30,26 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toDrawable
+import coil.decode.DataSource
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
+import coil.fetch.DrawableResult
+import coil.fetch.FetchResult
+import coil.fetch.Fetcher
import coil.request.ImageRequest
+import coil.size.Size
+import com.garnegsoft.hubs.api.CommonImageRequestFetcher
+import com.garnegsoft.hubs.api.offlineResourcesDir
import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
import me.saket.telephoto.zoomable.rememberZoomableState
+import java.io.File
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@@ -40,117 +57,137 @@ import kotlin.math.roundToInt
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ImageViewScreen(
- model: Any,
- onBack: () -> Unit
+ model: Any,
+ onBack: () -> Unit
) {
-
- val zoomableState = rememberZoomableState(
- autoApplyTransformations = false,
- zoomSpec = ZoomSpec(maxZoomFactor = 3f, preventOverOrUnderZoom = false)
- )
-
- val state = rememberZoomableImageState(zoomableState = zoomableState)
-
- val systemUiController = rememberSystemUiController()
- val statusBarColor = MaterialTheme.colors.let {
- if (it.isLight)
- it.primary
- else
- it.surface
- }
- DisposableEffect(key1 = Unit, effect = {
- systemUiController.setStatusBarColor(Color.Black)
- onDispose {
- systemUiController.setStatusBarColor(statusBarColor)
- }
- })
- var offset by remember {
- mutableStateOf(0f)
- }
-
- val draggableState = rememberDraggableState {
- offset += it
- }
- var isDragging by remember { mutableStateOf(false) }
- val animatedOffset by animateFloatAsState(
- targetValue = if (isDragging) offset else 0f)
-
-
- val screenHeight = LocalConfiguration.current.screenHeightDp * LocalDensity.current.density
-
- Box(modifier = Modifier
- .fillMaxSize()
- .draggable(
- state = draggableState,
- orientation = Orientation.Vertical,
- enabled = true,
- onDragStarted = {
- isDragging = true
- },
- onDragStopped = {
- if (it.absoluteValue > 8000f || offset.absoluteValue > screenHeight / 3){
- onBack()
- } else {
- isDragging = false
- offset = 0f
- }
- }
- )
- .background(Color.Black)){
-
-
- ZoomableAsyncImage(
- modifier = Modifier
- .fillMaxSize()
- .offset { IntOffset(0, (if (isDragging) offset else animatedOffset).roundToInt()) },
- state = state,
- model =
- ImageRequest.Builder(LocalContext.current)
- .data(model)
- .decoderFactory(SvgDecoder.Factory())
- .decoderFactory(
- if (Build.VERSION.SDK_INT >= 28) {
- ImageDecoderDecoder.Factory()
- } else {
- GifDecoder.Factory()
- }
- )
- .crossfade(true)
- .build(),
- contentDescription = null,
- clipToBounds = false
- )
- Row(modifier = Modifier
- .fillMaxWidth()
- .height(55.dp)
- .padding(horizontal = 4.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.End
- ) {
- CompositionLocalProvider(LocalRippleTheme provides customRippleTheme) {
- Box(modifier = Modifier
- .size(48.dp)
- .clip(CircleShape)
- .clickable(onClick = onBack)
- .background(Color.Black.copy(0.5f)),
- contentAlignment = Alignment.Center
- ) {
- Icon(imageVector = Icons.Default.Close, contentDescription = "", tint = Color.White)
- }
- }
- }
- }
+ val context = LocalContext.current
+ val zoomableState = rememberZoomableState(
+ autoApplyTransformations = false,
+ zoomSpec = ZoomSpec(maxZoomFactor = 3f, preventOverOrUnderZoom = false)
+ )
+
+ val state = rememberZoomableImageState(zoomableState = zoomableState)
+
+ val systemUiController = rememberSystemUiController()
+ val statusBarColor = MaterialTheme.colors.let {
+ if (it.isLight)
+ it.primary
+ else
+ it.surface
+ }
+ DisposableEffect(key1 = Unit, effect = {
+ systemUiController.setStatusBarColor(Color.Black)
+ onDispose {
+ systemUiController.setStatusBarColor(statusBarColor)
+ }
+ })
+ var offset by remember {
+ mutableStateOf(0f)
+ }
+
+ val draggableState = rememberDraggableState {
+ offset += it
+ }
+ var isDragging by remember { mutableStateOf(false) }
+ val animatedOffset by animateFloatAsState(
+ targetValue = if (isDragging) offset else 0f
+ )
+
+
+ val screenHeight = LocalConfiguration.current.screenHeightDp * LocalDensity.current.density
+ val splineBasedDecay = rememberSplineBasedDecay()
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .draggable(
+ state = draggableState,
+ orientation = Orientation.Vertical,
+ enabled = true,
+ onDragStarted = {
+ isDragging = true
+ },
+ onDragStopped = {
+ if (it.absoluteValue > 8000f || offset.absoluteValue > screenHeight / 3) {
+ launch {
+ var lastValue = 0f
+ AnimationState(
+ initialValue = 0f,
+ initialVelocity = it
+ ).animateDecay(splineBasedDecay) {
+ offset += value - lastValue
+ lastValue = value
+ }
+ }
+ launch {
+ delay(50)
+ onBack()
+ }
+ } else {
+ isDragging = false
+ offset = 0f
+ }
+ }
+ )
+ .background(Color.Black)) {
+
+
+ ZoomableAsyncImage(
+ modifier = Modifier
+ .fillMaxSize()
+ .offset { IntOffset(0, (if (isDragging) offset else animatedOffset).roundToInt()) },
+ state = state,
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(model)
+ //.decoderFactory(SvgDecoder.Factory())
+ .decoderFactory(
+ if (Build.VERSION.SDK_INT >= 28) {
+ ImageDecoderDecoder.Factory()
+ } else {
+ GifDecoder.Factory()
+ }
+ )
+ .size(Size.ORIGINAL)
+ .crossfade(true)
+ .build(),
+ contentDescription = null,
+ clipToBounds = false
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(55.dp)
+ .padding(horizontal = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ CompositionLocalProvider(LocalRippleTheme provides customRippleTheme) {
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .clickable(onClick = onBack)
+ .background(Color.Black.copy(0.5f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "",
+ tint = Color.White
+ )
+ }
+ }
+ }
+ }
}
val customRippleTheme = object : RippleTheme {
- @Composable
- override fun defaultColor(): Color {
- return Color.White
- }
-
- @Composable
- override fun rippleAlpha(): RippleAlpha {
- return RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f)
- }
-
+ @Composable
+ override fun defaultColor(): Color {
+ return Color.White
+ }
+
+ @Composable
+ override fun rippleAlpha(): RippleAlpha {
+ return RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleContent.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleContent.kt
index c6ff9294..65cf61ee 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleContent.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleContent.kt
@@ -35,14 +35,12 @@ import coil.compose.AsyncImage
import com.garnegsoft.hubs.R
import com.garnegsoft.hubs.api.EditorVersion
import com.garnegsoft.hubs.api.dataStore.HubsDataStore
-import com.garnegsoft.hubs.api.PostComplexity
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.api.PostType
import com.garnegsoft.hubs.api.article.Article
import com.garnegsoft.hubs.api.company.CompanyController
-import com.garnegsoft.hubs.api.dataStore.settingsDataStoreFlowWithDefault
-import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
import com.garnegsoft.hubs.ui.common.TitledColumn
-import com.garnegsoft.hubs.ui.screens.user.HubChip
+import com.garnegsoft.hubs.ui.common.HubChip
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jsoup.nodes.*
@@ -66,20 +64,13 @@ fun ArticleContent(
val mostReadingArticles by viewModel.mostReadingArticles.observeAsState()
Box() {
- val flingSpec = rememberSplineBasedDecay()
val contentNodes by viewModel.parsedArticleContent.observeAsState()
- val fontSize by context.settingsDataStoreFlowWithDefault(
- HubsDataStore.Settings.Keys.ArticleScreen.FontSize,
- MaterialTheme.typography.body1.fontSize.value
- ).collectAsState(
- initial = null
- )
- val lineHeightFactor by context.settingsDataStoreFlowWithDefault(
- HubsDataStore.Settings.Keys.ArticleScreen.LineHeightFactor,
- 1.5f
- ).collectAsState(
- initial = null
- )
+ val fontSize by HubsDataStore.Settings
+ .getValueFlow(context, HubsDataStore.Settings.ArticleScreen.FontSize)
+ .collectAsState(
+ initial = null
+ )
+
val color = MaterialTheme.colors.onSurface
val spanStyle = remember(fontSize, color) {
SpanStyle(
@@ -94,17 +85,13 @@ fun ArticleContent(
fitScreenWidth = false
)
}
-
val state = rememberLazyListState()
val updatedPolls by viewModel.updatedPolls.observeAsState()
-
LazyColumn(
- modifier = Modifier
- .fillMaxSize(),
+ modifier = Modifier.fillMaxSize(),
state = state,
- contentPadding = PaddingValues(16.dp),
- verticalArrangement = Arrangement.spacedBy(0.dp)
+ contentPadding = PaddingValues(16.dp)
) {
if (article.editorVersion == EditorVersion.FirstVersion) {
item {
@@ -126,7 +113,6 @@ fun ArticleContent(
color = MaterialTheme.colors.onError
)
}
-
Spacer(modifier = Modifier.height(16.dp))
}
}
@@ -134,16 +120,18 @@ fun ArticleContent(
if (article.postType == PostType.Megaproject && article.metadata != null) {
item {
AsyncImage(
- article.metadata.mainImageUrl,
- "",
- modifier = Modifier.clip(RoundedCornerShape(8.dp)),
+ model = article.metadata.mainImageUrl,
+ contentDescription = "",
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(8.dp)),
)
Spacer(Modifier.height(8.dp))
}
}
if (article.isCorporative) {
- item {
+ item {
val companyAlias = article.hubs.find { it.isCorporative }!!.alias
var companyTitle: String? by rememberSaveable {
@@ -185,8 +173,7 @@ fun ArticleContent(
fontSize = 14.sp,
color = MaterialTheme.colors.onBackground
)
- }
- else {
+ } else {
Box(modifier = Modifier.size(34.dp))
}
@@ -205,33 +192,16 @@ fun ArticleContent(
.clickable(onClick = { onAuthorClicked() }),
verticalAlignment = Alignment.CenterVertically,
) {
- if (article.author.avatarUrl != null)
+ if (article.author.avatarUrl != null) {
AsyncImage(
modifier = Modifier
- .size(34.dp)
+ .size(38.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color.White),
model = article.author.avatarUrl,
contentDescription = ""
)
- else
- Icon(
- modifier = Modifier
- .size(34.dp)
- .border(
- width = 2.dp,
- color = placeholderColorLegacy(article.author.alias),
- shape = RoundedCornerShape(8.dp)
- )
- .background(
- Color.White,
- shape = RoundedCornerShape(8.dp)
- )
- .padding(2.dp),
- painter = painterResource(id = R.drawable.user_avatar_placeholder),
- contentDescription = "",
- tint = placeholderColorLegacy(article.author.alias)
- )
+ }
Spacer(modifier = Modifier.width(4.dp))
Text(
text = article.author.alias, fontWeight = FontWeight.W600,
@@ -239,51 +209,64 @@ fun ArticleContent(
color = MaterialTheme.colors.onBackground
)
Spacer(modifier = Modifier.weight(1f))
- Text(
- article.timePublished, color = Color.Gray,
- fontSize = 12.sp, fontWeight = FontWeight.W400
- )
+
}
Spacer(modifier = Modifier.height(8.dp))
}
}
item {
+ fontSize?.let {
+ Text(
+ text = article.title,
+ fontSize = (it + 4f).sp,
+ fontWeight = FontWeight.W700,
+ color = MaterialTheme.colors.onBackground
+ )
+ }
- Text(
- text = article.title,
- fontSize = 22.sp,
- fontWeight = FontWeight.W700,
- color = MaterialTheme.colors.onBackground
- )
Spacer(modifier = Modifier.height(4.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Text(
+ text = article.timePublished,
+ color = MaterialTheme.colors.onBackground.copy(0.6f),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.W500
+ )
+ }
+
+ Spacer(Modifier.height(4.dp))
+
Row(
verticalAlignment = Alignment.CenterVertically,
) {
- if (article.complexity != PostComplexity.None) {
+ if (article.complexity != PublicationComplexity.None) {
Icon(
modifier = Modifier.size(height = 10.dp, width = 20.dp),
painter = painterResource(id = R.drawable.speedmeter_hard),
contentDescription = "",
tint = when (article.complexity) {
- PostComplexity.Low -> Color(0xFF4CBE51)
- PostComplexity.Medium -> Color(0xFFEEBC25)
- PostComplexity.High -> Color(0xFFEB3B2E)
+ PublicationComplexity.Low -> Color(0xFF4CBE51)
+ PublicationComplexity.Medium -> Color(0xFFEEBC25)
+ PublicationComplexity.High -> Color(0xFFEB3B2E)
else -> Color.Red
}
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = when (article.complexity) {
- PostComplexity.Low -> "Простой"
- PostComplexity.Medium -> "Средний"
- PostComplexity.High -> "Сложный"
+ PublicationComplexity.Low -> "Простой"
+ PublicationComplexity.Medium -> "Средний"
+ PublicationComplexity.High -> "Сложный"
else -> ""
},
color = when (article.complexity) {
- PostComplexity.Low -> Color(0xFF4CBE51)
- PostComplexity.Medium -> Color(0xFFEEBC25)
- PostComplexity.High -> Color(0xFFEB3B2E)
+ PublicationComplexity.Low -> Color(0xFF4CBE51)
+ PublicationComplexity.Medium -> Color(0xFFEEBC25)
+ PublicationComplexity.High -> Color(0xFFEB3B2E)
else -> Color.Red
},
fontWeight = FontWeight.W500,
@@ -379,10 +362,7 @@ fun ArticleContent(
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
article.hubs.forEach {
HubChip(
- if (it.isProfiled)
- it.title + "*"
- else
- it.title
+ title = if (it.isProfiled) it.title + "*" else it.title
) {
if (it.isCorporative)
onCompanyClick(it.alias)
@@ -444,20 +424,21 @@ fun ArticleContent(
}
mostReadingArticles?.let {
items(it) {
- Box(modifier = Modifier
- .clip(RoundedCornerShape(8.dp))
- .clickable { onArticleClick(it.id) }
- .padding(bottom = 8.dp)
- .padding(8.dp)
- ) {
- ArticleShort(article = it)
- }
+ ArticleShort(
+ article = it,
+ onClick = {
+ onArticleClick(it.id)
+ }
+ )
}
}
}
- val scrollBarAlpha by animateFloatAsState(targetValue = if (state.isScrollInProgress) 1f else 0f, tween(600))
+ val scrollBarAlpha by animateFloatAsState(
+ targetValue = if (state.isScrollInProgress) 1f else 0f,
+ tween(600)
+ )
val density = LocalDensity.current
val scrollBarColor = MaterialTheme.colors.onBackground.copy(0.25f)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreen.kt
index 87cd287e..86fe8878 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreen.kt
@@ -5,10 +5,8 @@ import android.content.Intent
import androidx.compose.animation.*
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.animateDecay
-import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
@@ -37,27 +35,24 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupPositionProvider
-import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
-import coil.compose.AsyncImage
import com.garnegsoft.hubs.R
+import com.garnegsoft.hubs.api.AsyncGifImage
import com.garnegsoft.hubs.api.PostType
import com.garnegsoft.hubs.api.dataStore.HubsDataStore
+import com.garnegsoft.hubs.api.dataStore.LastReadArticleController
+import com.garnegsoft.hubs.api.history.HistoryController
import com.garnegsoft.hubs.api.utils.formatLongNumbers
import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
-import com.garnegsoft.hubs.lastReadDataStore
-import com.garnegsoft.hubs.api.dataStore.settingsDataStoreFlowWithDefault
import com.garnegsoft.hubs.api.utils.formatTime
import com.garnegsoft.hubs.ui.common.TitledColumn
-import com.garnegsoft.hubs.ui.screens.user.HubChip
-import com.garnegsoft.hubs.ui.theme.RatingNegative
-import com.garnegsoft.hubs.ui.theme.RatingPositive
+import com.garnegsoft.hubs.ui.common.HubChip
+import com.garnegsoft.hubs.ui.theme.RatingNegativeColor
+import com.garnegsoft.hubs.ui.theme.RatingPositiveColor
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.jsoup.nodes.*
import kotlin.math.abs
@@ -78,12 +73,11 @@ fun ArticleScreen(
isOffline: Boolean = false
) {
val context = LocalContext.current
- val fontSize by context.settingsDataStoreFlowWithDefault(
- HubsDataStore.Settings.Keys.ArticleScreen.FontSize,
- MaterialTheme.typography.body1.fontSize.value
- ).collectAsState(
- initial = null
- )
+ val fontSize by HubsDataStore.Settings
+ .getValueFlow(context, HubsDataStore.Settings.ArticleScreen.FontSize)
+ .collectAsState(
+ initial = null
+ )
val viewModel = viewModel(viewModelStoreOwner)
val article by viewModel.article.observeAsState()
val offlineArticle by viewModel.offlineArticle.observeAsState()
@@ -93,6 +87,7 @@ fun ArticleScreen(
if (isOffline) {
viewModel.loadArticleFromLocalDatabase(articleId, context)
} else {
+
viewModel.loadArticle(articleId)
}
}
@@ -160,6 +155,7 @@ fun ArticleScreen(
backgroundColor = if (MaterialTheme.colors.isLight) MaterialTheme.colors.surface else MaterialTheme.colors.background,
bottomBar = {
article?.let { article ->
+
BottomAppBar(
elevation = 0.dp,
backgroundColor = MaterialTheme.colors.surface,
@@ -200,9 +196,9 @@ fun ArticleScreen(
else
article.statistics.score.toString(),
color = if (article.statistics.score > 0)
- RatingPositive
+ RatingPositiveColor
else if (article.statistics.score < 0)
- RatingNegative
+ RatingNegativeColor
else
statisticsColor,
fontWeight = FontWeight.W500
@@ -254,7 +250,11 @@ fun ArticleScreen(
addedToBookmarksCount--
addedToBookmarksCount =
addedToBookmarksCount.coerceAtLeast(0)
- if (!ArticleController.removeFromBookmarks(article.id, article.postType == PostType.News)) {
+ if (!ArticleController.removeFromBookmarks(
+ article.id,
+ article.postType == PostType.News
+ )
+ ) {
addedToBookmarks = true
addedToBookmarksCount++
addedToBookmarksCount =
@@ -264,7 +264,11 @@ fun ArticleScreen(
} else {
addedToBookmarks = true
addedToBookmarksCount++
- if (!ArticleController.addToBookmarks(article.id, article.postType == PostType.News)) {
+ if (!ArticleController.addToBookmarks(
+ article.id,
+ article.postType == PostType.News
+ )
+ ) {
addedToBookmarks = false
addedToBookmarksCount--
addedToBookmarksCount =
@@ -310,7 +314,7 @@ fun ArticleScreen(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
- .background(RatingPositive)
+ .background(RatingPositiveColor)
)
}
}
@@ -385,7 +389,7 @@ fun ArticleScreen(
verticalAlignment = Alignment.CenterVertically,
) {
if (article.authorAvatarUrl != null)
- AsyncImage(
+ AsyncGifImage(
modifier = Modifier
.size(34.dp)
.clip(RoundedCornerShape(8.dp))
@@ -587,13 +591,6 @@ fun ArticleScreen(
}
} else {
article?.let { article ->
-
- val lineHeightFactor by context.settingsDataStoreFlowWithDefault(
- HubsDataStore.Settings.Keys.ArticleScreen.LineHeightFactor,
- 1.5f
- ).collectAsState(
- initial = null
- )
val color = MaterialTheme.colors.onSurface
val spanStyle = remember(fontSize, color) {
SpanStyle(
@@ -612,8 +609,9 @@ fun ArticleScreen(
mutableStateOf(false)
}
LaunchedEffect(key1 = Unit, block = {
- context.lastReadDataStore.edit {
- it[HubsDataStore.LastRead.Keys.LastArticleRead] = articleId
+ LastReadArticleController.setLastArticle(context, articleId)
+ withContext(Dispatchers.IO){
+ HistoryController.insertArticle(articleId, context)
}
if (!viewModel.parsedArticleContent.isInitialized && fontSize != null) {
val element =
@@ -631,19 +629,20 @@ fun ArticleScreen(
nodeParsed = true
}
})
- LaunchedEffect(key1 = fontSize, block = {
- })
Box(modifier = Modifier.padding(it)) {
- if (nodeParsed && fontSize != null)
- ArticleContent(
- article = article,
- onAuthorClicked = { onAuthorClicked(article.author!!.alias) },
- onHubClicked = onHubClicked,
- onCompanyClick = onCompanyClick,
- onViewImageRequest = onViewImageRequest,
- onArticleClick = onArticleClick
- )
+ if (nodeParsed && fontSize != null) {
+ SelectionContainer {
+ ArticleContent(
+ article = article,
+ onAuthorClicked = { onAuthorClicked(article.author!!.alias) },
+ onHubClicked = onHubClicked,
+ onCompanyClick = onCompanyClick,
+ onViewImageRequest = onViewImageRequest,
+ onArticleClick = onArticleClick
+ )
+ }
+ }
}
} ?: Box(
modifier = Modifier
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreenViewModel.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreenViewModel.kt
index a79597fc..001cd14d 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreenViewModel.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleScreenViewModel.kt
@@ -6,105 +6,96 @@ import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.text.SpanStyle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.garnegsoft.hubs.api.HabrList
import com.garnegsoft.hubs.api.article.Article
import com.garnegsoft.hubs.api.article.list.ArticleSnippet
-import com.garnegsoft.hubs.api.article.offline.HubsList
import com.garnegsoft.hubs.api.article.offline.OfflineArticle
-import com.garnegsoft.hubs.api.article.offline.OfflineArticleSnippet
import com.garnegsoft.hubs.api.article.offline.OfflineArticlesController
import com.garnegsoft.hubs.api.article.offline.offlineArticlesDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ArticleScreenViewModel : ViewModel() {
- private var _article = MutableLiveData()
- val article: LiveData get() = _article
-
- private var _offlineArticle = MutableLiveData()
- val offlineArticle: LiveData get() = _offlineArticle
-
- fun loadArticle(id: Int) {
- viewModelScope.launch(Dispatchers.IO) {
- ArticleController.get(id)?.let {
- _article.postValue(it)
-
- }
- }
- }
-
- fun loadArticleFromLocalDatabase(id: Int, context: Context) {
- viewModelScope.launch(Dispatchers.IO){
- val dao = context.offlineArticlesDatabase.articlesDao()
- if (dao.exists(id)) {
- _offlineArticle.postValue(dao._getArticleById(id))
-
- } else {
- withContext(Dispatchers.Main){
- Toast.makeText(context, "Статья не найдена в скачанных\nПопробуйте скачать ее заново", Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
-
- fun saveArticle(id: Int, context: Context){
- viewModelScope.launch(Dispatchers.IO){
- OfflineArticlesController.downloadArticle(id, context)
- withContext(Dispatchers.Main){
- Toast.makeText(context, "Статья скачана!", Toast.LENGTH_SHORT).show()
- }
- Log.e("offlineArticle", "loading done")
- }
- }
-
- fun deleteSavedArticle(id: Int, context: Context){
- viewModelScope.launch(Dispatchers.IO) {
- OfflineArticlesController.deleteArticle(id, context)
- withContext(Dispatchers.Main) {
- Toast.makeText(context, "Статья удалена!", Toast.LENGTH_SHORT).show()
- }
- }
- }
-
- fun articleExists(context: Context, articleId: Int): Flow {
- return context.offlineArticlesDatabase.articlesDao().existsFlow(articleId)
- }
-
- private var _mostReadingArticles = MutableLiveData>()
- val mostReadingArticles: LiveData> get() = _mostReadingArticles
-
- fun loadMostReading() {
- viewModelScope.launch(Dispatchers.IO) {
- ArticlesListController.getArticlesSnippets("articles/most-reading")?.let {
- _mostReadingArticles.postValue(it.list.take(5))
- }
- }
- }
-
- private val _updatedPolls = MutableLiveData>()
- val updatedPolls: LiveData> get() = _updatedPolls
-
- fun vote(pollId: Int, variantsIds: List) {
- viewModelScope.launch(Dispatchers.IO) {
- ArticleController.vote(pollId = pollId, variantsIds = variantsIds)?.let { poll ->
- _updatedPolls.value?.let {
- _updatedPolls.postValue(it + poll)
- }
- }
- }
- }
-
- val parsedArticleContent = MutableLiveData Unit)?>>()
-
+ private var _article = MutableLiveData()
+ val article: LiveData get() = _article
+
+ private var _offlineArticle = MutableLiveData()
+ val offlineArticle: LiveData get() = _offlineArticle
+
+ fun loadArticle(id: Int) {
+ viewModelScope.launch(Dispatchers.IO) {
+ ArticleController.get(id)?.let {
+ _article.postValue(it)
+
+ }
+ }
+ }
+
+ fun loadArticleFromLocalDatabase(id: Int, context: Context) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val dao = context.offlineArticlesDatabase.articlesDao()
+ if (dao.exists(id)) {
+ _offlineArticle.postValue(dao.getArticleById(id))
+
+ } else {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ context,
+ "Статья не найдена в скачанных\nПопробуйте скачать ее заново",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+
+ fun saveArticle(id: Int, context: Context) {
+ viewModelScope.launch(Dispatchers.IO) {
+ OfflineArticlesController.downloadArticle(id, context)
+ Log.e("offlineArticle", "loading done")
+ }
+ }
+
+ fun deleteSavedArticle(id: Int, context: Context) {
+ OfflineArticlesController.deleteArticle(id, context)
+ }
+
+ fun articleExists(context: Context, articleId: Int): Flow {
+ return context.offlineArticlesDatabase.articlesDao().existsFlow(articleId)
+ }
+
+ private var _mostReadingArticles = MutableLiveData>()
+ val mostReadingArticles: LiveData> get() = _mostReadingArticles
+
+ fun loadMostReading() {
+ viewModelScope.launch(Dispatchers.IO) {
+ ArticlesListController.getArticlesSnippets("articles/most-reading")?.let {
+ _mostReadingArticles.postValue(it.list.take(5))
+ }
+ }
+ }
+
+ private val _updatedPolls = MutableLiveData>()
+ val updatedPolls: LiveData> get() = _updatedPolls
+
+ fun vote(pollId: Int, variantsIds: List) {
+ viewModelScope.launch(Dispatchers.IO) {
+ ArticleController.vote(pollId = pollId, variantsIds = variantsIds)?.let { poll ->
+ _updatedPolls.value?.let {
+ _updatedPolls.postValue(it + poll)
+ }
+ }
+ }
+ }
+
+ val parsedArticleContent =
+ MutableLiveData Unit)?>>()
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleShort.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleShort.kt
index 05078790..422db311 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleShort.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ArticleShort.kt
@@ -1,40 +1,46 @@
package com.garnegsoft.hubs.ui.screens.article
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.garnegsoft.hubs.R
import com.garnegsoft.hubs.api.article.list.ArticleSnippet
import com.garnegsoft.hubs.api.utils.formatLongNumbers
-import com.garnegsoft.hubs.ui.theme.HubsTheme
-import com.garnegsoft.hubs.ui.theme.RatingNegative
-import com.garnegsoft.hubs.ui.theme.RatingPositive
+import com.garnegsoft.hubs.ui.theme.RatingNegativeColor
+import com.garnegsoft.hubs.ui.theme.RatingPositiveColor
@Composable
fun ArticleShort(
- article: ArticleSnippet
+ article: ArticleSnippet,
+ onClick: () -> Unit
) {
-
-// Divider(modifier = Modifier.padding(16.dp))
-// Spacer(modifier = Modifier.height(32.dp))
- Column() {
+
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(12.dp))
+ .clickable(onClick = onClick)
+ .padding(bottom = 8.dp)
+ .padding(8.dp)
+ ) {
Text(
text = article.title,
color = MaterialTheme.colors.onBackground,
fontSize = 18.sp,
fontWeight = FontWeight.W600
)
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
@@ -44,19 +50,18 @@ fun ArticleShort(
painter = painterResource(id = R.drawable.rating),
contentDescription = null,
modifier = Modifier.size(18.dp),
-
)
Spacer(modifier = Modifier.width(2.dp))
if (article.statistics.score > 0) {
Text(
text = '+' + article.statistics.score.toString(),
- color = RatingPositive
+ color = RatingPositiveColor,
)
} else {
if (article.statistics.score < 0) {
Text(
text = article.statistics.score.toString(),
- color = RatingNegative
+ color = RatingNegativeColor
)
} else {
Text(
@@ -69,7 +74,7 @@ fun ArticleShort(
Icon(
painter = painterResource(id = R.drawable.views_icon),
contentDescription = null,
- modifier = Modifier.size(18.dp)
+ modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(2.dp))
Text(
@@ -80,13 +85,13 @@ fun ArticleShort(
Icon(
painter = painterResource(id = R.drawable.comments_icon),
contentDescription = null,
- modifier = Modifier.size(18.dp)
+ modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = formatLongNumbers(article.statistics.commentsCount),
overflow = TextOverflow.Clip,
- maxLines = 1
+ maxLines = 1,
)
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/HubsRow.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/HubsRow.kt
index 68620988..75ef5b2d 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/HubsRow.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/HubsRow.kt
@@ -1,19 +1,24 @@
package com.garnegsoft.hubs.ui.screens.article
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import com.garnegsoft.hubs.api.article.Article
+import com.garnegsoft.hubs.ui.theme.HubSubscribedColor
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -22,19 +27,24 @@ fun HubsRow(
onHubClicked: (alias: String) -> Unit,
onCompanyClicked: (alias: String) -> Unit,
) {
- FlowRow() {
- hubs.forEach {
- val hubTitle =
- (if (it.isProfiled) it.title + "*" else it.title) + ", "
+ FlowRow(
+ horizontalArrangement = Arrangement.spacedBy(2.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+
+ hubs.forEachIndexed() { index, it ->
+ val hubTitle = remember {
+ (if (it.isProfiled) it.title + "*" else it.title) + if (index != hubs.lastIndex) ", " else ""
+ }
Text(
modifier = Modifier
.clip(RoundedCornerShape(2.dp))
- .clickable { if (it.isCorporative) onCompanyClicked(it.alias) else onHubClicked(it.alias) }
- .padding(horizontal = 2.dp),
+ .clickable { if (it.isCorporative) onCompanyClicked(it.alias) else onHubClicked(it.alias) },
text = hubTitle,
style = TextStyle(
- color = Color.Gray,
+ fontSize = 16.sp,
+ color = if (it.relatedData?.isSubscribed == true) HubSubscribedColor else Color.Gray,
fontWeight = FontWeight.W500
)
)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/NewParseHtml.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/NewParseHtml.kt
index b72e8ade..7db6d60f 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/NewParseHtml.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/NewParseHtml.kt
@@ -1,14 +1,12 @@
package com.garnegsoft.hubs.ui.screens.article
-import android.content.Intent
-import android.net.Uri
-import android.util.Log
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@@ -204,6 +202,7 @@ fun parseChildElements(
childrenComposables.add { localSpanStyle, settings ->
//Text(text = thisElementCurrentText)
var context = LocalContext.current
+ val focusManager = LocalFocusManager.current
ClickableText(
text = thisElementCurrentText,
style = LocalTextStyle.current.copy(
@@ -212,17 +211,12 @@ fun parseChildElements(
)
),
onClick = {
+ focusManager.clearFocus(true)
thisElementCurrentText.getStringAnnotations(it, it)
.find { it.tag == "url" }
?.let {
if (it.item.startsWith("http")) {
- Log.e(
- "URL Clicked",
- it.item
- )
- var intent =
- Intent(Intent.ACTION_VIEW, Uri.parse(it.item))
- context.startActivity(intent)
+ handleUrl(context, it.item)
}
}
})
@@ -253,8 +247,8 @@ fun parseChildElements(
if (!currentText.text.isBlank() && isBlock)
childrenComposables.add { localSpanStyle, settings ->
- //Text(text = currentText)
val context = LocalContext.current
+ val focusManager = LocalFocusManager.current
ClickableText(
text = currentText,
style = LocalTextStyle.current.copy(
@@ -263,14 +257,11 @@ fun parseChildElements(
)
),
onClick = {
+ focusManager.clearFocus(true)
currentText.getStringAnnotations(it, it).find { it.tag == "url" }?.let {
if (it.item.startsWith("http")) {
- Log.e(
- "URL Clicked",
- it.item
- )
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(it.item))
- context.startActivity(intent)
+ handleUrl(context, it.item)
+
}
}
})
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ParseHtml.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ParseHtml.kt
index cbfc0200..20eda97b 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ParseHtml.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/article/ParseHtml.kt
@@ -1,9 +1,11 @@
package com.garnegsoft.hubs.ui.screens.article
+import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.webkit.WebView
+import android.widget.EditText
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -18,7 +20,6 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.rotate
@@ -29,6 +30,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
@@ -43,7 +45,9 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
+import coil.ImageLoader
import coil.compose.AsyncImagePainter
+import com.garnegsoft.hubs.BuildConfig
import com.garnegsoft.hubs.api.AsyncGifImage
import com.garnegsoft.hubs.ui.common.AsyncSvgImage
import com.garnegsoft.hubs.ui.theme.SecondaryColor
@@ -56,657 +60,678 @@ val HEADER_FONT_WEIGHT = FontWeight.W700
val LINE_HEIGHT_FACTOR = 1.5F
fun parseElement(
- html: String,
- spanStyle: SpanStyle,
- onViewImageRequest: ((imageUrl: String) -> Unit)? = null
+ html: String,
+ spanStyle: SpanStyle,
+ onViewImageRequest: ((imageUrl: String) -> Unit)? = null
): Pair Unit)?> =
- parseElement(Jsoup.parse(html), spanStyle, onViewImageRequest)
+ parseElement(Jsoup.parse(html), spanStyle, onViewImageRequest)
@Stable
@Composable
fun RenderHtml(
- html: String,
- spanStyle: SpanStyle =
- SpanStyle(
- color = MaterialTheme.colors.onSurface,
- fontSize = MaterialTheme.typography.body1.fontSize
- ),
- elementSettings: ElementSettings
+ html: String,
+ spanStyle: SpanStyle =
+ SpanStyle(
+ color = MaterialTheme.colors.onSurface,
+ fontSize = MaterialTheme.typography.body1.fontSize
+ ),
+ elementSettings: ElementSettings
) {
- val result = remember {
- parseElement(
- html = html,
- spanStyle = spanStyle
- )
- }
- Column {
- result.first?.let { Text(it) }
- result.second?.invoke(spanStyle, elementSettings)
- }
-
+ val result = remember {
+ parseElement(
+ html = html,
+ spanStyle = spanStyle
+ )
+ }
+ val context = LocalContext.current
+ Column {
+ result.first?.let { text ->
+ ClickableText(
+ text = text,
+ style = LocalTextStyle.current.copy(lineHeight = LocalTextStyle.current.fontSize * 1.5f),
+ onClick = {
+ text.getStringAnnotations(it, it)
+ .find { it.tag == "url" }
+ ?.let {
+ if (it.item.startsWith("http")) {
+ handleUrl(context, it.item)
+
+ }
+ }
+ }
+ )
+ }
+ result.second?.invoke(spanStyle, elementSettings)
+ }
+
}
/**
* WARNING! Specify fontSize with **spanStyle** or you will get exception
*/
fun parseElement(
- element: Element,
- spanStyle: SpanStyle,
- onViewImageRequest: ((imageUrl: String) -> Unit)? = null,
+ element: Element,
+ spanStyle: SpanStyle,
+ onViewImageRequest: ((imageUrl: String) -> Unit)? = null,
): Pair Unit)?> {
- var isBlock = element.isHabrBlock()
- var resultAnnotatedString: AnnotatedString = buildAnnotatedString { }
- var ChildrenSpanStyle = spanStyle
-
- // Applying Inline elements style
- when (element.tagName()) {
- "del" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(
- textDecoration = TextDecoration.combine(
- listOf(
- ChildrenSpanStyle.textDecoration ?: TextDecoration.None,
- TextDecoration.LineThrough
- )
- )
- )
-
- "b" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(fontWeight = STRONG_FONT_WEIGHT)
- "strong" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(fontWeight = STRONG_FONT_WEIGHT)
- "i" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(fontStyle = FontStyle.Italic)
- "em" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(fontStyle = FontStyle.Italic)
- if (element.hasClass("searched-item")) {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(background = Color(101, 238, 255, 76))
- }
- }
-
- "code" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- fontFamily = FontFamily.Monospace,
- background = Color(138, 156, 165, 20)
- )
- }
-
- "u" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- textDecoration = TextDecoration.combine(
- listOf(
- ChildrenSpanStyle.textDecoration ?: TextDecoration.None,
- TextDecoration.Underline
- )
- )
- )
- }
-
- "s" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- textDecoration = TextDecoration.combine(
- listOf(
- ChildrenSpanStyle.textDecoration ?: TextDecoration.None,
- TextDecoration.LineThrough
- )
- )
- )
- }
-
- "sup" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- baselineShift = BaselineShift.Superscript,
- fontSize = (ChildrenSpanStyle.fontSize.value - 4).coerceAtLeast(1f).sp
- )
- }
-
- "sub" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- baselineShift = BaselineShift.Subscript,
- fontSize = (ChildrenSpanStyle.fontSize.value - 4).coerceAtLeast(1f).sp
- )
- }
-
- "br" -> {
- return buildAnnotatedString { append("\n") } to null
- }
-
- "a" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(color = Color(88, 132, 185, 255))
- if (element.hasClass("user_link")) {
- resultAnnotatedString = buildAnnotatedString {
- withStyle(ChildrenSpanStyle) { append("@") }
- }
- }
- }
-
- "h1" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- fontSize = (ChildrenSpanStyle.fontSize.value + 4f).sp,
- fontWeight = HEADER_FONT_WEIGHT
- )
- }
-
- "h2" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- fontSize = (ChildrenSpanStyle.fontSize.value + 3f).sp,
- fontWeight = HEADER_FONT_WEIGHT
- )
- }
-
- "h3" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- fontSize = (ChildrenSpanStyle.fontSize.value + 2f).sp,
- fontWeight = HEADER_FONT_WEIGHT
- )
- }
-
- "h4" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- fontSize = (ChildrenSpanStyle.fontSize.value + 2f).sp,
- fontWeight = HEADER_FONT_WEIGHT
- )
- }
-
- "h5" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- fontSize = (ChildrenSpanStyle.fontSize.value + 1f).sp,
- fontWeight = HEADER_FONT_WEIGHT
- )
- }
-
- "h6" -> {
- ChildrenSpanStyle = ChildrenSpanStyle.copy(
- fontWeight = HEADER_FONT_WEIGHT
- )
- }
-
- "figcaption" -> {
- ChildrenSpanStyle =
- ChildrenSpanStyle.copy(
- color = Color.Gray,
- // TODO: Fix unspecified span style's fontSize that leads to NaN and exception
- fontSize = (ChildrenSpanStyle.fontSize.value - 4).sp
- )
-
- }
-
- "img" -> {
- if (element.attr("inline") == "true") {
- resultAnnotatedString = buildAnnotatedString {
- appendInlineContent("inlineImage_")
- }
- }
- }
-
- "summary" -> return buildAnnotatedString { } to null
- }
-
- // Child elements parsing and styling
- var childrenElementsResult: ArrayList Unit)?>> =
- ArrayList()
- element.children().forEach {
- childrenElementsResult.add(parseElement(it, ChildrenSpanStyle, onViewImageRequest))
- }
- var mainComposable: (@Composable (SpanStyle, ElementSettings) -> Unit)? = null
-
- var childrenComposables: ArrayList<@Composable (SpanStyle, ElementSettings) -> Unit> = ArrayList()
-
-
- // Text parsing and styling + validating children element
- var currentText = buildAnnotatedString { }
- var childElementsIndex = 0
-
- element.childNodes().forEach { thisNode ->
- if (thisNode is TextNode) {
- if (!thisNode.isBlank)
- currentText +=
- buildAnnotatedString {
- withStyle(ChildrenSpanStyle) {
- append(
- if (thisNode.previousSibling() == null ||
- thisNode.previousSibling() is Element &&
- (thisNode.previousSibling() as Element)?.tagName() == "br"
- )
- thisNode.text().trimStart()
- else
- thisNode.text()
- )
- }
- }
- }
- if (thisNode is Element) {
-
- if (childrenElementsResult[childElementsIndex].first != null)
- currentText += childrenElementsResult[childElementsIndex].first!!
-
-
- if (childrenElementsResult[childElementsIndex].second != null) {
- if (currentText.isNotEmpty() && thisNode.previousElementSibling() != null
- && thisNode.previousElementSibling()!!.tagName() != "pre"
- ) {
- var thisElementCurrentText = currentText
- childrenComposables.add { localSpanStyle, settings ->
- //Text(text = thisElementCurrentText)
- var context = LocalContext.current
- ClickableText(
- text = thisElementCurrentText,
- style = LocalTextStyle.current.copy(
- lineHeight = localSpanStyle.fontSize.times(
- LINE_HEIGHT_FACTOR
- ),
- color = localSpanStyle.color
- ),
- onClick = {
- thisElementCurrentText.getStringAnnotations(it, it)
- .find { it.tag == "url" }
- ?.let {
- if (it.item.startsWith("http")) {
- Log.e(
- "URL Clicked",
- it.item
- )
- var intent =
- Intent(Intent.ACTION_VIEW, Uri.parse(it.item))
- context.startActivity(intent)
- }
- }
- }
- )
- }
- }
- childrenComposables.add(childrenElementsResult[childElementsIndex].second!!)
- currentText = buildAnnotatedString { }
- }
-
-
- // if node is block element, break the currentText annotated string and place Text() Composable
- childElementsIndex++
- }
-
-
- }
- if (!currentText.text.isBlank() && !isBlock) {
- if (element.tagName() == "a") {
- resultAnnotatedString += buildAnnotatedString {
- var urlAnnotationId = pushStringAnnotation("url", element.attr("href"))
- append(currentText)
- pop(urlAnnotationId)
- }
- } else
- resultAnnotatedString += currentText
- }
-
- if (!currentText.text.isBlank() && isBlock)
-
- childrenComposables.add { localSpanStyle, settings ->
- //Text(text = currentText)
- val context = LocalContext.current
- val style = TextStyle(
- fontSize = localSpanStyle.fontSize,
- lineHeight = localSpanStyle.fontSize.times(
- LINE_HEIGHT_FACTOR
- ),
- color = localSpanStyle.color
- )
- ClickableText(
- text = currentText,
- style = style,
- onClick = {
- currentText.getStringAnnotations(it, it).find { it.tag == "url" }?.let {
- if (it.item.startsWith("http")) {
- Log.e(
- "URL Clicked",
- it.item
- )
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(it.item))
- context.startActivity(intent)
- }
- }
- }
- )
- }
-
- // Fetching composable
- mainComposable = when (element.tagName()) {
- "h2" -> { localSpanStyle, settings ->
- Column(Modifier.padding(top = 4.dp, bottom = 8.dp)) {
- childrenComposables.forEach { it(localSpanStyle, settings) }
- }
- }
-
- "h3" -> { localSpanStyle, settings ->
- Column(Modifier.padding(top = 4.dp, bottom = 6.dp)) {
- childrenComposables.forEach { it(localSpanStyle, settings) }
- }
- }
-
- "h4" -> { localSpanStyle, settings ->
- Column(Modifier.padding(top = 4.dp, bottom = 4.dp)) {
- childrenComposables.forEach { it(localSpanStyle, settings) }
- }
- }
-
- "h5" -> { localSpanStyle, settings ->
- Column(Modifier.padding(top = 4.dp, bottom = 3.dp)) {
- childrenComposables.forEach { it(localSpanStyle, settings) }
- }
- }
-
- "p" -> if (element.html().isNotEmpty()) { localSpanStyle, settings ->
- Column(Modifier.padding(bottom = 16.dp)) {
- childrenComposables.forEach {
- it(localSpanStyle, settings)
- }
- }
- }
- else null
-
- "a" -> if (element.hasClass("anchor"))
- { localSpanStyle, settings -> } else null
-
- "figcaption" -> if (element.text().isNotEmpty())
- { localSpanStyle, settings ->
- val context = LocalContext.current
- ClickableText(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 14.dp),
- text = currentText,
- style = LocalTextStyle.current.copy(
- lineHeight = ChildrenSpanStyle.fontSize.times(LINE_HEIGHT_FACTOR),
- textAlign = TextAlign.Center
- ),
- onClick = {
- currentText.getStringAnnotations(it, it).find { it.tag == "url" }?.let {
- if (it.item.startsWith("http")) {
- Log.e(
- "URL Clicked",
- it.item
- )
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(it.item))
- context.startActivity(intent)
- }
- }
- })
+ var isBlock = element.isHabrBlock()
+ var resultAnnotatedString: AnnotatedString = buildAnnotatedString { }
+ var ChildrenSpanStyle = spanStyle
+
+ // Applying Inline elements style
+ when (element.tagName()) {
+ "del" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ textDecoration = TextDecoration.combine(
+ listOf(
+ ChildrenSpanStyle.textDecoration ?: TextDecoration.None,
+ TextDecoration.LineThrough
+ )
+ )
+ )
+
+ "b" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(fontWeight = STRONG_FONT_WEIGHT)
+ "strong" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(fontWeight = STRONG_FONT_WEIGHT)
+ "i" -> ChildrenSpanStyle = ChildrenSpanStyle.copy(fontStyle = FontStyle.Italic)
+ "em" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(fontStyle = FontStyle.Italic)
+ if (element.hasClass("searched-item")) {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(background = Color(101, 238, 255, 76))
+ }
+ }
+
+ "code" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ fontFamily = FontFamily.Monospace,
+ background = Color(138, 156, 165, 20)
+ )
+ }
+
+ "u" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ textDecoration = TextDecoration.combine(
+ listOf(
+ ChildrenSpanStyle.textDecoration ?: TextDecoration.None,
+ TextDecoration.Underline
+ )
+ )
+ )
+ }
+
+ "s" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ textDecoration = TextDecoration.combine(
+ listOf(
+ ChildrenSpanStyle.textDecoration ?: TextDecoration.None,
+ TextDecoration.LineThrough
+ )
+ )
+ )
+ }
+
+ "sup" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ baselineShift = BaselineShift.Superscript,
+ fontSize = (ChildrenSpanStyle.fontSize.value - 4).coerceAtLeast(1f).sp
+ )
+ }
+
+ "sub" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ baselineShift = BaselineShift.Subscript,
+ fontSize = (ChildrenSpanStyle.fontSize.value - 4).coerceAtLeast(1f).sp
+ )
+ }
+
+ "br" -> {
+ return buildAnnotatedString { append("\n") } to null
+ }
+
+ "a" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(color = Color(88, 132, 185, 255))
+ if (element.hasClass("user_link")) {
+ resultAnnotatedString = buildAnnotatedString {
+ withStyle(ChildrenSpanStyle) { append("@") }
+ }
+ }
+ }
+
+ "h1" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ fontSize = (ChildrenSpanStyle.fontSize.value + 4f).sp,
+ fontWeight = HEADER_FONT_WEIGHT
+ )
+ }
+
+ "h2" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ fontSize = (ChildrenSpanStyle.fontSize.value + 3f).sp,
+ fontWeight = HEADER_FONT_WEIGHT
+ )
+ }
+
+ "h3" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ fontSize = (ChildrenSpanStyle.fontSize.value + 2f).sp,
+ fontWeight = HEADER_FONT_WEIGHT
+ )
+ }
+
+ "h4" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ fontSize = (ChildrenSpanStyle.fontSize.value + 2f).sp,
+ fontWeight = HEADER_FONT_WEIGHT
+ )
+ }
+
+ "h5" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ fontSize = (ChildrenSpanStyle.fontSize.value + 1f).sp,
+ fontWeight = HEADER_FONT_WEIGHT
+ )
+ }
+
+ "h6" -> {
+ ChildrenSpanStyle = ChildrenSpanStyle.copy(
+ fontWeight = HEADER_FONT_WEIGHT
+ )
+ }
+
+ "figcaption" -> {
+ ChildrenSpanStyle =
+ ChildrenSpanStyle.copy(
+ color = Color.Gray,
+ // TODO: Fix unspecified span style's fontSize that leads to NaN and exception
+ fontSize = (ChildrenSpanStyle.fontSize.value - 4).sp
+ )
+
+ }
+
+ "img" -> {
+ if (element.attr("inline") == "true") {
+ resultAnnotatedString = buildAnnotatedString {
+ appendInlineContent("inlineImage_")
+ }
+ }
+ }
+
+ "summary" -> return buildAnnotatedString { } to null
+ }
+
+ // Child elements parsing and styling
+ var childrenElementsResult: ArrayList Unit)?>> =
+ ArrayList()
+ element.children().forEach {
+ childrenElementsResult.add(parseElement(it, ChildrenSpanStyle, onViewImageRequest))
+ }
+ var mainComposable: (@Composable (SpanStyle, ElementSettings) -> Unit)? = null
+
+ var childrenComposables: ArrayList<@Composable (SpanStyle, ElementSettings) -> Unit> =
+ ArrayList()
+
+
+ // Text parsing and styling + validating children element
+ var currentText = buildAnnotatedString { }
+ var childElementsIndex = 0
+
+ element.childNodes().forEach { thisNode ->
+ if (thisNode is TextNode) {
+ if (!thisNode.isBlank)
+ currentText +=
+ buildAnnotatedString {
+ withStyle(ChildrenSpanStyle) {
+ append(
+ if (thisNode.previousSibling() == null ||
+ thisNode.previousSibling() is Element &&
+ (thisNode.previousSibling() as Element)?.tagName() == "br"
+ )
+ thisNode.text().trimStart()
+ else
+ thisNode.text()
+ )
+ }
+ }
+ }
+ if (thisNode is Element) {
+
+ if (childrenElementsResult[childElementsIndex].first != null)
+ currentText += childrenElementsResult[childElementsIndex].first!!
+
+
+ if (childrenElementsResult[childElementsIndex].second != null) {
+ if (currentText.isNotEmpty() && thisNode.previousElementSibling() != null
+ && thisNode.previousElementSibling()!!.tagName() != "pre"
+ ) {
+ var thisElementCurrentText = currentText
+ childrenComposables.add { localSpanStyle, settings ->
+ //Text(text = thisElementCurrentText)
+ var context = LocalContext.current
+ val focusManager = LocalFocusManager.current
+ ClickableText(
+ text = thisElementCurrentText,
+ style = LocalTextStyle.current.copy(
+ lineHeight = localSpanStyle.fontSize.times(
+ LINE_HEIGHT_FACTOR
+ ),
+ color = localSpanStyle.color
+ ),
+ onClick = {
+ focusManager.clearFocus(true)
+ thisElementCurrentText.getStringAnnotations(it, it)
+ .find { it.tag == "url" }
+ ?.let {
+ if (it.item.startsWith("http")) {
+ handleUrl(context, it.item)
+
+ }
+ }
+ }
+ )
+ }
+ }
+ childrenComposables.add(childrenElementsResult[childElementsIndex].second!!)
+ currentText = buildAnnotatedString { }
+ }
+
+
+ // if node is block element, break the currentText annotated string and place Text() Composable
+ childElementsIndex++
+ }
+
+
+ }
+ if (!currentText.text.isBlank() && !isBlock) {
+ if (element.tagName() == "a") {
+ resultAnnotatedString += buildAnnotatedString {
+ val url = if (element.hasClass("mention")){
+ "https://habr.com${element.attr("href")}"
+ } else {
+ element.attr("href")
+ }
+
+ var urlAnnotationId = pushStringAnnotation("url", url)
+ append(currentText)
+ pop(urlAnnotationId)
+ }
+ } else {
+ resultAnnotatedString += currentText
+ }
+ }
+
+ if (!currentText.text.isBlank() && isBlock)
+
+ childrenComposables.add { localSpanStyle, settings ->
+ //Text(text = currentText)
+ val context = LocalContext.current
+ val style = TextStyle(
+ fontSize = localSpanStyle.fontSize,
+ lineHeight = localSpanStyle.fontSize.times(
+ LINE_HEIGHT_FACTOR
+ ),
+ color = localSpanStyle.color
+ )
+ val focusManager = LocalFocusManager.current
+ ClickableText(
+ text = currentText,
+ style = style,
+ onClick = {
+ focusManager.clearFocus(true)
+ currentText.getStringAnnotations(it, it).find { it.tag == "url" }?.let {
+ if (it.item.startsWith("http")) {
+ handleUrl(context, it.item)
+ }
+ }
+ }
+ )
+ }
+
+ // Fetching composable
+ mainComposable = when (element.tagName()) {
+ "h2" -> { localSpanStyle, settings ->
+ Column(Modifier.padding(top = 4.dp, bottom = 8.dp)) {
+ childrenComposables.forEach { it(localSpanStyle, settings) }
+ }
+ }
+
+ "h3" -> { localSpanStyle, settings ->
+ Column(Modifier.padding(top = 4.dp, bottom = 6.dp)) {
+ childrenComposables.forEach { it(localSpanStyle, settings) }
+ }
+ }
+
+ "h4" -> { localSpanStyle, settings ->
+ Column(Modifier.padding(top = 4.dp, bottom = 4.dp)) {
+ childrenComposables.forEach { it(localSpanStyle, settings) }
+ }
+ }
+
+ "h5" -> { localSpanStyle, settings ->
+ Column(Modifier.padding(top = 4.dp, bottom = 3.dp)) {
+ childrenComposables.forEach { it(localSpanStyle, settings) }
+ }
+ }
+
+ "p" -> if (element.html().isNotEmpty()) { localSpanStyle, settings ->
+ Column(Modifier.padding(bottom = 16.dp)) {
+ childrenComposables.forEach {
+ it(localSpanStyle, settings)
+ }
+ }
+ }
+ else null
+
+ "a" -> if (element.hasClass("anchor"))
+ { localSpanStyle, settings -> } else null
+
+ "figcaption" -> if (element.text().isNotEmpty())
+ { localSpanStyle, settings ->
+ val context = LocalContext.current
+ val focusManager = LocalFocusManager.current
+ ClickableText(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 14.dp),
+ text = currentText,
+ style = LocalTextStyle.current.copy(
+ lineHeight = ChildrenSpanStyle.fontSize.times(LINE_HEIGHT_FACTOR),
+ textAlign = TextAlign.Center
+ ),
+ onClick = {
+ focusManager.clearFocus(true)
+ currentText.getStringAnnotations(it, it).find { it.tag == "url" }?.let {
+ if (it.item.startsWith("http")) {
+ handleUrl(context, it.item)
+ }
+ }
+ })
// Column(Modifier.padding(bottom = 12.dp)) {
// childrenComposables.forEach { it(localSpanStyle) }
// }
- }
- else null
-
- "img" -> if (element.hasClass("formula")) {
- { localSpanStyle, settings ->
- Box(
- modifier = Modifier
- .height(50.dp)
- .fillMaxWidth()
- ) {
- AsyncSvgImage(
- modifier = Modifier.align(Alignment.Center),
- data = element.attr("src"),
- contentScale = ContentScale.Inside
- )
- }
-
- }
- } else {
- { it: SpanStyle, settings ->
- val sourceUrl = remember {
- if (element.hasAttr("data-src")) {
- element.attr("data-src")
- } else {
- element.attr("src")
- }
- }
- var isLoaded by rememberSaveable { mutableStateOf(false) }
- var aspectRatio by rememberSaveable {
- mutableStateOf(16f / 9f)
- }
- AsyncGifImage(
- model = sourceUrl,
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 8.dp)
- .clip(RoundedCornerShape(4.dp))
- .clickable(enabled = onViewImageRequest != null) {
- onViewImageRequest?.invoke(sourceUrl)
- }
- .background(
- if (!MaterialTheme.colors.isLight) MaterialTheme.colors.onBackground.copy(
- 0.75f
- ) else Color.Transparent
- )
- .aspectRatio(aspectRatio),
- contentScale = ContentScale.FillWidth,
- onState = {
- if (!isLoaded && it is AsyncImagePainter.State.Success) {
- isLoaded = true
- it.painter.intrinsicSize.let {
- Log.e("painter_bounds", it.toString())
- aspectRatio = it.width / it.height
- }
-
- }
- }
- )
- }
- }
-
- "div" -> if (element.hasClass("tm-iframe_temp"))
- { localSpanStyle, settings ->
-
- AndroidView(modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(16f / 9f)
- .padding(vertical = 4.dp)
- .clip(RoundedCornerShape(4.dp)),
- factory = {
- WebView(it).apply {
-
- this.settings.javaScriptEnabled = true
- this.settings.databaseEnabled = true
- isFocusable = true
- isLongClickable = true
- loadUrl(element.attr("data-src"))
- }
- })
- }
- else
- { localSpanStyle, settings ->
- Column() {
- //Text(text = element.ownText())
- childrenComposables.forEach {
- it(localSpanStyle, settings)
- }
- }
- }
-
- "code" -> if (element.parent() != null && element.parent()!!
- .tagName() == "pre"
- ) { localSpanStyle, settings ->
- Box(Modifier.padding(bottom = 4.dp)) {
- Code(
- code = element.text(),
- language = LanguagesMap.getOrElse(
- element.attr("class"),
- { element.attr("class") }),
- spanStyle = localSpanStyle,
- elementSettings = settings
- )
- }
- resultAnnotatedString = buildAnnotatedString { }
- } else
- null
-
-
- "ul" ->
- if (element.parent() != null && element.parent()!!.tagName() == "li")
- { localSpanStyle, settings ->
- TextList(
- modifier = Modifier.padding(bottom = 8.dp),
- items = childrenComposables,
- spanStyle = localSpanStyle,
- ordered = false,
- nested = true,
- elementSettings = settings
- )
- }
- else
- { localSpanStyle, settings ->
- TextList(
- modifier = Modifier.padding(bottom = 8.dp),
- items = childrenComposables,
- spanStyle = localSpanStyle,
- ordered = false,
- elementSettings = settings
- )
- }
-
- "ol" -> if (element.hasAttr("start"))
- { localSpanStyle, settings ->
- TextList(
- modifier = Modifier.padding(bottom = 8.dp),
- items = childrenComposables,
- spanStyle = localSpanStyle,
- ordered = true,
- startNumber = element.attr("start").toIntOrNull() ?: 1,
- elementSettings = settings
- )
- }
- else
- { localSpanStyle, settings ->
- TextList(
- modifier = Modifier.padding(bottom = 8.dp),
- items = childrenComposables,
- spanStyle = localSpanStyle,
- ordered = true,
- elementSettings = settings
- )
- }
-
- "blockquote" -> { localSpanStyle, settings ->
- val quoteWidth = with(LocalDensity.current) { 4.dp.toPx() }
- Surface(
- color = Color.Transparent,
- modifier = Modifier
- .padding(vertical = 8.dp)
- .fillMaxWidth()
- ) {
- val blockQuoteColor =
- if (MaterialTheme.colors.isLight) SecondaryColor else MaterialTheme.colors.onBackground.copy(
- 0.75f
- )
- Column(modifier = Modifier
- .drawWithContent {
- drawContent()
- drawRoundRect(
- color = blockQuoteColor,
- size = Size(quoteWidth, size.height),
- cornerRadius = CornerRadius(quoteWidth / 2, quoteWidth / 2)
- )
-
- }
- .padding(start = 12.dp)) {
- childrenComposables.forEach { it(localSpanStyle.copy(fontStyle = FontStyle.Italic), settings) }
- }
- }
-
- }
-
- "hr" -> { localSpanStyle, settings ->
- Divider(
- thickness = 1.dp,
- modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp)
- )
-
- }
-
- "details" -> { localSpanStyle, settings ->
- var spoilerCaption = element.getElementsByTag("summary").first()?.text() ?: "Спойлер"
- var showDetails by rememberSaveable { mutableStateOf(false) }
- Surface(
- color = if (MaterialTheme.colors.isLight) Color(0x65EBEBEB) else Color(0x803C3C3C),
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 4.dp)
- .clip(RoundedCornerShape(4.dp))
- ) {
- Column(
- modifier = Modifier
- .animateContentSize()
-
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clickable { showDetails = !showDetails }
- .padding(12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- tint = Color(0xFF5587A3),
- modifier = Modifier
- .size(18.dp)
- .rotate(
- if (!showDetails) {
- -90f
- } else {
- 0f
- }
- ),
- imageVector = Icons.Outlined.ArrowDropDown, contentDescription = ""
+ }
+ else null
+
+ "img" -> if (element.hasClass("formula")) {
+ { localSpanStyle, settings ->
+ Box(
+ modifier = Modifier
+ .height(50.dp)
+ .fillMaxWidth()
+ ) {
+ AsyncSvgImage(
+ modifier = Modifier.align(Alignment.Center),
+ data = element.attr("src"),
+ contentScale = ContentScale.Inside
+ )
+ }
+
+ }
+ } else {
+ { it: SpanStyle, settings ->
+ val sourceUrl = remember {
+ if (element.hasAttr("data-src")) {
+ element.attr("data-src")
+ } else {
+ element.attr("src")
+ }
+ }
+ var isLoaded by rememberSaveable { mutableStateOf(false) }
+ var aspectRatio by rememberSaveable {
+ mutableStateOf(16f / 9f)
+ }
+ AsyncGifImage(
+ model = sourceUrl,
+ modifier = Modifier
+ .padding(bottom = 8.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .clickable(enabled = onViewImageRequest != null) {
+ onViewImageRequest?.invoke(sourceUrl)
+ }
+ .background(
+ if (!MaterialTheme.colors.isLight) MaterialTheme.colors.onBackground.copy(
+ 0.75f
+ ) else Color.Transparent
+ )
+ .aspectRatio(aspectRatio),
+ contentScale = ContentScale.FillWidth,
+ onState = {
+ if (!isLoaded && it is AsyncImagePainter.State.Success) {
+ isLoaded = true
+ it.painter.intrinsicSize.let {
+ Log.e("painter_bounds", it.toString())
+ aspectRatio = it.width / it.height
+ }
+
+ }
+ }
+ )
+ }
+ }
+
+ "div" -> if (element.hasClass("tm-iframe_temp"))
+ { localSpanStyle, settings ->
+
+ AndroidView(modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(16f / 9f)
+ .padding(vertical = 4.dp)
+ .clip(RoundedCornerShape(4.dp)),
+ factory = {
+ WebView(it).apply {
+
+ this.settings.javaScriptEnabled = true
+ this.settings.databaseEnabled = true
+ isFocusable = true
+ isLongClickable = true
+ loadUrl(element.attr("data-src"))
+ }
+ })
+ }
+ else
+ { localSpanStyle, settings ->
+ Column() {
+ //Text(text = element.ownText())
+ childrenComposables.forEach {
+ it(localSpanStyle, settings)
+ }
+ }
+ }
+
+ "code" -> if (element.parent() != null && element.parent()!!
+ .tagName() == "pre"
+ ) { localSpanStyle, settings ->
+ Box(Modifier.padding(bottom = 4.dp)) {
+ DisableSelection {
+ Code(
+ code = element.text(),
+ language = LanguagesMap.getOrElse(
+ element.attr("class"),
+ { element.attr("class") }),
+ spanStyle = localSpanStyle,
+ elementSettings = settings
+ )
+ }
+ }
+ resultAnnotatedString = buildAnnotatedString { }
+ } else
+ null
+
+
+ "ul" ->
+ if (element.parent() != null && element.parent()!!.tagName() == "li")
+ { localSpanStyle, settings ->
+ TextList(
+ modifier = Modifier.padding(bottom = 8.dp),
+ items = childrenComposables,
+ spanStyle = localSpanStyle,
+ ordered = false,
+ nested = true,
+ elementSettings = settings
+ )
+ }
+ else
+ { localSpanStyle, settings ->
+ TextList(
+ modifier = Modifier.padding(bottom = 8.dp),
+ items = childrenComposables,
+ spanStyle = localSpanStyle,
+ ordered = false,
+ elementSettings = settings
+ )
+ }
+
+ "ol" -> if (element.hasAttr("start"))
+ { localSpanStyle, settings ->
+ TextList(
+ modifier = Modifier.padding(bottom = 8.dp),
+ items = childrenComposables,
+ spanStyle = localSpanStyle,
+ ordered = true,
+ startNumber = element.attr("start").toIntOrNull() ?: 1,
+ elementSettings = settings
+ )
+ }
+ else
+ { localSpanStyle, settings ->
+ TextList(
+ modifier = Modifier.padding(bottom = 8.dp),
+ items = childrenComposables,
+ spanStyle = localSpanStyle,
+ ordered = true,
+ elementSettings = settings
+ )
+ }
+
+ "blockquote" -> { localSpanStyle, settings ->
+ val quoteWidth = with(LocalDensity.current) { 4.dp.toPx() }
+ Surface(
+ color = Color.Transparent,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ .fillMaxWidth()
+ ) {
+ val blockQuoteColor =
+ if (MaterialTheme.colors.isLight) SecondaryColor else MaterialTheme.colors.onBackground.copy(
+ 0.75f
+ )
+ Column(modifier = Modifier
+ .drawWithContent {
+ drawContent()
+ drawRoundRect(
+ color = blockQuoteColor,
+ size = Size(quoteWidth, size.height),
+ cornerRadius = CornerRadius(quoteWidth / 2, quoteWidth / 2)
)
- Spacer(modifier = Modifier.width(4.dp))
- DisableSelection {
- Text(
- text = spoilerCaption,
- color = Color(0xFF5587A3),
- fontSize = localSpanStyle.fontSize
- )
- }
+
}
- if (showDetails) {
- Divider()
- Column(
- modifier = Modifier.padding(
- start = 12.dp,
- end = 12.dp,
- bottom = 8.dp,
- top = 8.dp
- )
- ) {
- childrenComposables.forEach { it(localSpanStyle, settings) }
- }
- }
- }
- }
-
- }
-
- "table" -> { localSpanStyle, settings ->
- val backgroundColor =
- if (MaterialTheme.colors.isLight) MaterialTheme.colors.surface else MaterialTheme.colors.background
- val textColor = MaterialTheme.colors.onBackground
- val fontSize =
- LocalDensity.current.fontScale * MaterialTheme.typography.body1.fontSize.value
- AndroidView(modifier = Modifier
+ .padding(start = 12.dp)) {
+ childrenComposables.forEach {
+ it(
+ localSpanStyle.copy(fontStyle = FontStyle.Italic),
+ settings
+ )
+ }
+ }
+ }
+
+ }
+
+ "hr" -> { localSpanStyle, settings ->
+ Divider(
+ thickness = 1.dp,
+ modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp)
+ )
+
+ }
+
+ "details" -> { localSpanStyle, settings ->
+ var spoilerCaption = element.getElementsByTag("summary").first()?.text() ?: "Спойлер"
+ var showDetails by rememberSaveable { mutableStateOf(false) }
+ Surface(
+ color = if (MaterialTheme.colors.isLight) Color(0x65EBEBEB) else Color(0x803C3C3C),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .clip(RoundedCornerShape(4.dp))
+ ) {
+ Column(
+ modifier = Modifier
+ .animateContentSize()
+
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { showDetails = !showDetails }
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ tint = Color(0xFF5587A3),
+ modifier = Modifier
+ .size(18.dp)
+ .rotate(
+ if (!showDetails) {
+ -90f
+ } else {
+ 0f
+ }
+ ),
+ imageVector = Icons.Outlined.ArrowDropDown, contentDescription = ""
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ DisableSelection {
+ Text(
+ text = spoilerCaption,
+ color = Color(0xFF5587A3),
+ fontSize = localSpanStyle.fontSize
+ )
+ }
+ }
+ if (showDetails) {
+ Divider()
+ Column(
+ modifier = Modifier.padding(
+ start = 12.dp,
+ end = 12.dp,
+ bottom = 8.dp,
+ top = 8.dp
+ )
+ ) {
+ childrenComposables.forEach { it(localSpanStyle, settings) }
+ }
+ }
+ }
+ }
+
+ }
+
+ "table" -> { localSpanStyle, settings ->
+ val backgroundColor =
+ if (MaterialTheme.colors.isLight) MaterialTheme.colors.surface else MaterialTheme.colors.background
+ val textColor = MaterialTheme.colors.onBackground
+ val fontSize =
+ LocalDensity.current.fontScale * MaterialTheme.typography.body1.fontSize.value
+ AndroidView(modifier = Modifier
// .fillMaxWidth()
- .padding(vertical = 8.dp)
- .clip(RoundedCornerShape(0.dp)),
- factory = {
- WebView(it).apply {
- this.setBackgroundColor(backgroundColor.toArgb())
- this.isScrollContainer = false
- isFocusable = true
- isLongClickable = true
- isVerticalScrollBarEnabled = false
- val bodyElement = Element("body")
- .attr(
- "style",
- "color: rgb(${textColor.red * 255f}, ${textColor.green * 255f}, ${textColor.blue * 255f}); " +
+ .padding(vertical = 8.dp)
+ .clip(RoundedCornerShape(0.dp)),
+ factory = {
+ WebView(it).apply {
+ this.setBackgroundColor(backgroundColor.toArgb())
+ this.isScrollContainer = false
+ isFocusable = true
+ isLongClickable = true
+ isVerticalScrollBarEnabled = false
+ val bodyElement = Element("body")
+ .attr(
+ "style",
+ "color: rgb(${textColor.red * 255f}, ${textColor.green * 255f}, ${textColor.blue * 255f}); " +
// "background-color: rgb(${backgroundColor.red * 255f}, ${backgroundColor.green * 255f}, ${backgroundColor.blue * 255f}); " +
- "font-size: ${fontSize}px;" +
- "margin: 0px;"
- )
- .appendChild(
- Element("style").appendText(
- """
+ "font-size: ${fontSize}px;" +
+ "margin: 0px;"
+ )
+ .appendChild(
+ Element("style").appendText(
+ """
td {
padding: 10px;
border: 1px solid rgba(${textColor.red * 255f}, ${textColor.green * 255f}, ${textColor.blue * 255f}, 0.5);
@@ -719,224 +744,242 @@ fun parseElement(
width: auto;
}
""".trimIndent()
- )
- )
- .appendChild(element)
-
- loadData(bodyElement.outerHtml(), "text/html; charset=utf-8", "utf-8")
- }
- })
-
- }
-
- else -> if (childrenComposables.size == 0) {
- null
- } else {
- { localSpanStyle, settings ->
- Column() {
- childrenComposables.forEach { it(localSpanStyle, settings) }
- }
- }
- }
- }
- return resultAnnotatedString to mainComposable
+ )
+ )
+ .appendChild(element)
+
+ loadData(bodyElement.outerHtml(), "text/html; charset=utf-8", "utf-8")
+ }
+ })
+
+ }
+
+ else -> if (childrenComposables.size == 0) {
+ null
+ } else {
+ { localSpanStyle, settings ->
+ Column() {
+ childrenComposables.forEach { it(localSpanStyle, settings) }
+ }
+ }
+ }
+ }
+ return resultAnnotatedString to mainComposable
}
val LanguagesMap = mapOf(
- "" to "Язык неизвестен",
- "plaintext" to "Текст",
-
- "1c" to "1C",
-
- "assembly" to "Assembly",
-
- "bash" to "BASH",
-
- "css" to "CSS",
- "cmake" to "CMake",
- "cpp" to "C++",
- "cs" to "C#",
-
- "dart" to "Dart",
- "delphi" to "Delphi",
- "diff" to "Diff",
- "django" to "Django",
-
- "elixir" to "Elixir",
- "erlang" to "Erlang",
-
- "fs" to "F#",
-
- "go" to "Go",
-
- "html" to "HTML",
-
- "java" to "Java",
- "javascript" to "JavaScript",
- "json" to "JSON",
- "julia" to "Julia",
-
- "kotlin" to "Kotlin",
-
- "lisp" to "Lisp",
- "lua" to "Lua",
-
- "markdown" to "Markdown",
- "matlab" to "Matlab",
-
- "nginx" to "NGINX",
-
- "objectivec" to "Objective C",
-
- "perl" to "Perl",
- "pgsql" to "pgSQL",
- "php" to "PHP",
- "powershell" to "PowerShell",
- "python" to "Python",
-
- "r" to "R",
- "ruby" to "Ruby",
- "rust" to "Rust",
-
- "swift" to "Swift",
- "sql" to "SQL",
- "scala" to "Scala",
- "smalltalk" to "Smalltalk",
-
- "typescript" to "TypeScript",
-
- "vala" to "Vala",
- "vbscript" to "Vbscript",
- "vhdl" to "VHDL",
-
- "xml" to "XML",
-
- "yaml" to "YAML",
-
- "zig" to "Zig"
+ "" to "Язык неизвестен",
+ "plaintext" to "Текст",
+
+ "1c" to "1C",
+
+ "assembly" to "Assembly",
+
+ "bash" to "BASH",
+
+ "css" to "CSS",
+ "cmake" to "CMake",
+ "cpp" to "C++",
+ "cs" to "C#",
+
+ "dart" to "Dart",
+ "delphi" to "Delphi",
+ "diff" to "Diff",
+ "django" to "Django",
+
+ "elixir" to "Elixir",
+ "erlang" to "Erlang",
+
+ "fs" to "F#",
+
+ "go" to "Go",
+
+ "html" to "HTML",
+
+ "java" to "Java",
+ "javascript" to "JavaScript",
+ "json" to "JSON",
+ "julia" to "Julia",
+
+ "kotlin" to "Kotlin",
+
+ "lisp" to "Lisp",
+ "lua" to "Lua",
+
+ "markdown" to "Markdown",
+ "matlab" to "Matlab",
+
+ "nginx" to "NGINX",
+
+ "objectivec" to "Objective C",
+
+ "perl" to "Perl",
+ "pgsql" to "pgSQL",
+ "php" to "PHP",
+ "powershell" to "PowerShell",
+ "python" to "Python",
+
+ "r" to "R",
+ "ruby" to "Ruby",
+ "rust" to "Rust",
+
+ "swift" to "Swift",
+ "sql" to "SQL",
+ "scala" to "Scala",
+ "smalltalk" to "Smalltalk",
+
+ "typescript" to "TypeScript",
+
+ "vala" to "Vala",
+ "vbscript" to "Vbscript",
+ "vhdl" to "VHDL",
+
+ "xml" to "XML",
+
+ "yaml" to "YAML",
+
+ "zig" to "Zig"
)
// may be redundant
fun Element.isHabrBlock(): Boolean {
- val blocks = arrayListOf(
- "h1", "h2", "h3", "h4", "h5", "h6",
- "p", "div", "img", "table", "iframe",
- "li", "ul", "ol", "figcaption", "blockquote",
- "hr"
- )
-
- blocks.forEach {
- if (tagName() == it) return true
- }
- return false
+ val blocks = arrayListOf(
+ "h1", "h2", "h3", "h4", "h5", "h6",
+ "p", "div", "img", "table", "iframe",
+ "li", "ul", "ol", "figcaption", "blockquote",
+ "hr"
+ )
+
+ blocks.forEach {
+ if (tagName() == it) return true
+ }
+ return false
}
@Composable
fun TextList(
- modifier: Modifier = Modifier,
- items: List<@Composable (SpanStyle, ElementSettings) -> Unit>,
- spanStyle: SpanStyle,
- elementSettings: ElementSettings,
- ordered: Boolean,
- nested: Boolean = false,
- startNumber: Int = 1
+ modifier: Modifier = Modifier,
+ items: List<@Composable (SpanStyle, ElementSettings) -> Unit>,
+ spanStyle: SpanStyle,
+ elementSettings: ElementSettings,
+ ordered: Boolean,
+ nested: Boolean = false,
+ startNumber: Int = 1
) {
- var itemNumber = startNumber
- Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
- items.forEach {
-
- Row() {
- DisableSelection {
- if (ordered) {
- Text(buildAnnotatedString { withStyle(spanStyle) { append("$itemNumber.") } })
- } else
- if (nested) {
- Text(text = "◦", fontSize = spanStyle.fontSize)
- } else {
- Text(text = "•", fontSize = spanStyle.fontSize)
- }
- }
- Spacer(modifier = Modifier.width(4.dp))
- it(spanStyle, elementSettings)
- }
- itemNumber++
- }
- }
+ var itemNumber = startNumber
+ Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ items.forEach {
+
+ Row() {
+ DisableSelection {
+ if (ordered) {
+ Text(buildAnnotatedString { withStyle(spanStyle) { append("$itemNumber.") } })
+ } else
+ if (nested) {
+ Text(text = "◦", fontSize = spanStyle.fontSize)
+ } else {
+ Text(text = "•", fontSize = spanStyle.fontSize)
+ }
+ }
+ Spacer(modifier = Modifier.width(4.dp))
+ it(spanStyle, elementSettings)
+ }
+ itemNumber++
+ }
+ }
}
const val CODE_ALPHA_VALUE = 0.035f
@Composable
fun Code(
- code: String,
- language: String,
- spanStyle: SpanStyle,
- elementSettings: ElementSettings,
- modifier: Modifier = Modifier
+ code: String,
+ language: String,
+ spanStyle: SpanStyle,
+ elementSettings: ElementSettings,
+ modifier: Modifier = Modifier
) {
- Column(
- modifier
- .clip(RoundedCornerShape(4.dp))
- ) {
- Surface(
- color = MaterialTheme.colors.onBackground.copy(CODE_ALPHA_VALUE),
- modifier = Modifier.fillMaxWidth()
- ) {
- DisableSelection {
- Row(modifier = Modifier.padding(8.dp)) {
- Text(
- text = buildAnnotatedString { withStyle(spanStyle) { append(language) } },
- fontWeight = FontWeight.W600,
- fontFamily = FontFamily.SansSerif
- )
- Spacer(modifier = Modifier.width(6.dp))
- }
- }
-
-
- }
- Surface(
- color = MaterialTheme.colors.onBackground.copy(CODE_ALPHA_VALUE),
- modifier = Modifier.fillMaxWidth()
- ) {
- Row() {
- Surface(
- color = MaterialTheme.colors.onBackground.copy(0f),
- ) {
- Column(Modifier.padding(8.dp)) {
- var linesIndicator = String()
- for (i in 1..code.count { it == "\n"[0] } + 1) {
- linesIndicator += "$i\n"
- }
- linesIndicator = linesIndicator.take(linesIndicator.length - 1)
-
- DisableSelection {
- Text(
- text = buildAnnotatedString { withStyle(spanStyle.copy(color = MaterialTheme.colors.onBackground.copy(0.5f))) { append(linesIndicator) } },
- color = MaterialTheme.colors.onBackground.copy(0.5f),
- fontFamily = FontFamily.Monospace,
- textAlign = TextAlign.End
- )
- }
- }
- }
- Row(
- modifier = Modifier
- .horizontalScroll(rememberScrollState())
- .fillMaxWidth()
- .padding(8.dp)
- ) {
- Text(
- text = buildAnnotatedString { withStyle(spanStyle) { append(code) } },
+ Column(modifier.clip(RoundedCornerShape(4.dp))) {
+ Surface(
+ color = MaterialTheme.colors.onBackground.copy(CODE_ALPHA_VALUE),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ DisableSelection {
+ Row(modifier = Modifier.padding(8.dp)) {
+ Text(
+ text = buildAnnotatedString { withStyle(spanStyle) { append(language) } },
+ fontWeight = FontWeight.W600,
+ fontFamily = FontFamily.SansSerif
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+ }
+ }
+
+
+ }
+ Surface(
+ color = MaterialTheme.colors.onBackground.copy(CODE_ALPHA_VALUE),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row() {
+ Surface(
+ color = MaterialTheme.colors.onBackground.copy(0f),
+ ) {
+ Column(Modifier.padding(8.dp)) {
+ var linesIndicator = String()
+ for (i in 1..code.count { it == "\n"[0] } + 1) {
+ linesIndicator += "$i\n"
+ }
+ linesIndicator = linesIndicator.take(linesIndicator.length - 1)
+
+ DisableSelection {
+ Text(
+ text = buildAnnotatedString {
+ withStyle(
+ spanStyle.copy(
+ color = MaterialTheme.colors.onBackground.copy(
+ 0.5f
+ )
+ )
+ ) { append(linesIndicator) }
+ },
+ color = MaterialTheme.colors.onBackground.copy(0.5f),
+ fontFamily = FontFamily.Monospace,
+ textAlign = TextAlign.End
+ )
+ }
+ }
+ }
+ Row(
+ modifier = Modifier
+ .horizontalScroll(rememberScrollState())
+ .fillMaxWidth()
+ .padding(8.dp)
+ ) {
+ Text(
+ text = buildAnnotatedString { withStyle(spanStyle) { append(code) } },
+
+ fontFamily = FontFamily.Monospace,
+ )
+ }
+ }
+ }
+ }
+}
- fontFamily = FontFamily.Monospace,
- )
- }
- }
- }
- }
+fun handleUrl(context: Context, url: String) {
+ Log.e(
+ "URL Clicked",
+ url
+ )
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
+ if (url.startsWith("https://habr.com")){
+ this.`package` = BuildConfig.APPLICATION_ID
+ }
+ }
+ context.startActivity(intent)
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentItem.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentItem.kt
index 53c4a16f..ff07ae2a 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentItem.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentItem.kt
@@ -1,6 +1,5 @@
package com.garnegsoft.hubs.ui.screens.comments
-import android.util.Log
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
@@ -31,8 +30,8 @@ import coil.compose.AsyncImage
import com.garnegsoft.hubs.R
import com.garnegsoft.hubs.api.comment.Comment
import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
-import com.garnegsoft.hubs.ui.theme.RatingNegative
-import com.garnegsoft.hubs.ui.theme.RatingPositive
+import com.garnegsoft.hubs.ui.theme.RatingNegativeColor
+import com.garnegsoft.hubs.ui.theme.RatingPositiveColor
import kotlinx.coroutines.delay
import org.jsoup.Jsoup
@@ -86,25 +85,32 @@ fun CommentItem(
.background(MaterialTheme.colors.secondary)
)
Spacer(modifier = Modifier.width(4.dp))
- Column {
- Text(
- text = it.author.alias,
- color = MaterialTheme.colors.secondary,
- fontWeight = FontWeight.W500
- )
- Text(
- text = Jsoup.parse(it.message).child(0).child(1).child(0).children().firstOrNull {
- Log.e("current tag", it.tagName())
- it.tagName() != "blockquote"
- }?.text() ?: "",
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
+ if (it.deleted){
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp),
+ contentAlignment = Alignment.CenterStart){
+ Text(text = "Удаленное сообщение", color = MaterialTheme.colors.onSurface.copy(0.5f))
+ }
+ } else {
+ Column {
+ Text(
+ text = it.author.alias,
+ color = MaterialTheme.colors.secondary,
+ fontWeight = FontWeight.W500
+ )
+ Text(
+ text = Jsoup.parse(it.message).text() ?: "",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
}
}
Spacer(modifier = Modifier.height(8.dp))
+
}
@@ -276,8 +282,8 @@ fun CommentItem(
""
} + comment.score,
color = when {
- comment.score > 0 -> RatingPositive
- comment.score < 0 -> RatingNegative
+ comment.score > 0 -> RatingPositiveColor
+ comment.score < 0 -> RatingNegativeColor
else -> statisticsColor
}
)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentThreadScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentThreadScreen.kt
index a6427cf2..bbb6be59 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentThreadScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentThreadScreen.kt
@@ -156,49 +156,54 @@ fun CommentsThreadScreen(
items(
items = collection.comments.toList()
) {
- CommentItem(
- modifier = Modifier
- .padding(
- start = 20.dp.times(it.level.coerceAtMost(5))
- ),
- comment = it,
- highlight = false,
- showReplyButton = collection.commentAccess.canComment,
- onAuthorClick = { onAuthor(it.author.alias) },
- onShare = {
- val intent = Intent(Intent.ACTION_SEND)
- intent.putExtra(
- Intent.EXTRA_TEXT,
- "https://habr.com/p/${articleId}/comments/#comment_${it.id}"
- )
- intent.setType("text/plain")
- context.startActivity(Intent.createChooser(intent, null))
- },
- onReplyClick = {
- parentComment = it
- }
- ) {
- Column {
- it.let {
- SelectionContainer {
- ((parseElement(
- it.message, SpanStyle(
- fontSize = 16.sp,
- color = MaterialTheme.colors.onSurface
- ),
- onViewImageRequest = onImageClick
- ).second)?.let { it1 ->
- it1(
- SpanStyle(
+ if (it.deleted){
+ DeletedCommentItem(modifier = Modifier.padding(start = 20.dp.times(it.level.coerceAtMost(5))))
+ }
+ else {
+ CommentItem(
+ modifier = Modifier
+ .padding(
+ start = 20.dp.times(it.level.coerceAtMost(5))
+ ),
+ comment = it,
+ highlight = false,
+ showReplyButton = collection.commentAccess.canComment,
+ onAuthorClick = { onAuthor(it.author.alias) },
+ onShare = {
+ val intent = Intent(Intent.ACTION_SEND)
+ intent.putExtra(
+ Intent.EXTRA_TEXT,
+ "https://habr.com/p/${articleId}/comments/#comment_${it.id}"
+ )
+ intent.setType("text/plain")
+ context.startActivity(Intent.createChooser(intent, null))
+ },
+ onReplyClick = {
+ parentComment = it
+ }
+ ) {
+ Column {
+ it.let {
+ SelectionContainer {
+ ((parseElement(
+ it.message, SpanStyle(
fontSize = 16.sp,
color = MaterialTheme.colors.onSurface
),
- elementsSettings
- )
- })
+ onViewImageRequest = onImageClick
+ ).second)?.let { it1 ->
+ it1(
+ SpanStyle(
+ fontSize = 16.sp,
+ color = MaterialTheme.colors.onSurface
+ ),
+ elementsSettings
+ )
+ })
+ }
}
+
}
-
}
}
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentsScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentsScreen.kt
index 5e493fc4..7c987d71 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentsScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/CommentsScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.outlined.Close
+import androidx.compose.material.icons.sharp.KeyboardArrowDown
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable
@@ -30,9 +31,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
@@ -40,21 +43,25 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.ImageLoader
import coil.compose.AsyncImage
+import com.garnegsoft.hubs.api.HabrApi
import com.garnegsoft.hubs.api.article.list.ArticleSnippet
import com.garnegsoft.hubs.api.comment.Comment
import com.garnegsoft.hubs.api.comment.CommentsCollection
import com.garnegsoft.hubs.api.comment.Threads
import com.garnegsoft.hubs.api.comment.list.CommentsListController
import com.garnegsoft.hubs.api.dataStore.HubsDataStore
-import com.garnegsoft.hubs.api.dataStore.settingsDataStoreFlowWithDefault
-import com.garnegsoft.hubs.ui.common.ArticleCard
-import com.garnegsoft.hubs.ui.common.defaultArticleCardStyle
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCard
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
import com.garnegsoft.hubs.ui.screens.article.ElementSettings
import com.garnegsoft.hubs.ui.screens.article.parseElement
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import okhttp3.OkHttpClient
import org.jsoup.Jsoup
+import kotlin.math.roundToInt
class CommentsScreenViewModel : ViewModel() {
@@ -106,27 +113,26 @@ fun CommentsScreen(
val context = LocalContext.current
- val commentsDisplayMode by context.settingsDataStoreFlowWithDefault(
- HubsDataStore.Settings.Keys.Comments.CommentsDisplayMode,
- HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Default.ordinal
- ).collectAsState(
- initial = null
- )
+ val commentsDisplayMode by HubsDataStore.Settings
+ .getValueFlow(context, HubsDataStore.Settings.CommentsDisplayMode)
+ .collectAsState(initial = null)
+
+ var returnToCommentIndex by remember { mutableStateOf(null) }
commentsDisplayMode?.let {
- val mode = HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.values()[it]
+ val mode = HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.values()[it]
LaunchedEffect(key1 = Unit) {
if (
- (mode == HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Threads &&
+ (mode == HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Threads &&
!viewModel.threadsData.isInitialized)
||
- (mode == HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Default
+ (mode == HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Default
&&
!viewModel.commentsData.isInitialized)
) {
launch(Dispatchers.IO) {
viewModel.parentPostSnippet.postValue(ArticleController.getSnippet(parentPostId))
- if (mode == HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Default) {
+ if (mode == HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Default) {
CommentsListController.getComments(parentPostId)?.let {
viewModel.commentsData.postValue(it)
}
@@ -161,8 +167,17 @@ fun CommentsScreen(
}
}
})
+ val coroutineScope = rememberCoroutineScope()
+ var answeringComment: Comment? by remember {
+ mutableStateOf(null)
+ }
+ val commentTextFieldFocusRequester = remember { FocusRequester() }
+ var articleHeaderOffset by remember { mutableStateOf(0f) }
+
+ var itemOffsetCount by remember { mutableStateOf(1) }
Scaffold(
+ modifier = Modifier.imePadding(),
topBar = {
TopAppBar(
elevation = 0.dp,
@@ -173,15 +188,114 @@ fun CommentsScreen(
}
}
)
+ },
+ floatingActionButton = {
+ returnToCommentIndex?.let { index ->
+
+
+ FloatingActionButton(
+ modifier = Modifier.sizeIn(maxWidth = 52.dp, maxHeight = 52.dp),
+ onClick = {
+ coroutineScope.launch {
+ lazyListState.animateScrollToItem(
+ index + itemOffsetCount,
+ (-articleHeaderOffset).toInt()
+ )
+ returnToCommentIndex = null
+ }
+ },
+ content = {
+ Icon(
+ imageVector = Icons.Sharp.KeyboardArrowDown,
+ contentDescription = null
+ )
+ },
+ elevation = FloatingActionButtonDefaults.elevation(4.dp, 0.dp)
+ )
+
+ }
+
+ },
+ bottomBar = {
+ Column {
+ if (threadsData?.commentAccess?.canComment == true || commentsData?.commentAccess?.canComment == true) {
+ AnimatedVisibility(
+ visible = answeringComment != null,
+ enter = expandVertically(expandFrom = Alignment.Bottom),
+ exit = shrinkVertically(shrinkTowards = Alignment.Bottom)
+ ) {
+ val comment = answeringComment
+ Column {
+ Divider()
+ Row(modifier = Modifier
+ .clickable {
+ val index =
+ commentsData?.comments?.indexOf(answeringComment) ?: 0
+ coroutineScope.launch {
+ lazyListState.animateScrollToItem(index)
+ }
+ }
+ .background(MaterialTheme.colors.surface)
+ .padding(4.dp)
+ .padding(start = 4.dp)
+ .height(IntrinsicSize.Min),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Spacer(
+ modifier = Modifier
+ .width(4.dp)
+ .fillMaxHeight()
+ .clip(CircleShape)
+ .background(MaterialTheme.colors.secondary)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = comment?.author?.alias ?: "",
+ fontWeight = FontWeight.W500,
+ color = MaterialTheme.colors.primary.copy(0.9f)
+ )
+ val text = comment?.message ?: ""
+ Text(
+ maxLines = 1,
+ text = Jsoup.parse(text).text(),
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.body2,
+ color = MaterialTheme.colors.onSurface.copy(0.6f)
+ )
+
+ }
+ Spacer(modifier = Modifier.width(4.dp))
+ IconButton(onClick = { answeringComment = null }) {
+ Icon(
+ imageVector = Icons.Outlined.Close,
+ contentDescription = "",
+ tint = MaterialTheme.colors.secondary
+ )
+
+ }
+ }
+ }
+ }
+ Divider()
+ EnterCommentTextField(
+ focusRequester = commentTextFieldFocusRequester,
+ onSend = {
+ viewModel.comment(
+ text = it,
+ postId = parentPostId,
+ parentCommentId = answeringComment?.id
+ )
+ commentTextFieldFocusRequester.freeFocus()
+ })
+ }
+ }
}
) {
- var parentComment: Comment? by remember {
- mutableStateOf(null)
- }
+
val showArticleHeader by remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } }
- val commentTextFieldFocusRequester = remember { FocusRequester() }
val randomCoroutineScope = rememberCoroutineScope()
- var articleHeaderOffset by remember { mutableStateOf(0f) }
val elementsSettings = remember {
ElementSettings(
fontSize = 16.sp,
@@ -189,31 +303,47 @@ fun CommentsScreen(
fitScreenWidth = false
)
}
+
+ LaunchedEffect(key1 = remember { derivedStateOf { lazyListState.firstVisibleItemIndex } }, block = {
+ returnToCommentIndex?.let {
+ if (lazyListState.firstVisibleItemIndex >= it + itemOffsetCount) {
+ returnToCommentIndex = null
+ }
+ }
+ })
+
Box {
Column(
modifier = Modifier
.padding(it)
.imePadding()
) {
+ val articleCardStyle =
+ ArticleCardStyle.defaultArticleCardStyle()?.copy(
+ showImage = false,
+ showTextSnippet = false,
+ bookmarksButtonAllowedBeEnabled = articleSnippet?.relatedData != null
+ )
LazyColumn(
state = lazyListState,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
+
if (articleSnippet != null) {
item {
- ArticleCard(
- article = articleSnippet,
- onClick = onArticleClicked,
- style = defaultArticleCardStyle().copy(
- showImage = false,
- showTextSnippet = false,
- addToBookmarksButtonEnabled = articleSnippet.relatedData != null
- ),
- onAuthorClick = { onUserClicked(articleSnippet.author!!.alias) },
- onCommentsClick = { }
- )
+
+ articleCardStyle?.let {
+ ArticleCard(
+ article = articleSnippet,
+ onClick = onArticleClicked,
+ style = it,
+ onAuthorClick = { onUserClicked(articleSnippet.author!!.alias) },
+ onCommentsClick = { }
+ )
+ }
+
}
}
@@ -222,8 +352,6 @@ fun CommentsScreen(
items = threadsData.threads,
key = { index, it -> it.root.id }
) { index, it ->
- val context = LocalContext.current
-
Column(horizontalAlignment = Alignment.End) {
CommentItem(
@@ -247,7 +375,7 @@ fun CommentsScreen(
)
)
},
- onReplyClick = { }
+ onReplyClick = {}
) {
Column {
it.root.let {
@@ -299,60 +427,84 @@ fun CommentsScreen(
items = commentsData!!.comments,
key = { index, it -> it.id }
) { index, it ->
-
Column(horizontalAlignment = Alignment.End) {
-
- CommentItem(
- modifier = Modifier
- .padding(start = 20.dp * it.level.coerceAtMost(5)),
- comment = it,
- onAuthorClick = { onUserClicked(it.author.alias) },
- parentComment = commentsData!!.comments.firstOrNull { com -> com.id == it.parentCommentId },
- highlight = it.id == commentId,
- showReplyButton = commentsData!!.commentAccess.canComment,
- onShare = {
- val intent = Intent(Intent.ACTION_SEND)
- intent.putExtra(
- Intent.EXTRA_TEXT,
- "https://habr.com/p/${parentPostId}/comments/#comment_${it.id}"
- )
- intent.setType("text/plain")
- context.startActivity(
- Intent.createChooser(
- intent,
- null
+ val parentComment = remember {
+ commentsData!!.comments.firstOrNull { com -> com.id == it.parentCommentId }
+ }
+ val parentCommentIndex = remember {
+ parentComment?.let {
+ return@remember commentsData!!.comments.indexOf(it)
+ } ?: 0
+ }
+ if (it.deleted) {
+ DeletedCommentItem(
+ modifier = Modifier.padding(
+ start = 20.dp * it.level.coerceAtMost(
+ 5
)
)
- },
- onReplyClick = {
- parentComment = it
- }
- ) {
- Column {
- it.let {
- SelectionContainer {
- ((parseElement(
- it.message, SpanStyle(
- fontSize = 16.sp,
- color = MaterialTheme.colors.onSurface
- ),
- onViewImageRequest = onImageClick
- ).second)?.let { it1 ->
- it1(
- SpanStyle(
+ )
+ } else {
+ CommentItem(
+ modifier = Modifier
+ .padding(start = 20.dp * it.level.coerceAtMost(5)),
+ comment = it,
+ onAuthorClick = { onUserClicked(it.author.alias) },
+ parentComment = parentComment,
+ highlight = it.id == commentId,
+ showReplyButton = commentsData!!.commentAccess.canComment,
+ onShare = {
+ val intent = Intent(Intent.ACTION_SEND)
+ intent.putExtra(
+ Intent.EXTRA_TEXT,
+ "https://habr.com/p/${parentPostId}/comments/#comment_${it.id}"
+ )
+ intent.setType("text/plain")
+ context.startActivity(
+ Intent.createChooser(
+ intent,
+ null
+ )
+ )
+ },
+ onReplyClick = {
+ answeringComment = it
+ commentTextFieldFocusRequester.requestFocus()
+ },
+ onParentCommentSnippetClick = {
+ coroutineScope.launch(Dispatchers.Main) {
+ returnToCommentIndex = index
+ lazyListState.animateScrollToItem(
+ parentCommentIndex + 1,
+ -articleHeaderOffset.roundToInt()
+ )
+ }
+ }
+ ) {
+ Column {
+ it.let {
+ SelectionContainer {
+ ((parseElement(
+ it.message, SpanStyle(
fontSize = 16.sp,
color = MaterialTheme.colors.onSurface
),
- elementsSettings
- )
- })
+ onViewImageRequest = onImageClick
+ ).second)?.let { it1 ->
+ it1(
+ SpanStyle(
+ fontSize = 16.sp,
+ color = MaterialTheme.colors.onSurface
+ ),
+ elementsSettings
+ )
+ })
+ }
}
}
}
}
}
-
-
}
} else {
item {
@@ -366,79 +518,6 @@ fun CommentsScreen(
}
}
- if (threadsData?.commentAccess?.canComment == true || commentsData?.commentAccess?.canComment == true) {
- AnimatedVisibility(
- visible = if (parentComment != null) true else false,
- enter = expandVertically(expandFrom = Alignment.Bottom),
- exit = shrinkVertically(shrinkTowards = Alignment.Bottom)
- ) {
- val comment = parentComment
- Column {
- Divider()
- val coroutineScope = rememberCoroutineScope()
- Row(modifier = Modifier
- .clickable {
- val index =
- commentsData?.comments?.indexOf(parentComment) ?: 0
- coroutineScope.launch {
- lazyListState.animateScrollToItem(index)
- }
- }
- .background(MaterialTheme.colors.surface)
- .padding(4.dp)
- .padding(start = 4.dp)
- .height(IntrinsicSize.Min),
- verticalAlignment = Alignment.CenterVertically
- ) {
-
- Spacer(
- modifier = Modifier
- .width(4.dp)
- .fillMaxHeight()
- .clip(CircleShape)
- .background(MaterialTheme.colors.secondary)
- )
- Spacer(modifier = Modifier.width(8.dp))
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = comment?.author?.alias ?: "",
- fontWeight = FontWeight.W500,
- color = MaterialTheme.colors.primary.copy(0.9f)
- )
- val text = comment?.message ?: ""
- Text(
- maxLines = 1,
- text = Jsoup.parse(text).text(),
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface.copy(0.6f)
- )
-
- }
- Spacer(modifier = Modifier.width(4.dp))
- IconButton(onClick = { parentComment = null }) {
- Icon(
- imageVector = Icons.Outlined.Close,
- contentDescription = "",
- tint = MaterialTheme.colors.secondary
- )
-
- }
- }
- }
- }
- Divider()
- EnterCommentTextField(
- focusRequester = commentTextFieldFocusRequester,
- onSend = {
- viewModel.comment(
- text = it,
- postId = parentPostId,
- parentCommentId = parentComment?.id
- )
- commentTextFieldFocusRequester.freeFocus()
- })
- }
}
if (showArticleHeader) {
@@ -461,30 +540,18 @@ fun CommentsScreen(
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
- it.imageUrl?.let {
- AsyncImage(
- modifier = Modifier
- .fillMaxHeight()
- .aspectRatio(1f)
- .clip(RoundedCornerShape(4.dp)),
- model = articleSnippet?.imageUrl, contentDescription = null,
- contentScale = ContentScale.Crop
- )
- Spacer(modifier = Modifier.width(8.dp))
- }
- Column(
+ Column(
+ modifier = Modifier.weight(1f)
) {
- it.author?.run {
- Text(
- text = alias,
- style = MaterialTheme.typography.body2,
- fontWeight = FontWeight.W500,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
+ Text(
+ text = articleSnippet.author?.alias ?: "",
+ style = MaterialTheme.typography.body2,
+ fontWeight = FontWeight.W500,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
Text(
text = articleSnippet?.title!!,
@@ -493,6 +560,7 @@ fun CommentsScreen(
overflow = TextOverflow.Ellipsis
)
}
+
}
Divider(modifier = Modifier.align(Alignment.BottomCenter))
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/DeletedCommentItem.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/DeletedCommentItem.kt
new file mode 100644
index 00000000..fb73e0f4
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/comments/DeletedCommentItem.kt
@@ -0,0 +1,35 @@
+package com.garnegsoft.hubs.ui.screens.comments
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+
+@Composable
+fun DeletedCommentItem(
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(horizontal = 16.dp, vertical = 48.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "НЛО прилетело и опубликовало эту надпись здесь",
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onSurface.copy(0.5f))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/ArticleHistoryCard.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/ArticleHistoryCard.kt
new file mode 100644
index 00000000..d343df03
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/ArticleHistoryCard.kt
@@ -0,0 +1,103 @@
+package com.garnegsoft.hubs.ui.screens.history
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.compose.AsyncImage
+import com.garnegsoft.hubs.api.history.HistoryArticle
+import com.garnegsoft.hubs.api.history.HistoryEntity
+import com.garnegsoft.hubs.api.utils.formatTimestamp
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
+import com.garnegsoft.hubs.ui.theme.HubsTheme
+
+
+@Composable
+fun ArticleHistoryCard(
+ entity: HistoryEntity,
+ articleData: HistoryArticle,
+ onClick: () -> Unit,
+ style: ArticleCardStyle
+) {
+ HubsTheme {
+ Column() {
+// Row(
+// modifier = Modifier
+// .padding(horizontal = 12.dp)
+// .fillMaxWidth(),
+// ) {
+// Text(text = "Статья", color = MaterialTheme.colors.onBackground.copy(0.4f),
+// fontWeight = FontWeight.W500,)
+// Spacer(modifier = Modifier.weight(1f))
+// Text(
+// text = remember { formatTimestamp(entity.timestamp) }, color = MaterialTheme.colors.onBackground.copy(0.4f),
+// fontWeight = FontWeight.W500,
+// textAlign = TextAlign.End)
+// }
+//
+// Spacer(modifier = Modifier.height(8.dp))
+ Row(
+ modifier = Modifier
+ .clip(style.cardShape)
+ .clickable(onClick = onClick)
+ .background(style.backgroundColor)
+ .padding(style.innerPadding)
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ AsyncImage(
+ modifier = Modifier
+ .size(style.authorAvatarSize)
+ .clip(style.innerElementsShape),
+ model = articleData.authorAvatarUrl,
+ contentDescription = null)
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(
+ text = articleData.authorAlias,
+ fontWeight = FontWeight.W500,
+ color = MaterialTheme.colors.onSurface.copy(0.5f)
+ )
+ }
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = articleData.title,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.W500
+ )
+ }
+ articleData.thumbnailUrl?.let {
+ Spacer(modifier = Modifier.width(style.innerPadding.div(2)))
+ AsyncImage(
+ modifier = Modifier
+ .size(80.dp)
+ .clip(style.innerElementsShape),
+ model = articleData.thumbnailUrl,
+ contentDescription = null,
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/HistoryLazyColumn.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/HistoryLazyColumn.kt
new file mode 100644
index 00000000..f3832d6f
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/HistoryLazyColumn.kt
@@ -0,0 +1,144 @@
+package com.garnegsoft.hubs.ui.screens.history
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.garnegsoft.hubs.api.history.HistoryActionType
+import com.garnegsoft.hubs.api.history.HistoryEntityListModel
+import com.garnegsoft.hubs.api.history.getArticle
+import com.garnegsoft.hubs.api.utils.formatFoundationDate
+import com.garnegsoft.hubs.ui.common.BaseHubsLazyColumn
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
+import java.util.Calendar
+import java.util.Date
+
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun HistoryLazyColumn(
+ model: HistoryEntityListModel,
+ onArticleClick: (Int) -> Unit
+) {
+ LaunchedEffect(key1 = Unit, block = {
+ if (!model.data.isInitialized) {
+ model.loadFirstPage()
+ }
+ })
+
+ val list by model.data.observeAsState()
+
+ list?.let { data ->
+ val lastLoadedPage by model.lastLoadedPage.observeAsState()
+ BaseHubsLazyColumn(
+ data = data,
+ onScrollEnd = {
+ model.loadNextPage()
+ },
+ lazyList = { state ->
+ ArticleCardStyle.defaultArticleCardStyle()?.let {
+ LazyColumn(
+ state = state,
+ contentPadding = PaddingValues(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ var lastDay = 0
+
+ data.list.forEach { entity ->
+ val calendar =
+ Calendar.getInstance().apply { time = Date(entity.timestamp) }
+ val dayOfEvent = calendar.get(Calendar.DAY_OF_YEAR)
+
+ if (dayOfEvent != lastDay) {
+ stickyHeader {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .padding(vertical = 2.dp)
+ .shadow(0.dp, CircleShape)
+ .clip(CircleShape)
+ .background(MaterialTheme.colors.surface)
+ .border(
+ 1.dp,
+ MaterialTheme.colors.onBackground.copy(0.1f),
+ CircleShape
+ )
+ .padding(horizontal = 12.dp, vertical = 4.dp)
+ ) {
+ Text(
+ text = remember {
+ formatFoundationDate(
+ calendar.get(
+ Calendar.DAY_OF_MONTH
+ ).toString(),
+ (calendar.get(Calendar.MONTH) + 1).toString(),
+ calendar.get(Calendar.YEAR).toString()
+ )!!
+ },
+ fontWeight = FontWeight.W500,
+ color = MaterialTheme.colors.onBackground.copy(0.5f)
+ )
+ }
+ }
+ }
+ }
+ item {
+ if (entity.actionType == HistoryActionType.Article) {
+
+ ArticleHistoryCard(
+ entity = entity,
+ articleData = remember { entity.getArticle() },
+ onClick = { onArticleClick(entity.getArticle().articleId) },
+ style = it
+ )
+
+ }
+ }
+ lastDay = dayOfEvent
+ }
+ lastLoadedPage?.let {
+ if (data.pagesCount > it) {
+ item {
+ Box(modifier = Modifier.fillMaxWidth()) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(
+ Alignment.Center
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ lazyListState = rememberLazyListState()
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/HistoryScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/HistoryScreen.kt
new file mode 100644
index 00000000..0f82ab1b
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/history/HistoryScreen.kt
@@ -0,0 +1,92 @@
+package com.garnegsoft.hubs.ui.screens.history
+
+import android.content.Context
+import android.util.Log
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.garnegsoft.hubs.api.history.HistoryActionType
+import com.garnegsoft.hubs.api.history.HistoryDatabase
+import com.garnegsoft.hubs.api.history.HistoryEntity
+import com.garnegsoft.hubs.api.history.HistoryEntityListModel
+import com.garnegsoft.hubs.api.history.getArticle
+import com.garnegsoft.hubs.api.utils.formatFoundationDate
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import java.util.Calendar
+import java.util.Date
+
+
+class HistoryScreenViewModel(context: Context) : ViewModel() {
+ val model = HistoryEntityListModel(
+ coroutineScope = viewModelScope,
+ dao = HistoryDatabase.getDb(context).dao()
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun HistoryScreen(
+ onBack: () -> Unit,
+ onArticleClick: (articleId: Int) -> Unit,
+ onUserClick: (alias: String) -> Unit,
+ onHubClick: (alias: String) -> Unit,
+ onCompanyClick: (alias: String) -> Unit,
+) {
+ val context = LocalContext.current
+ val viewModel = viewModel { HistoryScreenViewModel(context)}
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ elevation = 0.dp,
+ title = { Text(text = "История") },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
+ }
+ }
+ )
+ }
+ ) {
+ Box(modifier = Modifier.padding(it)) {
+ HistoryLazyColumn(model = viewModel.model, onArticleClick = onArticleClick)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubArticlesFilter.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubArticlesFilter.kt
index eb2e61ab..6c87dd38 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubArticlesFilter.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubArticlesFilter.kt
@@ -16,17 +16,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.Filter
import com.garnegsoft.hubs.api.FilterPeriod
-import com.garnegsoft.hubs.api.PostComplexity
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.ui.common.BaseFilterDialog
import com.garnegsoft.hubs.ui.common.HubsFilterChip
import com.garnegsoft.hubs.ui.common.TitledColumn
-import com.garnegsoft.hubs.ui.screens.main.ArticlesFilterState
data class HubArticlesFilter(
val showLast: Boolean,
val minRating: Int = -1,
val period: FilterPeriod = FilterPeriod.Day,
- val complexity: PostComplexity
+ val complexity: PublicationComplexity
) : Filter {
override fun toArgsMap(): Map {
return if (showLast){
@@ -45,12 +44,12 @@ data class HubArticlesFilter(
FilterPeriod.AllTime -> "alltime"
}
)
- } + if (complexity != PostComplexity.None) {
+ } + if (complexity != PublicationComplexity.None) {
mapOf(
"complexity" to when (complexity) {
- PostComplexity.Low -> "easy"
- PostComplexity.Medium -> "medium"
- PostComplexity.High -> "hard"
+ PublicationComplexity.Low -> "easy"
+ PublicationComplexity.Medium -> "medium"
+ PublicationComplexity.High -> "hard"
else -> throw IllegalArgumentException("mapping of this complexity is not supported")
}
)
@@ -73,9 +72,9 @@ data class HubArticlesFilter(
FilterPeriod.AllTime -> "все время"
}
} + when (complexity) {
- PostComplexity.High -> ", сложные"
- PostComplexity.Medium -> ", средние"
- PostComplexity.Low -> ", простые"
+ PublicationComplexity.High -> ", сложные"
+ PublicationComplexity.Medium -> ", средние"
+ PublicationComplexity.Low -> ", простые"
else -> ""
}
@@ -214,23 +213,23 @@ fun HubArticlesFilter(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
HubsFilterChip(
- selected = complexity == PostComplexity.None,
- onClick = { complexity = PostComplexity.None }) {
+ selected = complexity == PublicationComplexity.None,
+ onClick = { complexity = PublicationComplexity.None }) {
Text(text = "Все")
}
HubsFilterChip(
- selected = complexity == PostComplexity.Low,
- onClick = { complexity = PostComplexity.Low }) {
+ selected = complexity == PublicationComplexity.Low,
+ onClick = { complexity = PublicationComplexity.Low }) {
Text(text = "Простой")
}
HubsFilterChip(
- selected = complexity == PostComplexity.Medium,
- onClick = { complexity = PostComplexity.Medium }) {
+ selected = complexity == PublicationComplexity.Medium,
+ onClick = { complexity = PublicationComplexity.Medium }) {
Text(text = "Средний")
}
HubsFilterChip(
- selected = complexity == PostComplexity.High,
- onClick = { complexity = PostComplexity.High }) {
+ selected = complexity == PublicationComplexity.High,
+ onClick = { complexity = PublicationComplexity.High }) {
Text(text = "Сложный")
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubProfile.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubProfile.kt
index 9b3abcb9..b6d8fa50 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubProfile.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubProfile.kt
@@ -4,12 +4,10 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ContentAlpha
-import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -21,7 +19,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
@@ -38,8 +35,6 @@ import com.garnegsoft.hubs.ui.common.BasicTitledColumn
import com.garnegsoft.hubs.ui.common.RefreshableContainer
import com.garnegsoft.hubs.ui.common.TitledColumn
import com.garnegsoft.hubs.ui.theme.DefaultRatingIndicatorColor
-import com.garnegsoft.hubs.ui.theme.RatingNegative
-import com.garnegsoft.hubs.ui.theme.RatingPositive
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreen.kt
index 203dbf55..4e40dff7 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreen.kt
@@ -1,14 +1,9 @@
package com.garnegsoft.hubs.ui.screens.hub
-import ArticlesListController
import android.content.Intent
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
@@ -17,44 +12,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStoreOwner
-import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
-import coil.compose.AsyncImage
-import com.garnegsoft.hubs.api.HabrList
-import com.garnegsoft.hubs.api.article.list.ArticleSnippet
-import com.garnegsoft.hubs.api.company.list.CompaniesListController
-import com.garnegsoft.hubs.api.company.list.CompanySnippet
-import com.garnegsoft.hubs.api.hub.Hub
import com.garnegsoft.hubs.api.hub.HubController
-import com.garnegsoft.hubs.api.user.list.UserSnippet
-import com.garnegsoft.hubs.api.user.list.UsersListController
import com.garnegsoft.hubs.ui.common.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.runtime.rememberCoroutineScope
-import com.garnegsoft.hubs.api.utils.formatLongNumbers
import com.garnegsoft.hubs.ui.common.snippetsPages.ArticlesListPageWithFilter
import com.garnegsoft.hubs.ui.common.snippetsPages.CompaniesListPage
import com.garnegsoft.hubs.ui.common.snippetsPages.UsersListPage
-import com.garnegsoft.hubs.ui.screens.search.ArticlesSearchFilter
import com.garnegsoft.hubs.ui.theme.HubInvestmentIndicatorColor
-import com.garnegsoft.hubs.ui.theme.RatingPositive
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
@OptIn(ExperimentalFoundationApi::class)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreenViewModel.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreenViewModel.kt
index b6d8082d..c18638ec 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreenViewModel.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/hub/HubScreenViewModel.kt
@@ -4,17 +4,12 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.garnegsoft.hubs.api.FilterPeriod
-import com.garnegsoft.hubs.api.HabrList
-import com.garnegsoft.hubs.api.PostComplexity
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.api.article.ArticlesListModel
-import com.garnegsoft.hubs.api.article.list.ArticleSnippet
import com.garnegsoft.hubs.api.company.CompaniesListModel
-import com.garnegsoft.hubs.api.company.list.CompanySnippet
import com.garnegsoft.hubs.api.hub.Hub
import com.garnegsoft.hubs.api.hub.HubController
-import com.garnegsoft.hubs.api.hub.HubsListModel
import com.garnegsoft.hubs.api.user.UsersListModel
-import com.garnegsoft.hubs.api.user.list.UserSnippet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -43,7 +38,7 @@ class HubScreenViewModel(val alias: String) : ViewModel() {
showLast = true,
minRating = -1,
period = FilterPeriod.Day,
- complexity = PostComplexity.None
+ complexity = PublicationComplexity.None
)
)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesFilterDialog.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesFilter.kt
similarity index 85%
rename from app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesFilterDialog.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesFilter.kt
index d4557621..bbb296cd 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesFilterDialog.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesFilter.kt
@@ -8,11 +8,10 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.Filter
import com.garnegsoft.hubs.api.FilterPeriod
-import com.garnegsoft.hubs.api.PostComplexity
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.ui.common.BaseFilterDialog
import com.garnegsoft.hubs.ui.common.HubsFilterChip
import com.garnegsoft.hubs.ui.common.TitledColumn
@@ -150,23 +149,23 @@ fun ArticlesFilterDialog(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
HubsFilterChip(
- selected = complexity == PostComplexity.None,
- onClick = { complexity = PostComplexity.None }) {
+ selected = complexity == PublicationComplexity.None,
+ onClick = { complexity = PublicationComplexity.None }) {
Text(text = "Все")
}
HubsFilterChip(
- selected = complexity == PostComplexity.Low,
- onClick = { complexity = PostComplexity.Low }) {
+ selected = complexity == PublicationComplexity.Low,
+ onClick = { complexity = PublicationComplexity.Low }) {
Text(text = "Простой")
}
HubsFilterChip(
- selected = complexity == PostComplexity.Medium,
- onClick = { complexity = PostComplexity.Medium }) {
+ selected = complexity == PublicationComplexity.Medium,
+ onClick = { complexity = PublicationComplexity.Medium }) {
Text(text = "Средний")
}
HubsFilterChip(
- selected = complexity == PostComplexity.High,
- onClick = { complexity = PostComplexity.High }) {
+ selected = complexity == PublicationComplexity.High,
+ onClick = { complexity = PublicationComplexity.High }) {
Text(text = "Сложный")
}
@@ -177,23 +176,19 @@ fun ArticlesFilterDialog(
}
}
-
-
}
data class ArticlesFilterState(
val showLast: Boolean,
val minRating: Int = -1,
val period: FilterPeriod = FilterPeriod.Day,
- val complexity: PostComplexity
+ val complexity: PublicationComplexity
) : Filter {
override fun toArgsMap(): Map {
var argsMap: Map =
if (showLast) {
if (minRating == -1) {
- mapOf(
- "sort" to "rating",
- )
+ mapOf("sort" to "rating",)
} else {
mapOf(
"sort" to "rating",
@@ -213,12 +208,12 @@ data class ArticlesFilterState(
)
}
- if (complexity != PostComplexity.None) {
+ if (complexity != PublicationComplexity.None) {
argsMap += mapOf(
"complexity" to when (complexity) {
- PostComplexity.Low -> "easy"
- PostComplexity.Medium -> "medium"
- PostComplexity.High -> "hard"
+ PublicationComplexity.Low -> "easy"
+ PublicationComplexity.Medium -> "medium"
+ PublicationComplexity.High -> "hard"
else -> throw IllegalArgumentException("mapping of this complexity is not supported")
}
)
@@ -242,10 +237,9 @@ data class ArticlesFilterState(
FilterPeriod.AllTime -> "все время"
}
} + when (complexity) {
- PostComplexity.High -> ", сложные"
- PostComplexity.Medium -> ", средние"
- PostComplexity.Low -> ", простые"
-
+ PublicationComplexity.High -> ", сложные"
+ PublicationComplexity.Medium -> ", средние"
+ PublicationComplexity.Low -> ", простые"
else -> ""
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesScreenViewModel.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesScreenViewModel.kt
index 652d35ec..21aa859d 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesScreenViewModel.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesScreenViewModel.kt
@@ -1,35 +1,27 @@
package com.garnegsoft.hubs.ui.screens.main
-import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.garnegsoft.hubs.api.FilterPeriod
-import com.garnegsoft.hubs.api.HabrList
-import com.garnegsoft.hubs.api.PostComplexity
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.api.article.ArticlesListModel
-import com.garnegsoft.hubs.api.article.list.ArticleSnippet
import com.garnegsoft.hubs.api.company.CompaniesListModel
-import com.garnegsoft.hubs.api.company.list.CompanySnippet
import com.garnegsoft.hubs.api.hub.HubsListModel
-import com.garnegsoft.hubs.api.hub.list.HubSnippet
import com.garnegsoft.hubs.api.user.UsersListModel
-import com.garnegsoft.hubs.api.user.list.UserSnippet
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
class ArticlesScreenViewModel : ViewModel() {
val myFeedArticlesListModel = ArticlesListModel(
path = "articles",
coroutineScope = viewModelScope,
- baseArgs = arrayOf("myFeed" to "true", "complexity" to "all", "score" to "all"),
- initialFilter = MyFeedFilter(showNews = false, showArticles = true)
+ baseArgs = arrayOf("myFeed" to "true"),
+ initialFilter = MyFeedFilter(showNews = false, showArticles = true, minRating = -1, complexity = PublicationComplexity.None)
)
val articlesListModel = ArticlesListModel(
path = "articles",
coroutineScope = viewModelScope,
- initialFilter = ArticlesFilterState(showLast = true, complexity = PostComplexity.None),
+ initialFilter = ArticlesFilterState(showLast = true, complexity = PublicationComplexity.None)
)
val newsListModel = ArticlesListModel(
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ContinueReadSnackBar.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ContinueReadSnackBar.kt
index 6837bdfc..1cdc54e7 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ContinueReadSnackBar.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ContinueReadSnackBar.kt
@@ -28,8 +28,8 @@ fun ContinueReadSnackBar(
Row(modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
- .shadow(8.dp, shape = RoundedCornerShape(10.dp))
- .clip(RoundedCornerShape(10.dp))
+ .shadow(8.dp, shape = RoundedCornerShape(18.dp))
+ .clip(RoundedCornerShape(18.dp))
.background(if (MaterialTheme.colors.isLight) MaterialTheme.colors.surface else Color(
0xFF414141
)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/MainScreen.kt
similarity index 91%
rename from app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesScreen.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/screens/main/MainScreen.kt
index 9e1b56fa..28097ffb 100755
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/ArticlesScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/MainScreen.kt
@@ -17,17 +17,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.*
-import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.garnegsoft.hubs.R
-import com.garnegsoft.hubs.api.dataStore.HubsDataStore
-import com.garnegsoft.hubs.api.dataStore.authDataStoreFlowWithDefault
-import com.garnegsoft.hubs.api.dataStore.lastReadDataStoreFlow
+import com.garnegsoft.hubs.api.dataStore.AuthDataController
+import com.garnegsoft.hubs.api.dataStore.LastReadArticleController
import com.garnegsoft.hubs.api.rememberCollapsingContentState
-import com.garnegsoft.hubs.lastReadDataStore
import com.garnegsoft.hubs.ui.common.*
-import com.garnegsoft.hubs.ui.common.snippetsPages.ArticlesListPage
import com.garnegsoft.hubs.ui.common.snippetsPages.ArticlesListPageWithFilter
import com.garnegsoft.hubs.ui.common.snippetsPages.CompaniesListPage
import com.garnegsoft.hubs.ui.common.snippetsPages.HubsListPage
@@ -37,7 +33,7 @@ import kotlinx.coroutines.*
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
-fun ArticlesScreen(
+fun MainScreen(
viewModelStoreOwner: ViewModelStoreOwner,
onArticleClicked: (articleId: Int) -> Unit,
onSearchClicked: () -> Unit,
@@ -49,11 +45,9 @@ fun ArticlesScreen(
) {
val context = LocalContext.current
var isAuthorized by rememberSaveable() { mutableStateOf(false) }
- val authorizedState by context.authDataStoreFlowWithDefault(
- HubsDataStore.Auth.Keys.Authorized,
- false
- )
+ val authorizedState by AuthDataController.isAuthorizedFlow(context)
.collectAsState(initial = null)
+
LaunchedEffect(key1 = authorizedState, block = {
isAuthorized = authorizedState == true
})
@@ -62,8 +56,8 @@ fun ArticlesScreen(
val scaffoldState = rememberScaffoldState()
- val lastArticleRead by context.lastReadDataStoreFlow(HubsDataStore.LastRead.Keys.LastArticleRead)
- .collectAsState(initial = -1)
+ val lastArticleRead by LastReadArticleController.getLastArticleFlow(context)
+ .collectAsState(initial = null)
var showSnackBar by rememberSaveable {
mutableStateOf(true)
@@ -85,9 +79,7 @@ fun ArticlesScreen(
if (snackbarResult == SnackbarResult.ActionPerformed) {
launch(Dispatchers.Main) { onArticleClicked(it.id) }
} else {
- context.lastReadDataStore.edit {
- it[HubsDataStore.LastRead.Keys.LastArticleRead] = 0
- }
+ LastReadArticleController.clearLastArticle(context)
}
showSnackBar = false
@@ -148,6 +140,8 @@ fun ArticlesScreen(
val authorsLazyListState = rememberLazyListState()
val companiesLazyListState = rememberLazyListState()
+
+
val pages = remember(key1 = isAuthorized) {
var map = mapOf Unit>(
"Статьи" to {
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/Menus.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/Menus.kt
index 9f1f65f0..0dcf5b04 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/Menus.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/Menus.kt
@@ -1,26 +1,30 @@
package com.garnegsoft.hubs.ui.screens.main
-import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExitToApp
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material.icons.outlined.Star
import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.IntOffset
@@ -34,7 +38,6 @@ import androidx.compose.ui.window.PopupProperties
import coil.compose.AsyncImage
import com.garnegsoft.hubs.R
import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
-import kotlinx.coroutines.delay
@Composable
fun AuthorizedMenu(
@@ -45,6 +48,7 @@ fun AuthorizedMenu(
onCommentsClick: () -> Unit,
onBookmarksClick: () -> Unit,
onSavedArticlesClick: () -> Unit,
+ onHistoryClick: () -> Unit,
onSettingsClick: () -> Unit,
onAboutClick: () -> Unit,
) {
@@ -55,7 +59,7 @@ fun AuthorizedMenu(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
- .background(Color.White),
+ .background(if (MaterialTheme.colors.isLight) Color.Transparent else Color.White),
contentScale = ContentScale.FillBounds,
model = avatarUrl, contentDescription = ""
)
@@ -78,27 +82,35 @@ fun AuthorizedMenu(
}
}
- var visible by rememberSaveable {
- mutableStateOf(false)
- }
- LaunchedEffect(key1 = expanded, block = {
- if (expanded) {
- visible = expanded
+ val menuTransition = updateTransition(targetState = expanded)
+
+ val alpha by menuTransition.animateFloat(
+ transitionSpec = {
+ if (this.targetState)
+ tween(150)
+ else
+ tween(150)
+
}
- })
- LaunchedEffect(key1 = visible, block = {
- delay(150)
- if (!visible) {
- expanded = false
+ ) {
+ if (it) 1f else 0.0f
+ }
+
+ val itemsAnimation by menuTransition.animateFloat(
+ transitionSpec = {
+ if (this.targetState)
+ tween(150, 50)
+ else
+ tween(100)
+
}
-
- })
- val alpha by animateFloatAsState(
- targetValue = if (visible) 1f else 0.0f,
- animationSpec = tween(150)
- )
+ ) {
+ if (it) 1f else 0.0f
+ }
+
+ val itemsOffset = 20
- if (expanded) {
+ if (menuTransition.targetState || expanded || menuTransition.currentState) {
Popup(
popupPositionProvider = object : PopupPositionProvider {
override fun calculatePosition(
@@ -113,8 +125,9 @@ fun AuthorizedMenu(
},
properties = PopupProperties(true),
- onDismissRequest = { visible = false }
+ onDismissRequest = { expanded = false }
) {
+
Box(
modifier = Modifier
.alpha(alpha)
@@ -131,111 +144,207 @@ fun AuthorizedMenu(
Modifier
.width(intrinsicSize = IntrinsicSize.Max)
.widthIn(min = 150.dp)
+ .verticalScroll(rememberScrollState())
) {
- MenuItem(title = userAlias, icon = {
- if (avatarUrl != null) {
- AsyncImage(
- modifier = Modifier
- .size(32.dp)
- .clip(RoundedCornerShape(8.dp))
- .background(Color.White),
- contentScale = ContentScale.FillBounds,
- model = avatarUrl, contentDescription = ""
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.6f
+ },
+ title = userAlias, icon = {
+ if (avatarUrl != null) {
+ AsyncImage(
+ modifier = Modifier
+ .size(32.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color.White),
+ contentScale = ContentScale.FillBounds,
+ model = avatarUrl, contentDescription = ""
+ )
+ } else {
+ Icon(
+ modifier = Modifier
+ .size(32.dp)
+ .border(
+ width = 2.dp,
+ color = placeholderColorLegacy(userAlias),
+ shape = RoundedCornerShape(8.dp)
+ )
+ .background(
+ Color.White,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .padding(2.5.dp),
+ painter = painterResource(id = R.drawable.user_avatar_placeholder),
+ contentDescription = "",
+ tint = placeholderColorLegacy(userAlias)
+ )
+ }
+ }, onClick = {
+ onProfileClick()
+ expanded = false
+ }
+ )
+ Divider(
+ modifier = Modifier
+ .padding(
+ horizontal = 12.dp,
+ vertical = 4.dp
)
- } else {
+ .graphicsLayer {
+ this.translationY =
+ -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.6f
+ }
+ )
+
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.55f
+ },
+ title = "Статьи", icon = {
Icon(
- modifier = Modifier
- .size(32.dp)
- .border(
- width = 2.dp, color = placeholderColorLegacy(userAlias),
- shape = RoundedCornerShape(8.dp)
- )
- .background(
- Color.White,
- shape = RoundedCornerShape(8.dp)
- )
- .padding(2.5.dp),
- painter = painterResource(id = R.drawable.user_avatar_placeholder),
+ painter = painterResource(id = R.drawable.article),
contentDescription = "",
- tint = placeholderColorLegacy(userAlias)
+ tint = MaterialTheme.colors.onBackground
)
+ }, onClick = {
+ onArticlesClick()
+ expanded = false
}
- }, onClick = onProfileClick)
- Divider(
- modifier = Modifier.padding(
- horizontal = 12.dp,
- vertical = 4.dp
- )
)
- MenuItem(title = "Статьи", icon = {
- Icon(
- painter = painterResource(id = R.drawable.article),
- contentDescription = "",
- tint = MaterialTheme.colors.onBackground
- )
- }, onClick = onArticlesClick)
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.5f
+ },
+ title = "Комментарии", icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.comments_icon),
+ contentDescription = "",
+ tint = MaterialTheme.colors.onBackground
+ )
+ }, onClick = {
+ onCommentsClick()
+ expanded = false
+ }
+ )
- MenuItem(title = "Комментарии", icon = {
- Icon(
- painter = painterResource(id = R.drawable.comments_icon),
- contentDescription = "",
- tint = MaterialTheme.colors.onBackground
- )
- }, onClick = onCommentsClick)
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.4f
+ },
+ title = "Закладки", icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.bookmark),
+ contentDescription = "",
+ tint = MaterialTheme.colors.onBackground
+ )
+ }, onClick = {
+ onBookmarksClick()
+ expanded = false
+ }
+ )
- MenuItem(title = "Закладки", icon = {
- Icon(
- painter = painterResource(id = R.drawable.bookmark),
- contentDescription = "",
- tint = MaterialTheme.colors.onBackground
- )
- }, onClick = onBookmarksClick)
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.3f
+ },
+ title = "Скачанные", icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.download),
+ contentDescription = "",
+ tint = MaterialTheme.colors.onBackground
+ )
+ }, onClick = {
+ onSavedArticlesClick()
+ expanded = false
+ }
+ )
- MenuItem(title = "Скачанные", icon = {
- Icon(
- painter = painterResource(id = R.drawable.download),
- contentDescription = "",
- tint = MaterialTheme.colors.onBackground
- )
- }, onClick = onSavedArticlesClick)
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.2f
+ },
+ title = "История",
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.history),
+ contentDescription = null,
+ tint = MaterialTheme.colors.onBackground
+ )
+ },
+ onClick = {
+ onHistoryClick()
+ expanded = false
+ }
+ )
- MenuItem(title = "Настройки", icon = {
- Icon(
- imageVector = Icons.Outlined.Settings,
- contentDescription = "",
- tint = MaterialTheme.colors.onBackground
- )
- }, onClick = onSettingsClick)
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.1f
+ },
+ title = "Настройки", icon = {
+ Icon(
+ imageVector = Icons.Outlined.Settings,
+ contentDescription = "",
+ tint = MaterialTheme.colors.onBackground
+ )
+ }, onClick = {
+ onSettingsClick()
+ expanded = false
+ }
+ )
Divider(
- modifier = Modifier.padding(
- horizontal = 12.dp,
- vertical = 4.dp
- )
+ modifier = Modifier
+ .padding(
+ horizontal = 12.dp,
+ vertical = 4.dp
+ )
+ .graphicsLayer {
+ this.translationY =
+ -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.05f
+ }
)
- MenuItem(title = "О приложении", icon = {
- Icon(
- imageVector = Icons.Outlined.Info,
- contentDescription = "",
- tint = MaterialTheme.colors.onBackground
- )
- }, onClick = onAboutClick)
+ MenuItem(
+ modifier = Modifier.graphicsLayer {
+ this.translationY = -itemsOffset + itemsOffset * itemsAnimation
+ this.alpha = itemsAnimation + 0.0f
+ },
+ title = "О приложении", icon = {
+ Icon(
+ imageVector = Icons.Outlined.Info,
+ contentDescription = "",
+ tint = MaterialTheme.colors.onBackground
+ )
+ }, onClick = {
+ onAboutClick()
+ expanded = false
+ }
+ )
}
}
}
-
}
}
-
}
+
@Composable
fun UnauthorizedMenu(
onLoginClick: () -> Unit,
onAboutClick: () -> Unit,
onSettingsClick: () -> Unit,
+ onHistoryClick: () -> Unit,
onSavedArticlesClick: () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
@@ -254,7 +363,10 @@ fun UnauthorizedMenu(
contentDescription = "",
tint = MaterialTheme.colors.onBackground
)
- }, onClick = onLoginClick)
+ }, onClick = {
+ onLoginClick()
+ expanded = false
+ })
MenuItem(title = "Скачанные", icon = {
Icon(
@@ -262,7 +374,25 @@ fun UnauthorizedMenu(
contentDescription = "",
tint = MaterialTheme.colors.onBackground
)
- }, onClick = onSavedArticlesClick)
+ }, onClick = {
+ onSavedArticlesClick()
+ expanded = false
+ })
+
+ MenuItem(
+ title = "История",
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.history),
+ contentDescription = null,
+ tint = MaterialTheme.colors.onBackground
+ )
+ },
+ onClick = {
+ onHistoryClick()
+ expanded = false
+ }
+ )
MenuItem(title = "Настройки", icon = {
Icon(
@@ -270,7 +400,10 @@ fun UnauthorizedMenu(
contentDescription = "",
tint = MaterialTheme.colors.onBackground
)
- }, onClick = onSettingsClick)
+ }, onClick = {
+ onSettingsClick()
+ expanded = false
+ })
Divider(modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp))
@@ -281,18 +414,22 @@ fun UnauthorizedMenu(
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.onBackground
)
- }, onClick = onAboutClick)
+ }, onClick = {
+ onAboutClick()
+ expanded = false
+ })
}
}
@Composable
fun MenuItem(
title: String,
+ modifier: Modifier = Modifier,
icon: @Composable () -> Unit,
onClick: () -> Unit
) {
Row(
- modifier = Modifier
+ modifier = modifier
.clickable(onClick = onClick)
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/MyFeedFilter.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/MyFeedFilter.kt
index 13a12f85..ecc96181 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/MyFeedFilter.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/MyFeedFilter.kt
@@ -1,18 +1,22 @@
package com.garnegsoft.hubs.ui.screens.main
import android.widget.Toast
-import androidx.appcompat.view.menu.ShowableListMenu
+import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.Filter
+import com.garnegsoft.hubs.api.PublicationComplexity
import com.garnegsoft.hubs.ui.common.BaseFilterDialog
import com.garnegsoft.hubs.ui.common.HubsFilterChip
import com.garnegsoft.hubs.ui.common.TitledColumn
@@ -20,7 +24,9 @@ import com.garnegsoft.hubs.ui.common.TitledColumn
data class MyFeedFilter(
val showArticles: Boolean,
- val showNews: Boolean
+ val showNews: Boolean,
+ val minRating: Int,
+ val complexity: PublicationComplexity
) : Filter {
override fun toArgsMap(): Map {
return mutableMapOf().apply {
@@ -31,12 +37,42 @@ data class MyFeedFilter(
}
if (showNews)
this.put("types[$argsCount]", "news")
+
+ put(
+ "complexity", when (complexity) {
+ PublicationComplexity.None -> "all"
+ PublicationComplexity.Low -> "easy"
+ PublicationComplexity.Medium -> "medium"
+ PublicationComplexity.High -> "hard"
+ else -> throw IllegalArgumentException("mapping of this complexity is not supported")
+ }
+ )
+
+ if (minRating == -1) {
+ put("score", "all")
+ } else {
+ put("score", minRating.toString())
+ }
+
}
}
override fun getTitle(): String {
- return if (showNews && showArticles) "Статьи & Новости"
- else if (showArticles) "Статьи" else "Новости"
+ return buildString {
+ if (showNews && showArticles) append("Статьи & Новости")
+ else if (showArticles) append("Статьи") else append("Новости")
+
+ if (minRating > -1)
+ append(" с рейтингом ≥${minRating}")
+
+ when (complexity) {
+ PublicationComplexity.High -> append(", сложные")
+ PublicationComplexity.Medium -> append(", средние")
+ PublicationComplexity.Low -> append(", простые")
+ else -> {}
+ }
+ }
+
}
}
@@ -47,7 +83,6 @@ fun MyFeedFilter(
onDismiss: () -> Unit,
onDone: (MyFeedFilter) -> Unit
) {
- val context = LocalContext.current
var showArticles by rememberSaveable {
mutableStateOf(defaultValues.showArticles)
}
@@ -55,23 +90,132 @@ fun MyFeedFilter(
mutableStateOf(defaultValues.showNews)
}
+ var minRating by rememberSaveable {
+ mutableStateOf(defaultValues.minRating)
+ }
+
+ var complexity by rememberSaveable {
+ mutableStateOf(defaultValues.complexity)
+ }
+
+ val context = LocalContext.current
+
BaseFilterDialog(
onDismiss = onDismiss,
onDone = {
- if (!showNews && !showArticles){
- Toast.makeText(context, "Выберите хотя бы 1 тип публикаций", Toast.LENGTH_SHORT).show()
+ if (!showArticles && !showNews){
+ Toast.makeText(context, "Выберите 1 тип публикаций", Toast.LENGTH_SHORT).show()
} else {
- onDone(MyFeedFilter(showNews = showNews, showArticles = showArticles))
+ onDone(
+ MyFeedFilter(
+ showNews = showNews,
+ showArticles = showArticles,
+ minRating = minRating,
+ complexity = complexity
+ )
+ )
}
}
) {
- TitledColumn(title = "Тип публикации") {
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- HubsFilterChip(selected = showArticles, onClick = { showArticles = !showArticles }) {
- Text(text = "Статьи")
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+
+
+ TitledColumn(title = "Тип публикации") {
+ Row(
+ modifier = Modifier.horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ HubsFilterChip(
+ selected = showArticles,
+ onClick = { showArticles = !showArticles }) {
+ Text(text = "Статьи")
+ }
+ HubsFilterChip(
+ selected = showNews,
+ onClick = { showNews = !showNews }) {
+ Text(text = "Новости")
+ }
}
- HubsFilterChip(selected = showNews, onClick = { showNews = !showNews }) {
- Text(text = "Новости")
+ }
+
+ TitledColumn(title = "Порог рейтинга") {
+ Row(
+ modifier = Modifier.horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ HubsFilterChip(
+ selected = minRating == -1,
+ onClick = { minRating = -1 }
+ ) {
+ Text(text = "Все")
+ }
+ HubsFilterChip(
+ selected = minRating == 0,
+ onClick = { minRating = 0 }
+ ) {
+ Text(text = "≥0")
+ }
+ HubsFilterChip(
+ selected = minRating == 10,
+ onClick = { minRating = 10 }
+ ) {
+ Text(text = "≥10")
+ }
+ HubsFilterChip(
+ selected = minRating == 25,
+ onClick = { minRating = 25 }
+ ) {
+ Text(text = "≥25")
+ }
+ HubsFilterChip(
+ selected = minRating == 50,
+ onClick = { minRating = 50 }
+ ) {
+ Text(text = "≥50")
+ }
+ HubsFilterChip(
+ selected = minRating == 100,
+ onClick = { minRating = 100 }
+ ) {
+ Text(text = "≥100")
+ }
+ }
+ }
+
+ TitledColumn(title = "Уровень сложности") {
+ Row(
+ modifier = Modifier.horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ HubsFilterChip(
+ selected = complexity == PublicationComplexity.None,
+ onClick = { complexity = PublicationComplexity.None }
+ ) {
+ Text(text = "Все")
+ }
+
+ HubsFilterChip(
+ selected = complexity == PublicationComplexity.Low,
+ onClick = { complexity = PublicationComplexity.Low }
+ ) {
+ Text(text = "Простой")
+ }
+
+ HubsFilterChip(
+ selected = complexity == PublicationComplexity.Medium,
+ onClick = { complexity = PublicationComplexity.Medium }
+ ) {
+ Text(text = "Средний")
+ }
+
+ HubsFilterChip(
+ selected = complexity == PublicationComplexity.High,
+ onClick = { complexity = PublicationComplexity.High }
+ ) {
+ Text(text = "Сложный")
+ }
+
+
}
}
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/NewsFilterDialog.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/NewsFilter.kt
similarity index 99%
rename from app/src/main/java/com/garnegsoft/hubs/ui/screens/main/NewsFilterDialog.kt
rename to app/src/main/java/com/garnegsoft/hubs/ui/screens/main/NewsFilter.kt
index e5bfd611..ebee73c6 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/NewsFilterDialog.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/NewsFilter.kt
@@ -19,7 +19,6 @@ import com.garnegsoft.hubs.api.FilterPeriod
import com.garnegsoft.hubs.ui.common.BaseFilterDialog
import com.garnegsoft.hubs.ui.common.HubsFilterChip
import com.garnegsoft.hubs.ui.common.TitledColumn
-import java.lang.StringBuilder
@Composable
fun NewsFilterDialog(
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/pagesRegistry.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/pagesRegistry.kt
deleted file mode 100644
index 3e02f770..00000000
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/main/pagesRegistry.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.garnegsoft.hubs.ui.screens.main
-
-import androidx.compose.runtime.Composable
-
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleCard.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleCard.kt
index e6d6291c..1c79e784 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleCard.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleCard.kt
@@ -19,17 +19,18 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.garnegsoft.hubs.R
+import com.garnegsoft.hubs.api.AsyncGifImage
import com.garnegsoft.hubs.api.article.offline.OfflineArticleSnippet
import com.garnegsoft.hubs.api.utils.formatTime
import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
-import com.garnegsoft.hubs.ui.common.ArticleCardStyle
-import com.garnegsoft.hubs.ui.common.defaultArticleCardStyle
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle.Companion.defaultArticleCardStyle
@Composable
fun OfflineArticleCard(
article: OfflineArticleSnippet,
onClick: () -> Unit,
- style: ArticleCardStyle = defaultArticleCardStyle()
+ style: ArticleCardStyle
) {
Column(
modifier = Modifier
@@ -42,7 +43,7 @@ fun OfflineArticleCard(
article.authorName?.let {
Row(verticalAlignment = Alignment.CenterVertically) {
if (article.authorAvatarUrl != null) {
- AsyncImage(
+ AsyncGifImage(
modifier = Modifier
.size(style.authorAvatarSize)
.clip(style.innerElementsShape),
@@ -121,7 +122,7 @@ fun OfflineArticleCard(
)
if (article.thumbnailUrl != null) {
Spacer(modifier = Modifier.height(4.dp))
- AsyncImage(
+ AsyncGifImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleScreen.kt
index abf5b8cf..85f05124 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/offline/OfflineArticleScreen.kt
@@ -1,8 +1,14 @@
package com.garnegsoft.hubs.ui.screens.offline
+import android.content.Context
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -13,47 +19,101 @@ import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.garnegsoft.hubs.api.article.offline.OfflineArticle
+import com.garnegsoft.hubs.api.article.offline.OfflineArticleSnippet
+import com.garnegsoft.hubs.api.article.offline.OfflineArticlesDatabase
import com.garnegsoft.hubs.api.article.offline.offlineArticlesDatabase
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
+import kotlinx.coroutines.flow.Flow
+
+class OfflineArticleScreenViewModel(context: Context) : ViewModel() {
+ private val dao = OfflineArticlesDatabase.getDb(context).articlesDao()
+ val articles: Flow> = dao.getAllSnippetsSortedByIdDesc()
+}
@Composable
fun OfflineArticlesScreen(
- onBack: () -> Unit,
- onArticleClick: (articleId: Int) -> Unit
+ onBack: () -> Unit,
+ onArticleClick: (articleId: Int) -> Unit
) {
- val articlesDao = LocalContext.current.offlineArticlesDatabase.articlesDao()
-
- val articles by articlesDao.getAllSnippetsSortedByIdDesc().collectAsState(initial = emptyList())
-
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text("Сохраненные публикации")},
- elevation = 0.dp,
- navigationIcon = {
- IconButton(onClick = onBack) {
- Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "back")
- }
- }
- )
-
- }
- ) {
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(it),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- contentPadding = PaddingValues(8.dp)
- ) {
- items(
- items = articles,
- key = { it.articleId }
- ) {
- OfflineArticleCard(article = it, onClick = { onArticleClick(it.articleId) })
- }
- }
- }
+
+ val context = LocalContext.current
+ val viewModel = viewModel { OfflineArticleScreenViewModel(context) }
+
+ val articles by viewModel.articles.collectAsState(initial = null)
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Сохраненные публикации") },
+ elevation = 0.dp,
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "back")
+ }
+ }
+ )
+
+ }
+ ) {
+ val cardsStyle = ArticleCardStyle.defaultArticleCardStyle()
+ cardsStyle?.let { style ->
+ articles?.let { articlesList ->
+ if (articlesList.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ .padding(32.dp)
+ ){
+ Column(modifier = Modifier.align(Alignment.Center),) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = "Нет скачанных статей",
+ style = MaterialTheme.typography.subtitle1,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onBackground.copy(0.5f),
+ text = "Для того, чтобы скачать статью вы можете зайти на статью и нажать на иконку скачивания справа сверху или после долгого нажатия на кнопку добавления в закладки в лентах нажать на кнопку с той же иконкой. Второй способ работает только после входа в аккаунт",
+ )
+ }
+
+ }
+ }
+ else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(8.dp)
+ ) {
+ items(
+ items = articlesList,
+ key = { it.articleId }
+ ) {
+ OfflineArticleCard(
+ article = it,
+ onClick = { onArticleClick(it.articleId) },
+ style = style
+ )
+ }
+ }
+ }
+ }
+ }
+
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/search/SearchScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/search/SearchScreen.kt
index 158e5c56..adbc550a 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/search/SearchScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/search/SearchScreen.kt
@@ -50,6 +50,9 @@ import com.garnegsoft.hubs.api.rememberCollapsingContentState
import com.garnegsoft.hubs.api.user.list.UserSnippet
import com.garnegsoft.hubs.api.user.list.UsersListController
import com.garnegsoft.hubs.ui.common.*
+import com.garnegsoft.hubs.ui.common.feedCards.company.CompanyCard
+import com.garnegsoft.hubs.ui.common.feedCards.hub.HubCard
+import com.garnegsoft.hubs.ui.common.feedCards.user.UserCard
import com.garnegsoft.hubs.ui.common.snippetsPages.ArticlesListPageWithFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -146,21 +149,23 @@ fun SearchScreen(
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions {
- keyboardController?.hide()
- if (searchTextValue.startsWith(".id")) {
- if (searchTextValue.drop(3).isDigitsOnly())
- onArticleClicked(searchTextValue.drop(3).toInt())
- } else {
- currentQuery = searchTextValue
-
-
- viewModel.articlesListModel.editFilter(
- ArticlesSearchFilter(
- order = (viewModel.articlesListModel.filter.value as ArticlesSearchFilter).order,
- query = currentQuery
+ if (searchTextValue.isNotBlank()) {
+ keyboardController?.hide()
+ if (searchTextValue.startsWith(".id")) {
+ if (searchTextValue.drop(3).isDigitsOnly())
+ onArticleClicked(searchTextValue.drop(3).toInt())
+ } else {
+ currentQuery = searchTextValue
+
+
+ viewModel.articlesListModel.editFilter(
+ ArticlesSearchFilter(
+ order = (viewModel.articlesListModel.filter.value as ArticlesSearchFilter).order,
+ query = currentQuery
+ )
)
- )
- showPages = true
+ showPages = true
+ }
}
},
singleLine = true,
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/ArticleScreenSettings.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/ArticleScreenSettings.kt
index 7a5154b3..b9489f32 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/ArticleScreenSettings.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/ArticleScreenSettings.kt
@@ -20,16 +20,17 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import coil.compose.rememberAsyncImagePainter
import com.garnegsoft.hubs.R
+import com.garnegsoft.hubs.api.article.Article
import com.garnegsoft.hubs.api.dataStore.HubsDataStore
import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
-import com.garnegsoft.hubs.settingsDataStore
-import com.garnegsoft.hubs.api.dataStore.settingsDataStoreFlow
import com.garnegsoft.hubs.ui.common.TitledColumn
+import com.garnegsoft.hubs.ui.screens.article.HubsRow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
@@ -37,16 +38,19 @@ import kotlinx.coroutines.launch
class ArticleScreenSettingsScreenViewModel : ViewModel() {
- val Context.fontSize: Flow
+ val Context.fontSize: Flow
get() {
- return this.settingsDataStoreFlow(HubsDataStore.Settings.Keys.ArticleScreen.FontSize)
+ return HubsDataStore.Settings
+ .getValueFlow(this, HubsDataStore.Settings.ArticleScreen.FontSize)
}
fun Context.setFontSize(size: Float) {
viewModelScope.launch(Dispatchers.IO) {
- settingsDataStore.edit {
- it.set(HubsDataStore.Settings.Keys.ArticleScreen.FontSize, size)
- }
+ HubsDataStore.Settings.edit(
+ this@setFontSize,
+ HubsDataStore.Settings.ArticleScreen.FontSize,
+ size
+ )
}
}
@@ -60,9 +64,7 @@ fun ArticleScreenSettingsScreen(
val viewModel = viewModel()
val context = LocalContext.current
-
-
-
+
val defaultFontSize = MaterialTheme.typography.body1.fontSize.value
val originalFontSize: Float? by with(viewModel) { context.fontSize.collectAsState(initial = defaultFontSize) }
var fontSize: Float? by remember { mutableStateOf(null) }
@@ -218,26 +220,13 @@ fun ArticleScreenSettingsScreen(
Row(
verticalAlignment = Alignment.CenterVertically,
) {
- Icon(
- modifier = Modifier
- .size(34.dp)
- .border(
- width = 2.dp,
- color = placeholderColorLegacy("Boomburum"),
- shape = RoundedCornerShape(8.dp)
- )
- .background(
- Color.White,
- shape = RoundedCornerShape(8.dp)
- )
- .padding(2.dp),
- painter = painterResource(id = R.drawable.user_avatar_placeholder),
- contentDescription = "",
- tint = placeholderColorLegacy("Boomburum")
- )
+ AsyncImage(
+ modifier = Modifier.size(34.dp).clip(RoundedCornerShape(8.dp)),
+ model = "https://assets.habr.com/habr-web/img/avatars/012.png", contentDescription = null)
+
Spacer(modifier = Modifier.width(4.dp))
Text(
- text = "Boomburum", fontWeight = FontWeight.W600,
+ text = "squada", fontWeight = FontWeight.W600,
fontSize = 14.sp,
color = MaterialTheme.colors.onBackground
)
@@ -245,7 +234,7 @@ fun ArticleScreenSettingsScreen(
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "Пример публикации",
- fontSize = 22.sp,
+ fontSize = ((fontSize ?: originalFontSize ?: defaultFontSize) + 4f).sp,
fontWeight = FontWeight.W700,
color = MaterialTheme.colors.onBackground
)
@@ -270,12 +259,18 @@ fun ArticleScreenSettingsScreen(
)
}
Spacer(modifier = Modifier.width(12.dp))
- Text(
- text = "Разработка, Программирование*, Habr, Jetpack Compose*", style = TextStyle(
- color = Color.Gray,
- fontWeight = FontWeight.W500
- )
+ HubsRow(
+ hubs = listOf(
+ Article.Hub("", false, false, "Разработка", null),
+ Article.Hub("", true, false, "Программирование", Article.Hub.RelatedData(true)),
+ Article.Hub("", false, false, "Habr", null),
+ Article.Hub("", true, false, "Jetpack Compose", null)
+
+ ),
+ onHubClicked = { },
+ onCompanyClicked = { }
)
+
Spacer(modifier = Modifier.height(8.dp))
val animatedLineHeightFactor by animateFloatAsState(targetValue = lineHeightFactor)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/FeedSettingsScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/FeedSettingsScreen.kt
new file mode 100644
index 00000000..869192b0
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/FeedSettingsScreen.kt
@@ -0,0 +1,363 @@
+package com.garnegsoft.hubs.ui.screens.settings
+
+import android.content.Context
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.BottomSheetScaffold
+import androidx.compose.material.Checkbox
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.garnegsoft.hubs.R
+import com.garnegsoft.hubs.api.EditorVersion
+import com.garnegsoft.hubs.api.PublicationComplexity
+import com.garnegsoft.hubs.api.PostType
+import com.garnegsoft.hubs.api.article.Article
+import com.garnegsoft.hubs.api.article.list.ArticleSnippet
+import com.garnegsoft.hubs.api.dataStore.HubsDataStore
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCard
+import com.garnegsoft.hubs.ui.common.feedCards.article.ArticleCardStyle
+import com.garnegsoft.hubs.ui.screens.settings.cards.SettingsCardItem
+import kotlinx.coroutines.launch
+
+
+class FeedSettingsScreenViewModel : ViewModel() {
+
+ fun ChangeShowImageSetting(context: Context, show: Boolean) {
+ viewModelScope.launch {
+ HubsDataStore.Settings.edit(context, HubsDataStore.Settings.ArticleCard.ShowImage, show)
+
+ }
+ }
+
+ fun ChangeShowTextSnippetSetting(context: Context, show: Boolean) {
+ viewModelScope.launch {
+ HubsDataStore.Settings.edit(
+ context,
+ HubsDataStore.Settings.ArticleCard.ShowTextSnippet,
+ show
+ )
+
+ }
+
+ }
+
+ fun ChangeTitleFontSizeSetting(context: Context, size: Float) {
+ viewModelScope.launch {
+ HubsDataStore.Settings.edit(
+ context,
+ HubsDataStore.Settings.ArticleCard.TitleFontSize,
+ size
+ )
+ }
+ }
+
+ fun ChangeSnippetMaxLinesSetting(context: Context, lines: Int) {
+ viewModelScope.launch {
+ HubsDataStore.Settings.edit(context, HubsDataStore.Settings.ArticleCard.TextSnippetMaxLines, lines)
+ }
+ }
+
+ fun ChangeSnippetFontSize(context: Context, size: Float) {
+ viewModelScope.launch {
+ HubsDataStore.Settings.edit(context, HubsDataStore.Settings.ArticleCard.TextSnippetFontSize, size)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun FeedSettingsScreen(
+ onBack: () -> Unit
+) {
+ val viewModel = viewModel()
+ val context = LocalContext.current
+
+ val scaffoldState = rememberBottomSheetScaffoldState()
+ BottomSheetScaffold(
+ modifier = Modifier.imePadding(),
+ scaffoldState = scaffoldState,
+ topBar = {
+ TopAppBar(
+ elevation = 0.dp,
+ title = { Text(text = "Лента") },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
+ }
+ })
+ },
+ sheetContent = {
+ Column(modifier = Modifier.fillMaxHeight(0.5f)) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(16.dp)
+ ) {
+ Spacer(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .height(4.dp)
+ .width(32.dp)
+ .clip(RoundedCornerShape(50))
+ .background(MaterialTheme.colors.onBackground.copy(0.15f))
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = 16.dp)
+ .padding(bottom = 20.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ val showImage by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.ShowImage
+ ).collectAsState(initial = null)
+ showImage?.let {
+ SettingsCardItem(
+ title = "Показывать КДПВ",
+ onClick = { viewModel.ChangeShowImageSetting(context, !it) },
+ trailingIcon = {
+ Checkbox(checked = it, onCheckedChange = {
+ viewModel.ChangeShowImageSetting(context, it)
+ })
+ }
+ )
+ }
+
+ val showTextSnippet by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.ShowTextSnippet
+ ).collectAsState(initial = null)
+ showTextSnippet?.let {
+ SettingsCardItem(
+ title = "Показать начало статьи",
+ onClick = {
+ viewModel.ChangeShowTextSnippetSetting(context, !it)
+ },
+ trailingIcon = {
+ Checkbox(checked = it, onCheckedChange = {
+ viewModel.ChangeShowTextSnippetSetting(context, it)
+ })
+ })
+
+ }
+
+ val titleFontSize by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.TitleFontSize
+ ).collectAsState(initial = null)
+
+ titleFontSize?.let {
+ Column(modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp)) {
+ var sliderValue by remember { mutableStateOf(it) }
+ Text(text = "Размер шрифта заголовка: ${"%.1f".format(sliderValue)}")
+ Slider(
+ value = sliderValue,
+ valueRange = 16f..28f,
+ steps = 5,
+ onValueChange = {
+ sliderValue = it
+ },
+ onValueChangeFinished = {
+ viewModel.ChangeTitleFontSizeSetting(context, sliderValue)
+ },
+ colors = ArticleScreenSettingsSliderColors
+ )
+ }
+
+
+ }
+
+ val snippetFontSize by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.TextSnippetFontSize
+ ).collectAsState(initial = null)
+
+ snippetFontSize?.let {
+ Column(
+ modifier = Modifier.padding(4.dp)
+ ) {
+ var sliderValue by remember { mutableStateOf(it) }
+
+ Text(text = "Размер шрифта начала статьи: ${"%.0f".format(sliderValue)}",)
+ Slider(
+ value = sliderValue,
+ enabled = showTextSnippet ?: false,
+ valueRange = 12f..24f,
+ steps = 5,
+ onValueChange = {
+ sliderValue = it
+ },
+ onValueChangeFinished = {
+ viewModel.ChangeSnippetFontSize(context, sliderValue)
+ },
+ colors = ArticleScreenSettingsSliderColors
+ )
+ }
+ }
+
+ val snippetMaxLines by HubsDataStore.Settings.getValueFlow(
+ LocalContext.current,
+ HubsDataStore.Settings.ArticleCard.TextSnippetMaxLines
+ ).collectAsState(initial = null)
+
+ snippetMaxLines?.let {
+ Column(
+ modifier = Modifier.padding(4.dp)
+ ) {
+ var sliderValue by remember { mutableStateOf(it.toFloat()) }
+
+ Text(text = "Показывать строк начала статьи: ${"%.0f".format(sliderValue)}",)
+ Slider(
+ value = sliderValue,
+ enabled = showTextSnippet ?: false,
+ valueRange = 2f..10f,
+ steps = 7,
+ onValueChange = {
+ sliderValue = it
+ },
+ onValueChangeFinished = {
+ viewModel.ChangeSnippetMaxLinesSetting(context, sliderValue.toInt())
+ },
+ colors = ArticleScreenSettingsSliderColors
+ )
+ }
+ }
+
+ }
+
+ }
+ },
+ sheetElevation = 8.dp,
+ sheetPeekHeight = 80.dp,
+ sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(it)
+ ) {
+ val fakeArticles = listOf(TestArticle, SecondArticle)
+ ArticleCardStyle.defaultArticleCardStyle()?.let { style ->
+ LazyColumn(
+ contentPadding = PaddingValues(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(fakeArticles) {
+
+ Box(modifier = Modifier.animateContentSize()) {
+ ArticleCard(
+ article = it,
+ onClick = { /*TODO*/ },
+ onAuthorClick = { /*TODO*/ },
+ onCommentsClick = { /*TODO*/ },
+ style = style
+ )
+ Box(modifier = Modifier
+ .matchParentSize()
+ .pointerInput(Unit) {})
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+val TestArticle
+ @Composable
+ get() = ArticleSnippet(
+ 0,
+ "вчера, 06:32",
+ false,
+ "Что делать дальше?",
+ EditorVersion.FirstVersion,
+ PostType.Unknown,
+ listOf(),
+ Article.Author(
+ "squada",
+ avatarUrl = "https://assets.habr.com/habr-web/img/avatars/012.png"
+ ),
+ Article.Statistics(5, 9, 2000, 4, 0, 0),
+ listOf(
+ Article.Hub("", true, false, "Программирование", null),
+ Article.Hub("", true, false, ".NET", Article.Hub.RelatedData(true)),
+ Article.Hub("", false, false, "Карьера в IT", null)
+ ),
+ stringResource(id = R.string.settings_first_article_snippet_text),
+ "https://megapicture.com/non-existing-picture.png",
+ null,
+ 15,
+ PublicationComplexity.Low,
+ null,
+ false
+)
+
+
+val SecondArticle
+ @Composable
+ get() = ArticleSnippet(
+ 0,
+ "вчера, 03:28",
+ false,
+ "Игорь уничтожил нам серверную!!!",
+ EditorVersion.FirstVersion,
+ PostType.Unknown,
+ listOf(),
+ Article.Author(
+ "gohonor",
+ avatarUrl = "https://assets.habr.com/habr-web/img/avatars/016.png"
+ ),
+ Article.Statistics(45, 12, 4000, -14, 0, 0),
+ listOf(
+ Article.Hub("", false, false, "Системное администрирование", null),
+ ),
+ stringResource(R.string.settings_second_article_snippet_text),
+ "https://megapicture.com/non-existing-picture.png",
+ null,
+ 10,
+ PublicationComplexity.None,
+ null,
+ false
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/LogFileProvider.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/LogFileProvider.kt
new file mode 100644
index 00000000..2a9c6a15
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/LogFileProvider.kt
@@ -0,0 +1,8 @@
+package com.garnegsoft.hubs.ui.screens.settings
+
+import androidx.core.content.FileProvider
+import com.garnegsoft.hubs.R
+
+class LogFileProvider : FileProvider(R.xml.log_file_path) {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/NoRipple.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/NoRipple.kt
new file mode 100644
index 00000000..b7c48b0d
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/NoRipple.kt
@@ -0,0 +1,14 @@
+package com.garnegsoft.hubs.ui.screens.settings
+
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+val noRipple = object : RippleTheme {
+ @Composable
+ override fun defaultColor() = Color.Unspecified
+
+ @Composable
+ override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/SettingsScreen.kt
index e08aecd9..35897330 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/SettingsScreen.kt
@@ -1,62 +1,89 @@
package com.garnegsoft.hubs.ui.screens.settings
import android.content.Context
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.isSystemInDarkTheme
+import android.content.Intent
+import android.os.Build
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.ArrowForward
-import androidx.compose.material.ripple.LocalRippleTheme
-import androidx.compose.material.ripple.RippleAlpha
-import androidx.compose.material.ripple.RippleTheme
-import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.datastore.preferences.core.edit
+import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
+import com.garnegsoft.hubs.BuildConfig
import com.garnegsoft.hubs.api.dataStore.HubsDataStore
-import com.garnegsoft.hubs.settingsDataStore
-import com.garnegsoft.hubs.api.dataStore.settingsDataStoreFlow
-import com.garnegsoft.hubs.api.dataStore.settingsDataStoreFlowWithDefault
-import com.garnegsoft.hubs.ui.common.BasicTitledColumn
+import com.garnegsoft.hubs.authorized
+import com.garnegsoft.hubs.ui.screens.settings.cards.AppearanceSettingsCard
+import com.garnegsoft.hubs.ui.screens.settings.cards.ExperimentalFeaturesSettingsCard
+import com.garnegsoft.hubs.ui.screens.settings.cards.OtherCard
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
class SettingsScreenViewModel : ViewModel() {
- fun getTheme(context: Context): Flow {
- return context.settingsDataStoreFlow(HubsDataStore.Settings.Keys.Theme)
- .map {
- it?.let {
- HubsDataStore.Settings.Keys.ThemeModes.values().get(it)
- } ?: HubsDataStore.Settings.Keys.ThemeModes.Undetermined
- }
+ fun getTheme(context: Context): Flow {
+ return HubsDataStore.Settings
+ .getValueFlow(context, HubsDataStore.Settings.Theme.ColorSchemeMode)
+ .run { HubsDataStore.Settings.Theme.ColorSchemeMode.mapValues(this) }
}
- fun setTheme(context: Context, theme: HubsDataStore.Settings.Keys.ThemeModes) {
+ fun setTheme(
+ context: Context,
+ theme: HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme
+ ) {
viewModelScope.launch(Dispatchers.IO) {
- context.settingsDataStore.edit {
- it.set(HubsDataStore.Settings.Keys.Theme, theme.ordinal)
+ HubsDataStore.Settings.edit(
+ context,
+ HubsDataStore.Settings.Theme.ColorSchemeMode,
+ theme.ordinal
+ )
+ }
+ }
+
+
+ fun captureLogsAndShare(context: Context) {
+ val dateFormat = SimpleDateFormat("ddMMyyHHmm", Locale.US)
+ val date = dateFormat.format(Calendar.getInstance().time)
+ val name = "Отчет об ошибке $date.txt"
+
+ val info = """
+ Версия приложения: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
+ Был авторизован: ${authorized}
+ Имя устройства: ${Build.BRAND} ${Build.MODEL} (${android.os.Build.DEVICE})
+ Версия ОС (SDK): ${Build.VERSION.SDK_INT}
+ Процессор: ${Build.HARDWARE}
+ """.trimIndent()
+
+ Runtime.getRuntime()
+ .exec("logcat -d")
+ .inputStream.use { input ->
+
+ context.openFileOutput(name, Context.MODE_PRIVATE).use {
+ it.write(input.readBytes() + "\n".toByteArray() + info.toByteArray())
+
+ }
}
+ val logsPath = File(context.filesDir, "")
+ val file = File(logsPath, name)
+ val logFileUri = FileProvider.getUriForFile(context, "com.garnegsoft.hubs.fileprovider", file)
+ val sendIntent = Intent(Intent.ACTION_SEND).apply {
+ type = "message/rfc822"
+ putExtra(Intent.EXTRA_EMAIL, arrayOf("garnegsoft@gmail.com"))
+ putExtra(Intent.EXTRA_SUBJECT, "Отчет об ошибке (логи)")
+ putExtra(Intent.EXTRA_STREAM, logFileUri)
}
+ context.startActivity(Intent.createChooser(sendIntent, null))
+
}
}
@@ -64,11 +91,9 @@ class SettingsScreenViewModel : ViewModel() {
fun SettingsScreen(
onBack: () -> Unit,
onArticleScreenSettings: () -> Unit,
+ onFeedSettings: () -> Unit
) {
val viewModel = viewModel()
- val context = LocalContext.current
-
- val theme by viewModel.getTheme(context).collectAsState(initial = null)
Scaffold(
topBar = {
@@ -86,13 +111,7 @@ fun SettingsScreen(
)
},
) {
- val noRipple = object : RippleTheme {
- @Composable
- override fun defaultColor() = Color.Unspecified
-
- @Composable
- override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
- }
+
Column(
modifier = Modifier
.padding(it)
@@ -100,271 +119,13 @@ fun SettingsScreen(
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- theme?.let {
- Column(
- modifier = Modifier
- .clip(RoundedCornerShape(26.dp))
- .background(MaterialTheme.colors.surface)
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- BasicTitledColumn(
- title = {
- Text(
- modifier = Modifier.padding(bottom = 12.dp),
- text = "Внешний вид", style = MaterialTheme.typography.subtitle1
- )
- },
- divider = {
- // Divider()
- }
- ) {
-
- Column(
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- // TODO: Create special item for settings. Replace checkboxes with switches in Material3
- val isSystemInDarkTheme = isSystemInDarkTheme()
- var useSystemDefinedTheme by rememberSaveable {
- mutableStateOf(
- it == HubsDataStore.Settings.Keys.ThemeModes.SystemDefined ||
- it == HubsDataStore.Settings.Keys.ThemeModes.Undetermined
- )
- }
- val sharedInteractionSource = remember { MutableInteractionSource() }
- var useDarkTheme by remember {
- mutableStateOf(
- if (it == HubsDataStore.Settings.Keys.ThemeModes.SystemDefined) {
- isSystemInDarkTheme
- } else it == HubsDataStore.Settings.Keys.ThemeModes.Dark
- )
- }
-
- Row(modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(10.dp))
- .clickable(
- interactionSource = sharedInteractionSource,
- indication = rememberRipple()
- ) {
- useSystemDefinedTheme = !useSystemDefinedTheme
- viewModel.setTheme(
- context,
- if (useSystemDefinedTheme) {
- HubsDataStore.Settings.Keys.ThemeModes.SystemDefined
- } else {
- if (isSystemInDarkTheme)
- HubsDataStore.Settings.Keys.ThemeModes.Dark
- else
- HubsDataStore.Settings.Keys.ThemeModes.Light
- }
- )
- }
- .padding(start = 4.dp)
- .height(48.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(modifier = Modifier.weight(1f), text = "Системная тема")
- CompositionLocalProvider(LocalRippleTheme provides noRipple) {
-
- Checkbox(
- checked = useSystemDefinedTheme,
- onCheckedChange = {
- useSystemDefinedTheme = !useSystemDefinedTheme
- viewModel.setTheme(
- context,
- if (useSystemDefinedTheme) {
- HubsDataStore.Settings.Keys.ThemeModes.SystemDefined
- } else {
- if (isSystemInDarkTheme)
- HubsDataStore.Settings.Keys.ThemeModes.Dark
- else
- HubsDataStore.Settings.Keys.ThemeModes.Light
- }
- )
- },
- interactionSource = sharedInteractionSource
- )
- }
- }
-
- val isDarkThemeInteractionSource =
- remember { MutableInteractionSource() }
-
- LaunchedEffect(
- key1 = useSystemDefinedTheme,
- key2 = isSystemInDarkTheme,
- block = {
- when {
- (useSystemDefinedTheme && isSystemInDarkTheme && !useDarkTheme) -> useDarkTheme =
- true
-
- (useSystemDefinedTheme && !isSystemInDarkTheme && useDarkTheme) -> useDarkTheme =
- false
- }
- })
- Row(modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(10.dp))
- .clickable(
- enabled = !useSystemDefinedTheme,
- interactionSource = isDarkThemeInteractionSource,
- indication = rememberRipple()
- ) {
- useDarkTheme = !useDarkTheme
- viewModel.setTheme(
- context,
- if (useDarkTheme)
- HubsDataStore.Settings.Keys.ThemeModes.Dark
- else
- HubsDataStore.Settings.Keys.ThemeModes.Light
- )
- }
- .padding(start = 4.dp)
- .heightIn(min = 48.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- modifier = Modifier
- .weight(1f)
- .alpha(if (useSystemDefinedTheme) 0.5f else 1f),
- text = "Тёмная тема"
- )
- CompositionLocalProvider(LocalRippleTheme provides noRipple) {
-
- Checkbox(
- checked = useDarkTheme,
- enabled = !useSystemDefinedTheme,
- onCheckedChange = {
- useDarkTheme = it
- viewModel.setTheme(
- context,
- if (useDarkTheme)
- HubsDataStore.Settings.Keys.ThemeModes.Dark
- else
- HubsDataStore.Settings.Keys.ThemeModes.Light
- )
- },
- interactionSource = isDarkThemeInteractionSource
- )
- }
- }
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(10.dp))
- .clickable(onClick = onArticleScreenSettings)
- .padding(start = 4.dp)
- .heightIn(min = 48.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(modifier = Modifier.weight(1f)) {
- Text(text = "Внешний вид статьи")
- Text(
- text = "Размер шрифта, межстрочный интервал и т.д.",
- fontSize = 11.sp,
- color = MaterialTheme.colors.onSurface.copy(0.5f)
- )
- }
- Icon(
- modifier = Modifier.padding(12.dp),
- imageVector = Icons.Default.ArrowForward,
- contentDescription = null
- )
- }
- }
- }
-
-
- }
- }
- val commentsDisplayMode by context.settingsDataStoreFlowWithDefault(
- HubsDataStore.Settings.Keys.Comments.CommentsDisplayMode,
- HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Default.ordinal
- ).collectAsState(initial = null)
-
- commentsDisplayMode?.let {
- Column(
- modifier = Modifier
- .clip(RoundedCornerShape(26.dp))
- .background(MaterialTheme.colors.surface)
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- BasicTitledColumn(
- title = {
- Text(
- modifier = Modifier.padding(bottom = 12.dp),
- text = "Комментарии", style = MaterialTheme.typography.subtitle1
- )
- },
- divider = {
- // Divider()
- }
- ) {
- val commentsModeSwitchInteractionSource =
- remember { MutableInteractionSource() }
- var useThreadsComments by remember { mutableStateOf(it == HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Threads.ordinal) }
- Column(
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
-
- Row(modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(10.dp))
- .clickable(
- interactionSource = commentsModeSwitchInteractionSource,
- indication = rememberRipple()
- ) {
- useThreadsComments = !useThreadsComments
- viewModel.viewModelScope.launch {
- context.settingsDataStore.edit { prefs ->
- prefs.set(
- HubsDataStore.Settings.Keys.Comments.CommentsDisplayMode,
- if (useThreadsComments) HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Threads.ordinal else HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Default.ordinal
- )
- }
- }
- }
- .padding(start = 4.dp)
- .height(48.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- modifier = Modifier.weight(1f),
- text = "Скрывать ветки комментариев"
- )
-
- CompositionLocalProvider(LocalRippleTheme provides noRipple) {
- Checkbox(
- checked = useThreadsComments,
- onCheckedChange = {
- useThreadsComments = it
- viewModel.viewModelScope.launch {
- context.settingsDataStore.edit { prefs ->
- prefs.set(
- HubsDataStore.Settings.Keys.Comments.CommentsDisplayMode,
- if (it) HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Threads.ordinal else HubsDataStore.Settings.Keys.Comments.CommentsDisplayModes.Default.ordinal
- )
- }
- }
-
- },
- interactionSource = commentsModeSwitchInteractionSource
- )
- }
- }
-
-
- }
- }
-
-
- }
- }
-
-
+ AppearanceSettingsCard(
+ viewModel = viewModel,
+ onArticleScreenSettings = onArticleScreenSettings,
+ onFeedSettings = onFeedSettings
+ )
+ ExperimentalFeaturesSettingsCard(viewModel = viewModel)
+ OtherCard(viewModel = viewModel)
}
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/AppearanceSettingsCard.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/AppearanceSettingsCard.kt
new file mode 100644
index 00000000..0c4adfd9
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/AppearanceSettingsCard.kt
@@ -0,0 +1,213 @@
+package com.garnegsoft.hubs.ui.screens.settings.cards
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Checkbox
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.material.ripple.LocalRippleTheme
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.garnegsoft.hubs.api.dataStore.HubsDataStore
+import com.garnegsoft.hubs.ui.screens.settings.SettingsScreenViewModel
+import com.garnegsoft.hubs.ui.screens.settings.noRipple
+
+@Composable
+fun AppearanceSettingsCard(
+ viewModel: SettingsScreenViewModel,
+ onFeedSettings: () -> Unit,
+ onArticleScreenSettings: () -> Unit
+) {
+ val context = LocalContext.current
+ val theme by viewModel.getTheme(context).collectAsState(initial = null)
+ theme.let {
+ SettingsCard(title = "Внешний вид") {
+ // TODO: Create special item for settings. Replace checkboxes with switches in Material3
+ val isSystemInDarkTheme = isSystemInDarkTheme()
+ var useSystemDefinedTheme by rememberSaveable {
+ mutableStateOf(
+ it == HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.SystemDefined ||
+ it == HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Undetermined
+ )
+ }
+ val sharedInteractionSource = remember { MutableInteractionSource() }
+ var useDarkTheme by remember {
+ mutableStateOf(
+ if (it == HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.SystemDefined) {
+ isSystemInDarkTheme
+ } else it == HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Dark
+ )
+ }
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .clickable(
+ interactionSource = sharedInteractionSource,
+ indication = rememberRipple()
+ ) {
+ useSystemDefinedTheme = !useSystemDefinedTheme
+ viewModel.setTheme(
+ context,
+ if (useSystemDefinedTheme) {
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.SystemDefined
+ } else {
+ if (isSystemInDarkTheme)
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Dark
+ else
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Light
+ }
+ )
+ }
+ .padding(start = 4.dp)
+ .height(48.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(modifier = Modifier.weight(1f), text = "Системная тема")
+ CompositionLocalProvider(LocalRippleTheme provides noRipple) {
+
+ Checkbox(
+ checked = useSystemDefinedTheme,
+ onCheckedChange = {
+ useSystemDefinedTheme = !useSystemDefinedTheme
+ viewModel.setTheme(
+ context,
+ if (useSystemDefinedTheme) {
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.SystemDefined
+ } else {
+ if (isSystemInDarkTheme)
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Dark
+ else
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Light
+ }
+ )
+ },
+ interactionSource = sharedInteractionSource
+ )
+ }
+ }
+
+ val isDarkThemeInteractionSource =
+ remember { MutableInteractionSource() }
+
+ LaunchedEffect(
+ key1 = useSystemDefinedTheme,
+ key2 = isSystemInDarkTheme,
+ block = {
+ when {
+ (useSystemDefinedTheme && isSystemInDarkTheme && !useDarkTheme) -> useDarkTheme =
+ true
+
+ (useSystemDefinedTheme && !isSystemInDarkTheme && useDarkTheme) -> useDarkTheme =
+ false
+ }
+ })
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .clickable(
+ enabled = !useSystemDefinedTheme,
+ interactionSource = isDarkThemeInteractionSource,
+ indication = rememberRipple()
+ ) {
+ useDarkTheme = !useDarkTheme
+ viewModel.setTheme(
+ context,
+ if (useDarkTheme)
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Dark
+ else
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Light
+ )
+ }
+ .padding(start = 4.dp)
+ .heightIn(min = 48.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier
+ .weight(1f)
+ .alpha(if (useSystemDefinedTheme) 0.5f else 1f),
+ text = "Тёмная тема"
+ )
+ CompositionLocalProvider(LocalRippleTheme provides noRipple) {
+
+ Checkbox(
+ checked = useDarkTheme,
+ enabled = !useSystemDefinedTheme,
+ onCheckedChange = {
+ useDarkTheme = it
+ viewModel.setTheme(
+ context,
+ if (useDarkTheme)
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Dark
+ else
+ HubsDataStore.Settings.Theme.ColorSchemeMode.ColorScheme.Light
+ )
+ },
+ interactionSource = isDarkThemeInteractionSource
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .clickable(onClick = onArticleScreenSettings)
+ .padding(start = 4.dp)
+ .heightIn(min = 48.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(text = "Внешний вид статьи")
+ Text(
+ text = "Размер шрифта, межстрочный интервал и т.д.",
+ fontSize = 11.sp,
+ color = MaterialTheme.colors.onSurface.copy(0.5f)
+ )
+ }
+ Icon(
+ modifier = Modifier.padding(12.dp),
+ imageVector = Icons.Default.ArrowForward,
+ contentDescription = null
+ )
+ }
+ SettingsCardItem(
+ title = "Внешний вид ленты", onClick = onFeedSettings,
+ trailingIcon = {
+ Icon(
+ modifier = Modifier.padding(12.dp),
+ imageVector = Icons.Default.ArrowForward,
+ contentDescription = null)
+ }
+ )
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/BaseSettingsCard.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/BaseSettingsCard.kt
new file mode 100644
index 00000000..762ef1db
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/BaseSettingsCard.kt
@@ -0,0 +1,47 @@
+package com.garnegsoft.hubs.ui.screens.settings.cards
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import com.garnegsoft.hubs.ui.common.BasicTitledColumn
+
+@Composable
+fun SettingsCard(
+ title: String,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ BasicTitledColumn(
+ title = {
+ Text(
+ modifier = Modifier.padding(bottom = 12.dp),
+ text = title, style = MaterialTheme.typography.subtitle1
+ )
+ },
+ divider = {
+ // Divider()
+ },
+
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ content = content
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/BaseSettingsCardItem.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/BaseSettingsCardItem.kt
new file mode 100644
index 00000000..15d7aba1
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/BaseSettingsCardItem.kt
@@ -0,0 +1,44 @@
+package com.garnegsoft.hubs.ui.screens.settings.cards
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun SettingsCardItem(
+ title: String,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ onClick: () -> Unit,
+ trailingIcon: @Composable () -> Unit = {},
+ enabled: Boolean = true
+) {
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .clickable(
+ interactionSource = interactionSource,
+ indication = rememberRipple(),
+ enabled = enabled,
+ onClick = onClick
+ )
+ .padding(start = 4.dp)
+ .height(48.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(modifier = Modifier.weight(1f), text = title)
+ trailingIcon()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/ExperimentalFeaturesSettingsCard.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/ExperimentalFeaturesSettingsCard.kt
new file mode 100644
index 00000000..b5ba5eea
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/ExperimentalFeaturesSettingsCard.kt
@@ -0,0 +1,109 @@
+package com.garnegsoft.hubs.ui.screens.settings.cards
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Checkbox
+import androidx.compose.material.Text
+import androidx.compose.material.ripple.LocalRippleTheme
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewModelScope
+import com.garnegsoft.hubs.api.dataStore.HubsDataStore
+import com.garnegsoft.hubs.ui.screens.settings.SettingsScreenViewModel
+import com.garnegsoft.hubs.ui.screens.settings.noRipple
+import kotlinx.coroutines.launch
+
+
+@Composable
+fun ExperimentalFeaturesSettingsCard(
+ viewModel: SettingsScreenViewModel
+) {
+ val context = LocalContext.current
+ val commentsDisplayMode by HubsDataStore.Settings
+ .getValueFlow(context, HubsDataStore.Settings.CommentsDisplayMode)
+ .collectAsState(initial = null)
+
+ commentsDisplayMode?.let {
+ SettingsCard(
+ title = "Экспериментальные функции"
+ ) {
+ val commentsModeSwitchInteractionSource =
+ remember { MutableInteractionSource() }
+ var useThreadsComments by remember { mutableStateOf(it == HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Threads.ordinal) }
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .clickable(
+ interactionSource = commentsModeSwitchInteractionSource,
+ indication = rememberRipple()
+ ) {
+ useThreadsComments = !useThreadsComments
+ viewModel.viewModelScope.launch {
+ HubsDataStore.Settings
+ .edit(
+ context,
+ HubsDataStore.Settings.CommentsDisplayMode,
+ if (useThreadsComments) HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Threads.ordinal
+ else HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Default.ordinal
+ )
+ }
+ }
+ .padding(start = 4.dp)
+ .height(48.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = "Скрывать ветки комментариев"
+ )
+
+ CompositionLocalProvider(LocalRippleTheme provides noRipple) {
+ Checkbox(
+ checked = useThreadsComments,
+ onCheckedChange = {
+ useThreadsComments = it
+ viewModel.viewModelScope.launch {
+ HubsDataStore.Settings
+ .edit(
+ context,
+ HubsDataStore.Settings.CommentsDisplayMode,
+ if (useThreadsComments) HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Threads.ordinal
+ else HubsDataStore.Settings.CommentsDisplayMode.CommentsDisplayModes.Default.ordinal
+ )
+ }
+
+ },
+ interactionSource = commentsModeSwitchInteractionSource
+ )
+ }
+ }
+
+
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/OtherCard.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/OtherCard.kt
new file mode 100644
index 00000000..b0c1e83f
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/settings/cards/OtherCard.kt
@@ -0,0 +1,102 @@
+package com.garnegsoft.hubs.ui.screens.settings.cards
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.garnegsoft.hubs.ui.screens.settings.SettingsScreenViewModel
+
+@Composable
+fun OtherCard(
+ viewModel: SettingsScreenViewModel
+) {
+ val context = LocalContext.current
+ SettingsCard(title = "Другое") {
+ var showDialog by remember { mutableStateOf(false) }
+ ShareLogsDialog(
+ show = showDialog,
+ onDismiss = { showDialog = false },
+ onDone = {
+ viewModel.captureLogsAndShare(context)
+ showDialog = false
+ })
+ SettingsCardItem(title = "Отправить отчёт об ошибке", onClick = { showDialog = true })
+
+ }
+}
+
+@Composable
+fun ShareLogsDialog(
+ show: Boolean,
+ onDismiss: () -> Unit,
+ onDone: () -> Unit,
+) {
+ if (show) {
+ Dialog(
+ properties = DialogProperties(true, true),
+ onDismissRequest = onDismiss
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(20.dp)
+ ) {
+ Column(modifier = Modifier.height(IntrinsicSize.Min)) {
+ Text(
+ text = "Внимание!",
+ color = MaterialTheme.colors.onSurface,
+ style = MaterialTheme.typography.subtitle1
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Box(modifier = Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())) {
+ Text(text = "Для отправки отчета выберите приложение почты во всплывающем меню.\n\nВ приложении почты уже будет введен адрес и прикреплен нужный файл. Обязательно опишите проблему с которой вы столкнулись!")
+ }
+ Spacer(modifier = Modifier.height(12.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = onDismiss, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)) {
+ Text(text = "Отмена")
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Button(
+ onClick = onDone, elevation = null) {
+ Text(text = "Продолжить")
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/LogoutConfirmDialog.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/LogoutConfirmDialog.kt
new file mode 100644
index 00000000..b68aa0ac
--- /dev/null
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/LogoutConfirmDialog.kt
@@ -0,0 +1,80 @@
+package com.garnegsoft.hubs.ui.screens.user
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+
+
+@Composable
+fun LogoutConfirmDialog(
+ show: Boolean,
+ onDismiss: () -> Unit,
+ onProceed: () -> Unit,
+) {
+ if (show) {
+ Dialog(
+ properties = DialogProperties(true, true),
+ onDismissRequest = onDismiss
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(20.dp)
+ ) {
+ Column(modifier = Modifier.height(IntrinsicSize.Min)) {
+ Text(
+ text = "Вы хотите выйти?",
+ color = MaterialTheme.colors.onSurface,
+ style = MaterialTheme.typography.subtitle1
+ )
+// Spacer(modifier = Modifier.height(12.dp))
+// Box(modifier = Modifier
+// .weight(1f)
+// .verticalScroll(rememberScrollState())) {
+// Text(text = "")
+// }
+ Spacer(modifier = Modifier.height(12.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(
+ onClick = onDismiss,
+ contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
+ ) {
+ Text(text = "Отмена")
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Button(onClick = onProceed, elevation = null) {
+ Text(text = "Выйти")
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserArticlesFilter.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserArticlesFilter.kt
index aba3b5cf..026771df 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserArticlesFilter.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserArticlesFilter.kt
@@ -1,11 +1,14 @@
package com.garnegsoft.hubs.ui.screens.user
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.Filter
import com.garnegsoft.hubs.ui.common.BaseFilterDialog
import com.garnegsoft.hubs.ui.common.HubsFilterChip
@@ -37,11 +40,13 @@ fun UserArticlesFilter(
BaseFilterDialog(onDismiss = onDismiss, onDone = { onDone(UserArticlesFilter(showNews))}) {
TitledColumn(title = "Тип публикаций") {
- HubsFilterChip(selected = !showNews, onClick = { showNews = false }) {
- Text(text = "Статьи")
- }
- HubsFilterChip(selected = showNews, onClick = { showNews = true }) {
- Text(text = "Новости")
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ HubsFilterChip(selected = !showNews, onClick = { showNews = false }) {
+ Text(text = "Статьи")
+ }
+ HubsFilterChip(selected = showNews, onClick = { showNews = true }) {
+ Text(text = "Новости")
+ }
}
}
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserBookmarksFilter.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserBookmarksFilter.kt
index 8ba0465d..28fa256e 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserBookmarksFilter.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserBookmarksFilter.kt
@@ -1,11 +1,14 @@
package com.garnegsoft.hubs.ui.screens.user
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.dp
import com.garnegsoft.hubs.api.Filter
import com.garnegsoft.hubs.ui.common.BaseFilterDialog
import com.garnegsoft.hubs.ui.common.HubsFilterChip
@@ -54,17 +57,19 @@ fun UserBookmarksFilter(
}
) {
TitledColumn(title = "Тип закладок") {
- HubsFilterChip(
- selected = bookmarksType == UserBookmarksFilter.Bookmarks.Articles,
- onClick = { bookmarksType = UserBookmarksFilter.Bookmarks.Articles }
- ) {
- Text(text = "Статьи")
- }
- HubsFilterChip(
- selected = bookmarksType == UserBookmarksFilter.Bookmarks.News,
- onClick = { bookmarksType = UserBookmarksFilter.Bookmarks.News }
- ) {
- Text(text = "Новости")
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ HubsFilterChip(
+ selected = bookmarksType == UserBookmarksFilter.Bookmarks.Articles,
+ onClick = { bookmarksType = UserBookmarksFilter.Bookmarks.Articles }
+ ) {
+ Text(text = "Статьи")
+ }
+ HubsFilterChip(
+ selected = bookmarksType == UserBookmarksFilter.Bookmarks.News,
+ onClick = { bookmarksType = UserBookmarksFilter.Bookmarks.News }
+ ) {
+ Text(text = "Новости")
+ }
}
// TODO: Add comments bookmarks
// HubsFilterChip(
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserProfile.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserProfile.kt
index e65f181e..bb21f15d 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserProfile.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserProfile.kt
@@ -28,541 +28,546 @@ import com.garnegsoft.hubs.api.user.UserController
import com.garnegsoft.hubs.api.utils.placeholderColorLegacy
import com.garnegsoft.hubs.ui.common.AsyncSvgImage
import com.garnegsoft.hubs.ui.common.BasicTitledColumn
+import com.garnegsoft.hubs.ui.common.HubChip
import com.garnegsoft.hubs.ui.common.RefreshableContainer
import com.garnegsoft.hubs.ui.common.TitledColumn
import com.garnegsoft.hubs.ui.screens.article.ElementSettings
import com.garnegsoft.hubs.ui.screens.article.RenderHtml
import com.garnegsoft.hubs.ui.theme.DefaultRatingIndicatorColor
-import com.garnegsoft.hubs.ui.theme.RatingNegative
-import com.garnegsoft.hubs.ui.theme.RatingPositive
+import com.garnegsoft.hubs.ui.theme.RatingNegativeColor
+import com.garnegsoft.hubs.ui.theme.RatingPositiveColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun UserProfile(
- isAppUser: Boolean,
- onUserLogout: (() -> Unit)? = null,
- onHubClick: (alias: String) -> Unit,
- onWorkPlaceClick: (alias: String) -> Unit,
- scrollState: ScrollState,
- viewModel: UserScreenViewModel
+ isAppUser: Boolean,
+ onUserLogout: (() -> Unit)? = null,
+ onHubClick: (alias: String) -> Unit,
+ onWorkPlaceClick: (alias: String) -> Unit,
+ scrollState: ScrollState,
+ viewModel: UserScreenViewModel
) {
- val userState by viewModel.user.observeAsState()
- val isRefreshing by viewModel.isRefreshingUser.observeAsState(false)
- RefreshableContainer(onRefresh = viewModel::refreshUser, refreshing = isRefreshing) {
- userState?.let { user ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(scrollState)
- ) {
- Column(
- modifier = Modifier.padding(8.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(26.dp))
- .background(MaterialTheme.colors.surface)
- .padding(12.dp)
- ) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(12.dp)
- ) {
- if (user.avatarUrl != null) {
- AsyncImage(
- model = user.avatarUrl,
- modifier = Modifier
- .size(65.dp)
- .align(Alignment.Center)
- .clip(
- RoundedCornerShape(12.dp)
- )
- .background(Color.White),
- contentDescription = ""
- )
- } else {
- Icon(
- modifier = Modifier
- .size(65.dp)
- .background(Color.White, shape = RoundedCornerShape(12.dp))
- .border(
- width = 4.dp,
- color = placeholderColorLegacy(user.alias),
- shape = RoundedCornerShape(12.dp)
- )
- .align(Alignment.Center)
- .padding(5.dp),
- painter = painterResource(id = R.drawable.user_avatar_placeholder),
- contentDescription = "",
- tint = placeholderColorLegacy(user.alias)
- )
- }
- }
- Box(modifier = Modifier.fillMaxWidth()) {
- if (user.fullname != null)
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = "${user.fullname}\n@${user.alias}",
- fontWeight = FontWeight.W700,
- fontSize = 26.sp,
- textAlign = TextAlign.Center
- )
- else
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = "@${user.alias}",
- fontWeight = FontWeight.W700,
- fontSize = 26.sp,
- textAlign = TextAlign.Center
- )
- }
- if (user.isReadonly)
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(0.dp)
- ) {
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = "Read Only",
- fontWeight = FontWeight.W400,
- color = MaterialTheme.colors.onSurface.copy(0.2f),
- textAlign = TextAlign.Center
- )
- }
- if (user.speciality != null)
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- bottom = 8.dp,
- start = 8.dp,
- end = 8.dp,
- top = 8.dp
- )
- ) {
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = user.speciality,
- fontWeight = FontWeight.W500,
- color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled),
- textAlign = TextAlign.Center
- )
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp),
- horizontalArrangement = Arrangement.SpaceEvenly
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = user.score.toString(),
- fontSize = 24.sp,
- fontWeight = FontWeight.W600,
- color =
- if (user.score > 0)
- RatingPositive
- else
- if (user.score == 0)
- MaterialTheme.colors.onSurface
- else
- RatingNegative
- )
- Text(
- text = "Карма",
- color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled)
- )
- }
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = user.rating.toString(),
- fontSize = 24.sp,
- fontWeight = FontWeight.W600,
- color = DefaultRatingIndicatorColor
- )
- Text(
- text = "Рейтинг",
- color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled)
- )
- }
- }
- if (!isAppUser && !user.isReadonly) {
- user.relatedData?.let {
- var subscribed by rememberSaveable {
- mutableStateOf(it.isSubscribed)
- }
- val subscriptionCoroutineScope = rememberCoroutineScope()
- Box(modifier = Modifier
- .padding(8.dp)
- .height(45.dp)
- .fillMaxWidth()
- .clip(RoundedCornerShape(10.dp))
- .background(if (subscribed) Color(0xFF4CB025) else Color.Transparent)
- .border(
- width = 1.dp,
- shape = RoundedCornerShape(10.dp),
- color = if (subscribed) Color.Transparent else Color(
- 0xFF4CB025
- )
- )
- .clickable {
- subscriptionCoroutineScope.launch(Dispatchers.IO) {
- subscribed = !subscribed
- subscribed = UserController.subscription(user.alias)
- }
- }
- ) {
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = if (subscribed) "Вы подписаны" else "Подписаться",
- color = if (subscribed) Color.White else Color(0xFF4CB025)
- )
- }
- }
- }
-
- }
- val note by viewModel.note.observeAsState()
- if (!isAppUser && note?.text != null) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(26.dp))
- .background(MaterialTheme.colors.surface)
- .padding(8.dp)
- ) {
- BasicTitledColumn(
- title = {
- Text(
- modifier = Modifier.padding(12.dp),
- text = "Заметка", style = MaterialTheme.typography.subtitle1
- )
- },
- divider = {
- // Divider()
- }
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- start = 12.dp,
- end = 12.dp,
- bottom = 12.dp
- )
- .clip(RoundedCornerShape(10.dp))
- .background(
- MaterialTheme.colors.onSurface.copy(0.04f)
- )
- .padding(8.dp)
- ) {
- Text(
- color = MaterialTheme.colors.onSurface.copy(0.75f),
- text = note?.text ?: ""
- )
- }
- }
- }
- }
- val whoIs by viewModel.whoIs.observeAsState()
- whoIs?.let { whoIs ->
- if (!whoIs.aboutHtml.isNullOrBlank() || whoIs.badges.isNotEmpty() || whoIs.invite != null || whoIs.contacts.isNotEmpty()) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(26.dp))
- .background(MaterialTheme.colors.surface)
- .padding(8.dp)
- ) {
- BasicTitledColumn(title = {
- Text(
- modifier = Modifier.padding(12.dp),
- text = "Описание",
- style = MaterialTheme.typography.subtitle1
- )
- }, divider = {
+ val userState by viewModel.user.observeAsState()
+ val isRefreshing by viewModel.isRefreshingUser.observeAsState(false)
+ RefreshableContainer(onRefresh = viewModel::refreshUser, refreshing = isRefreshing) {
+ userState?.let { user ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ ) {
+ Column(
+ modifier = Modifier.padding(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(12.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp)
+ ) {
+ if (user.avatarUrl != null) {
+ AsyncImage(
+ model = user.avatarUrl,
+ modifier = Modifier
+ .size(65.dp)
+ .align(Alignment.Center)
+ .clip(
+ RoundedCornerShape(12.dp)
+ )
+ .background(Color.White),
+ contentDescription = ""
+ )
+ } else {
+ Icon(
+ modifier = Modifier
+ .size(65.dp)
+ .background(Color.White, shape = RoundedCornerShape(12.dp))
+ .border(
+ width = 4.dp,
+ color = placeholderColorLegacy(user.alias),
+ shape = RoundedCornerShape(12.dp)
+ )
+ .align(Alignment.Center)
+ .padding(5.dp),
+ painter = painterResource(id = R.drawable.user_avatar_placeholder),
+ contentDescription = "",
+ tint = placeholderColorLegacy(user.alias)
+ )
+ }
+ }
+ Box(modifier = Modifier.fillMaxWidth()) {
+ if (user.fullname != null)
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "${user.fullname}\n@${user.alias}",
+ fontWeight = FontWeight.W700,
+ fontSize = 26.sp,
+ textAlign = TextAlign.Center
+ )
+ else
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "@${user.alias}",
+ fontWeight = FontWeight.W700,
+ fontSize = 26.sp,
+ textAlign = TextAlign.Center
+ )
+ }
+ if (user.isReadonly)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(0.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "Read Only",
+ fontWeight = FontWeight.W400,
+ color = MaterialTheme.colors.onSurface.copy(0.2f),
+ textAlign = TextAlign.Center
+ )
+ }
+ if (user.speciality != null)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ bottom = 8.dp,
+ start = 8.dp,
+ end = 8.dp,
+ top = 8.dp
+ )
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = user.speciality,
+ fontWeight = FontWeight.W500,
+ color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled),
+ textAlign = TextAlign.Center
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = user.score.toString(),
+ fontSize = 24.sp,
+ fontWeight = FontWeight.W600,
+ color =
+ if (user.score > 0)
+ RatingPositiveColor
+ else
+ if (user.score == 0)
+ MaterialTheme.colors.onSurface
+ else
+ RatingNegativeColor
+ )
+ Text(
+ text = "Карма",
+ color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled)
+ )
+ }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = user.rating.toString(),
+ fontSize = 24.sp,
+ fontWeight = FontWeight.W600,
+ color = DefaultRatingIndicatorColor
+ )
+ Text(
+ text = "Рейтинг",
+ color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled)
+ )
+ }
+ }
+ if (!isAppUser && !user.isReadonly) {
+ user.relatedData?.let {
+ var subscribed by rememberSaveable {
+ mutableStateOf(it.isSubscribed)
+ }
+ val subscriptionCoroutineScope = rememberCoroutineScope()
+ Box(modifier = Modifier
+ .padding(8.dp)
+ .height(45.dp)
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .background(if (subscribed) Color(0xFF4CB025) else Color.Transparent)
+ .border(
+ width = 1.dp,
+ shape = RoundedCornerShape(10.dp),
+ color = if (subscribed) Color.Transparent else Color(
+ 0xFF4CB025
+ )
+ )
+ .clickable {
+ subscriptionCoroutineScope.launch(Dispatchers.IO) {
+ subscribed = !subscribed
+ subscribed = UserController.subscription(user.alias)
+ }
+ }
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = if (subscribed) "Вы подписаны" else "Подписаться",
+ color = if (subscribed) Color.White else Color(0xFF4CB025)
+ )
+ }
+ }
+ }
+
+ }
+ val note by viewModel.note.observeAsState()
+ if (!isAppUser && note?.text != null) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(8.dp)
+ ) {
+ BasicTitledColumn(
+ title = {
+ Text(
+ modifier = Modifier.padding(12.dp),
+ text = "Заметка", style = MaterialTheme.typography.subtitle1
+ )
+ },
+ divider = { }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = 12.dp,
+ end = 12.dp,
+ bottom = 12.dp
+ )
+ .clip(RoundedCornerShape(10.dp))
+ .background(
+ MaterialTheme.colors.onSurface.copy(0.04f)
+ )
+ .padding(8.dp)
+ ) {
+ Text(
+ color = MaterialTheme.colors.onSurface.copy(0.75f),
+ text = note?.text ?: ""
+ )
+ }
+ }
+ }
+ }
+ val whoIs by viewModel.whoIs.observeAsState()
+ val hubs by viewModel.subscribedHubs.observeAsState()
+
+ if ((whoIs != null && (!whoIs?.aboutHtml.isNullOrBlank() || whoIs!!.badges.isNotEmpty()
+ || whoIs!!.invite != null || whoIs!!.contacts.isNotEmpty()))
+ || !hubs?.list.isNullOrEmpty()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(8.dp)
+ ) {
+ BasicTitledColumn(title = {
+ Text(
+ modifier = Modifier.padding(12.dp),
+ text = "Описание",
+ style = MaterialTheme.typography.subtitle1
+ )
+ }, divider = {
// Divider()
- }) {
- Column(
- modifier = Modifier.padding(
- start = 12.dp,
- end = 12.dp,
- bottom = 12.dp,
- top = 4.dp
- ),
- verticalArrangement = Arrangement.spacedBy(20.dp)
- ) {
- whoIs.badges.let {
- TitledColumn(title = "Значки") {
- FlowRow(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- it.forEach { Badge(title = it.title) }
- }
- }
- }
-
- whoIs.aboutHtml?.let {
- if (it.isNotBlank()) {
- TitledColumn(title = "О Себе") {
- RenderHtml(
- html = it,
- elementSettings = remember {
- ElementSettings(
- fontSize = 16.sp,
- lineHeight = 16.sp,
- fitScreenWidth = false
- )
- })
- }
- }
- }
-
- whoIs.invite?.let {
- TitledColumn(title = "Приглашен") {
- Text(text = "${it.inviteDate} по приглашению от ${it.inviterAlias ?: "НЛО"}")
- }
- }
-
- whoIs.let {
- if (it.contacts.size > 0) {
- TitledColumn(title = "Контакты") {
- Column(
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- val context = LocalContext.current
- it.contacts.forEach {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(8.dp))
- .background(
- MaterialTheme.colors.onSurface.copy(
- 0.04f
- )
- )
- .clickable {
- context.startActivity(
- Intent(
- Intent.ACTION_VIEW,
- Uri.parse(it.url)
- )
- )
- }
- .padding(12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (it.faviconUrl != null && it.faviconUrl.isNotBlank()) {
- AsyncSvgImage(
- modifier = Modifier
- .size(24.dp)
- .clip(
- RoundedCornerShape(4.dp)
- )
- .background(if (MaterialTheme.colors.isLight) Color.Transparent else MaterialTheme.colors.onSurface),
- data = it.faviconUrl,
- revertColorsOnDarkTheme = false,
- contentDescription = it.title,
- contentScale = ContentScale.Fit
- )
- } else {
- Icon(
- modifier = Modifier.size(24.dp),
- painter = painterResource(id = R.drawable.website_favicon_placeholder),
- contentDescription = "website",
- tint = MaterialTheme.colors.onSurface.copy(
- 0.4f
- )
- )
- }
-
- Spacer(modifier = Modifier.width(12.dp))
-
- Text(it.title)
- }
- }
- }
- }
- }
- }
-
- val hubs by viewModel.subscribedHubs.observeAsState()
- hubs?.let {
- if (it.list.size > 0) {
- Column() {
- TitledColumn(title = "Состоит в хабах") {
- FlowRow(
- horizontalArrangement = Arrangement.spacedBy(
- 8.dp
- )
- ) {
- it.list.forEach {
- HubChip(hub = it) {
- onHubClick(it.alias)
- }
- }
- }
- }
- if (viewModel.moreHubsAvailable) {
- TextButton(
- onClick = {
- viewModel.loadSubscribedHubs()
- }
- ) {
- Text(
- "Показать ещё",
- color = MaterialTheme.colors.secondary,
- letterSpacing = 0.sp
- )
- }
- }
- }
- }
- }
-
- }
- }
- }
- }
- }
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(26.dp))
- .background(MaterialTheme.colors.surface)
- .padding(8.dp)
- ) {
- BasicTitledColumn(title = {
- Text(
- modifier = Modifier.padding(12.dp),
- text = "Информация", style = MaterialTheme.typography.subtitle1
- )
- }, divider = {
+ }) {
+ Column(
+ modifier = Modifier.padding(
+ start = 12.dp,
+ end = 12.dp,
+ bottom = 12.dp,
+ top = 4.dp
+ ),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ whoIs?.let { whoIs ->
+ whoIs.badges.let {
+
+ TitledColumn(title = "Значки") {
+ FlowRow(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ it.forEach { Badge(title = it.title) }
+ }
+ }
+ }
+
+ whoIs.aboutHtml?.let {
+ if (it.isNotBlank()) {
+ TitledColumn(title = "О Себе") {
+ RenderHtml(
+ html = it,
+ elementSettings = remember {
+ ElementSettings(
+ fontSize = 16.sp,
+ lineHeight = 16.sp,
+ fitScreenWidth = false
+ )
+ })
+ }
+ }
+ }
+
+ whoIs.invite?.let {
+ TitledColumn(title = "Приглашен") {
+ Text(text = "${it.inviteDate} по приглашению от ${it.inviterAlias ?: "НЛО"}")
+ }
+ }
+
+ whoIs.let {
+ if (it.contacts.size > 0) {
+ TitledColumn(title = "Контакты") {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ val context = LocalContext.current
+ it.contacts.forEach {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(8.dp))
+ .background(
+ MaterialTheme.colors.onSurface.copy(
+ 0.04f
+ )
+ )
+ .clickable {
+ context.startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(it.url)
+ )
+ )
+ }
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (it.faviconUrl != null && it.faviconUrl.isNotBlank()) {
+ AsyncSvgImage(
+ modifier = Modifier
+ .size(24.dp)
+ .clip(
+ RoundedCornerShape(4.dp)
+ )
+ .background(if (MaterialTheme.colors.isLight) Color.Transparent else MaterialTheme.colors.onSurface),
+ data = it.faviconUrl,
+ revertColorsOnDarkTheme = false,
+ contentDescription = it.title,
+ contentScale = ContentScale.Fit
+ )
+ } else {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(id = R.drawable.website_favicon_placeholder),
+ contentDescription = "website",
+ tint = MaterialTheme.colors.onSurface.copy(
+ 0.4f
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Text(it.title)
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ }
+ hubs?.let {
+ if (it.list.size > 0) {
+ Column() {
+ TitledColumn(title = "Состоит в хабах") {
+ FlowRow(
+ horizontalArrangement = Arrangement.spacedBy(
+ 8.dp
+ )
+ ) {
+ it.list.forEach {
+ HubChip(hub = it) {
+ onHubClick(it.alias)
+ }
+ }
+ }
+ }
+ if (viewModel.moreHubsAvailable) {
+ TextButton(
+ onClick = {
+ viewModel.loadSubscribedHubs()
+ }
+ ) {
+ Text(
+ "Показать ещё",
+ color = MaterialTheme.colors.secondary,
+ letterSpacing = 0.sp
+ )
+ }
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .background(MaterialTheme.colors.surface)
+ .padding(8.dp)
+ ) {
+ BasicTitledColumn(title = {
+ Text(
+ modifier = Modifier.padding(12.dp),
+ text = "Информация", style = MaterialTheme.typography.subtitle1
+ )
+ }, divider = {
// Divider()
- }) {
- Column(
- modifier = Modifier.padding(
- start = 12.dp,
- end = 12.dp,
- bottom = 12.dp,
- top = 4.dp
- ),
- verticalArrangement = Arrangement.spacedBy(20.dp)
- ) {
- TitledColumn(
- title = "Место в рейтинге"
- ) {
- Text(
- text = if (user.ratingPosition == null) "Не участвует" else user.ratingPosition.toString() + "-й",
- )
- }
-
- user.location?.let {
- TitledColumn(title = "Откуда") {
- Text(
- text = it,
- )
- }
- }
-
- if (user.workPlaces.size > 0) {
- TitledColumn(title = "Работает в") {
- Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
- user.workPlaces.forEach {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(8.dp))
- .background(
- MaterialTheme.colors.onSurface.copy(
- 0.04f
- )
- )
- .clickable {
- onWorkPlaceClick(it.alias)
- }
- .padding(12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(it.title)
- }
- }
- }
- }
- }
-
- if (user.birthday != null) {
-
- TitledColumn(title = "Дата рождения") {
- Text(
- text = user.birthday,
- )
- }
- }
-
- TitledColumn(title = "Дата регистрации") {
- Text(
- text = user.registrationDate,
- )
- }
-
- if (user.lastActivityDate != null) {
-
- TitledColumn(title = "Активность") {
- Text(
- text = user.lastActivityDate
- )
- }
- }
-
-
- }
- }
- }
- if (isAppUser) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(26.dp))
- .clickable(onClick = onUserLogout!!),
- elevation = 0.dp,
- shape = RoundedCornerShape(26.dp),
- backgroundColor = MaterialTheme.colors.surface,
- ) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(12.dp)
- ) {
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = "Выйти",
- color = MaterialTheme.colors.error
- )
- }
- }
- }
- }
-
- }
- }
- }
- if (userState == null){
- Box(modifier = Modifier.fillMaxSize()){
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
- }
- }
-
+ }) {
+ Column(
+ modifier = Modifier.padding(
+ start = 12.dp,
+ end = 12.dp,
+ bottom = 12.dp,
+ top = 4.dp
+ ),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ TitledColumn(
+ title = "Место в рейтинге"
+ ) {
+ Text(
+ text = if (user.ratingPosition == null) "Не участвует" else user.ratingPosition.toString() + "-й",
+ )
+ }
+
+ user.location?.let {
+ TitledColumn(title = "Откуда") {
+ Text(
+ text = it,
+ )
+ }
+ }
+
+ if (user.workPlaces.size > 0) {
+ TitledColumn(title = "Работает в") {
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ user.workPlaces.forEach {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(8.dp))
+ .background(
+ MaterialTheme.colors.onSurface.copy(
+ 0.04f
+ )
+ )
+ .clickable {
+ onWorkPlaceClick(it.alias)
+ }
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(it.title)
+ }
+ }
+ }
+ }
+ }
+
+ if (user.birthday != null) {
+
+ TitledColumn(title = "Дата рождения") {
+ Text(
+ text = user.birthday,
+ )
+ }
+ }
+
+ TitledColumn(title = "Дата регистрации") {
+ Text(
+ text = user.registrationDate,
+ )
+ }
+
+ if (user.lastActivityDate != null) {
+
+ TitledColumn(title = "Активность") {
+ Text(
+ text = user.lastActivityDate
+ )
+ }
+ }
+
+
+ }
+ }
+ }
+ if (isAppUser) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(26.dp))
+ .clickable(onClick = onUserLogout!!),
+ elevation = 0.dp,
+ shape = RoundedCornerShape(26.dp),
+ backgroundColor = MaterialTheme.colors.surface,
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "Выйти",
+ color = MaterialTheme.colors.error
+ )
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ if (userState == null) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ }
+
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserScreen.kt b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserScreen.kt
index c1c5a480..b210424b 100755
--- a/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserScreen.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/screens/user/UserScreen.kt
@@ -25,7 +25,7 @@ import com.garnegsoft.hubs.api.comment.list.CommentSnippet
import com.garnegsoft.hubs.api.rememberCollapsingContentState
import com.garnegsoft.hubs.api.utils.formatLongNumbers
import com.garnegsoft.hubs.ui.common.*
-import com.garnegsoft.hubs.ui.common.snippetsPages.ArticlesListPage
+import com.garnegsoft.hubs.ui.common.feedCards.comment.CommentCard
import com.garnegsoft.hubs.ui.common.snippetsPages.ArticlesListPageWithFilter
import com.garnegsoft.hubs.ui.common.snippetsPages.CommentsListPage
import com.garnegsoft.hubs.ui.common.snippetsPages.CommonPageWithFilter
@@ -74,17 +74,19 @@ fun UserScreen(
IconButton(
onClick = {
- val sendIntent = Intent(Intent.ACTION_SEND)
- sendIntent.putExtra(
- Intent.EXTRA_TEXT,
- if (viewModel.user.value?.fullname != null)
- "${viewModel.user.value!!.fullname} — https://habr.com/ru/users/${viewModel.user.value!!.alias}/"
- else
- "${viewModel.user.value!!.alias} — https://habr.com/ru/users/${viewModel.user.value!!.alias}/"
- )
- sendIntent.setType("text/plain")
- val shareIntent = Intent.createChooser(sendIntent, null)
- context.startActivity(shareIntent)
+ viewModel.user.value?.let { user ->
+ val sendIntent = Intent(Intent.ACTION_SEND)
+ sendIntent.putExtra(
+ Intent.EXTRA_TEXT,
+ if (user.fullname != null)
+ "${user.fullname} (@${user.alias}) — https://habr.com/ru/users/${user.alias}/"
+ else
+ "${user.alias} — https://habr.com/ru/users/${user.alias}/"
+ )
+ sendIntent.setType("text/plain")
+ val shareIntent = Intent.createChooser(sendIntent, null)
+ context.startActivity(shareIntent)
+ }
}) {
Icon(imageVector = Icons.Outlined.Share, contentDescription = "")
}
@@ -193,8 +195,7 @@ fun UserScreen(
}
)
}
- }
- else {
+ } else {
CommonPageWithFilter(
listModel = viewModel.commentsBookmarksModel,
filterDialog = { defaultValues, onDismiss, onDone ->
@@ -210,7 +211,12 @@ fun UserScreen(
) {
CommentCard(
comment = it,
- onCommentClick = { onCommentClicked(it.parentPost.id, it.id) },
+ onCommentClick = {
+ onCommentClicked(
+ it.parentPost.id,
+ it.id
+ )
+ },
onAuthorClick = { onUserClicked(it.author.alias) },
onParentPostClick = { onArticleClicked(it.parentPost.id) })
}
@@ -286,18 +292,31 @@ fun UserScreen(
if (pagesMap.size > 1) {
HabrScrollableTabRow(pagerState = pagerState, tabs = tabs) { index, title ->
when {
- title.startsWith("Профиль") -> { ScrollUpMethods.scrollNormalList(profilePageScrollState) }
+ title.startsWith("Профиль") -> {
+ ScrollUpMethods.scrollNormalList(profilePageScrollState)
+ }
+
title.startsWith("Публикации") -> {
articlesFilterContentState.show()
ScrollUpMethods.scrollLazyList(articlesLazyListState)
}
- title.startsWith("Комментарии") -> { ScrollUpMethods.scrollLazyList(commentsLazyListState) }
+
+ title.startsWith("Комментарии") -> {
+ ScrollUpMethods.scrollLazyList(commentsLazyListState)
+ }
+
title.startsWith("Закладки") -> {
bookmarksFilterContentState.show()
ScrollUpMethods.scrollLazyList(bookmarksLazyListState)
}
- title.startsWith("Подписчики") -> { ScrollUpMethods.scrollLazyList(followersLazyListState) }
- title.startsWith("Подписки") -> { ScrollUpMethods.scrollLazyList(subscriptionsLazyListState) }
+
+ title.startsWith("Подписчики") -> {
+ ScrollUpMethods.scrollLazyList(followersLazyListState)
+ }
+
+ title.startsWith("Подписки") -> {
+ ScrollUpMethods.scrollLazyList(subscriptionsLazyListState)
+ }
}
}
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/theme/Color.kt b/app/src/main/java/com/garnegsoft/hubs/ui/theme/Color.kt
index 5195721a..6b51ce9e 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/theme/Color.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/theme/Color.kt
@@ -2,19 +2,16 @@ package com.garnegsoft.hubs.ui.theme
import androidx.compose.ui.graphics.Color
-val Purple200 = Color(0xFFBB86FC)
-val Purple500 = Color(0xFF6200EE)
-val Purple700 = Color(0xFF3700B3)
-val Teal200 = Color(0xFF03DAC5)
val DefaultRatingIndicatorColor = Color(0xFFE719A9)
val HubInvestmentIndicatorColor = Color(0xFF4CBE51)
-
val SecondaryColor = Color(0xFF_52_64_74)
val PrimaryColor = Color(0xFF_30_3B_44)
val PrimaryVariantColor = Color(0xFF_20_2A_32)
val SecondaryVariantColor = Color(0xFF628DA8)
-val RatingPositive = Color(0xFF4CBE51)
-val RatingNegative = Color(0xFFC43333)
+val RatingPositiveColor = Color(0xFF4CBE51)
+val RatingNegativeColor = Color(0xFFC43333)
+
+val HubSubscribedColor = Color(0xE351A843)
diff --git a/app/src/main/java/com/garnegsoft/hubs/ui/theme/Theme.kt b/app/src/main/java/com/garnegsoft/hubs/ui/theme/Theme.kt
index 456eeec8..ce4e81a1 100644
--- a/app/src/main/java/com/garnegsoft/hubs/ui/theme/Theme.kt
+++ b/app/src/main/java/com/garnegsoft/hubs/ui/theme/Theme.kt
@@ -20,13 +20,13 @@ import androidx.compose.material3.MaterialTheme as Material3Theme
private val DarkColorPalette = darkColors(
primary = Color(0xFFE7E7E7),
- primaryVariant = Color(0xFFDADEDF),
- secondary = Color(0xFFECECEC),
+ primaryVariant = Color(0xFFE0E0E0),
+ secondary = Color(0xFFD3D3D3),
onSecondary = Color(0x88FFFFFF),
background = Color(32, 32, 32, 255),
surface = Color(49, 49, 49, 255),
onSurface = Color(0xFFDADADA),
- onBackground = Color(0xFFC2C2C2),
+ onBackground = Color(0xFFD8D8D8),
secondaryVariant = Color(0xFFB4B4B4),
onError = Color.White
)
diff --git a/app/src/main/res/drawable/bookmarks_shortcut_icon.xml b/app/src/main/res/drawable/bookmarks_shortcut_icon.xml
new file mode 100644
index 00000000..fda1969a
--- /dev/null
+++ b/app/src/main/res/drawable/bookmarks_shortcut_icon.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/history.xml b/app/src/main/res/drawable/history.xml
new file mode 100644
index 00000000..95294f14
--- /dev/null
+++ b/app/src/main/res/drawable/history.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/logo2.xml b/app/src/main/res/drawable/logo2.xml
index ff5916c1..ebbf336d 100644
--- a/app/src/main/res/drawable/logo2.xml
+++ b/app/src/main/res/drawable/logo2.xml
@@ -9,7 +9,7 @@
android:fillColor="#588ab2"
android:strokeColor="#00000000"/>
+
+
+
diff --git a/app/src/main/res/drawable/settings_first_article_thumbnail.png b/app/src/main/res/drawable/settings_first_article_thumbnail.png
new file mode 100644
index 00000000..c341fc68
Binary files /dev/null and b/app/src/main/res/drawable/settings_first_article_thumbnail.png differ
diff --git a/app/src/main/res/drawable/views_icon.xml b/app/src/main/res/drawable/views_icon.xml
index ea83639c..e4fd2635 100755
--- a/app/src/main/res/drawable/views_icon.xml
+++ b/app/src/main/res/drawable/views_icon.xml
@@ -10,7 +10,7 @@
android:strokeColor="#000000"
android:strokeLineCap="square"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/winter_launcher_icon_foreground.xml b/app/src/main/res/drawable/winter_launcher_icon_foreground.xml
new file mode 100644
index 00000000..28290f59
--- /dev/null
+++ b/app/src/main/res/drawable/winter_launcher_icon_foreground.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 62172795..f6dda7b7 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,7 +1,7 @@
-
-
+
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 26f6e166..bffca60d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -33,4 +33,6 @@
Публикации
Подписаться
Вы подписаны
+ Сохраненные статьи
+ Сохраненные
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b946d3cd..8d42a0c1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -34,5 +34,9 @@
Publications
Follow
Following
+ Saved Articles
+ Saved
+ В современном мире информационных технологий (IT) карьерный рост становится неотъемлемой частью профессиональной жизни специалистов. Развитие технологий и постоянные изменения в отрасли требуют от профессионалов IT глубокого понимания своей области, навыков адаптации и стратегического мышления. В данном реферате мы рассмотрим основные тенденции карьерного роста в IT-сфере, выявим факторы, влияющие на успешное продвижение по карьерной лестнице, и рассмотрим стратегии, которые могут помочь индивидуам достичь успеха в данной области. Тенденции карьерного роста в IT: Современная IT-сфера характеризуется высокой динамикой изменений. Технологии развиваются стремительно, и успешный профессионал должен постоянно совершенствовать свои навыки и следить за новыми тенденциями. Одной из ключевых тенденций является переход к облачным технологиям, искусственному интеллекту, большим данным и кибербезопасности. Профессионалы, специализирующиеся в этих областях, часто имеют больше возможностей для карьерного роста. Факторы успешного карьерного роста: Непрерывное обучение: Один из ключевых факторов успеха в IT - это готовность к постоянному обучению. Технологии быстро меняются, и специалистам приходится адаптироваться к новым требованиям. Участие в курсах, тренингах и сертификационных программых может значительно повысить конкурентоспособность. Специализация и экспертиза: Специализация в узкой области и развитие экспертизы позволяют выделиться среди конкурентов. Работа в специализированной области также может открывать дополнительные возможности для карьерного роста. Сетевые связи и общение: В мире IT большое значение имеют профессиональные связи. Участие в индустриальных мероприятиях, конференциях, и общение с коллегами и профессионалами из отрасли могут открыть новые перспективы для роста и развития.Лидерские навыки: Специалистам в IT необходимы не только технические навыки, но и лидерские качества. Умение эффективно управлять проектами, командами и принимать стратегические решения существенно влияет на карьерный успех.Стратегии карьерного роста: Разработка карьерного плана: Определение краткосрочных и долгосрочных целей, разработка плана и последовательное его выполнение помогут достигнуть успеха. Активное участие в сообществе: Участие в профессиональных сообществах и форумах позволяет узнавать о новых тенденциях, обмениваться опытом и создавать полезные контакты. Развитие мягких навыков: Помимо технических навыков, важно развивать мягкие навыки, такие как коммуникация, управление временем, и решение проблем. Поиск ментора: Общение с опытными профессионалами и наставничество могут быть весьма полезными в процессе карьерного роста. Карьерный рост в IT-сфере – это процесс, требующий постоянного самосовершенствования и адаптации к изменениям в отрасли. Специалисты, обладающие стратегическим мышлением, лидерскими качествами и готовые к постоянному обучению, находятся в лучшем положении для успешного продвижения по карьерной лестнице. Развитие технологий, акцент на инновациях и высокая конкуренция делают IT-сферу одним из наиболее динамичных и перспективных направлений для карьерного роста.
+ В тихом офисном здании, где монотонный гул вентиляции сливался с жужжанием компьютеров, работал неряшлевый системный администратор по имени Игорь. Игорь был тем человеком, у которого волосы всегда были в неразглаженных завитках, одежда вечно мятая, а копчик белым на брюках - его неотъемлемой чертой. Его рабочее место было настоящим хаосом: валялись бумаги, провода сплетались в путанице, клавиатура покрыта была слоем пыли, а в чашке, стоящей рядом, воду заменяли каким-то странным на вид жидким. Однажды, Игорь решил провести профилактику в серверной. Ему было неудобно, что его коллеги могли видеть внутреннее убранство того места, где он чувствовал себя в своей тарелке. \"Нет ничего хуже, чем думать, что тебя могут судить по внешнему виду, правда?\" – думал Игорь. Так началась его эпопея по уборке серверной. Он начал вытаскивать провода, пытаясь разобраться, что к чему. Однако его неаккуратные движения стоили ему дорого: нечаянно он задел стойку с серверами, и один из них начал медленно наклоняться. Не замечая изменений в устойчивости сервера, Игорь продолжал свою борьбу с проводами. И вот, в какой-то момент, сервер не выдержал и с треском упал на пол. Сильный удар вызвал короткое замыкание в электропитании. Тем временем, не замечая последствий своего творчества, Игорь разливал чашку кофе на сервера, оправдываясь перед собой, что это \"улучшит их работу\". В какой-то момент, когда он снова протягивался к своей цели, запах гари донесся до его носа. Огонь! Он вспыхнул в самом центре серверной. Паника охватила Игоря, но вместо того, чтобы вызывать помощь, он решил самостоятельно справиться с огнем. Он подбежал с чашкой воды и начал лить ее на пламя, не понимая, что вода и электроника – не самая лучшая комбинация. Привет смотрящему ресурсы)
\ No newline at end of file
diff --git a/app/src/main/res/xml-v25/shortcuts.xml b/app/src/main/res/xml-v25/shortcuts.xml
new file mode 100644
index 00000000..84a9cf4b
--- /dev/null
+++ b/app/src/main/res/xml-v25/shortcuts.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/log_file_path.xml b/app/src/main/res/xml/log_file_path.xml
new file mode 100644
index 00000000..8a69e334
--- /dev/null
+++ b/app/src/main/res/xml/log_file_path.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 00000000..e67bb188
--- /dev/null
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/garnegsoft/hubs/ArticleResourceTests.kt b/app/src/test/java/com/garnegsoft/hubs/ArticleResourceTests.kt
new file mode 100644
index 00000000..7416df3f
--- /dev/null
+++ b/app/src/test/java/com/garnegsoft/hubs/ArticleResourceTests.kt
@@ -0,0 +1,45 @@
+package com.garnegsoft.hubs
+
+import ArticleController
+import com.garnegsoft.hubs.api.HabrApi
+import okhttp3.OkHttpClient
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ArticleResourceTests {
+
+ @Test
+ fun getAllImages(){
+ HabrApi.setHttpClient(OkHttpClient())
+
+ ArticleController.get(729112)?.let {
+ var urls = mutableListOf()
+ println("OG html: \n${it.contentHtml}")
+ var imageCounter = 0
+ val result = Jsoup.parse(it.contentHtml).forEachNode {
+ if(it is Element && it.tagName() == "img"){
+ var url = if (it.hasAttr("data-src")){
+ it.attr("data-src")
+ } else {
+ it.attr("src")
+ }
+
+ urls.add(url)
+ it.attr("data-src", "img$imageCounter.jpg")
+
+ imageCounter++
+ }
+
+ } as Document
+
+ println("\n\n\nafter jsoup: \n${result.body().html()}")
+
+ println("\n\nOG URLS:\n${urls.joinToString("\n")}")
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/garnegsoft/hubs/ExampleUnitTest.kt b/app/src/test/java/com/garnegsoft/hubs/ExampleUnitTest.kt
index d5b60ef8..37620101 100644
--- a/app/src/test/java/com/garnegsoft/hubs/ExampleUnitTest.kt
+++ b/app/src/test/java/com/garnegsoft/hubs/ExampleUnitTest.kt
@@ -1,10 +1,17 @@
package com.garnegsoft.hubs
+import android.app.appsearch.GlobalSearchSession
import com.garnegsoft.hubs.api.FilterPeriod
+import com.garnegsoft.hubs.api.history.HistoryArticle
import com.garnegsoft.hubs.ui.screens.main.NewsFilter
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.launch
import org.junit.Test
import org.junit.Assert.*
+import java.util.Date
/**
* Example local unit test, which will execute on the development machine (host).
@@ -60,6 +67,31 @@ class ExampleUnitTest {
assertEquals("https://assets.habr.com/habr-web/img/avatars/076.png", endString)
}
-
+
+ @Test
+ fun test_flowShit() {
+ val flow = getFlow()
+ GlobalScope.launch {
+ flow.collectLatest {
+ println(it)
+ }
+ }
+ while (true){}
+ }
+
+ fun getFlow() = flow {
+ var counter = 0
+ while (true) {
+ counter++
+ emit(counter)
+ }
+ }
+
+ @Test
+ fun test_serializer() {
+ val data = HistoryArticle(0, "aboba", "amongus", null)
+ println(data.toHistoryEntity().data)
+ }
+
}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 2572cdcf..c6666da0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,17 +1,17 @@
buildscript {
ext {
- compose_ui_version = '1.5.0'
+ compose_ui_version = '1.5.3'
accompanist_version = '0.30.1'
coil_version = "2.4.0"
- room_version = "2.5.2"
- navigation_version = "2.7.0"
+ room_version = "2.6.0"
+ navigation_version = "2.7.5"
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.1.0' apply false
id 'com.android.library' version '8.1.0' apply false
- id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
- id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'
- id "org.jetbrains.kotlin.kapt" version "1.8.21"
+ id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
+ id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.10'
+ id "org.jetbrains.kotlin.kapt" version "1.9.10"
id 'com.android.test' version '8.1.0' apply false
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 0f4fb3a2..772048cd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,27 +1,19 @@
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
+## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 -XX:+UseParallelGC
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
-# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app's APK
-# https://developer.android.com/topic/libraries/support-library/androidx-rn
-android.useAndroidX=true
-# Kotlin code style for this project: "official" or "obsolete":
-kotlin.code.style=official
-# Enables namespacing of each library's R class so that its R class includes only the
-# resources declared in the library itself and none from the library's dependencies,
-# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
+#Sat Sep 09 15:20:53 CEST 2023
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
-#org.gradle.unsafe.configuration-cache=true
-#org.gradle.unsafe.configuration-cache-problems=warn
\ No newline at end of file
+android.nonTransitiveRClass=true
+android.useAndroidX=true
+kotlin.code.style=official
+org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -Dfile.encoding\=UTF-8 -XX\:+UseParallelGC
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 63d65f9f..e807ebd8 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue May 30 17:13:41 CEST 2023
+#Sat Sep 09 15:23:57 CEST 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists