diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 51f8212c..7e2ddd98 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ android:roundIcon="@mipmap/app_icon_round" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/Theme.Start.Splash" + android:theme="@style/Splash" android:requestLegacyExternalStorage="true" android:enableOnBackInvokedCallback="true" android:usesCleartextTraffic="false" @@ -33,11 +33,11 @@ tools:ignore="DataExtractionRules"> + android:theme="@style/Splash"> diff --git a/app/src/main/java/ua/acclorite/book_story/Activity.kt b/app/src/main/java/ua/acclorite/book_story/Activity.kt deleted file mode 100644 index dd704c8d..00000000 --- a/app/src/main/java/ua/acclorite/book_story/Activity.kt +++ /dev/null @@ -1,224 +0,0 @@ -package ua.acclorite.book_story - -import android.annotation.SuppressLint -import android.database.CursorWindow -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.ime -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFocusManager -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dagger.hilt.android.AndroidEntryPoint -import ua.acclorite.book_story.presentation.core.components.navigation_bar.NavigationBar -import ua.acclorite.book_story.presentation.core.components.navigation_rail.NavigationRail -import ua.acclorite.book_story.presentation.core.navigation.NavigationHost -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.about.AboutScreenRoot -import ua.acclorite.book_story.presentation.screens.about.nested.credits.CreditsScreenRoot -import ua.acclorite.book_story.presentation.screens.about.nested.license_info.LicenseInfoScreenRoot -import ua.acclorite.book_story.presentation.screens.about.nested.licenses.LicensesScreenRoot -import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreenRoot -import ua.acclorite.book_story.presentation.screens.browse.BrowseScreenRoot -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.help.HelpScreenRoot -import ua.acclorite.book_story.presentation.screens.history.HistoryScreenRoot -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.LibraryScreenRoot -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.screens.reader.ReaderScreenRoot -import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.BrowseSettingsRoot -import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettingsRoot -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettingsRoot -import ua.acclorite.book_story.presentation.screens.start.StartScreenRoot -import ua.acclorite.book_story.presentation.ui.BookStoryTheme -import ua.acclorite.book_story.presentation.ui.Transitions -import ua.acclorite.book_story.presentation.ui.isDark -import ua.acclorite.book_story.presentation.ui.isPureDark -import java.lang.reflect.Field - - -@Suppress("unused") -@SuppressLint("DiscouragedPrivateApi") -@AndroidEntryPoint -class Activity : AppCompatActivity() { - // Initializing all required viewModels on app startup - private val mainViewModel: MainViewModel by viewModels() - private val libraryViewModel: LibraryViewModel by viewModels() - private val historyViewModel: HistoryViewModel by viewModels() - private val browseViewModel: BrowseViewModel by viewModels() - private val settingsViewModel: SettingsViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // New Cursor size for Room - try { - val field: Field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize") - field.isAccessible = true - field.set(null, 100 * 1024 * 1024) - } catch (e: Exception) { - e.printStackTrace() - } - - // Initializing all variables - mainViewModel.onEvent( - MainEvent.OnInit( - libraryViewModelReady = libraryViewModel.isReady, - settingsViewModelReady = settingsViewModel.isReady - ) - ) - - // Splash screen - installSplashScreen().apply { - setKeepOnScreenCondition { - !mainViewModel.isReady.value - } - } - - // Edge to edge - enableEdgeToEdge() - WindowCompat.setDecorFitsSystemWindows(window, false) - - setContent { - val state = MainViewModel.getState() - val isLoaded = mainViewModel.isReady.collectAsStateWithLifecycle() - - val density = LocalDensity.current - val imeInsets = WindowInsets.ime - val focusManager = LocalFocusManager.current - val isKeyboardVisible by remember { - derivedStateOf { - imeInsets.getBottom(density) > 0 - } - } - - LaunchedEffect(isKeyboardVisible) { - if (!isKeyboardVisible) { - focusManager.clearFocus() - } - } - - if (isLoaded.value) { - BookStoryTheme( - theme = state.value.theme, - isDark = state.value.darkTheme.isDark(), - isPureDark = state.value.pureDark.isPureDark(this), - themeContrast = state.value.themeContrast - ) { - NavigationHost( - startScreen = if (state.value.showStartScreen) Screen.Start - else Screen.Library - ) { - navigation( - screens = arrayOf( - Screen.Library.getRoute(), - Screen.History.getRoute(), - Screen.Browse.getRoute() - ), - bottomBar = { - NavigationBar() - }, - navigationRail = { - NavigationRail() - } - ) { - // Library - composable( - enterAnim = Transitions.FadeTransitionIn, - exitAnim = Transitions.FadeTransitionOut - ) { - LibraryScreenRoot() - } - - // History - composable( - enterAnim = Transitions.FadeTransitionIn, - exitAnim = Transitions.FadeTransitionOut - ) { - HistoryScreenRoot() - } - - // Browse - composable( - enterAnim = Transitions.FadeTransitionIn, - exitAnim = Transitions.FadeTransitionOut - ) { - BrowseScreenRoot() - } - } - - // Book Info - composable { - BookInfoScreenRoot(it) - } - - // Reader - composable { - ReaderScreenRoot(it) - } - - // Settings - composable { - SettingsScreenRoot() - } - - // Nested settings categories - composable { - GeneralSettingsRoot() - } - composable { - AppearanceSettingsRoot() - } - composable { - ReaderSettingsRoot() - } - composable { - BrowseSettingsRoot() - } - - // About screen - composable { - AboutScreenRoot() - } - - // Nested about categories - composable { - LicensesScreenRoot() - } - composable { - LicenseInfoScreenRoot(it) - } - composable { - CreditsScreenRoot() - } - - // Help screen - composable { - HelpScreenRoot(it) - } - - // Start screen - composable(enterAnim = Transitions.FadeTransitionIn) { - StartScreenRoot() - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/converter/ChapterConverter.kt b/app/src/main/java/ua/acclorite/book_story/data/local/converter/ChapterConverter.kt index 249b7d4d..32af0a13 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/converter/ChapterConverter.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/converter/ChapterConverter.kt @@ -4,7 +4,7 @@ import androidx.room.TypeConverter import com.google.common.reflect.TypeToken import com.google.gson.Gson import com.google.gson.JsonSyntaxException -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.reader.Chapter class ChapterConverter { @TypeConverter diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt index 620470c7..fcacf462 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt @@ -5,8 +5,8 @@ import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverters import ua.acclorite.book_story.data.local.converter.ChapterConverter -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.reader.Chapter @Entity @TypeConverters(ChapterConverter::class) diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt index 358807a3..d3e95fa7 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapper.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.data.mapper.book import ua.acclorite.book_story.data.local.dto.BookEntity -import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.library.book.Book interface BookMapper { suspend fun toBookEntity(book: Book): BookEntity diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt index fcb3944a..7eaba3ab 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt @@ -3,8 +3,8 @@ package ua.acclorite.book_story.data.mapper.book import android.net.Uri import ua.acclorite.book_story.R import ua.acclorite.book_story.data.local.dto.BookEntity -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.ui.UIText import javax.inject.Inject class BookMapperImpl @Inject constructor() : BookMapper { diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapper.kt index 5f552807..c02b9627 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapper.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapper.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.data.mapper.color_preset import ua.acclorite.book_story.data.local.dto.ColorPresetEntity -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset interface ColorPresetMapper { suspend fun toColorPresetEntity(colorPreset: ColorPreset, order: Int): ColorPresetEntity diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapperImpl.kt index 86a6e12a..dd4ee5fd 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapperImpl.kt @@ -2,7 +2,7 @@ package ua.acclorite.book_story.data.mapper.color_preset import androidx.compose.ui.graphics.Color import ua.acclorite.book_story.data.local.dto.ColorPresetEntity -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import javax.inject.Inject class ColorPresetMapperImpl @Inject constructor() : ColorPresetMapper { diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt index 2d607770..649a3195 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapper.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.data.mapper.history import ua.acclorite.book_story.data.local.dto.HistoryEntity -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History interface HistoryMapper { suspend fun toHistoryEntity(history: History): HistoryEntity diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt index d8cab3d1..292685e7 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.data.mapper.history import ua.acclorite.book_story.data.local.dto.HistoryEntity -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History import javax.inject.Inject class HistoryMapperImpl @Inject constructor() : HistoryMapper { diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt index e599de97..3959e094 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.data.parser -import ua.acclorite.book_story.domain.model.BookWithCover +import ua.acclorite.book_story.domain.library.book.BookWithCover import java.io.File diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt index 005189ed..b2cbaade 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt @@ -6,7 +6,7 @@ import ua.acclorite.book_story.data.parser.fb2.Fb2FileParser import ua.acclorite.book_story.data.parser.html.HtmlFileParser import ua.acclorite.book_story.data.parser.pdf.PdfFileParser import ua.acclorite.book_story.data.parser.txt.TxtFileParser -import ua.acclorite.book_story.domain.model.BookWithCover +import ua.acclorite.book_story.domain.library.book.BookWithCover import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt index ec9d35ac..0a466927 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.data.parser -import ua.acclorite.book_story.domain.model.ChapterWithText +import ua.acclorite.book_story.domain.reader.ChapterWithText import ua.acclorite.book_story.domain.util.Resource import java.io.File diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt index 93fb034f..df5536a4 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt @@ -7,9 +7,9 @@ import ua.acclorite.book_story.data.parser.fb2.Fb2TextParser import ua.acclorite.book_story.data.parser.html.HtmlTextParser import ua.acclorite.book_story.data.parser.pdf.PdfTextParser import ua.acclorite.book_story.data.parser.txt.TxtTextParser -import ua.acclorite.book_story.domain.model.ChapterWithText +import ua.acclorite.book_story.domain.reader.ChapterWithText +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import java.io.File import javax.inject.Inject @@ -70,12 +70,4 @@ class TextParserImpl @Inject constructor( } } } -} - - - - - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index 23362fcf..4550f5f7 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -7,10 +7,10 @@ import kotlinx.coroutines.withContext import org.jsoup.Jsoup import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.FileParser -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.BookWithCover -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.BookWithCover +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.ui.UIText import java.io.File import java.util.zip.ZipFile import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt index 8a1dde9f..62e0180f 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt @@ -15,10 +15,10 @@ import org.jsoup.Jsoup import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.TextParser -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.domain.model.ChapterWithText +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.ChapterWithText +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.core.util.addAll import ua.acclorite.book_story.presentation.core.util.clearMarkdown import java.io.File diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt index edc70bb8..1e587a92 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt @@ -6,10 +6,10 @@ import org.w3c.dom.Document import org.w3c.dom.Element import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.FileParser -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.BookWithCover -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.BookWithCover +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.ui.UIText import java.io.File import javax.inject.Inject import javax.xml.parsers.DocumentBuilderFactory diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt index cb3d409d..3797373e 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt @@ -8,10 +8,10 @@ import org.w3c.dom.Element import org.w3c.dom.NodeList import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.TextParser -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.domain.model.ChapterWithText +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.ChapterWithText +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt index 4f332e7e..b536d264 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt @@ -3,10 +3,10 @@ package ua.acclorite.book_story.data.parser.html import org.jsoup.Jsoup import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.FileParser -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.BookWithCover -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.BookWithCover +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.ui.UIText import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt index 9d22641b..5148b0b0 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt @@ -6,10 +6,10 @@ import org.jsoup.Jsoup import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.TextParser -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.domain.model.ChapterWithText +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.ChapterWithText +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt index 305f8849..ca08a01a 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt @@ -5,10 +5,10 @@ import com.tom_roush.pdfbox.android.PDFBoxResourceLoader import com.tom_roush.pdfbox.pdmodel.PDDocument import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.FileParser -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.BookWithCover -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.BookWithCover +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.ui.UIText import java.io.File import javax.inject.Inject @@ -27,7 +27,7 @@ class PdfFileParser @Inject constructor( if (isNullOrBlank()) UIText.StringResource(R.string.unknown_author) else UIText.StringValue(this) } - val description = document.documentInformation.subject ?: null + val description = document.documentInformation.subject document.close() diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt index 6e4abcec..fe499ee3 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt @@ -8,10 +8,10 @@ import com.tom_roush.pdfbox.text.PDFTextStripper import kotlinx.coroutines.yield import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.TextParser -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.domain.model.ChapterWithText +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.ChapterWithText +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt index cd344fdd..b374d5c6 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt @@ -2,10 +2,10 @@ package ua.acclorite.book_story.data.parser.txt import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.FileParser -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.BookWithCover -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.BookWithCover +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.ui.UIText import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt index acb11db4..216ed8a9 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt @@ -6,10 +6,10 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.TextParser -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.domain.model.ChapterWithText +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.ChapterWithText +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import java.io.BufferedReader import java.io.File diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt index 30a5f265..640a86c1 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt @@ -16,14 +16,14 @@ import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.MarkdownParser import ua.acclorite.book_story.data.parser.TextParser -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.BookWithText -import ua.acclorite.book_story.domain.model.BookWithTextAndCover -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.BookWithText +import ua.acclorite.book_story.domain.library.book.BookWithTextAndCover +import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.repository.BookRepository +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import java.io.BufferedReader import java.io.ByteArrayOutputStream import java.io.File diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/ColorPresetRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/ColorPresetRepositoryImpl.kt index ea8da082..f475593b 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/ColorPresetRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/ColorPresetRepositoryImpl.kt @@ -2,7 +2,7 @@ package ua.acclorite.book_story.data.repository import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.repository.ColorPresetRepository import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/DataStoreRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/DataStoreRepositoryImpl.kt index 32987ab6..5f3bcdd4 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/DataStoreRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/DataStoreRepositoryImpl.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.domain.repository.DataStoreRepository -import ua.acclorite.book_story.presentation.data.MainState +import ua.acclorite.book_story.ui.main.MainState import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt index c4bb2f9c..3ecf67b8 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt @@ -7,14 +7,14 @@ import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.TextParser -import ua.acclorite.book_story.domain.model.BookWithTextAndCover -import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.domain.model.NullableBook.NotNull -import ua.acclorite.book_story.domain.model.NullableBook.Null -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.domain.library.book.BookWithTextAndCover +import ua.acclorite.book_story.domain.library.book.NullableBook +import ua.acclorite.book_story.domain.library.book.NullableBook.NotNull +import ua.acclorite.book_story.domain.library.book.NullableBook.Null import ua.acclorite.book_story.domain.repository.FileSystemRepository +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideSupportedExtensions import java.io.File diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt index 16f3970c..5df0c4b5 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/HistoryRepositoryImpl.kt @@ -2,7 +2,7 @@ package ua.acclorite.book_story.data.repository import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.mapper.history.HistoryMapper -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.repository.HistoryRepository import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Badge.kt b/app/src/main/java/ua/acclorite/book_story/domain/about/Badge.kt similarity index 88% rename from app/src/main/java/ua/acclorite/book_story/domain/model/Badge.kt rename to app/src/main/java/ua/acclorite/book_story/domain/about/Badge.kt index db28da0b..9c40db99 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Badge.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/about/Badge.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.about import androidx.annotation.DrawableRes import androidx.annotation.StringRes diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Credit.kt b/app/src/main/java/ua/acclorite/book_story/domain/about/Credit.kt similarity index 64% rename from app/src/main/java/ua/acclorite/book_story/domain/model/Credit.kt rename to app/src/main/java/ua/acclorite/book_story/domain/about/Credit.kt index b6fa387f..aac327be 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Credit.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/about/Credit.kt @@ -1,7 +1,7 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.about import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.ui.UIText @Immutable data class Credit( @@ -9,4 +9,4 @@ data class Credit( val source: String?, val credits: List, val website: String? -) +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseFilesStructure.kt b/app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseFilesStructure.kt similarity index 73% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseFilesStructure.kt rename to app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseFilesStructure.kt index cee79da9..1210b148 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseFilesStructure.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseFilesStructure.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.data +package ua.acclorite.book_story.domain.browse import androidx.compose.runtime.Immutable diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseLayout.kt b/app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseLayout.kt similarity index 61% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseLayout.kt rename to app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseLayout.kt index c3f051f7..f6c0c61b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseLayout.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.data +package ua.acclorite.book_story.domain.browse enum class BrowseLayout { LIST, GRID diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseSortOrder.kt b/app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseSortOrder.kt similarity index 71% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseSortOrder.kt rename to app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseSortOrder.kt index db1462f2..2553ae92 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/data/BrowseSortOrder.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/browse/BrowseSortOrder.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.data +package ua.acclorite.book_story.domain.browse enum class BrowseSortOrder { NAME, diff --git a/app/src/main/java/ua/acclorite/book_story/domain/browse/FileWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/domain/browse/FileWithTitle.kt new file mode 100644 index 00000000..7473e20c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/browse/FileWithTitle.kt @@ -0,0 +1,11 @@ +package ua.acclorite.book_story.domain.browse + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.ui.UIText +import java.io.File + +@Immutable +data class FileWithTitle( + val title: UIText, + val file: File +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/SelectableFile.kt b/app/src/main/java/ua/acclorite/book_story/domain/browse/SelectableFile.kt similarity index 86% rename from app/src/main/java/ua/acclorite/book_story/domain/model/SelectableFile.kt rename to app/src/main/java/ua/acclorite/book_story/domain/browse/SelectableFile.kt index aba649de..5218918a 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/SelectableFile.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/browse/SelectableFile.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.browse import androidx.compose.runtime.Immutable import ua.acclorite.book_story.domain.util.Selected diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/HelpTip.kt b/app/src/main/java/ua/acclorite/book_story/domain/help/HelpTip.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/domain/model/HelpTip.kt rename to app/src/main/java/ua/acclorite/book_story/domain/help/HelpTip.kt index 5367a87c..7eef0854 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/HelpTip.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/help/HelpTip.kt @@ -1,13 +1,12 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.help import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.text.AnnotatedString -import ua.acclorite.book_story.domain.util.OnNavigate @Immutable data class HelpTip( @StringRes val title: Int, - val description: @Composable AnnotatedString.Builder.(onNavigate: OnNavigate, fromStart: Boolean) -> Unit + val description: @Composable AnnotatedString.Builder.() -> Unit ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/GroupedHistory.kt b/app/src/main/java/ua/acclorite/book_story/domain/history/GroupedHistory.kt similarity index 74% rename from app/src/main/java/ua/acclorite/book_story/domain/model/GroupedHistory.kt rename to app/src/main/java/ua/acclorite/book_story/domain/history/GroupedHistory.kt index 2c51a68c..3822625b 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/GroupedHistory.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/history/GroupedHistory.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.history import androidx.compose.runtime.Immutable diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt b/app/src/main/java/ua/acclorite/book_story/domain/history/History.kt similarity index 60% rename from app/src/main/java/ua/acclorite/book_story/domain/model/History.kt rename to app/src/main/java/ua/acclorite/book_story/domain/history/History.kt index dca5300c..b518fbb9 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/history/History.kt @@ -1,6 +1,7 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.history import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.library.book.Book @Immutable data class History( diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/Book.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/Book.kt new file mode 100644 index 00000000..e37703c2 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/Book.kt @@ -0,0 +1,31 @@ +package ua.acclorite.book_story.domain.library.book + +import android.net.Uri +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.ui.UIText + +@Parcelize +@Immutable +data class Book( + val id: Int = 0, + + val title: String, + val author: UIText, + val description: String?, + + val textPath: String, + val filePath: String, + val coverImage: Uri?, + + val scrollIndex: Int, + val scrollOffset: Int, + val progress: Float, + val chapters: List = emptyList(), + + val lastOpened: Long?, + val category: Category, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithCover.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithCover.kt new file mode 100644 index 00000000..1e23fe60 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithCover.kt @@ -0,0 +1,10 @@ +package ua.acclorite.book_story.domain.library.book + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.util.CoverImage + +@Immutable +data class BookWithCover( + val book: Book, + val coverImage: CoverImage? +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithText.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithText.kt new file mode 100644 index 00000000..55251007 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithText.kt @@ -0,0 +1,9 @@ +package ua.acclorite.book_story.domain.library.book + +import androidx.compose.runtime.Immutable + +@Immutable +data class BookWithText( + val book: Book, + val text: List +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithTextAndCover.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithTextAndCover.kt new file mode 100644 index 00000000..a7bf3bc1 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/BookWithTextAndCover.kt @@ -0,0 +1,11 @@ +package ua.acclorite.book_story.domain.library.book + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.util.CoverImage + +@Immutable +data class BookWithTextAndCover( + val book: Book, + val coverImage: CoverImage?, + val text: List +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/NullableBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/NullableBook.kt new file mode 100644 index 00000000..6b769c81 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/NullableBook.kt @@ -0,0 +1,28 @@ +package ua.acclorite.book_story.domain.library.book + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.ui.UIText + +@Immutable +sealed class NullableBook( + val bookWithTextAndCover: BookWithTextAndCover?, + val fileName: String?, + val message: UIText? +) { + class NotNull( + bookWithTextAndCover: BookWithTextAndCover + ) : NullableBook( + bookWithTextAndCover = bookWithTextAndCover, + fileName = null, + message = null + ) + + class Null( + fileName: String, + message: UIText? + ) : NullableBook( + bookWithTextAndCover = null, + fileName = fileName, + message = message + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/SelectableBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/SelectableBook.kt new file mode 100644 index 00000000..7d2046c9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/SelectableBook.kt @@ -0,0 +1,10 @@ +package ua.acclorite.book_story.domain.library.book + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.util.Selected + +@Immutable +data class SelectableBook( + val data: Book, + val selected: Selected +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/SelectableNullableBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/SelectableNullableBook.kt new file mode 100644 index 00000000..c228e5c7 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/SelectableNullableBook.kt @@ -0,0 +1,10 @@ +package ua.acclorite.book_story.domain.library.book + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.util.Selected + +@Immutable +data class SelectableNullableBook( + val data: NullableBook, + val selected: Selected +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/category/Category.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/category/Category.kt new file mode 100644 index 00000000..f3c1deb4 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/category/Category.kt @@ -0,0 +1,11 @@ +package ua.acclorite.book_story.domain.library.category + +import androidx.compose.runtime.Immutable + +@Immutable +enum class Category { + READING, + ALREADY_READ, + PLANNING, + DROPPED +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/category/CategoryWithBooks.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/category/CategoryWithBooks.kt new file mode 100644 index 00000000..ed312377 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/category/CategoryWithBooks.kt @@ -0,0 +1,10 @@ +package ua.acclorite.book_story.domain.library.category + +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.ui.UIText + +data class CategoryWithBooks( + val category: Category, + val title: UIText, + val books: List +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt deleted file mode 100644 index 99d1e240..00000000 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt +++ /dev/null @@ -1,70 +0,0 @@ -package ua.acclorite.book_story.domain.model - -import android.net.Uri -import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.util.CoverImage -import ua.acclorite.book_story.domain.util.UIText - -@Immutable -data class Book( - val id: Int = 0, - - val title: String, - val author: UIText, - val description: String?, - - val textPath: String, - val filePath: String, - val coverImage: Uri?, - - val scrollIndex: Int, - val scrollOffset: Int, - val progress: Float, - val chapters: List = emptyList(), - - val lastOpened: Long?, - val category: Category, -) - -@Immutable -data class BookWithText( - val book: Book, - val text: List -) - -@Immutable -data class BookWithCover( - val book: Book, - val coverImage: CoverImage? -) - -@Immutable -data class BookWithTextAndCover( - val book: Book, - val coverImage: CoverImage?, - val text: List -) - -@Immutable -sealed class NullableBook( - val bookWithTextAndCover: BookWithTextAndCover?, - val fileName: String?, - val message: UIText? -) { - class NotNull( - bookWithTextAndCover: BookWithTextAndCover - ) : NullableBook( - bookWithTextAndCover = bookWithTextAndCover, - fileName = null, - message = null - ) - - class Null( - fileName: String, - message: UIText? - ) : NullableBook( - bookWithTextAndCover = null, - fileName = fileName, - message = message - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt deleted file mode 100644 index 0f8083bf..00000000 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Category.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ua.acclorite.book_story.domain.model - -import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.util.Selected - -@Immutable -enum class Category { - READING, ALREADY_READ, PLANNING, DROPPED -} - -@Immutable -data class CategorizedBooks( - val category: Category, - val books: List> -) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/ReaderLine.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/ReaderLine.kt deleted file mode 100644 index cd86cd85..00000000 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/ReaderLine.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ua.acclorite.book_story.domain.model - -import android.os.Parcelable -import androidx.compose.runtime.Immutable -import kotlinx.parcelize.Parcelize - -@Parcelize -@Immutable -data class ReaderLine( - val line: String -) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/navigator/Navigator.kt b/app/src/main/java/ua/acclorite/book_story/domain/navigator/Navigator.kt new file mode 100644 index 00000000..89bc0e37 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/navigator/Navigator.kt @@ -0,0 +1,76 @@ +package ua.acclorite.book_story.domain.navigator + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlin.collections.plus + +@HiltViewModel(assistedFactory = Navigator.Factory::class) +class Navigator @AssistedInject constructor( + private val savedStateHandle: SavedStateHandle, + @Assisted private val initialScreen: Screen +) : ViewModel() { + + val items = savedStateHandle.getStateFlow("items", mutableListOf(initialScreen)) + private fun StateFlow>.removeLast() { + savedStateHandle["items"] = value.dropLast(1) + } + + private fun StateFlow>.add(item: Screen) { + savedStateHandle["items"] = value + item + } + + val lastItem = items.map { + it.lastOrNull() ?: initialScreen + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = initialScreen + ) + + val lastEvent = savedStateHandle.getStateFlow("stack_event", StackEvent.Default) + private fun StateFlow.change(stackEvent: StackEvent) { + savedStateHandle["stack_event"] = stackEvent + } + + + fun push( + targetScreen: Screen, + popping: Boolean = false, + saveInBackStack: Boolean = true + ) { + if (lastItem.value::class == targetScreen::class) return + if (!saveInBackStack) items.removeLast() + + lastEvent.change( + if (popping) StackEvent.Pop + else StackEvent.Default + ) + + if (lastItem.value::class == targetScreen::class) items.removeLast() + items.add(targetScreen) + } + + fun pop(popping: Boolean = true) { + if (items.value.count() > 1) { + lastEvent.change( + if (popping) StackEvent.Pop + else StackEvent.Default + ) + items.removeLast() + } + } + + @AssistedFactory + interface Factory { + fun create(startScreen: Screen): Navigator + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt b/app/src/main/java/ua/acclorite/book_story/domain/navigator/NavigatorItem.kt similarity index 68% rename from app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt rename to app/src/main/java/ua/acclorite/book_story/domain/navigator/NavigatorItem.kt index a2aa9518..a2861d1a 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/NavigationItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/navigator/NavigatorItem.kt @@ -1,12 +1,13 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.navigator import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.presentation.core.navigation.Screen +import kotlinx.serialization.Serializable +@Serializable @Immutable -data class NavigationItem( +data class NavigatorItem( val screen: Screen, @StringRes val title: Int, @StringRes val tooltip: Int, diff --git a/app/src/main/java/ua/acclorite/book_story/domain/navigator/Screen.kt b/app/src/main/java/ua/acclorite/book_story/domain/navigator/Screen.kt new file mode 100644 index 00000000..043f1662 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/navigator/Screen.kt @@ -0,0 +1,8 @@ +package ua.acclorite.book_story.domain.navigator + +import androidx.compose.runtime.Composable + +interface Screen { + @Composable + fun Content() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/navigator/StackEvent.kt b/app/src/main/java/ua/acclorite/book_story/domain/navigator/StackEvent.kt new file mode 100644 index 00000000..5e66da5c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/navigator/StackEvent.kt @@ -0,0 +1,10 @@ +package ua.acclorite.book_story.domain.navigator + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +enum class StackEvent { + Default, Pop +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Chapter.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/Chapter.kt similarity index 61% rename from app/src/main/java/ua/acclorite/book_story/domain/model/Chapter.kt rename to app/src/main/java/ua/acclorite/book_story/domain/reader/Chapter.kt index 6000471f..63e3f64f 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Chapter.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/Chapter.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.reader import android.os.Parcelable import androidx.compose.runtime.Immutable @@ -11,10 +11,4 @@ data class Chapter( val title: String, val startIndex: Int, val endIndex: Int -) : Parcelable - -@Immutable -data class ChapterWithText( - val chapter: Chapter, - val text: List -) \ No newline at end of file +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/reader/ChapterWithText.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/ChapterWithText.kt new file mode 100644 index 00000000..5922b322 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/ChapterWithText.kt @@ -0,0 +1,9 @@ +package ua.acclorite.book_story.domain.reader + +import androidx.compose.runtime.Immutable + +@Immutable +data class ChapterWithText( + val chapter: Chapter, + val text: List +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/reader/Checkpoint.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/Checkpoint.kt new file mode 100644 index 00000000..af33c309 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/Checkpoint.kt @@ -0,0 +1,9 @@ +package ua.acclorite.book_story.domain.reader + +import androidx.compose.runtime.Immutable + +@Immutable +data class Checkpoint( + val index: Int, + val offset: Int +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/ColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/ColorPreset.kt similarity index 86% rename from app/src/main/java/ua/acclorite/book_story/domain/model/ColorPreset.kt rename to app/src/main/java/ua/acclorite/book_story/domain/reader/ColorPreset.kt index 484c46b7..2c1e9e5d 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/ColorPreset.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/ColorPreset.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.reader import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/FontWithName.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/FontWithName.kt similarity index 67% rename from app/src/main/java/ua/acclorite/book_story/domain/model/FontWithName.kt rename to app/src/main/java/ua/acclorite/book_story/domain/reader/FontWithName.kt index fd1fef61..4c2e4d01 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/FontWithName.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/FontWithName.kt @@ -1,8 +1,8 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.reader import androidx.compose.runtime.Immutable import androidx.compose.ui.text.font.FontFamily -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.ui.UIText @Immutable data class FontWithName( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/data/ReaderScreenOrientation.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderScreenOrientation.kt similarity index 87% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/data/ReaderScreenOrientation.kt rename to app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderScreenOrientation.kt index fc26f4b0..c04b1566 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/data/ReaderScreenOrientation.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderScreenOrientation.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.data +package ua.acclorite.book_story.domain.reader import android.content.pm.ActivityInfo diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/data/ReaderTextAlignment.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderTextAlignment.kt similarity index 67% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/data/ReaderTextAlignment.kt rename to app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderTextAlignment.kt index 5aa06c4e..1e1ba915 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/data/ReaderTextAlignment.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderTextAlignment.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.data +package ua.acclorite.book_story.domain.reader enum class ReaderTextAlignment { START, JUSTIFY, CENTER, END diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt index 6387ea98..9748292d 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt @@ -1,10 +1,10 @@ package ua.acclorite.book_story.domain.repository import androidx.compose.ui.text.AnnotatedString -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.BookWithText -import ua.acclorite.book_story.domain.model.BookWithTextAndCover -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.BookWithText +import ua.acclorite.book_story.domain.library.book.BookWithTextAndCover +import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.Resource diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/ColorPresetRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/ColorPresetRepository.kt index ba7e48c0..2657de1f 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/ColorPresetRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/ColorPresetRepository.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.repository -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset interface ColorPresetRepository { diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/DataStoreRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/DataStoreRepository.kt index ae2d1758..99bbf9bf 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/DataStoreRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/DataStoreRepository.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.domain.repository import androidx.datastore.preferences.core.Preferences -import ua.acclorite.book_story.presentation.data.MainState +import ua.acclorite.book_story.ui.main.MainState interface DataStoreRepository { diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt index 74a2d4a7..213ae41c 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.domain.repository -import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.domain.library.book.NullableBook import java.io.File interface FileSystemRepository { diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/HistoryRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/HistoryRepository.kt index 8e4f7f5f..89d4bb5b 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/HistoryRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/HistoryRepository.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.repository -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History interface HistoryRepository { diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/ButtonItem.kt b/app/src/main/java/ua/acclorite/book_story/domain/ui/ButtonItem.kt similarity index 83% rename from app/src/main/java/ua/acclorite/book_story/domain/model/ButtonItem.kt rename to app/src/main/java/ua/acclorite/book_story/domain/ui/ButtonItem.kt index c5f5d9ea..bd5b43ea 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/ButtonItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/ui/ButtonItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.domain.model +package ua.acclorite.book_story.domain.ui import androidx.compose.runtime.Immutable import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/UIText.kt b/app/src/main/java/ua/acclorite/book_story/domain/ui/UIText.kt similarity index 95% rename from app/src/main/java/ua/acclorite/book_story/domain/util/UIText.kt rename to app/src/main/java/ua/acclorite/book_story/domain/ui/UIText.kt index 9acc0778..9c8cd18b 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/UIText.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/ui/UIText.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.domain.util +package ua.acclorite.book_story.domain.ui import android.content.Context import android.os.Parcelable diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CheckForTextUpdate.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CheckForTextUpdate.kt index 29c82fb7..1e5e961a 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CheckForTextUpdate.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/CheckForTextUpdate.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.util.Resource import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/DeleteBooks.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/DeleteBooks.kt index 896eaca7..6d104f66 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/DeleteBooks.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/DeleteBooks.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.HistoryRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBookById.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBookById.kt index 6e18eb89..8e5347c0 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBookById.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBookById.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.repository.BookRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooks.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooks.kt index eb0c39b0..76dff81c 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooks.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooks.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.repository.BookRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooksById.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooksById.kt index 3a71574f..a1b5a727 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooksById.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/GetBooksById.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.repository.BookRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt index 8dde9166..87e6d4e0 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.BookWithTextAndCover +import ua.acclorite.book_story.domain.library.book.BookWithTextAndCover import ua.acclorite.book_story.domain.repository.BookRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBook.kt index 1be96a20..490290d5 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBook.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.repository.BookRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBookWithText.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBookWithText.kt index 4640b08d..270b21ef 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBookWithText.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateBookWithText.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.BookWithText +import ua.acclorite.book_story.domain.library.book.BookWithText import ua.acclorite.book_story.domain.repository.BookRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageOfBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageOfBook.kt index 549141f7..41e40f01 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageOfBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/UpdateCoverImageOfBook.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.book -import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.util.CoverImage import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/DeleteColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/DeleteColorPreset.kt index fe53563d..7f7c1e20 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/DeleteColorPreset.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/DeleteColorPreset.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.color_preset -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.repository.ColorPresetRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/GetColorPresets.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/GetColorPresets.kt index bbc5d09e..75dee1d8 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/GetColorPresets.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/GetColorPresets.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.color_preset -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.repository.ColorPresetRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/ReorderColorPresets.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/ReorderColorPresets.kt index fafba519..e0aa8061 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/ReorderColorPresets.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/ReorderColorPresets.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.color_preset -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.repository.ColorPresetRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/SelectColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/SelectColorPreset.kt index f26c8a6f..d1a24521 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/SelectColorPreset.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/SelectColorPreset.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.color_preset -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.repository.ColorPresetRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/UpdateColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/UpdateColorPreset.kt index f571706a..2fd9eae2 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/UpdateColorPreset.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/color_preset/UpdateColorPreset.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.color_preset -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.repository.ColorPresetRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/data_store/GetAllSettings.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/data_store/GetAllSettings.kt index 505345cb..548b5c3a 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/data_store/GetAllSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/data_store/GetAllSettings.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.domain.use_case.data_store import ua.acclorite.book_story.domain.repository.DataStoreRepository -import ua.acclorite.book_story.presentation.data.MainState +import ua.acclorite.book_story.ui.main.MainState import javax.inject.Inject class GetAllSettings @Inject constructor( diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt index 529885d0..c4f8ae88 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.file_system -import ua.acclorite.book_story.domain.model.NullableBook +import ua.acclorite.book_story.domain.library.book.NullableBook import ua.acclorite.book_story.domain.repository.FileSystemRepository import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFilesFromDevice.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFilesFromDevice.kt index 1dad257e..01edeaff 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFilesFromDevice.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFilesFromDevice.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.file_system -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile import ua.acclorite.book_story.domain.repository.FileSystemRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/DeleteHistory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/DeleteHistory.kt index b23bc93d..8b1ac36e 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/DeleteHistory.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/DeleteHistory.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.history -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.repository.HistoryRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistory.kt index 1bfddaee..5dfbe096 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistory.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetHistory.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.history -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.repository.HistoryRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetLatestHistory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetLatestHistory.kt index e61f7819..68d9eac8 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetLatestHistory.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/GetLatestHistory.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.history -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.repository.HistoryRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/InsertHistory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/InsertHistory.kt index 94a6bd38..4dc746dd 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/InsertHistory.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/history/InsertHistory.kt @@ -1,6 +1,6 @@ package ua.acclorite.book_story.domain.use_case.history -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.repository.HistoryRepository import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/Resource.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/Resource.kt index 22a2f162..8e02c3ff 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/Resource.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/Resource.kt @@ -1,5 +1,7 @@ package ua.acclorite.book_story.domain.util +import ua.acclorite.book_story.domain.ui.UIText + sealed class Resource(val data: T? = null, val message: UIText? = null) { class Success(data: T?) : Resource(data) class Error(message: UIText, data: T? = null) : Resource(data, message) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/Typealias.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/Typealias.kt index 305e8b5b..cb7c344c 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/Typealias.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/Typealias.kt @@ -1,10 +1,10 @@ package ua.acclorite.book_story.domain.util import android.graphics.Bitmap -import ua.acclorite.book_story.presentation.core.navigation.Navigator typealias CoverImage = Bitmap typealias Selected = Boolean typealias ID = Int -typealias Route = String -typealias OnNavigate = (Navigator.() -> Unit) -> Unit +typealias BottomSheet = String +typealias Dialog = String +typealias Drawer = String \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/UIViewModel.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/UIViewModel.kt deleted file mode 100644 index ac82b684..00000000 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/UIViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ua.acclorite.book_story.domain.util - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.flow.StateFlow - - -abstract class UIViewModel : ViewModel() { - abstract val state: StateFlow - abstract fun onEvent(event: E) - - companion object { - @Composable - inline fun , S, E> getState(): State { - return hiltViewModel().state.collectAsStateWithLifecycle() - } - - @Composable - inline fun , S, E> getEvent(): (E) -> Unit { - return hiltViewModel()::onEvent - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/badges/AboutBadgeItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutBadgeItem.kt similarity index 83% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/badges/AboutBadgeItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/about/AboutBadgeItem.kt index 2687aca9..3909fa8d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/badges/AboutBadgeItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutBadgeItem.kt @@ -1,19 +1,13 @@ -package ua.acclorite.book_story.presentation.screens.about.components.badges +package ua.acclorite.book_story.presentation.about import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.Badge +import ua.acclorite.book_story.domain.about.Badge import ua.acclorite.book_story.presentation.core.components.common.IconButton -/** - * About Badge Item. - * - * @param badge [Badge]. - * @param onClick OnClick callback. - */ @Composable fun AboutBadgeItem( badge: Badge, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/badges/AboutBadges.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutBadges.kt similarity index 62% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/badges/AboutBadges.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/about/AboutBadges.kt index b097266f..b90451b9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/badges/AboutBadges.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutBadges.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.about.components.badges +package ua.acclorite.book_story.presentation.about import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -10,24 +10,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideAboutBadges import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent -import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel +import ua.acclorite.book_story.ui.about.AboutEvent -/** - * About Badges. - * Links all ways to contact me. - * - * @param verticalPadding Vertical item padding. - */ @Composable -fun AboutBadges(verticalPadding: Dp = 18.dp) { - val onEvent = AboutViewModel.getEvent() +fun AboutBadges( + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit +) { val context = LocalContext.current Box( @@ -36,11 +29,14 @@ fun AboutBadges(verticalPadding: Dp = 18.dp) { ) { LazyRow( Modifier - .padding(horizontal = 18.dp, vertical = verticalPadding), + .padding(18.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(13.dp) ) { - items(Constants.provideAboutBadges(), key = { it.id }) { badge -> + items( + Constants.provideAboutBadges(), + key = { it.id } + ) { badge -> AboutBadgeItem(badge = badge) { when (badge.id) { "tryzub" -> { @@ -50,14 +46,10 @@ fun AboutBadges(verticalPadding: Dp = 18.dp) { else -> { badge.url?.let { - onEvent( + navigateToBrowserPage( AboutEvent.OnNavigateToBrowserPage( - it, - context, - noAppsFound = { - context.getString(R.string.error_no_browser) - .showToast(context = context, longToast = false) - } + page = it, + context = context ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutContent.kt new file mode 100644 index 00000000..f7ddca2d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutContent.kt @@ -0,0 +1,43 @@ +package ua.acclorite.book_story.presentation.about + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.about.AboutEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutContent( + scrollBehavior: TopAppBarScrollBehavior, + dialog: Dialog? = null, + updateLoading: Boolean, + updateInfo: LatestReleaseInfo?, + listState: LazyListState, + checkForUpdate: (AboutEvent.OnCheckForUpdate) -> Unit, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + dismissDialog: (AboutEvent.OnDismissDialog) -> Unit, + navigateToLicenses: () -> Unit, + navigateToCredits: () -> Unit, + navigateBack: () -> Unit +) { + AboutDialog( + dialog = dialog, + updateInfo = updateInfo, + navigateToBrowserPage = navigateToBrowserPage, + dismissDialog = dismissDialog + ) + + AboutScaffold( + scrollBehavior = scrollBehavior, + updateLoading = updateLoading, + listState = listState, + checkForUpdate = checkForUpdate, + navigateToBrowserPage = navigateToBrowserPage, + navigateToLicenses = navigateToLicenses, + navigateToCredits = navigateToCredits, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutDialog.kt new file mode 100644 index 00000000..82d29a1a --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutDialog.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.about + +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.about.AboutEvent +import ua.acclorite.book_story.ui.about.AboutScreen + +@Composable +fun AboutDialog( + dialog: Dialog? = null, + updateInfo: LatestReleaseInfo?, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + dismissDialog: (AboutEvent.OnDismissDialog) -> Unit +) { + when (dialog) { + AboutScreen.UPDATE_DIALOG -> { + AboutUpdateDialog( + updateInfo = updateInfo, + navigateToBrowserPage = navigateToBrowserPage, + dismissDialog = dismissDialog + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutItem.kt similarity index 81% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/about/AboutItem.kt index 1c9a2b26..56a37baa 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.about.components +package ua.acclorite.book_story.presentation.about import androidx.compose.animation.core.EaseInOut import androidx.compose.animation.core.tween @@ -21,20 +21,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.presentation.core.components.progress_indicator.CircularProgressIndicator -import ua.acclorite.book_story.presentation.ui.FadeTransitionPreservingSpace +import ua.acclorite.book_story.ui.theme.FadeTransitionPreservingSpace -/** - * About Screen Item. - * Clickable item, that has title, description and loading state. - * - * @param modifier [Modifier]. - * @param title Title of the item. - * @param description Description of the item. - * @param verticalPadding Vertical item padding. - * @param showLoading Whether loading indicator is shown. - * @param isOnClickEnabled Whether this item is clickable. - * @param onClick OnClick callback. - */ @Composable fun AboutItem( modifier: Modifier = Modifier, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutLayout.kt new file mode 100644 index 00000000..3b437fb3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutLayout.kt @@ -0,0 +1,181 @@ +package ua.acclorite.book_story.presentation.about + +import androidx.compose.foundation.layout.Box +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.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideContributorsPage +import ua.acclorite.book_story.presentation.core.constants.provideIssuesPage +import ua.acclorite.book_story.presentation.core.constants.provideReleasesPage +import ua.acclorite.book_story.presentation.core.constants.provideTranslationPage +import ua.acclorite.book_story.ui.about.AboutEvent + +@Composable +fun AboutLayout( + updateLoading: Boolean, + paddingValues: PaddingValues, + listState: LazyListState, + checkForUpdate: (AboutEvent.OnCheckForUpdate) -> Unit, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + navigateToLicenses: () -> Unit, + navigateToCredits: () -> Unit +) { + val context = LocalContext.current + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + item { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(id = R.drawable.app_icon), + contentDescription = stringResource(id = R.string.app_icon_content_desc), + modifier = Modifier + .padding(14.dp) + .size(120.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + item { + Spacer(modifier = Modifier.height(36.dp)) + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.outlineVariant + ) + } + + item { + AboutItem( + title = stringResource(id = R.string.app_version_option), + description = buildAnnotatedString { + append( + stringResource( + id = R.string.app_version_option_desc_1, + context.getString(R.string.app_version) + ) + ) + append("\n") + append(stringResource(id = R.string.app_version_option_desc_2)) + }, + showLoading = updateLoading + ) { + checkForUpdate( + AboutEvent.OnCheckForUpdate( + context = context + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.report_bug_option), + description = null + ) { + navigateToBrowserPage( + AboutEvent.OnNavigateToBrowserPage( + page = Constants.provideIssuesPage(), + context = context + ) + ) + } + } + + + item { + AboutItem( + title = stringResource(id = R.string.contributors_option), + description = null + ) { + navigateToBrowserPage( + AboutEvent.OnNavigateToBrowserPage( + page = Constants.provideContributorsPage(), + context = context + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.whats_new_option), + description = null + ) { + navigateToBrowserPage( + AboutEvent.OnNavigateToBrowserPage( + page = Constants.provideReleasesPage(), + context = context + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.licenses_option), + description = null + ) { + navigateToLicenses() + } + } + + item { + AboutItem( + title = stringResource(id = R.string.credits_option), + description = null + ) { + navigateToCredits() + } + } + + item { + AboutItem( + title = stringResource(id = R.string.help_translate_option), + description = null + ) { + navigateToBrowserPage( + AboutEvent.OnNavigateToBrowserPage( + page = Constants.provideTranslationPage(), + context = context + ) + ) + } + } + + item { + AboutBadges( + navigateToBrowserPage = navigateToBrowserPage + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutScaffold.kt new file mode 100644 index 00000000..7ed8933b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutScaffold.kt @@ -0,0 +1,52 @@ +package ua.acclorite.book_story.presentation.about + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import ua.acclorite.book_story.ui.about.AboutEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutScaffold( + scrollBehavior: TopAppBarScrollBehavior, + updateLoading: Boolean, + listState: LazyListState, + checkForUpdate: (AboutEvent.OnCheckForUpdate) -> Unit, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + navigateToLicenses: () -> Unit, + navigateToCredits: () -> Unit, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + AboutTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + AboutLayout( + updateLoading = updateLoading, + paddingValues = paddingValues, + listState = listState, + checkForUpdate = checkForUpdate, + navigateToBrowserPage = navigateToBrowserPage, + navigateToLicenses = navigateToLicenses, + navigateToCredits = navigateToCredits + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutTopBar.kt new file mode 100644 index 00000000..8d612b3b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutTopBar.kt @@ -0,0 +1,33 @@ +package ua.acclorite.book_story.presentation.about + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.about_screen)) + }, + navigationIcon = { + NavigatorBackIconButton(navigateBack = navigateBack) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutUpdateDialog.kt new file mode 100644 index 00000000..0c46ddbe --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/about/AboutUpdateDialog.kt @@ -0,0 +1,44 @@ +package ua.acclorite.book_story.presentation.about + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Update +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideReleasesPage +import ua.acclorite.book_story.ui.about.AboutEvent + +@Composable +fun AboutUpdateDialog( + updateInfo: LatestReleaseInfo?, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + dismissDialog: (AboutEvent.OnDismissDialog) -> Unit +) { + val context = LocalContext.current + + Dialog( + title = stringResource(id = R.string.update_query, updateInfo?.tagName ?: ""), + imageVectorIcon = Icons.Default.Update, + description = stringResource( + id = R.string.update_app_description + ), + actionText = stringResource(id = R.string.proceed), + actionEnabled = true, + disableOnClick = false, + onDismiss = { dismissDialog(AboutEvent.OnDismissDialog) }, + onAction = { + navigateToBrowserPage( + AboutEvent.OnNavigateToBrowserPage( + page = Constants.provideReleasesPage(), + context = context + ) + ) + dismissDialog(AboutEvent.OnDismissDialog) + }, + withDivider = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoBackHandler.kt new file mode 100644 index 00000000..00dee2fa --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoBackHandler.kt @@ -0,0 +1,46 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.ui.book_info.BookInfoEvent + +@Composable +fun BookInfoBackHandler( + editTitle: Boolean, + editAuthor: Boolean, + editDescription: Boolean, + isUpdating: Boolean, + editTitleMode: (BookInfoEvent.OnEditTitleMode) -> Unit, + editAuthorMode: (BookInfoEvent.OnEditAuthorMode) -> Unit, + editDescriptionMode: (BookInfoEvent.OnEditDescriptionMode) -> Unit, + cancelTextUpdate: (BookInfoEvent.OnCancelTextUpdate) -> Unit, + navigateBack: () -> Unit +) { + BackHandler { + var exitedEditMode = false + + if (editTitle) { + editTitleMode(BookInfoEvent.OnEditTitleMode(false)) + exitedEditMode = true + } + + if (editAuthor) { + editAuthorMode(BookInfoEvent.OnEditAuthorMode(false)) + exitedEditMode = true + } + + if (editDescription) { + editDescriptionMode(BookInfoEvent.OnEditDescriptionMode(false)) + exitedEditMode = true + } + + if (exitedEditMode) { + return@BackHandler + } + + if (!isUpdating) { + cancelTextUpdate(BookInfoEvent.OnCancelTextUpdate) + navigateBack() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoBottomSheet.kt new file mode 100644 index 00000000..6a064ed3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoBottomSheet.kt @@ -0,0 +1,57 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.ui.book_info.BookInfoEvent +import ua.acclorite.book_story.ui.book_info.BookInfoScreen + +@Composable +fun BookInfoBottomSheet( + bottomSheet: BottomSheet?, + book: Book, + snackbarState: SnackbarHostState, + canResetCover: Boolean, + changeCover: (BookInfoEvent.OnChangeCover) -> Unit, + resetCover: (BookInfoEvent.OnResetCover) -> Unit, + deleteCover: (BookInfoEvent.OnDeleteCover) -> Unit, + checkCoverReset: (BookInfoEvent.OnCheckCoverReset) -> Unit, + copyToClipboard: (BookInfoEvent.OnCopyToClipboard) -> Unit, + showDetailsBottomSheet: (BookInfoEvent.OnShowDetailsBottomSheet) -> Unit, + showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit, + showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit, + dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit +) { + when (bottomSheet) { + BookInfoScreen.CHANGE_COVER_BOTTOM_SHEET -> { + BookInfoChangeCoverBottomSheet( + book = book, + canResetCover = canResetCover, + changeCover = changeCover, + resetCover = resetCover, + deleteCover = deleteCover, + checkCoverReset = checkCoverReset, + dismissBottomSheet = dismissBottomSheet + ) + } + + BookInfoScreen.MORE_BOTTOM_SHEET -> { + BookInfoMoreBottomSheet( + snackbarState = snackbarState, + showDetailsBottomSheet = showDetailsBottomSheet, + showDeleteDialog = showDeleteDialog, + showMoveDialog = showMoveDialog, + dismissBottomSheet = dismissBottomSheet + ) + } + + BookInfoScreen.DETAILS_BOTTOM_SHEET -> { + BookInfoDetailsBottomSheet( + book = book, + copyToClipboard = copyToClipboard, + dismissBottomSheet = dismissBottomSheet + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoChangeCoverBottomSheet.kt similarity index 54% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoChangeCoverBottomSheet.kt index 081ff32a..d817b6ab 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoChangeCoverBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet +package ua.acclorite.book_story.presentation.book_info import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -18,74 +18,58 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +import ua.acclorite.book_story.ui.book_info.BookInfoEvent -/** - * Change cover bottom sheet. Lets user select photo from the gallery and replaces old one. - */ @Composable -fun BookInfoChangeCoverBottomSheet() { - val state = BookInfoViewModel.getState() - val onEvent = BookInfoViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() +fun BookInfoChangeCoverBottomSheet( + book: Book, + canResetCover: Boolean, + changeCover: (BookInfoEvent.OnChangeCover) -> Unit, + resetCover: (BookInfoEvent.OnResetCover) -> Unit, + deleteCover: (BookInfoEvent.OnDeleteCover) -> Unit, + checkCoverReset: (BookInfoEvent.OnCheckCoverReset) -> Unit, + dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit +) { val context = LocalContext.current val photoPicker = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), onResult = { uri -> if (uri != null) { - onEvent( + changeCover( BookInfoEvent.OnChangeCover( - uri, - context, - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - } + uri = uri, + context = context ) ) - context.getString(R.string.cover_image_changed) - .showToast(context = context) } } ) LaunchedEffect(Unit) { - onEvent(BookInfoEvent.OnCheckCoverReset) + checkCoverReset(BookInfoEvent.OnCheckCoverReset) } ModalBottomSheet( modifier = Modifier.fillMaxWidth(), onDismissRequest = { - onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet) + dismissBottomSheet(BookInfoEvent.OnDismissBottomSheet) }, sheetGesturesEnabled = true ) { LazyColumn(modifier = Modifier.fillMaxWidth()) { - if (state.value.canResetCover) { + if (canResetCover) { item { BookInfoChangeCoverBottomSheetItem( icon = Icons.Default.Restore, text = stringResource(id = R.string.reset_cover), description = stringResource(id = R.string.reset_cover_desc) ) { - onEvent( - BookInfoEvent.OnResetCoverImage( - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - }, - showResult = { - it.asString(context).showToast(context = context) - } + resetCover( + BookInfoEvent.OnResetCover( + context = context ) ) } @@ -104,23 +88,18 @@ fun BookInfoChangeCoverBottomSheet() { } } - if (state.value.book.coverImage != null) { + if (book.coverImage != null) { item { BookInfoChangeCoverBottomSheetItem( icon = Icons.Default.HideImage, text = stringResource(id = R.string.delete_cover), description = stringResource(id = R.string.delete_cover_desc) ) { - onEvent( + deleteCover( BookInfoEvent.OnDeleteCover( - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - } + context = context ) ) - context.getString(R.string.cover_image_deleted) - .showToast(context = context) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoChangeCoverBottomSheetItem.kt similarity index 81% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoChangeCoverBottomSheetItem.kt index de2b5cdb..1c5eeb6a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoChangeCoverBottomSheetItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet +package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,25 +17,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** - * Change cover bottom sheet item. - * - * @param icon Icon. - * @param text Text. - * @param description Description of the item. - * @param verticalPadding Vertical item padding. - * @param onClick OnClick callback. - */ @Composable fun LazyItemScope.BookInfoChangeCoverBottomSheetItem( icon: ImageVector, text: String, description: String, - verticalPadding: Dp = 12.dp, onClick: () -> Unit ) { Row( @@ -45,7 +34,7 @@ fun LazyItemScope.BookInfoChangeCoverBottomSheetItem( .clickable { onClick() } - .padding(vertical = verticalPadding, horizontal = 18.dp), + .padding(vertical = 12.dp, horizontal = 18.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoContent.kt new file mode 100644 index 00000000..25fec44c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoContent.kt @@ -0,0 +1,134 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.book_info.BookInfoEvent + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BookInfoContent( + book: Book, + bottomSheet: BottomSheet?, + dialog: Dialog?, + refreshState: PullRefreshState, + listState: LazyListState, + snackbarState: SnackbarHostState, + canResetCover: Boolean, + isUpdating: Boolean, + checkingForUpdate: Boolean, + editTitle: Boolean, + titleValue: String, + editAuthor: Boolean, + authorValue: String, + editDescription: Boolean, + descriptionValue: String, + editTitleMode: (BookInfoEvent.OnEditTitleMode) -> Unit, + editTitleValueChange: (BookInfoEvent.OnEditTitleValueChange) -> Unit, + editTitleRequestFocus: (BookInfoEvent.OnEditTitleRequestFocus) -> Unit, + editAuthorMode: (BookInfoEvent.OnEditAuthorMode) -> Unit, + editAuthorValueChange: (BookInfoEvent.OnEditAuthorValueChange) -> Unit, + editAuthorRequestFocus: (BookInfoEvent.OnEditAuthorRequestFocus) -> Unit, + editDescriptionMode: (BookInfoEvent.OnEditDescriptionMode) -> Unit, + editDescriptionValueChange: (BookInfoEvent.OnEditDescriptionValueChange) -> Unit, + editDescriptionRequestFocus: (BookInfoEvent.OnEditDescriptionRequestFocus) -> Unit, + showChangeCoverBottomSheet: (BookInfoEvent.OnShowChangeCoverBottomSheet) -> Unit, + showDetailsBottomSheet: (BookInfoEvent.OnShowDetailsBottomSheet) -> Unit, + showMoreBottomSheet: (BookInfoEvent.OnShowMoreBottomSheet) -> Unit, + showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit, + showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit, + actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit, + actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, + changeCover: (BookInfoEvent.OnChangeCover) -> Unit, + resetCover: (BookInfoEvent.OnResetCover) -> Unit, + deleteCover: (BookInfoEvent.OnDeleteCover) -> Unit, + checkCoverReset: (BookInfoEvent.OnCheckCoverReset) -> Unit, + actionUpdateDialog: (BookInfoEvent.OnActionUpdateDialog) -> Unit, + dismissUpdateDialog: (BookInfoEvent.OnDismissUpdateDialog) -> Unit, + dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, + dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit, + updateData: (BookInfoEvent.OnUpdateData) -> Unit, + copyToClipboard: (BookInfoEvent.OnCopyToClipboard) -> Unit, + checkForTextUpdate: (BookInfoEvent.OnCheckForTextUpdate) -> Unit, + cancelTextUpdate: (BookInfoEvent.OnCancelTextUpdate) -> Unit, + navigateToReader: () -> Unit, + navigateToLibrary: () -> Unit, + navigateBack: () -> Unit +) { + BookInfoDialog( + dialog = dialog, + book = book, + snackbarState = snackbarState, + actionDeleteDialog = actionDeleteDialog, + actionMoveDialog = actionMoveDialog, + dismissDialog = dismissDialog, + actionUpdateDialog = actionUpdateDialog, + dismissUpdateDialog = dismissUpdateDialog, + navigateToLibrary = navigateToLibrary, + navigateBack = navigateBack + ) + + BookInfoBottomSheet( + bottomSheet = bottomSheet, + book = book, + snackbarState = snackbarState, + canResetCover = canResetCover, + changeCover = changeCover, + resetCover = resetCover, + deleteCover = deleteCover, + checkCoverReset = checkCoverReset, + copyToClipboard = copyToClipboard, + showDetailsBottomSheet = showDetailsBottomSheet, + showMoveDialog = showMoveDialog, + showDeleteDialog = showDeleteDialog, + dismissBottomSheet = dismissBottomSheet + ) + + BookInfoScaffold( + book = book, + refreshState = refreshState, + listState = listState, + snackbarState = snackbarState, + isUpdating = isUpdating, + checkingForUpdate = checkingForUpdate, + editTitle = editTitle, + titleValue = titleValue, + editAuthor = editAuthor, + authorValue = authorValue, + editDescription = editDescription, + descriptionValue = descriptionValue, + editTitleMode = editTitleMode, + editTitleValueChange = editTitleValueChange, + editTitleRequestFocus = editTitleRequestFocus, + editAuthorMode = editAuthorMode, + editAuthorValueChange = editAuthorValueChange, + editAuthorRequestFocus = editAuthorRequestFocus, + editDescriptionMode = editDescriptionMode, + editDescriptionValueChange = editDescriptionValueChange, + editDescriptionRequestFocus = editDescriptionRequestFocus, + showChangeCoverBottomSheet = showChangeCoverBottomSheet, + showMoreBottomSheet = showMoreBottomSheet, + updateData = updateData, + checkForTextUpdate = checkForTextUpdate, + cancelTextUpdate = cancelTextUpdate, + navigateToReader = navigateToReader, + navigateBack = navigateBack + ) + + BookInfoBackHandler( + editTitle = editTitle, + editAuthor = editAuthor, + editDescription = editDescription, + isUpdating = isUpdating, + editTitleMode = editTitleMode, + editAuthorMode = editAuthorMode, + editDescriptionMode = editDescriptionMode, + cancelTextUpdate = cancelTextUpdate, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDeleteDialog.kt new file mode 100644 index 00000000..aa30e2bc --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDeleteDialog.kt @@ -0,0 +1,39 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.ui.book_info.BookInfoEvent + +@Composable +fun BookInfoDeleteDialog( + actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit, + dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, + navigateBack: () -> Unit +) { + val context = LocalContext.current + + Dialog( + title = stringResource(id = R.string.delete_book), + imageVectorIcon = Icons.Outlined.DeleteOutline, + description = stringResource( + id = R.string.delete_book_description + ), + actionText = stringResource(id = R.string.delete), + onDismiss = { dismissDialog(BookInfoEvent.OnDismissDialog) }, + withDivider = false, + actionEnabled = true, + onAction = { + actionDeleteDialog( + BookInfoEvent.OnActionDeleteDialog( + context = context, + navigateBack = navigateBack + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheet.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheet.kt index abb2bff0..8dfc7415 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet +package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth @@ -9,34 +9,32 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel +import ua.acclorite.book_story.ui.book_info.BookInfoEvent import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * Details bottom sheet. Displays name, path, last opened and size of the file. - */ @Composable -fun BookInfoDetailsBottomSheet() { - val state = BookInfoViewModel.getState() - val onEvent = BookInfoViewModel.getEvent() +fun BookInfoDetailsBottomSheet( + book: Book, + copyToClipboard: (BookInfoEvent.OnCopyToClipboard) -> Unit, + dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit +) { val context = LocalContext.current val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) } val lastOpened = remember { - pattern.format(Date(state.value.book.lastOpened ?: 0)) + pattern.format(Date(book.lastOpened ?: 0)) } val sizeBytes = remember { - val file = File(state.value.book.filePath) + val file = File(book.filePath) if (file.exists()) { file.length() } else 0 @@ -58,7 +56,7 @@ fun BookInfoDetailsBottomSheet() { ModalBottomSheet( modifier = Modifier.fillMaxWidth(), onDismissRequest = { - onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet) + dismissBottomSheet(BookInfoEvent.OnDismissBottomSheet) }, sheetGesturesEnabled = true ) { @@ -69,53 +67,44 @@ fun BookInfoDetailsBottomSheet() { item { BookInfoDetailsBottomSheetItem( title = stringResource(id = R.string.file_name), - description = state.value.book.filePath.substringAfterLast("/").trim() + description = book.filePath.substringAfterLast("/").trim() ) { - onEvent( + copyToClipboard( BookInfoEvent.OnCopyToClipboard( - context, - state.value.book.filePath.substringAfterLast("/").trim(), - success = { - context.getString(R.string.copied) - .showToast(context = context, longToast = false) - } - )) + text = book.filePath.substringAfterLast("/").trim(), + context = context + ) + ) } } item { BookInfoDetailsBottomSheetItem( title = stringResource(id = R.string.file_path), - description = state.value.book.filePath.trim() + description = book.filePath.trim() ) { - onEvent( + copyToClipboard( BookInfoEvent.OnCopyToClipboard( - context, - state.value.book.filePath.trim(), - success = { - context.getString(R.string.copied) - .showToast(context = context, longToast = false) - } - )) + text = book.filePath.trim(), + context = context + ) + ) } } item { BookInfoDetailsBottomSheetItem( title = stringResource(id = R.string.file_last_opened), - description = if (state.value.book.lastOpened != null) lastOpened + description = if (book.lastOpened != null) lastOpened else stringResource(id = R.string.never) ) { - if (state.value.book.lastOpened != null) { - onEvent( + if (book.lastOpened != null) { + copyToClipboard( BookInfoEvent.OnCopyToClipboard( - context, - lastOpened, - success = { - context.getString(R.string.copied) - .showToast(context = context, longToast = false) - } - )) + text = lastOpened, + context = context + ) + ) } } } @@ -126,14 +115,10 @@ fun BookInfoDetailsBottomSheet() { description = fileSize.ifBlank { stringResource(id = R.string.unknown) } ) { if (fileSize.isNotBlank()) { - onEvent( + copyToClipboard( BookInfoEvent.OnCopyToClipboard( - context, - fileSize, - success = { - context.getString(R.string.copied) - .showToast(context = context, longToast = false) - } + text = fileSize, + context = context ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheetItem.kt similarity index 83% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheetItem.kt index 6bf9a574..d06f1765 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheetItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet +package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -14,24 +14,18 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 -/** - * Details bottom sheet item. - */ @OptIn(ExperimentalFoundationApi::class) @Composable fun BookInfoDetailsBottomSheetItem( - modifier: Modifier = Modifier, title: String, description: String, - verticalPadding: Dp = 12.dp, onClick: () -> Unit ) { Column( - modifier + Modifier .fillMaxWidth() .combinedClickable( onClick = {}, @@ -39,7 +33,7 @@ fun BookInfoDetailsBottomSheetItem( onClick() } ) - .padding(horizontal = 16.dp, vertical = verticalPadding), + .padding(horizontal = 16.dp, vertical = 12.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDialog.kt new file mode 100644 index 00000000..34e23373 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDialog.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.book_info.BookInfoEvent +import ua.acclorite.book_story.ui.book_info.BookInfoScreen + +@Composable +fun BookInfoDialog( + dialog: Dialog?, + book: Book, + snackbarState: SnackbarHostState, + actionDeleteDialog: (BookInfoEvent.OnActionDeleteDialog) -> Unit, + actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, + dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, + actionUpdateDialog: (BookInfoEvent.OnActionUpdateDialog) -> Unit, + dismissUpdateDialog: (BookInfoEvent.OnDismissUpdateDialog) -> Unit, + navigateBack: () -> Unit, + navigateToLibrary: () -> Unit +) { + when (dialog) { + BookInfoScreen.UPDATE_DIALOG -> { + BookInfoUpdateDialog( + snackbarState = snackbarState, + actionUpdateDialog = actionUpdateDialog, + dismissUpdateDialog = dismissUpdateDialog + ) + } + + BookInfoScreen.DELETE_DIALOG -> { + BookInfoDeleteDialog( + actionDeleteDialog = actionDeleteDialog, + dismissDialog = dismissDialog, + navigateBack = navigateBack + ) + } + + BookInfoScreen.MOVE_DIALOG -> { + BookInfoMoveDialog( + book = book, + actionMoveDialog = actionMoveDialog, + dismissDialog = dismissDialog, + navigateToLibrary = navigateToLibrary + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoFloatingActionButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoFloatingActionButton.kt new file mode 100644 index 00000000..4963dfc3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoFloatingActionButton.kt @@ -0,0 +1,77 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility + +@Composable +fun BoxScope.BookInfoFloatingActionButton( + book: Book, + listState: LazyListState, + isUpdating: Boolean, + navigateToReader: () -> Unit +) { + FloatingActionButton( + onClick = { + if (!isUpdating) { + navigateToReader() + } + }, + shape = MaterialTheme.shapes.large, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + elevation = FloatingActionButtonDefaults.elevation(), + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + content = { + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.width(13.dp)) + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.continue_reading_content_desc), + Modifier.size(24.dp) + ) + AnimatedVisibility( + visible = !listState.canScrollBackward, + enter = fadeIn() + expandHorizontally(), + exit = fadeOut() + shrinkHorizontally() + ) { + Text( + text = stringResource( + if (book.progress == 0f) R.string.start + else R.string.continue_read + ), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + modifier = Modifier.padding(start = 8.dp) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayout.kt new file mode 100644 index 00000000..dcb41c22 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayout.kt @@ -0,0 +1,99 @@ +package ua.acclorite.book_story.presentation.book_info + +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.ui.book_info.BookInfoEvent + +@Composable +fun BookInfoLayout( + book: Book, + listState: LazyListState, + paddingValues: PaddingValues, + editTitle: Boolean, + titleValue: String, + editAuthor: Boolean, + authorValue: String, + editDescription: Boolean, + descriptionValue: String, + editTitleMode: (BookInfoEvent.OnEditTitleMode) -> Unit, + editTitleValueChange: (BookInfoEvent.OnEditTitleValueChange) -> Unit, + editTitleRequestFocus: (BookInfoEvent.OnEditTitleRequestFocus) -> Unit, + editAuthorMode: (BookInfoEvent.OnEditAuthorMode) -> Unit, + editAuthorValueChange: (BookInfoEvent.OnEditAuthorValueChange) -> Unit, + editAuthorRequestFocus: (BookInfoEvent.OnEditAuthorRequestFocus) -> Unit, + editDescriptionMode: (BookInfoEvent.OnEditDescriptionMode) -> Unit, + editDescriptionValueChange: (BookInfoEvent.OnEditDescriptionValueChange) -> Unit, + editDescriptionRequestFocus: (BookInfoEvent.OnEditDescriptionRequestFocus) -> Unit, + showChangeCoverBottomSheet: (BookInfoEvent.OnShowChangeCoverBottomSheet) -> Unit +) { + val backgroundCoverImageHeight = remember { + derivedStateOf { + paddingValues.calculateTopPadding() + 12.dp + 195.dp + } + } + + LazyColumn( + Modifier + .fillMaxSize(), + state = listState + ) { + item { + Box(modifier = Modifier.fillMaxWidth()) { + if (book.coverImage != null) { + BookInfoLayoutBackground( + height = backgroundCoverImageHeight.value, + image = book.coverImage + ) + } + + Column(Modifier.fillMaxWidth()) { + Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 12.dp)) + BookInfoLayoutInfo( + book = book, + editTitle = editTitle, + titleValue = titleValue, + editAuthor = editAuthor, + authorValue = authorValue, + editTitleMode = editTitleMode, + editTitleValueChange = editTitleValueChange, + editTitleRequestFocus = editTitleRequestFocus, + editAuthorMode = editAuthorMode, + editAuthorValueChange = editAuthorValueChange, + editAuthorRequestFocus = editAuthorRequestFocus, + showChangeCoverBottomSheet = showChangeCoverBottomSheet + ) + } + } + } + + item { + Spacer(modifier = Modifier.height(24.dp)) + BookInfoLayoutStatistic(book = book) + } + + item { + Spacer(modifier = Modifier.height(24.dp)) + BookInfoLayoutDescription( + book = book, + editDescription = editDescription, + descriptionValue = descriptionValue, + editDescriptionMode = editDescriptionMode, + editDescriptionValueChange = editDescriptionValueChange, + editDescriptionRequestFocus = editDescriptionRequestFocus + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutBackground.kt similarity index 87% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutBackground.kt index 706bdefe..93717bff 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoBackground.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutBackground.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.book_info import android.net.Uri import androidx.compose.foundation.layout.fillMaxWidth @@ -14,11 +14,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import ua.acclorite.book_story.presentation.core.components.common.AsyncImage -/** - * Background of the BookInfoScreen. - */ @Composable -fun BookInfoBackground(height: Dp, image: Uri) { +fun BookInfoLayoutBackground(height: Dp, image: Uri) { val background = MaterialTheme.colorScheme.surface AsyncImage( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutDescription.kt similarity index 68% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutDescription.kt index 24d7588b..4069ac7c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoDescriptionSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutDescription.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer @@ -22,23 +22,24 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel +import ua.acclorite.book_story.ui.book_info.BookInfoEvent -/** - * Description section. - */ @Composable -fun BookInfoDescriptionSection() { - val state = BookInfoViewModel.getState() - val onEvent = BookInfoViewModel.getEvent() - +fun BookInfoLayoutDescription( + book: Book, + editDescription: Boolean, + descriptionValue: String, + editDescriptionMode: (BookInfoEvent.OnEditDescriptionMode) -> Unit, + editDescriptionValueChange: (BookInfoEvent.OnEditDescriptionValueChange) -> Unit, + editDescriptionRequestFocus: (BookInfoEvent.OnEditDescriptionRequestFocus) -> Unit, +) { val descriptionFocusRequester = remember { FocusRequester() } - if (!state.value.editDescription) { + if (!editDescription) { Text( - if (state.value.book.description?.isNotBlank() == true) state.value.book.description!! + if (book.description?.isNotBlank() == true) book.description else stringResource(id = R.string.error_no_description), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -48,26 +49,30 @@ fun BookInfoDescriptionSection() { .noRippleClickable( onClick = {}, onLongClick = { - onEvent(BookInfoEvent.OnShowHideEditDescription) + editDescriptionMode(BookInfoEvent.OnEditDescriptionMode(true)) } ), ) } else { BasicTextField( - value = state.value.descriptionValue, + value = descriptionValue, textStyle = MaterialTheme.typography.bodyLarge.copy( color = MaterialTheme.colorScheme.onSurface ), onValueChange = { - if (it.length < 5000 || it.length < state.value.authorValue.length) { - onEvent(BookInfoEvent.OnDescriptionValueChange(it)) + if (it.length < 5000 || it.length < descriptionValue.length) { + editDescriptionValueChange(BookInfoEvent.OnEditDescriptionValueChange(it)) } }, modifier = Modifier .padding(horizontal = 24.dp) .focusRequester(descriptionFocusRequester) .onGloballyPositioned { - onEvent(BookInfoEvent.OnDescriptionRequestFocus(descriptionFocusRequester)) + editDescriptionRequestFocus( + BookInfoEvent.OnEditDescriptionRequestFocus( + descriptionFocusRequester + ) + ) }, keyboardOptions = KeyboardOptions(KeyboardCapitalization.Sentences), cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutInfo.kt similarity index 74% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutInfo.kt index 619d174d..8224bd7b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutInfo.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -35,24 +35,32 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.presentation.core.components.common.AsyncImage import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel +import ua.acclorite.book_story.ui.book_info.BookInfoEvent -/** - * BookInfo's Info section. - */ @Composable -fun BookInfoInfoSection() { - val state = BookInfoViewModel.getState() - val onEvent = BookInfoViewModel.getEvent() - +fun BookInfoLayoutInfo( + book: Book, + editTitle: Boolean, + titleValue: String, + editAuthor: Boolean, + authorValue: String, + editTitleMode: (BookInfoEvent.OnEditTitleMode) -> Unit, + editTitleValueChange: (BookInfoEvent.OnEditTitleValueChange) -> Unit, + editTitleRequestFocus: (BookInfoEvent.OnEditTitleRequestFocus) -> Unit, + editAuthorMode: (BookInfoEvent.OnEditAuthorMode) -> Unit, + editAuthorValueChange: (BookInfoEvent.OnEditAuthorValueChange) -> Unit, + editAuthorRequestFocus: (BookInfoEvent.OnEditAuthorRequestFocus) -> Unit, + showChangeCoverBottomSheet: (BookInfoEvent.OnShowChangeCoverBottomSheet) -> Unit +) { val titleFocusRequester = remember { FocusRequester() } val authorFocusRequester = remember { FocusRequester() } Row( - verticalAlignment = Alignment.CenterVertically, modifier = Modifier + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) ) { @@ -68,13 +76,13 @@ fun BookInfoInfoSection() { .noRippleClickable( onClick = {}, onLongClick = { - onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet) + showChangeCoverBottomSheet(BookInfoEvent.OnShowChangeCoverBottomSheet) } ) ) { - if (state.value.book.coverImage != null) { + if (book.coverImage != null) { AsyncImage( - uri = state.value.book.coverImage!!, + uri = book.coverImage, modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.medium) @@ -95,9 +103,9 @@ fun BookInfoInfoSection() { Spacer(modifier = Modifier.width(12.dp)) Column(verticalArrangement = Arrangement.Center) { - if (!state.value.editTitle) { + if (!editTitle) { Text( - state.value.book.title, + book.title, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.headlineSmall, modifier = Modifier @@ -105,7 +113,7 @@ fun BookInfoInfoSection() { .noRippleClickable( onClick = {}, onLongClick = { - onEvent(BookInfoEvent.OnShowHideEditTitle) + editTitleMode(BookInfoEvent.OnEditTitleMode(true)) } ), maxLines = 4, @@ -114,19 +122,23 @@ fun BookInfoInfoSection() { } else { BasicTextField( maxLines = 4, - value = state.value.titleValue, + value = titleValue, textStyle = MaterialTheme.typography.headlineSmall.copy( color = MaterialTheme.colorScheme.onSurface ), onValueChange = { - if (it.length < 100 || it.length < state.value.titleValue.length) { - onEvent(BookInfoEvent.OnTitleValueChange(it)) + if (it.length < 100 || it.length < titleValue.length) { + editTitleValueChange(BookInfoEvent.OnEditTitleValueChange(it)) } }, modifier = Modifier .focusRequester(titleFocusRequester) .onGloballyPositioned { - onEvent(BookInfoEvent.OnTitleRequestFocus(titleFocusRequester)) + editTitleRequestFocus( + BookInfoEvent.OnEditTitleRequestFocus( + titleFocusRequester + ) + ) }, keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words), cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) @@ -150,9 +162,9 @@ fun BookInfoInfoSection() { ) Spacer(modifier = Modifier.width(4.dp)) - if (!state.value.editAuthor) { + if (!editAuthor) { Text( - state.value.book.author.asString(), + book.author.asString(), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge, modifier = Modifier @@ -160,7 +172,7 @@ fun BookInfoInfoSection() { .noRippleClickable( onClick = {}, onLongClick = { - onEvent(BookInfoEvent.OnShowHideEditAuthor) + editAuthorMode(BookInfoEvent.OnEditAuthorMode(true)) } ), maxLines = 1, @@ -169,19 +181,23 @@ fun BookInfoInfoSection() { } else { BasicTextField( singleLine = true, - value = state.value.authorValue, + value = authorValue, textStyle = MaterialTheme.typography.bodyLarge.copy( color = MaterialTheme.colorScheme.onSurfaceVariant ), onValueChange = { - if (it.length < 50 || it.length < state.value.authorValue.length) { - onEvent(BookInfoEvent.OnAuthorValueChange(it)) + if (it.length < 50 || it.length < authorValue.length) { + editAuthorValueChange(BookInfoEvent.OnEditAuthorValueChange(it)) } }, modifier = Modifier .focusRequester(authorFocusRequester) .onGloballyPositioned { - onEvent(BookInfoEvent.OnAuthorRequestFocus(authorFocusRequester)) + editAuthorRequestFocus( + BookInfoEvent.OnEditAuthorRequestFocus( + authorFocusRequester + ) + ) }, keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words), cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutStatistic.kt similarity index 75% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutStatistic.kt index 3b07900d..5d42b26d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoLayoutStatistic.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -11,32 +11,28 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.presentation.core.util.calculateProgress -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel -/** - * Statistic section. - */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun BookInfoStatisticSection() { - val state = BookInfoViewModel.getState() - - val progress by remember { +fun BookInfoLayoutStatistic( + book: Book +) { + val progress = remember(book.progress) { derivedStateOf { - "${state.value.book.progress.calculateProgress(1)}%" + "${book.progress.calculateProgress(1)}%" } } val description = stringResource( - if (state.value.book.progress == 1f) R.string.read_done - else if (state.value.book.progress > 0.2f) R.string.read_keep + if (book.progress == 1f) R.string.read_done + else if (book.progress > 0.2f) R.string.read_keep else R.string.read_more ) @@ -46,14 +42,14 @@ fun BookInfoStatisticSection() { .padding(horizontal = 24.dp) ) { Text( - text = progress, + text = progress.value, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.height(6.dp)) LinearWavyProgressIndicator( - progress = { state.value.book.progress.coerceIn(0f, 1f) }, + progress = { book.progress.coerceIn(0f, 1f) }, trackColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.7f), modifier = Modifier.fillMaxWidth(), amplitude = { 0.5f }, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoreBottomSheet.kt similarity index 64% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoreBottomSheet.kt index dfebc5ad..f2b8a0e6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoreBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components +package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -12,36 +12,33 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.util.Position import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet -import ua.acclorite.book_story.presentation.core.navigation.NavigationItem -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel +import ua.acclorite.book_story.presentation.navigator.NavigatorBottomSheetItem +import ua.acclorite.book_story.ui.book_info.BookInfoEvent -/** - * Book Info More Bottom Sheet. - * Replaces DropDown. - * - * @param snackbarState [SnackbarHostState]. - */ @Composable -fun BookInfoMoreBottomSheet(snackbarState: SnackbarHostState) { - val onEvent = BookInfoViewModel.getEvent() - +fun BookInfoMoreBottomSheet( + snackbarState: SnackbarHostState, + showDetailsBottomSheet: (BookInfoEvent.OnShowDetailsBottomSheet) -> Unit, + showMoveDialog: (BookInfoEvent.OnShowMoveDialog) -> Unit, + showDeleteDialog: (BookInfoEvent.OnShowDeleteDialog) -> Unit, + dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit, +) { ModalBottomSheet( modifier = Modifier.fillMaxWidth(), onDismissRequest = { - onEvent(BookInfoEvent.OnShowHideMoreBottomSheet(false)) + dismissBottomSheet(BookInfoEvent.OnDismissBottomSheet) }, sheetGesturesEnabled = true ) { LazyColumn(modifier = Modifier.fillMaxWidth()) { item { - NavigationItem( + NavigatorBottomSheetItem( title = stringResource(id = R.string.details_book), primary = false, position = Position.SOLO ) { snackbarState.currentSnackbarData?.dismiss() - onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet) + showDetailsBottomSheet(BookInfoEvent.OnShowDetailsBottomSheet) } } @@ -50,22 +47,22 @@ fun BookInfoMoreBottomSheet(snackbarState: SnackbarHostState) { } item { - NavigationItem( + NavigatorBottomSheetItem( title = stringResource(id = R.string.delete_this_book), primary = true, position = Position.TOP ) { - onEvent(BookInfoEvent.OnShowHideDeleteDialog) + showDeleteDialog(BookInfoEvent.OnShowDeleteDialog) } } item { - NavigationItem( + NavigatorBottomSheetItem( title = stringResource(id = R.string.move_this_book), primary = true, position = Position.BOTTOM ) { - onEvent(BookInfoEvent.OnShowHideMoveDialog) + showMoveDialog(BookInfoEvent.OnShowMoveDialog) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoveDialog.kt new file mode 100644 index 00000000..d4e36c15 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoveDialog.kt @@ -0,0 +1,71 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.DriveFileMove +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem +import ua.acclorite.book_story.ui.book_info.BookInfoEvent + +@Composable +fun BookInfoMoveDialog( + book: Book, + actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, + dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, + navigateToLibrary: () -> Unit +) { + val context = LocalContext.current + + val categories = remember { + Category.entries.filter { book.category != it } + } + val selectedCategory = remember { + mutableStateOf(categories[0]) + } + + Dialog( + title = stringResource(id = R.string.move_book), + imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove, + description = stringResource( + id = R.string.move_book_description + ), + actionText = stringResource(id = R.string.move), + actionEnabled = true, + onDismiss = { dismissDialog(BookInfoEvent.OnDismissDialog) }, + onAction = { + actionMoveDialog( + BookInfoEvent.OnActionMoveDialog( + category = selectedCategory.value, + context = context, + navigateToLibrary = navigateToLibrary + ) + ) + }, + withDivider = false, + items = { + items(categories, key = { it.name }) { + val category = when (it) { + Category.READING -> stringResource(id = R.string.reading_tab) + Category.ALREADY_READ -> stringResource(id = R.string.already_read_tab) + Category.PLANNING -> stringResource(id = R.string.planning_tab) + Category.DROPPED -> stringResource(id = R.string.dropped_tab) + } + + SelectableDialogItem( + selected = it == selectedCategory.value, + title = category + ) { + selectedCategory.value = it + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoRefreshIndicator.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoRefreshIndicator.kt new file mode 100644 index 00000000..e9bf6d0b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoRefreshIndicator.kt @@ -0,0 +1,31 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BoxScope.BookInfoRefreshIndicator( + isUpdating: Boolean, + checkingForUpdate: Boolean, + refreshState: PullRefreshState, + paddingValues: PaddingValues +) { + PullRefreshIndicator( + refreshing = isUpdating || checkingForUpdate, + state = refreshState, + Modifier + .align(Alignment.TopCenter) + .padding(top = paddingValues.calculateTopPadding()), + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoScaffold.kt new file mode 100644 index 00000000..8b711e1f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoScaffold.kt @@ -0,0 +1,131 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.presentation.core.components.common.Snackbar +import ua.acclorite.book_story.ui.book_info.BookInfoEvent + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BookInfoScaffold( + book: Book, + refreshState: PullRefreshState, + listState: LazyListState, + snackbarState: SnackbarHostState, + isUpdating: Boolean, + checkingForUpdate: Boolean, + editTitle: Boolean, + titleValue: String, + editAuthor: Boolean, + authorValue: String, + editDescription: Boolean, + descriptionValue: String, + editTitleMode: (BookInfoEvent.OnEditTitleMode) -> Unit, + editTitleValueChange: (BookInfoEvent.OnEditTitleValueChange) -> Unit, + editTitleRequestFocus: (BookInfoEvent.OnEditTitleRequestFocus) -> Unit, + editAuthorMode: (BookInfoEvent.OnEditAuthorMode) -> Unit, + editAuthorValueChange: (BookInfoEvent.OnEditAuthorValueChange) -> Unit, + editAuthorRequestFocus: (BookInfoEvent.OnEditAuthorRequestFocus) -> Unit, + editDescriptionMode: (BookInfoEvent.OnEditDescriptionMode) -> Unit, + editDescriptionValueChange: (BookInfoEvent.OnEditDescriptionValueChange) -> Unit, + editDescriptionRequestFocus: (BookInfoEvent.OnEditDescriptionRequestFocus) -> Unit, + showChangeCoverBottomSheet: (BookInfoEvent.OnShowChangeCoverBottomSheet) -> Unit, + showMoreBottomSheet: (BookInfoEvent.OnShowMoreBottomSheet) -> Unit, + updateData: (BookInfoEvent.OnUpdateData) -> Unit, + checkForTextUpdate: (BookInfoEvent.OnCheckForTextUpdate) -> Unit, + cancelTextUpdate: (BookInfoEvent.OnCancelTextUpdate) -> Unit, + navigateToReader: () -> Unit, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .imePadding() + .windowInsetsPadding(WindowInsets.navigationBars) + .pullRefresh(refreshState), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + BookInfoTopBar( + book = book, + listState = listState, + snackbarState = snackbarState, + isUpdating = isUpdating, + checkingForUpdate = checkingForUpdate, + editTitle = editTitle, + titleValue = titleValue, + editAuthor = editAuthor, + authorValue = authorValue, + editDescription = editDescription, + descriptionValue = descriptionValue, + editTitleMode = editTitleMode, + editAuthorMode = editAuthorMode, + editDescriptionMode = editDescriptionMode, + updateData = updateData, + showMoreBottomSheet = showMoreBottomSheet, + checkForTextUpdate = checkForTextUpdate, + cancelTextUpdate = cancelTextUpdate, + navigateBack = navigateBack + ) + }, + bottomBar = { + Snackbar( + modifier = Modifier.padding(bottom = 70.dp), + snackbarState = snackbarState + ) + } + ) { paddingValues -> + Box(Modifier.fillMaxSize()) { + BookInfoLayout( + book = book, + listState = listState, + paddingValues = paddingValues, + editTitle = editTitle, + titleValue = titleValue, + editAuthor = editAuthor, + authorValue = authorValue, + editDescription = editDescription, + descriptionValue = descriptionValue, + editTitleMode = editTitleMode, + editTitleValueChange = editTitleValueChange, + editTitleRequestFocus = editTitleRequestFocus, + editAuthorMode = editAuthorMode, + editAuthorValueChange = editAuthorValueChange, + editAuthorRequestFocus = editAuthorRequestFocus, + editDescriptionMode = editDescriptionMode, + editDescriptionValueChange = editDescriptionValueChange, + editDescriptionRequestFocus = editDescriptionRequestFocus, + showChangeCoverBottomSheet = showChangeCoverBottomSheet + ) + + BookInfoFloatingActionButton( + book = book, + listState = listState, + isUpdating = isUpdating, + navigateToReader = navigateToReader + ) + + BookInfoRefreshIndicator( + isUpdating = isUpdating, + checkingForUpdate = checkingForUpdate, + refreshState = refreshState, + paddingValues = paddingValues + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoTopBar.kt new file mode 100644 index 00000000..24250882 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoTopBar.kt @@ -0,0 +1,174 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility +import ua.acclorite.book_story.presentation.core.components.common.IconButton +import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBar +import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBarData +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton +import ua.acclorite.book_story.presentation.navigator.NavigatorIconButton +import ua.acclorite.book_story.ui.book_info.BookInfoEvent +import ua.acclorite.book_story.ui.theme.DefaultTransition +import ua.acclorite.book_story.ui.theme.Transitions + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BookInfoTopBar( + book: Book, + listState: LazyListState, + snackbarState: SnackbarHostState, + isUpdating: Boolean, + checkingForUpdate: Boolean, + editTitle: Boolean, + titleValue: String, + editAuthor: Boolean, + authorValue: String, + editDescription: Boolean, + descriptionValue: String, + editTitleMode: (BookInfoEvent.OnEditTitleMode) -> Unit, + editAuthorMode: (BookInfoEvent.OnEditAuthorMode) -> Unit, + editDescriptionMode: (BookInfoEvent.OnEditDescriptionMode) -> Unit, + updateData: (BookInfoEvent.OnUpdateData) -> Unit, + showMoreBottomSheet: (BookInfoEvent.OnShowMoreBottomSheet) -> Unit, + checkForTextUpdate: (BookInfoEvent.OnCheckForTextUpdate) -> Unit, + cancelTextUpdate: (BookInfoEvent.OnCancelTextUpdate) -> Unit, + navigateBack: () -> Unit +) { + val context = LocalContext.current + + val firstVisibleItemIndex = remember { + derivedStateOf { + listState.firstVisibleItemIndex + } + } + + val titleChanged = titleValue.isNotBlank() + && titleValue.trim() != book.title.trim() + val authorChanged = authorValue.isNotBlank() + && authorValue.trim() != book.author.getAsString()?.trim() + val descriptionChanged = descriptionValue.trim() != (book.description?.trim() ?: "") + + TopAppBar( + containerColor = MaterialTheme.colorScheme.surface.copy(0f), + scrollBehavior = null, + isTopBarScrolled = listState.canScrollBackward, + + shownTopBar = 0, + topBars = listOf( + TopAppBarData( + contentID = 0, + contentNavigationIcon = { + if ( + editTitle || + editAuthor || + editDescription + ) { + IconButton( + icon = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = R.string.exit_editing_content_desc, + disableOnClick = true + ) { + if (editTitle) { + editTitleMode(BookInfoEvent.OnEditTitleMode(false)) + } + + if (editAuthor) { + editAuthorMode(BookInfoEvent.OnEditAuthorMode(false)) + } + + if (editDescription) { + editDescriptionMode(BookInfoEvent.OnEditDescriptionMode(false)) + } + } + } else { + NavigatorBackIconButton(enabled = !isUpdating) { + cancelTextUpdate(BookInfoEvent.OnCancelTextUpdate) + navigateBack() + } + } + }, + contentTitle = { + DefaultTransition(firstVisibleItemIndex.value > 0 && !editTitle) { + Text( + book.title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + }, + contentActions = { + IconButton( + icon = Icons.Outlined.Refresh, + contentDescription = R.string.refresh_book_content_desc, + disableOnClick = false, + enabled = !isUpdating && !checkingForUpdate + ) { + checkForTextUpdate( + BookInfoEvent.OnCheckForTextUpdate( + snackbarState = snackbarState, + context = context + ) + ) + } + + Box { + DefaultTransition( + visible = !editTitle && + !editAuthor && + !editDescription + ) { + NavigatorIconButton { + showMoreBottomSheet(BookInfoEvent.OnShowMoreBottomSheet) + } + } + AnimatedVisibility( + visible = editTitle || + editAuthor || + editDescription, + enter = Transitions.DefaultTransitionIn, + exit = fadeOut(tween(200)) + ) { + IconButton( + icon = Icons.Outlined.Done, + contentDescription = R.string.apply_changes_content_desc, + disableOnClick = true, + enabled = !isUpdating && + titleChanged || + authorChanged || + descriptionChanged, + color = if ( + titleChanged || + authorChanged || + descriptionChanged + ) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant + ) { + updateData(BookInfoEvent.OnUpdateData) + } + } + } + } + ) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoUpdateDialog.kt new file mode 100644 index 00000000..1666af50 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoUpdateDialog.kt @@ -0,0 +1,40 @@ +package ua.acclorite.book_story.presentation.book_info + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.ui.book_info.BookInfoEvent + +@Composable +fun BookInfoUpdateDialog( + snackbarState: SnackbarHostState, + actionUpdateDialog: (BookInfoEvent.OnActionUpdateDialog) -> Unit, + dismissUpdateDialog: (BookInfoEvent.OnDismissUpdateDialog) -> Unit +) { + val context = LocalContext.current + + Dialog( + title = stringResource(id = R.string.confirm_update), + imageVectorIcon = Icons.Default.Update, + description = stringResource( + id = R.string.confirm_update_description + ), + actionText = stringResource(id = R.string.confirm), + actionEnabled = true, + onDismiss = { dismissUpdateDialog(BookInfoEvent.OnDismissUpdateDialog) }, + onAction = { + actionUpdateDialog( + BookInfoEvent.OnActionUpdateDialog( + snackbarState = snackbarState, + context = context + ) + ) + }, + withDivider = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseAddDialog.kt similarity index 50% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseAddDialog.kt index cc7e1702..489e798f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseAddDialog.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -14,59 +14,42 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithLazyColumn +import ua.acclorite.book_story.domain.library.book.NullableBook +import ua.acclorite.book_story.domain.library.book.SelectableNullableBook +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog import ua.acclorite.book_story.presentation.core.components.progress_indicator.CircularProgressIndicator -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +import ua.acclorite.book_story.ui.browse.BrowseEvent import java.util.UUID -/** - * Adding dialog. - * Adds all selected books to the Library. - */ @Composable -fun BrowseAddingDialog() { +fun BrowseAddDialog( + loadingAddDialog: Boolean, + selectedBooksAddDialog: List, + dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit, + actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit, + selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit, + navigateToLibrary: () -> Unit +) { val context = LocalContext.current - val state = BrowseViewModel.getState() - val onEvent = BrowseViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onNavigate = LocalNavigator.current - - DialogWithLazyColumn( + Dialog( title = stringResource(id = R.string.add_books), imageVectorIcon = Icons.Default.AddChart, description = stringResource(id = R.string.add_books_description), actionText = stringResource(id = R.string.add), - isActionEnabled = !state.value.isBooksLoading && - state.value.selectedBooks.any { it.first is NullableBook.NotNull }, - onDismiss = { onEvent(BrowseEvent.OnAddingDialogDismiss) }, + actionEnabled = !loadingAddDialog && selectedBooksAddDialog.any { it.data is NullableBook.NotNull }, + onDismiss = { dismissAddDialog(BrowseEvent.OnDismissAddDialog) }, onAction = { - onEvent( - BrowseEvent.OnAddBooks( - onNavigate = onNavigate, - resetScroll = { - onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(0)) - onLibraryEvent(LibraryEvent.OnLoadList) - }, - onFailed = { - context.getString(R.string.error_something_went_wrong) - .showToast(context = context) - }, - onSuccess = { - context.getString(R.string.books_added) - .showToast(context = context) - } + actionAddDialog( + BrowseEvent.OnActionAddDialog( + context = context, + navigateToLibrary = navigateToLibrary ) ) }, withDivider = true, items = { - if (state.value.isBooksLoading) { + if (loadingAddDialog) { item { Box( modifier = Modifier @@ -82,16 +65,20 @@ fun BrowseAddingDialog() { } } else { items( - state.value.selectedBooks, - key = { it.first.fileName ?: UUID.randomUUID() } + selectedBooksAddDialog, + key = { it.data.fileName ?: UUID.randomUUID() } ) { book -> - BrowseAddingDialogItem( + BrowseAddDialogItem( result = book ) { if (it) { - onEvent(BrowseEvent.OnSelectBook(book)) + selectAddDialog( + BrowseEvent.OnSelectAddDialog( + book = book + ) + ) } else { - book.first.message?.asString(context) + book.data.message?.asString(context) ?.showToast(context = context) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseAddDialogItem.kt similarity index 79% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseAddDialogItem.kt index 9fed3ddc..b2b1658b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseAddDialogItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -20,19 +20,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.domain.util.Selected +import ua.acclorite.book_story.domain.library.book.NullableBook +import ua.acclorite.book_story.domain.library.book.SelectableNullableBook import ua.acclorite.book_story.presentation.core.components.common.CircularCheckbox -/** - * Adding dialog item. - * - * @param result Result of the file parsing, can be Null or Not Null. - * @param onClick OnClick event(both error/not error). - */ @Composable -fun BrowseAddingDialogItem(result: Pair, onClick: (Boolean) -> Unit) { - if (result.first is NullableBook.NotNull) { +fun BrowseAddDialogItem(result: SelectableNullableBook, onClick: (Boolean) -> Unit) { + if (result.data is NullableBook.NotNull) { Row( Modifier .fillMaxWidth() @@ -46,14 +40,14 @@ fun BrowseAddingDialogItem(result: Pair, onClick: (Boole Modifier.weight(1f) ) { Text( - text = result.first.bookWithTextAndCover!!.book.title, + text = result.data.bookWithTextAndCover!!.book.title, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( - text = result.first.bookWithTextAndCover!!.book.author.asString(), + text = result.data.bookWithTextAndCover.book.author.asString(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -63,7 +57,7 @@ fun BrowseAddingDialogItem(result: Pair, onClick: (Boole Row { Spacer(modifier = Modifier.width(24.dp)) CircularCheckbox( - selected = result.second, + selected = result.selected, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh ) } @@ -86,7 +80,7 @@ fun BrowseAddingDialogItem(result: Pair, onClick: (Boole ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = result.first.fileName!!, + text = result.data.fileName!!, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.error, maxLines = 2, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseBackHandler.kt new file mode 100644 index 00000000..b177a409 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseBackHandler.kt @@ -0,0 +1,35 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.ui.browse.BrowseEvent + +@Composable +fun BrowseBackHandler( + hasSelectedItems: Boolean, + showSearch: Boolean, + inNestedDirectory: Boolean, + searchVisibility: (BrowseEvent.OnSearchVisibility) -> Unit, + clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit, + goBackDirectory: (BrowseEvent.OnGoBackDirectory) -> Unit, + navigateToLibrary: () -> Unit +) { + BackHandler { + if (hasSelectedItems) { + clearSelectedFiles(BrowseEvent.OnClearSelectedFiles) + return@BackHandler + } + + if (showSearch) { + searchVisibility(BrowseEvent.OnSearchVisibility(false)) + return@BackHandler + } + + if (inNestedDirectory) { + goBackDirectory(BrowseEvent.OnGoBackDirectory) + return@BackHandler + } + + navigateToLibrary() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseBottomSheet.kt new file mode 100644 index 00000000..3dabd551 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseBottomSheet.kt @@ -0,0 +1,20 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.ui.browse.BrowseEvent +import ua.acclorite.book_story.ui.browse.BrowseScreen + +@Composable +fun BrowseBottomSheet( + bottomSheet: BottomSheet?, + dismissBottomSheet: (BrowseEvent.OnDismissBottomSheet) -> Unit +) { + when (bottomSheet) { + BrowseScreen.FILTER_BOTTOM_SHEET -> { + BrowseFilterBottomSheet( + dismissBottomSheet = dismissBottomSheet + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt new file mode 100644 index 00000000..3df57b39 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt @@ -0,0 +1,143 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.ui.focus.FocusRequester +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.domain.library.book.SelectableNullableBook +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.browse.BrowseEvent +import java.io.File + +@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class) +@Composable +fun BrowseContent( + files: List, + selectedBooksAddDialog: List, + refreshState: PullRefreshState, + storagePermissionState: PermissionState, + loadingAddDialog: Boolean, + dialog: Dialog?, + bottomSheet: BottomSheet?, + listState: LazyListState, + gridState: LazyGridState, + layout: BrowseLayout, + filesStructure: BrowseFilesStructure, + gridSize: Int, + autoGridSize: Boolean, + includedFilterItems: List, + canScrollBackList: Boolean, + canScrollBackGrid: Boolean, + selectedDirectory: File, + inNestedDirectory: Boolean, + hasSelectedItems: Boolean, + selectedItemsCount: Int, + isRefreshing: Boolean, + isLoading: Boolean, + isError: Boolean, + dialogHidden: Boolean, + filesEmpty: Boolean, + showSearch: Boolean, + searchQuery: String, + hasSearched: Boolean, + focusRequester: FocusRequester, + searchVisibility: (BrowseEvent.OnSearchVisibility) -> Unit, + searchQueryChange: (BrowseEvent.OnSearchQueryChange) -> Unit, + search: (BrowseEvent.OnSearch) -> Unit, + requestFocus: (BrowseEvent.OnRequestFocus) -> Unit, + clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit, + goBackDirectory: (BrowseEvent.OnGoBackDirectory) -> Unit, + selectFiles: (BrowseEvent.OnSelectFiles) -> Unit, + selectFile: (BrowseEvent.OnSelectFile) -> Unit, + permissionCheck: (BrowseEvent.OnPermissionCheck) -> Unit, + updateFavoriteDirectory: (BrowseEvent.OnUpdateFavoriteDirectory) -> Unit, + changeDirectory: (BrowseEvent.OnChangeDirectory) -> Unit, + dismissBottomSheet: (BrowseEvent.OnDismissBottomSheet) -> Unit, + showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit, + actionPermissionDialog: (BrowseEvent.OnActionPermissionDialog) -> Unit, + dismissPermissionDialog: (BrowseEvent.OnDismissPermissionDialog) -> Unit, + showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit, + dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit, + actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit, + selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit, + navigateToLibrary: () -> Unit, + navigateToHelp: () -> Unit, +) { + BrowseDialog( + dialog = dialog, + storagePermissionState = storagePermissionState, + loadingAddDialog = loadingAddDialog, + actionPermissionDialog = actionPermissionDialog, + dismissPermissionDialog = dismissPermissionDialog, + actionAddDialog = actionAddDialog, + dismissAddDialog = dismissAddDialog, + selectedBooksAddDialog = selectedBooksAddDialog, + selectAddDialog = selectAddDialog, + navigateToLibrary = navigateToLibrary + ) + + BrowseBottomSheet( + bottomSheet = bottomSheet, + dismissBottomSheet = dismissBottomSheet + ) + + BrowseScaffold( + files = files, + refreshState = refreshState, + listState = listState, + gridState = gridState, + layout = layout, + filesStructure = filesStructure, + gridSize = gridSize, + autoGridSize = autoGridSize, + includedFilterItems = includedFilterItems, + canScrollBackList = canScrollBackList, + canScrollBackGrid = canScrollBackGrid, + selectedDirectory = selectedDirectory, + inNestedDirectory = inNestedDirectory, + hasSelectedItems = hasSelectedItems, + selectedItemsCount = selectedItemsCount, + isRefreshing = isRefreshing, + dialogHidden = dialogHidden, + showSearch = showSearch, + searchQuery = searchQuery, + hasSearched = hasSearched, + focusRequester = focusRequester, + searchVisibility = searchVisibility, + searchQueryChange = searchQueryChange, + search = search, + requestFocus = requestFocus, + clearSelectedFiles = clearSelectedFiles, + goBackDirectory = goBackDirectory, + selectFiles = selectFiles, + storagePermissionState = storagePermissionState, + isLoading = isLoading, + isError = isError, + filesEmpty = filesEmpty, + permissionCheck = permissionCheck, + selectFile = selectFile, + updateFavoriteDirectory = updateFavoriteDirectory, + changeDirectory = changeDirectory, + showFilterBottomSheet = showFilterBottomSheet, + showAddDialog = showAddDialog, + navigateToHelp = navigateToHelp + ) + + BrowseBackHandler( + hasSelectedItems = hasSelectedItems, + showSearch = showSearch, + inNestedDirectory = inNestedDirectory, + searchVisibility = searchVisibility, + clearSelectedFiles = clearSelectedFiles, + goBackDirectory = goBackDirectory, + navigateToLibrary = navigateToLibrary + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseDialog.kt new file mode 100644 index 00000000..0a592710 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseDialog.kt @@ -0,0 +1,46 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.runtime.Composable +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.library.book.SelectableNullableBook +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.browse.BrowseEvent +import ua.acclorite.book_story.ui.browse.BrowseScreen + + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun BrowseDialog( + dialog: Dialog?, + storagePermissionState: PermissionState, + loadingAddDialog: Boolean, + selectedBooksAddDialog: List, + dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit, + actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit, + selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit, + actionPermissionDialog: (BrowseEvent.OnActionPermissionDialog) -> Unit, + dismissPermissionDialog: (BrowseEvent.OnDismissPermissionDialog) -> Unit, + navigateToLibrary: () -> Unit +) { + when (dialog) { + BrowseScreen.PERMISSION_DIALOG -> { + BrowsePermissionDialog( + storagePermissionState = storagePermissionState, + actionPermissionDialog = actionPermissionDialog, + dismissPermissionDialog = dismissPermissionDialog + ) + } + + BrowseScreen.ADD_DIALOG -> { + BrowseAddDialog( + loadingAddDialog = loadingAddDialog, + selectedBooksAddDialog = selectedBooksAddDialog, + dismissAddDialog = dismissAddDialog, + actionAddDialog = actionAddDialog, + selectAddDialog = selectAddDialog, + navigateToLibrary = navigateToLibrary + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseEmptyPlaceholder.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseEmptyPlaceholder.kt similarity index 58% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseEmptyPlaceholder.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseEmptyPlaceholder.kt index 682dad13..3aef155d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseEmptyPlaceholder.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseEmptyPlaceholder.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable @@ -12,31 +12,23 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility import ua.acclorite.book_story.presentation.core.components.placeholder.EmptyPlaceholder import ua.acclorite.book_story.presentation.core.components.placeholder.ErrorPlaceholder -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.ui.Transitions +import ua.acclorite.book_story.ui.browse.BrowseEvent +import ua.acclorite.book_story.ui.theme.Transitions -/** - * Browse Empty Placeholder. - * Shows error or empty message. - * - * @param isFilesEmpty Whether the list is empty. - * @param storagePermissionState Storage [PermissionState]. - */ @OptIn(ExperimentalPermissionsApi::class) @Composable fun BoxScope.BrowseEmptyPlaceholder( - isFilesEmpty: Boolean, - storagePermissionState: PermissionState + filesEmpty: Boolean, + dialogHidden: Boolean, + isError: Boolean, + isLoading: Boolean, + isRefreshing: Boolean, + storagePermissionState: PermissionState, + permissionCheck: (BrowseEvent.OnPermissionCheck) -> Unit, + navigateToHelp: () -> Unit ) { - val state = BrowseViewModel.getState() - val onEvent = BrowseViewModel.getEvent() - val onNavigate = LocalNavigator.current - AnimatedVisibility( - visible = state.value.isError, + visible = isError, modifier = Modifier.align(Alignment.Center), enter = Transitions.DefaultTransitionIn, exit = Transitions.NoExitAnimation @@ -47,18 +39,20 @@ fun BoxScope.BrowseEmptyPlaceholder( icon = painterResource(id = R.drawable.error), actionTitle = stringResource(id = R.string.grant_permission) ) { - onEvent( - BrowseEvent.OnPermissionCheck(storagePermissionState) + permissionCheck( + BrowseEvent.OnPermissionCheck( + storagePermissionState = storagePermissionState + ) ) } } AnimatedVisibility( - visible = !state.value.isLoading - && isFilesEmpty - && !state.value.isError - && !state.value.requestPermissionDialog - && !state.value.isRefreshing, + visible = !isLoading + && dialogHidden + && filesEmpty + && !isError + && !isRefreshing, modifier = Modifier.align(Alignment.Center), enter = Transitions.DefaultTransitionIn, exit = Transitions.NoExitAnimation @@ -68,9 +62,7 @@ fun BoxScope.BrowseEmptyPlaceholder( icon = painterResource(id = R.drawable.empty_browse), actionTitle = stringResource(id = R.string.get_help), action = { - onNavigate { - navigate(Screen.Help(false)) - } + navigateToHelp() } ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/filter_bottom_sheet/BrowseFilterBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheet.kt similarity index 63% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/filter_bottom_sheet/BrowseFilterBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheet.kt index 5f7448f4..981252ed 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/filter_bottom_sheet/BrowseFilterBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.filter_bottom_sheet +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -6,36 +6,27 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseFilterSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseGeneralSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseSortSubcategory +import ua.acclorite.book_story.presentation.settings.browse.filter.BrowseFilterSubcategory +import ua.acclorite.book_story.presentation.settings.browse.general.BrowseGeneralSubcategory +import ua.acclorite.book_story.presentation.settings.browse.sort.BrowseSortSubcategory +import ua.acclorite.book_story.ui.browse.BrowseEvent + +private var initialPage = 0 -/** - * Browse Filter Bottom Sheet. - * Lets user sort, filter and change appearance of the Browse. - */ @Composable -fun BrowseFilterBottomSheet() { - val state = BrowseViewModel.getState() - val onEvent = BrowseViewModel.getEvent() - - val pagerState = rememberPagerState(state.value.currentPage) { 3 } - - LaunchedEffect(pagerState.currentPage) { - onEvent( - BrowseEvent.OnScrollToFilterPage( - page = pagerState.currentPage, - pagerState = null - ) - ) - } +fun BrowseFilterBottomSheet( + dismissBottomSheet: (BrowseEvent.OnDismissBottomSheet) -> Unit +) { + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState(initialPage) { 3 } + DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } } ModalBottomSheet( hasFixedHeight = true, @@ -44,13 +35,17 @@ fun BrowseFilterBottomSheet() { .fillMaxHeight(0.5f), dragHandle = {}, onDismissRequest = { - onEvent(BrowseEvent.OnShowHideFilterBottomSheet) + dismissBottomSheet(BrowseEvent.OnDismissBottomSheet) }, sheetGesturesEnabled = false ) { BrowseFilterBottomSheetTabRow( - onEvent = onEvent, - pagerState = pagerState + currentPage = pagerState.currentPage, + scrollToPage = { + scope.launch { + pagerState.animateScrollToPage(it) + } + } ) HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheetTabRow.kt new file mode 100644 index 00000000..432b4208 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheetTabRow.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheetTabRow + +@Composable +fun BrowseFilterBottomSheetTabRow( + currentPage: Int, + scrollToPage: (Int) -> Unit +) { + val tabItems = listOf( + stringResource(id = R.string.general_tab), + stringResource(id = R.string.filter_tab), + stringResource(id = R.string.sort_tab) + ) + + ModalBottomSheetTabRow( + selectedTabIndex = currentPage, + tabs = tabItems + ) { index -> + scrollToPage(index) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridDirectoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridDirectoryItem.kt similarity index 94% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridDirectoryItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridDirectoryItem.kt index 63acf75e..12f85584 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridDirectoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridDirectoryItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -22,14 +22,15 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile import ua.acclorite.book_story.presentation.core.components.common.CircularCheckbox import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.ui.DefaultTransition +import ua.acclorite.book_story.ui.theme.DefaultTransition import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -108,7 +109,8 @@ fun BrowseGridDirectoryItem( file.fileOrDirectory.name, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge, - maxLines = 1, + maxLines = 2, + textAlign = TextAlign.Center, lineHeight = 18.sp, overflow = TextOverflow.Ellipsis ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridFileItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridFileItem.kt similarity index 93% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridFileItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridFileItem.kt index 6c2d6e70..14592459 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridFileItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridFileItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -19,13 +19,14 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile import ua.acclorite.book_story.presentation.core.components.common.CircularCheckbox -import ua.acclorite.book_story.presentation.ui.DefaultTransition +import ua.acclorite.book_story.ui.theme.DefaultTransition import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -91,7 +92,8 @@ fun BrowseGridFileItem(file: SelectableFile, hasSelectedFiles: Boolean) { file.fileOrDirectory.name, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge, - maxLines = 1, + maxLines = 2, + textAlign = TextAlign.Center, lineHeight = 18.sp, overflow = TextOverflow.Ellipsis ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridItem.kt similarity index 92% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridItem.kt index e2362748..763b7662 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt similarity index 66% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridLayout.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt index a48d5283..428baad6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/grid/BrowseGridLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt @@ -1,53 +1,55 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.presentation.core.components.common.LazyVerticalGridWithScrollbar import ua.acclorite.book_story.presentation.core.components.common.header -import ua.acclorite.book_story.presentation.screens.browse.components.layout.BrowseItem -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar @Composable fun BrowseGridLayout( gridSize: Int, autoGridSize: Boolean, - filteredFiles: List, + gridState: LazyGridState, + files: List, + hasSelectedFiles: Boolean, onLongItemClick: (SelectableFile) -> Unit, onFavoriteItemClick: (SelectableFile) -> Unit, onItemClick: (SelectableFile) -> Unit, ) { - val state = BrowseViewModel.getState() - - LazyVerticalGrid( + LazyVerticalGridWithScrollbar( columns = if (autoGridSize) GridCells.Adaptive(170.dp) else GridCells.Fixed(gridSize.coerceAtLeast(1)), - state = state.value.gridState, + state = gridState, modifier = Modifier .fillMaxSize(), - contentPadding = PaddingValues(horizontal = 8.dp) + contentPadding = PaddingValues(horizontal = 8.dp), + scrollbarSettings = Constants.providePrimaryScrollbar(false) ) { header { Spacer(modifier = Modifier.height(8.dp)) } items( - filteredFiles, + files, key = { it.fileOrDirectory.path } ) { selectableFile -> BrowseItem( file = selectableFile, modifier = Modifier.animateItem(), layout = BrowseLayout.GRID, - hasSelectedFiles = state.value.selectableFiles.any { it.isSelected }, + hasSelectedFiles = hasSelectedFiles, onLongClick = { onLongItemClick(selectableFile) }, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/BrowseItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseItem.kt similarity index 58% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/BrowseItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseItem.kt index 6709d738..f8fc6ece 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/BrowseItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseItem.kt @@ -1,22 +1,10 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout +package ua.acclorite.book_story.presentation.browse import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import ua.acclorite.book_story.domain.model.SelectableFile -import ua.acclorite.book_story.presentation.screens.browse.components.layout.grid.BrowseGridItem -import ua.acclorite.book_story.presentation.screens.browse.components.layout.list.BrowseListItem -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout -import java.io.File +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.SelectableFile -/** - * Browse list element item. Can be selected. - * - * @param file [File]. - * @param hasSelectedFiles Whether parent list has selected items. - * @param modifier Modifier. - * @param onClick OnClick callback. - * @param onLongClick OnLongClick callback. - */ @Composable fun BrowseItem( layout: BrowseLayout, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt new file mode 100644 index 00000000..ff063267 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt @@ -0,0 +1,47 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.SelectableFile + +@Composable +fun BrowseLayout( + files: List, + hasSelectedFiles: Boolean, + layout: BrowseLayout, + gridSize: Int, + autoGridSize: Boolean, + listState: LazyListState, + gridState: LazyGridState, + onLongItemClick: (SelectableFile) -> Unit, + onFavoriteItemClick: (SelectableFile) -> Unit, + onItemClick: (SelectableFile) -> Unit +) { + when (layout) { + BrowseLayout.LIST -> { + BrowseListLayout( + files = files, + hasSelectedFiles = hasSelectedFiles, + listState = listState, + onLongItemClick = onLongItemClick, + onFavoriteItemClick = onFavoriteItemClick, + onItemClick = onItemClick + ) + } + + BrowseLayout.GRID -> { + BrowseGridLayout( + gridSize = gridSize, + autoGridSize = autoGridSize, + files = files, + hasSelectedFiles = hasSelectedFiles, + gridState = gridState, + onLongItemClick = onLongItemClick, + onFavoriteItemClick = onFavoriteItemClick, + onItemClick = onItemClick + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListDirectoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListDirectoryItem.kt similarity index 95% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListDirectoryItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListDirectoryItem.kt index e6c3c9fd..0e4b21de 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListDirectoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListDirectoryItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.list +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -24,7 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile import java.text.SimpleDateFormat import java.util.Date import java.util.Locale diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListFileItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListFileItem.kt similarity index 96% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListFileItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListFileItem.kt index cf0139f0..395f081b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListFileItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListFileItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.list +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -27,7 +27,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile import java.text.SimpleDateFormat import java.util.Date import java.util.Locale diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListItem.kt similarity index 95% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListItem.kt index e8b33420..fb618ba1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.list +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -24,10 +24,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.SelectableFile import ua.acclorite.book_story.presentation.core.components.common.CircularCheckbox import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.ui.FadeTransitionPreservingSpace +import ua.acclorite.book_story.ui.theme.FadeTransitionPreservingSpace @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt similarity index 60% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListLayout.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt index 41d4b615..d34a8abf 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/list/BrowseListLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt @@ -1,45 +1,46 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout.list +package ua.acclorite.book_story.presentation.browse import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.SelectableFile -import ua.acclorite.book_story.presentation.screens.browse.components.layout.BrowseItem -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar @Composable fun BrowseListLayout( - filteredFiles: List, + files: List, + hasSelectedFiles: Boolean, + listState: LazyListState, onLongItemClick: (SelectableFile) -> Unit, onFavoriteItemClick: (SelectableFile) -> Unit, onItemClick: (SelectableFile) -> Unit, ) { - val state = BrowseViewModel.getState() - - LazyColumn( - state = state.value.listState, - modifier = Modifier - .fillMaxSize() + LazyColumnWithScrollbar( + state = listState, + modifier = Modifier.fillMaxSize(), + scrollbarSettings = Constants.providePrimaryScrollbar(false) ) { item { Spacer(modifier = Modifier.height(8.dp)) } items( - filteredFiles, + files, key = { it.fileOrDirectory.path } ) { selectableFile -> BrowseItem( file = selectableFile, layout = BrowseLayout.LIST, modifier = Modifier.animateItem(), - hasSelectedFiles = state.value.selectableFiles.any { it.isSelected }, + hasSelectedFiles = hasSelectedFiles, onLongClick = { onLongItemClick(selectableFile) }, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowsePermissionDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowsePermissionDialog.kt new file mode 100644 index 00000000..2306b93d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowsePermissionDialog.kt @@ -0,0 +1,47 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.SdStorage +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.ui.browse.BrowseEvent + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun BrowsePermissionDialog( + storagePermissionState: PermissionState, + actionPermissionDialog: (BrowseEvent.OnActionPermissionDialog) -> Unit, + dismissPermissionDialog: (BrowseEvent.OnDismissPermissionDialog) -> Unit +) { + val activity = LocalActivity.current + + Dialog( + title = stringResource(id = R.string.storage_permission), + imageVectorIcon = Icons.Outlined.SdStorage, + description = stringResource(id = R.string.storage_permission_description), + actionText = stringResource(id = R.string.grant), + actionEnabled = true, + disableOnClick = false, + onDismiss = { + dismissPermissionDialog( + BrowseEvent.OnDismissPermissionDialog( + storagePermissionState = storagePermissionState + ) + ) + }, + onAction = { + actionPermissionDialog( + BrowseEvent.OnActionPermissionDialog( + activity = activity, + storagePermissionState = storagePermissionState + ) + ) + }, + withDivider = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseRefreshIndicator.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseRefreshIndicator.kt new file mode 100644 index 00000000..5ea2d14c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseRefreshIndicator.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BoxScope.BrowseRefreshIndicator( + isRefreshing: Boolean, + refreshState: PullRefreshState +) { + PullRefreshIndicator( + isRefreshing, + refreshState, + Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt new file mode 100644 index 00000000..33a06d38 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt @@ -0,0 +1,198 @@ +package ua.acclorite.book_story.presentation.browse + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.browse.BrowseEvent +import ua.acclorite.book_story.ui.theme.DefaultTransition +import java.io.File + +@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class) +@Composable +fun BrowseScaffold( + files: List, + refreshState: PullRefreshState, + storagePermissionState: PermissionState, + listState: LazyListState, + gridState: LazyGridState, + layout: BrowseLayout, + filesStructure: BrowseFilesStructure, + gridSize: Int, + autoGridSize: Boolean, + includedFilterItems: List, + canScrollBackList: Boolean, + canScrollBackGrid: Boolean, + selectedDirectory: File, + inNestedDirectory: Boolean, + hasSelectedItems: Boolean, + selectedItemsCount: Int, + isRefreshing: Boolean, + isLoading: Boolean, + isError: Boolean, + dialogHidden: Boolean, + filesEmpty: Boolean, + showSearch: Boolean, + searchQuery: String, + hasSearched: Boolean, + focusRequester: FocusRequester, + searchVisibility: (BrowseEvent.OnSearchVisibility) -> Unit, + searchQueryChange: (BrowseEvent.OnSearchQueryChange) -> Unit, + search: (BrowseEvent.OnSearch) -> Unit, + requestFocus: (BrowseEvent.OnRequestFocus) -> Unit, + goBackDirectory: (BrowseEvent.OnGoBackDirectory) -> Unit, + clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit, + selectFiles: (BrowseEvent.OnSelectFiles) -> Unit, + selectFile: (BrowseEvent.OnSelectFile) -> Unit, + permissionCheck: (BrowseEvent.OnPermissionCheck) -> Unit, + updateFavoriteDirectory: (BrowseEvent.OnUpdateFavoriteDirectory) -> Unit, + changeDirectory: (BrowseEvent.OnChangeDirectory) -> Unit, + showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit, + showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit, + navigateToHelp: () -> Unit, +) { + val context = LocalActivity.current + + Scaffold( + modifier = Modifier + .fillMaxSize() + .pullRefresh(refreshState), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + BrowseTopBar( + files = files, + layout = layout, + filesStructure = filesStructure, + includedFilterItems = includedFilterItems, + canScrollBackList = canScrollBackList, + canScrollBackGrid = canScrollBackGrid, + selectedDirectory = selectedDirectory, + inNestedDirectory = inNestedDirectory, + hasSelectedItems = hasSelectedItems, + selectedItemsCount = selectedItemsCount, + showSearch = showSearch, + searchQuery = searchQuery, + hasSearched = hasSearched, + isError = isError, + focusRequester = focusRequester, + searchVisibility = searchVisibility, + searchQueryChange = searchQueryChange, + search = search, + requestFocus = requestFocus, + goBackDirectory = goBackDirectory, + clearSelectedFiles = clearSelectedFiles, + selectFiles = selectFiles, + changeDirectory = changeDirectory, + showFilterBottomSheet = showFilterBottomSheet, + showAddDialog = showAddDialog + ) + } + ) { padding -> + Box( + Modifier + .fillMaxSize() + .padding(top = padding.calculateTopPadding()) + ) { + DefaultTransition(visible = !isLoading) { + BrowseLayout( + files = files, + layout = layout, + hasSelectedFiles = files.any { it.isSelected }, + gridSize = gridSize, + autoGridSize = autoGridSize, + listState = listState, + gridState = gridState, + onLongItemClick = { file -> + when (file.isDirectory) { + false -> { + context.getString( + R.string.file_path_query, + file.fileOrDirectory.path + ).showToast(context = context) + } + + true -> { + selectFile( + BrowseEvent.OnSelectFile( + includedFileFormats = includedFilterItems, + file = file + ) + ) + } + } + }, + onFavoriteItemClick = { file -> + updateFavoriteDirectory( + BrowseEvent.OnUpdateFavoriteDirectory( + file.fileOrDirectory.path + ) + ) + }, + onItemClick = { file -> + when (file.isDirectory) { + false -> { + selectFile( + BrowseEvent.OnSelectFile( + includedFileFormats = includedFilterItems, + file = file + ) + ) + } + + true -> { + if (!hasSelectedItems) { + changeDirectory( + BrowseEvent.OnChangeDirectory( + file.fileOrDirectory, + savePreviousDirectory = true + ) + ) + } else { + selectFile( + BrowseEvent.OnSelectFile( + includedFileFormats = includedFilterItems, + file = file + ) + ) + } + } + } + } + ) + } + + BrowseEmptyPlaceholder( + filesEmpty = filesEmpty, + dialogHidden = dialogHidden, + isError = isError, + isLoading = isLoading, + isRefreshing = isRefreshing, + storagePermissionState = storagePermissionState, + permissionCheck = permissionCheck, + navigateToHelp = navigateToHelp + ) + + BrowseRefreshIndicator( + isRefreshing = isRefreshing, + refreshState = refreshState + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/top_bar/BrowseTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseTopBar.kt similarity index 60% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/top_bar/BrowseTopBar.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseTopBar.kt index d8ad0973..2c0b9fe1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/top_bar/BrowseTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseTopBar.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.top_bar +package ua.acclorite.book_story.presentation.browse import androidx.compose.animation.animateColorAsState import androidx.compose.material.icons.Icons @@ -24,66 +24,78 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.SelectableFile +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.SelectableFile import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.components.common.SearchTextField import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBar import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBarData -import ua.acclorite.book_story.presentation.core.navigation.NavigationIconButton -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout +import ua.acclorite.book_story.presentation.navigator.NavigatorIconButton +import ua.acclorite.book_story.ui.browse.BrowseEvent +import java.io.File -/** - * Browse Top Bar. - * - * @param filteredFiles Filtered files. - */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BrowseTopBar(filteredFiles: List) { - val state = BrowseViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = BrowseViewModel.getEvent() - - val focusRequester = remember { FocusRequester() } +fun BrowseTopBar( + files: List, + layout: BrowseLayout, + filesStructure: BrowseFilesStructure, + includedFilterItems: List, + canScrollBackList: Boolean, + canScrollBackGrid: Boolean, + selectedDirectory: File, + inNestedDirectory: Boolean, + hasSelectedItems: Boolean, + selectedItemsCount: Int, + showSearch: Boolean, + searchQuery: String, + hasSearched: Boolean, + isError: Boolean, + focusRequester: FocusRequester, + searchVisibility: (BrowseEvent.OnSearchVisibility) -> Unit, + searchQueryChange: (BrowseEvent.OnSearchQueryChange) -> Unit, + search: (BrowseEvent.OnSearch) -> Unit, + requestFocus: (BrowseEvent.OnRequestFocus) -> Unit, + goBackDirectory: (BrowseEvent.OnGoBackDirectory) -> Unit, + clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit, + selectFiles: (BrowseEvent.OnSelectFiles) -> Unit, + changeDirectory: (BrowseEvent.OnChangeDirectory) -> Unit, + showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit, + showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit +) { val selectedDirectoryName = remember { - mutableStateOf(state.value.selectedDirectory.name) + mutableStateOf(selectedDirectory.name) } - val inNestedDirectory = remember { + val isScrolled = remember(layout, canScrollBackList, canScrollBackGrid) { derivedStateOf { - state.value.inNestedDirectory - && mainState.value.browseFilesStructure != BrowseFilesStructure.ALL_FILES - } - } - val isScrolled = remember { - derivedStateOf { - when (mainState.value.browseLayout) { - BrowseLayout.LIST -> state.value.listState.canScrollBackward - BrowseLayout.GRID -> state.value.gridState.canScrollBackward + when (layout) { + BrowseLayout.GRID -> canScrollBackGrid + BrowseLayout.LIST -> canScrollBackList } } } - LaunchedEffect( - state.value.inNestedDirectory, - state.value.selectedDirectory - ) { - if (state.value.inNestedDirectory) { - selectedDirectoryName.value = state.value.selectedDirectory.name + val animatedFilterIconColor = animateColorAsState( + if (includedFilterItems.isNotEmpty()) { + MaterialTheme.colorScheme.primary + } else LocalContentColor.current + ) + + LaunchedEffect(inNestedDirectory, selectedDirectory) { + if (inNestedDirectory) { + selectedDirectoryName.value = selectedDirectory.name } } TopAppBar( scrollBehavior = null, - isTopBarScrolled = isScrolled.value || state.value.hasSelectedItems, + isTopBarScrolled = isScrolled.value || hasSelectedItems, shownTopBar = when { - state.value.hasSelectedItems -> 3 - state.value.showSearch -> 2 - state.value.inNestedDirectory -> 1 + hasSelectedItems -> 3 + showSearch -> 2 + inNestedDirectory -> 1 else -> 0 }, topBars = listOf( @@ -103,23 +115,17 @@ fun BrowseTopBar(filteredFiles: List) { contentDescription = R.string.search_content_desc, disableOnClick = true ) { - onEvent(BrowseEvent.OnSearchShowHide) + searchVisibility(BrowseEvent.OnSearchVisibility(true)) } IconButton( icon = Icons.Default.FilterList, contentDescription = R.string.filter_content_desc, disableOnClick = false, - color = animateColorAsState( - if (mainState.value.browseIncludedFilterItems.isNotEmpty()) { - MaterialTheme.colorScheme.primary - } else LocalContentColor.current, - label = "" - ).value, - enabled = !state.value.showFilterBottomSheet + color = animatedFilterIconColor.value ) { - onEvent(BrowseEvent.OnShowHideFilterBottomSheet) + showFilterBottomSheet(BrowseEvent.OnShowFilterBottomSheet) } - NavigationIconButton() + NavigatorIconButton() } ), @@ -130,9 +136,9 @@ fun BrowseTopBar(filteredFiles: List) { icon = Icons.AutoMirrored.Default.ArrowBack, contentDescription = R.string.go_back_content_desc, disableOnClick = false, - enabled = inNestedDirectory.value + enabled = inNestedDirectory ) { - onEvent(BrowseEvent.OnGoBackDirectory) + goBackDirectory(BrowseEvent.OnGoBackDirectory) } }, contentTitle = { @@ -148,23 +154,17 @@ fun BrowseTopBar(filteredFiles: List) { contentDescription = R.string.search_content_desc, disableOnClick = true ) { - onEvent(BrowseEvent.OnSearchShowHide) + searchVisibility(BrowseEvent.OnSearchVisibility(true)) } IconButton( icon = Icons.Default.FilterList, contentDescription = R.string.filter_content_desc, disableOnClick = false, - color = animateColorAsState( - if (mainState.value.browseIncludedFilterItems.isNotEmpty()) { - MaterialTheme.colorScheme.primary - } else LocalContentColor.current, - label = "" - ).value, - enabled = !state.value.showFilterBottomSheet + color = animatedFilterIconColor.value ) { - onEvent(BrowseEvent.OnShowHideFilterBottomSheet) + showFilterBottomSheet(BrowseEvent.OnShowFilterBottomSheet) } - NavigationIconButton() + NavigatorIconButton() } ), @@ -176,7 +176,7 @@ fun BrowseTopBar(filteredFiles: List) { contentDescription = R.string.exit_search_content_desc, disableOnClick = true ) { - onEvent(BrowseEvent.OnSearchShowHide) + searchVisibility(BrowseEvent.OnSearchVisibility(false)) } }, contentTitle = { @@ -184,19 +184,19 @@ fun BrowseTopBar(filteredFiles: List) { modifier = Modifier .focusRequester(focusRequester) .onGloballyPositioned { - onEvent(BrowseEvent.OnRequestFocus(focusRequester)) + requestFocus(BrowseEvent.OnRequestFocus(focusRequester)) }, - query = state.value.searchQuery, + query = searchQuery, onQueryChange = { - onEvent(BrowseEvent.OnSearchQueryChange(it)) + searchQueryChange(BrowseEvent.OnSearchQueryChange(it)) }, onSearch = { - onEvent(BrowseEvent.OnSearch) + search(BrowseEvent.OnSearch) } ) }, contentActions = { - NavigationIconButton() + NavigatorIconButton() } ), @@ -208,14 +208,14 @@ fun BrowseTopBar(filteredFiles: List) { contentDescription = R.string.clear_selected_items_content_desc, disableOnClick = true ) { - onEvent(BrowseEvent.OnClearSelectedFiles) + clearSelectedFiles(BrowseEvent.OnClearSelectedFiles) } }, contentTitle = { Text( stringResource( id = R.string.selected_items_count_query, - state.value.selectedItemsCount.coerceAtLeast(1) + selectedItemsCount.coerceAtLeast(1) ), overflow = TextOverflow.Ellipsis, maxLines = 1 @@ -227,26 +227,32 @@ fun BrowseTopBar(filteredFiles: List) { contentDescription = R.string.select_all_files_content_desc, disableOnClick = false, ) { - onEvent( + selectFiles( BrowseEvent.OnSelectFiles( - includedFileFormats = mainState.value.browseIncludedFilterItems, - files = filteredFiles + includedFileFormats = includedFilterItems, + files = files ) ) } IconButton( icon = Icons.Default.Check, contentDescription = R.string.add_files_content_desc, - disableOnClick = false, - enabled = !state.value.showAddingDialog + disableOnClick = false ) { - onEvent(BrowseEvent.OnAddingDialogRequest) + showAddDialog(BrowseEvent.OnShowAddDialog) } } ) ), customContent = { - BrowseTopBarDirectoryPath() + BrowseTopBarDirectoryPath( + selectedDirectory = selectedDirectory, + hasSelectedItems = hasSelectedItems, + hasSearched = hasSearched, + isError = isError, + filesStructure = filesStructure, + changeDirectory = changeDirectory + ) } ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/top_bar/BrowseTopBarDirectoryPath.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseTopBarDirectoryPath.kt similarity index 65% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/top_bar/BrowseTopBarDirectoryPath.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseTopBarDirectoryPath.kt index 15ff82e3..81004771 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/top_bar/BrowseTopBarDirectoryPath.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseTopBarDirectoryPath.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.top_bar +package ua.acclorite.book_story.presentation.browse import android.os.Environment import androidx.compose.animation.animateColorAsState @@ -22,40 +22,47 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.browse.FileWithTitle +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure +import ua.acclorite.book_story.ui.browse.BrowseEvent import java.io.File @Composable -fun BrowseTopBarDirectoryPath() { - val state = BrowseViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = BrowseViewModel.getEvent() - val context = LocalContext.current +fun BrowseTopBarDirectoryPath( + selectedDirectory: File, + hasSelectedItems: Boolean, + hasSearched: Boolean, + isError: Boolean, + filesStructure: BrowseFilesStructure, + changeDirectory: (BrowseEvent.OnChangeDirectory) -> Unit +) { + val rootDirectory = Environment.getExternalStorageDirectory() - val rootDirectory = remember { - Environment.getExternalStorageDirectory() - } - val directories = remember(state.value.selectedDirectory) { - val directories = mutableListOf>() + val directories = remember(selectedDirectory) { + val directories = mutableListOf() - if (state.value.selectedDirectory == rootDirectory) { + if (selectedDirectory == rootDirectory) { return@remember listOf( - context.getString(R.string.internal_storage) to rootDirectory + FileWithTitle( + title = UIText.StringResource(R.string.internal_storage), + file = rootDirectory + ) ) } - directories.add(state.value.selectedDirectory.name to state.value.selectedDirectory) - var currentDirectory = state.value.selectedDirectory.parentFile - ?: return@remember emptyList() + directories.add( + FileWithTitle( + title = UIText.StringValue(selectedDirectory.name), + file = selectedDirectory + ) + ) + var currentDirectory = selectedDirectory.parentFile ?: return@remember emptyList() while (true) { if (currentDirectory == rootDirectory) { @@ -63,13 +70,19 @@ fun BrowseTopBarDirectoryPath() { } directories.add( - currentDirectory.name to currentDirectory + FileWithTitle( + title = UIText.StringValue(currentDirectory.name), + file = currentDirectory + ) ) currentDirectory = currentDirectory.parentFile ?: continue } directories.add( - context.getString(R.string.internal_storage) to rootDirectory + FileWithTitle( + title = UIText.StringResource(R.string.internal_storage), + file = rootDirectory + ) ) directories.reversed() @@ -78,17 +91,17 @@ fun BrowseTopBarDirectoryPath() { LaunchedEffect(directories) { try { - listState.scrollToItem(directories.lastIndex) + listState.animateScrollToItem(directories.lastIndex) } catch (e: Exception) { e.printStackTrace() } } AnimatedVisibility( - visible = !state.value.hasSelectedItems - && !state.value.hasSearched - && !state.value.isError - && mainState.value.browseFilesStructure == BrowseFilesStructure.DIRECTORIES, + visible = !hasSelectedItems + && !hasSearched + && !isError + && filesStructure == BrowseFilesStructure.DIRECTORIES, enter = expandVertically(), exit = shrinkVertically() ) { @@ -108,7 +121,7 @@ fun BrowseTopBarDirectoryPath() { } Text( - text = directory.first, + text = directory.title.asString(), color = animateColorAsState( if (index == directories.lastIndex) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, @@ -116,11 +129,11 @@ fun BrowseTopBarDirectoryPath() { ).value, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.noRippleClickable( - enabled = state.value.selectedDirectory != directory.second + enabled = selectedDirectory != directory.file ) { - onEvent( + changeDirectory( BrowseEvent.OnChangeDirectory( - directory.second, + directory = directory.file, savePreviousDirectory = false ) ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/GoBackButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/GoBackButton.kt deleted file mode 100644 index 9d9d8f42..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/GoBackButton.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ua.acclorite.book_story.presentation.core.components.common - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.runtime.Composable -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.util.OnNavigate - -/** - * Go back arrow button. - * Prevents double or triple clicking while going back action is performed. - * - * @param onNavigate [OnNavigate]. - * @param enabled Whether this button is enabled. - * @param customOnClick Call before going back. - */ -@Composable -fun GoBackButton( - onNavigate: OnNavigate, - enabled: Boolean = true, - customOnClick: () -> Unit = {} -) { - IconButton( - icon = Icons.AutoMirrored.Outlined.ArrowBack, - contentDescription = R.string.go_back_content_desc, - disableOnClick = true, - enabled = enabled - ) { - customOnClick() - onNavigate { - navigateBack() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/LazyColumnWithScrollbar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/LazyColumnWithScrollbar.kt index ae19e9aa..e2416d23 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/LazyColumnWithScrollbar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/LazyColumnWithScrollbar.kt @@ -17,18 +17,6 @@ import my.nanihadesuka.compose.ScrollbarSettings import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideSecondaryScrollbar -/** - * Lazy Column. - * Has a scrollbar. - * - * @param modifier Modifier. - * @param state [LazyListState]. - * @param scrollbarSettings [ScrollbarSettings]. [provideSecondaryScrollbar] by default. - * @param enableScrollbar Whether scrollbar is enabled. - * @param contentPadding [PaddingValues]. - * @param verticalArrangement Vertical item arrangement. - * @param content Content of the column. - */ @Composable fun LazyColumnWithScrollbar( modifier: Modifier = Modifier, @@ -45,8 +33,9 @@ fun LazyColumnWithScrollbar( } } - Box(modifier = modifier) { + Box { LazyColumn( + modifier = modifier, state = state, verticalArrangement = verticalArrangement, contentPadding = contentPadding diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/LazyVerticalGridWithScrollbar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/LazyVerticalGridWithScrollbar.kt new file mode 100644 index 00000000..2f773357 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/common/LazyVerticalGridWithScrollbar.kt @@ -0,0 +1,55 @@ +package ua.acclorite.book_story.presentation.core.components.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import my.nanihadesuka.compose.InternalLazyVerticalGridScrollbar +import my.nanihadesuka.compose.ScrollbarSettings +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideSecondaryScrollbar + +@Composable +fun LazyVerticalGridWithScrollbar( + modifier: Modifier = Modifier, + columns: GridCells, + state: LazyGridState = rememberLazyGridState(), + scrollbarSettings: ScrollbarSettings = Constants.provideSecondaryScrollbar(), + enableScrollbar: Boolean = true, + contentPadding: PaddingValues = PaddingValues(0.dp), + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + content: LazyGridScope.() -> Unit +) { + val enabled = remember { + derivedStateOf { + enableScrollbar && (state.canScrollBackward || state.canScrollForward) + } + } + + Box { + LazyVerticalGrid( + modifier = modifier, + columns = columns, + state = state, + verticalArrangement = verticalArrangement, + contentPadding = contentPadding + ) { + content() + } + if (enabled.value) { + InternalLazyVerticalGridScrollbar( + state = state, + settings = scrollbarSettings + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/DialogWithLazyColumn.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/Dialog.kt similarity index 87% rename from app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/DialogWithLazyColumn.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/Dialog.kt index 4a216bec..4a58eb5c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/DialogWithLazyColumn.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/Dialog.kt @@ -39,26 +39,9 @@ import androidx.compose.ui.window.DialogWindowProvider import ua.acclorite.book_story.R -/** - * Dialog with lazy column. - * - * @param modifier Modifier to be applied. - * @param drawableIcon Drawable icon to show(1/2). - * @param imageVectorIcon Image vector icon to show(2/2). - * @param backgroundTransparency The transparency of the background behind the dialog. - * @param title The title of the dialog. - * @param description The description of the dialog. - * @param actionText The action text of the dialog. - * @param disableOnClick Whether should all actions be disabled after clicking it. - * @param isActionEnabled Whether action is enabled or not. - * @param onDismiss Code to execute when the dialog is dismissed. - * @param onAction Code to execute when action is clicked. - * @param withDivider Should divider be shown or not. - * @param items The items of this dialog, use items(..) { .. } or item { .. }. - */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DialogWithLazyColumn( +fun Dialog( modifier: Modifier = Modifier, drawableIcon: Painter? = null, imageVectorIcon: ImageVector? = null, @@ -67,7 +50,7 @@ fun DialogWithLazyColumn( description: String?, actionText: String?, disableOnClick: Boolean = true, - isActionEnabled: Boolean?, + actionEnabled: Boolean?, onDismiss: () -> Unit, onAction: () -> Unit, withDivider: Boolean, @@ -191,13 +174,13 @@ fun DialogWithLazyColumn( } onAction() }, - enabled = isActionEnabled == true && !actionClicked + enabled = actionEnabled == true && !actionClicked ) { Text( text = actionText, style = MaterialTheme.typography.labelLarge, color = - if (isActionEnabled == true) MaterialTheme.colorScheme.primary + if (actionEnabled == true) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy(0.5f) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/DialogWithContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/DialogWithContent.kt deleted file mode 100644 index 267ed07c..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/DialogWithContent.kt +++ /dev/null @@ -1,201 +0,0 @@ -package ua.acclorite.book_story.presentation.core.components.dialog - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -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.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.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.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.compose.ui.window.DialogWindowProvider -import ua.acclorite.book_story.R - -/** - * Dialog with custom content. - * - * @param modifier Modifier to be applied. - * @param drawableIcon Drawable icon to show(1/2). - * @param imageVectorIcon Image vector icon to show(2/2). - * @param backgroundTransparency The transparency of the background behind the dialog. - * @param title The title of the dialog. - * @param description The description of the dialog. - * @param actionText The action text of the dialog. - * @param disableOnClick Whether should all actions be disabled after clicking it. - * @param isActionEnabled Whether action is enabled or not. - * @param onDismiss Code to execute when the dialog is dismissed. - * @param onAction Code to execute when action is clicked. - * @param withDivider Should divider be shown or not. - * @param customContent Custom content(not necessary, dialog also can be use just as alert dialog). - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DialogWithContent( - modifier: Modifier = Modifier, - drawableIcon: Painter? = null, - imageVectorIcon: ImageVector? = null, - backgroundTransparency: Float = 0.5f, - title: String, - description: String?, - actionText: String?, - disableOnClick: Boolean = true, - isActionEnabled: Boolean?, - onDismiss: () -> Unit, - onAction: () -> Unit, - withDivider: Boolean, - customContent: @Composable (ColumnScope.() -> Unit) = {} -) { - var actionClicked by remember { mutableStateOf(false) } - - BasicAlertDialog( - modifier = modifier - .fillMaxWidth() - .navigationBarsPadding() - .statusBarsPadding() - .clip(MaterialTheme.shapes.extraLarge) - .background( - MaterialTheme.colorScheme.surfaceContainerHigh, - MaterialTheme.shapes.extraLarge - ) - .padding(top = 24.dp, bottom = 12.dp), - onDismissRequest = { onDismiss() }, - properties = DialogProperties( - dismissOnBackPress = !actionClicked, - dismissOnClickOutside = !actionClicked - ) - ) { - (LocalView.current.parent as DialogWindowProvider).window.setDimAmount( - backgroundTransparency - ) - Column { - if (drawableIcon != null) { - Icon( - painter = drawableIcon, - contentDescription = title, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .size(24.dp), - tint = MaterialTheme.colorScheme.secondary - ) - Spacer(modifier = Modifier.height(16.dp)) - } - - if (imageVectorIcon != null) { - Icon( - imageVector = imageVectorIcon, - contentDescription = title, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .size(24.dp), - tint = MaterialTheme.colorScheme.secondary - ) - Spacer(modifier = Modifier.height(16.dp)) - } - - Text( - text = title, - modifier = Modifier - .align( - if (drawableIcon != null || imageVectorIcon != null) Alignment.CenterHorizontally - else Alignment.Start - ) - .padding(horizontal = 24.dp), - textAlign = if (drawableIcon != null || imageVectorIcon != null) TextAlign.Center - else TextAlign.Start, - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - if (description != null) { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Start, - modifier = Modifier.padding(horizontal = 24.dp) - ) - } - if (withDivider) { - Spacer(modifier = Modifier.height(12.dp)) - HorizontalDivider( - modifier = Modifier.padding(horizontal = 24.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - Spacer(modifier = Modifier.height(12.dp)) - } else { - Spacer(modifier = Modifier.height(8.dp)) - } - - customContent() - - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier - .align(Alignment.End) - .padding(horizontal = 24.dp) - ) { - TextButton( - onClick = { - if (disableOnClick) { - actionClicked = true - } - onDismiss() - }, - enabled = !actionClicked - ) { - Text( - text = stringResource(id = R.string.cancel), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary - ) - } - if (actionText != null) { - Spacer(modifier = Modifier.width(4.dp)) - TextButton( - onClick = { - if (disableOnClick) { - actionClicked = true - } - onAction() - }, - enabled = isActionEnabled == true && !actionClicked - ) { - Text( - text = actionText, - style = MaterialTheme.typography.labelLarge, - color = - if (isActionEnabled == true) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.primary.copy(0.5f) - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/SelectableDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/SelectableDialogItem.kt index 6c1c6b1e..ee8c94b6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/SelectableDialogItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/SelectableDialogItem.kt @@ -18,15 +18,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -/** - * Selectable dialog item. - * Perfectly combines with [DialogWithLazyColumn]. - * - * @param selected Whether item is selected. - * @param title Title of the item. - * @param horizontalPadding Horizontal padding of the item. - * @param onClick OnClick callback. - */ @Composable fun SelectableDialogItem( selected: Boolean, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBar.kt index aa702cea..ce4bc434 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBar.kt @@ -2,44 +2,23 @@ package ua.acclorite.book_story.presentation.core.components.navigation_bar import androidx.compose.material3.NavigationBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import ua.acclorite.book_story.domain.util.Route -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideNavigationItems -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigatorInstance +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.domain.navigator.NavigatorItem +import ua.acclorite.book_story.presentation.navigator.LocalNavigator -/** - * Navigation bar, uses default [NavigationBar]. - */ @Composable -fun NavigationBar() { - var currentScreen: Route? by remember { mutableStateOf(null) } - val navigator = LocalNavigatorInstance.current - - LaunchedEffect(Unit) { - navigator.currentScreen.collect { route -> - if ( - Constants.provideNavigationItems().any { - navigator.run { it.screen.getRoute() } == route - } - ) { - currentScreen = route - } - } - } +fun NavigationBar(tabs: List) { + val navigator = LocalNavigator.current + val currentTab = navigator.lastItem.collectAsStateWithLifecycle() NavigationBar { - Constants.provideNavigationItems().forEach { + tabs.forEach { tab -> NavigationBarItem( - item = it, - isSelected = currentScreen == navigator.run { it.screen.getRoute() } + item = tab, + isSelected = currentTab.value::class == tab.screen::class ) { - navigator.navigate(it.screen, false) + navigator.push(tab.screen) } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBarItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBarItem.kt index e91f5046..1548b14e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBarItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_bar/NavigationBarItem.kt @@ -13,21 +13,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.NavigationItem +import ua.acclorite.book_story.domain.navigator.NavigatorItem import ua.acclorite.book_story.presentation.core.components.common.Tooltip -/** - * Navigation Bar Item. - * - * @param modifier Modifier. - * @param item [NavigationItem]. - * @param isSelected Whether this screen is currently selected. - * @param onClick OnClick callback. - */ @Composable fun RowScope.NavigationBarItem( modifier: Modifier = Modifier, - item: NavigationItem, + item: NavigatorItem, isSelected: Boolean, onClick: () -> Unit ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRail.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRail.kt index 8ec490a7..0910b66b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRail.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRail.kt @@ -14,41 +14,20 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationRail import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.util.Route -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideNavigationItems -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigatorInstance +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.domain.navigator.NavigatorItem +import ua.acclorite.book_story.presentation.navigator.LocalNavigator -/** - * Navigation Rail. It is used to be shown on Tablets. - */ @Composable -fun NavigationRail() { - var currentScreen: Route? by remember { mutableStateOf(null) } - val navigator = LocalNavigatorInstance.current +fun NavigationRail(tabs: List) { + val navigator = LocalNavigator.current + val currentTab = navigator.lastItem.collectAsStateWithLifecycle() val layoutDirection = LocalLayoutDirection.current - LaunchedEffect(Unit) { - navigator.currentScreen.collect { route -> - if ( - Constants.provideNavigationItems().any { - navigator.run { it.screen.getRoute() } == route - } - ) { - currentScreen = route - } - } - } - NavigationRail( modifier = Modifier .background(MaterialTheme.colorScheme.surfaceContainer) @@ -71,12 +50,12 @@ fun NavigationRail() { verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally ) { - Constants.provideNavigationItems().forEach { + tabs.forEach { tab -> NavigationRailItem( - item = it, - isSelected = currentScreen == navigator.run { it.screen.getRoute() } + item = tab, + isSelected = currentTab.value::class == tab.screen::class ) { - navigator.navigate(it.screen, false) + navigator.push(tab.screen) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRailItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRailItem.kt index d1d8ee96..0f193930 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRailItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/navigation_rail/NavigationRailItem.kt @@ -12,21 +12,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.NavigationItem +import ua.acclorite.book_story.domain.navigator.NavigatorItem import ua.acclorite.book_story.presentation.core.components.common.Tooltip -/** - * Navigation Rail item. - * - * @param modifier Modifier. - * @param item [NavigationItem]. - * @param isSelected Whether this screen is currently selected. - * @param onClick OnClick callback. - */ @Composable fun NavigationRailItem( modifier: Modifier = Modifier, - item: NavigationItem, + item: NavigatorItem, isSelected: Boolean, onClick: () -> Unit ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ChipsWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ChipsWithTitle.kt index 2fdfe029..d31ba470 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ChipsWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ChipsWithTitle.kt @@ -14,8 +14,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.ButtonItem -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle +import ua.acclorite.book_story.domain.ui.ButtonItem +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle /** * Chips with title. Use list of [ButtonItem]s to display chips. @@ -35,7 +35,7 @@ fun ChipsWithTitle( .fillMaxWidth() .padding(horizontal = horizontalPadding, vertical = verticalPadding) ) { - SettingsCategoryTitle(title = title, padding = 0.dp) + SettingsSubcategoryTitle(title = title, padding = 0.dp) Spacer(modifier = Modifier.height(8.dp)) FlowRow( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ColorPickerWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ColorPickerWithTitle.kt index 8e3f530d..9e6a0f99 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ColorPickerWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/ColorPickerWithTitle.kt @@ -31,19 +31,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.common.IconButton -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle -/** - * Color picker with title. - * - * @param modifier Modifier. - * @param value Target [Color]. - * @param presetId Current Color Preset id(to trigger initial value change). - * @param title Title. - * @param horizontalPadding Horizontal item padding. - * @param verticalPadding Vertical item padding. - * @param onValueChange Callback when target color changes. - */ @OptIn(FlowPreview::class) @Composable fun ColorPickerWithTitle( @@ -71,7 +60,7 @@ fun ColorPickerWithTitle( .fillMaxWidth() .padding(vertical = verticalPadding, horizontal = horizontalPadding) ) { - SettingsCategoryTitle( + SettingsSubcategoryTitle( title = title, padding = 0.dp ) @@ -145,4 +134,4 @@ private fun RevertibleSlider( onValueChange(initialValue) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SegmentedButtonWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SegmentedButtonWithTitle.kt index 4fa50253..1a5dc583 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SegmentedButtonWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SegmentedButtonWithTitle.kt @@ -38,9 +38,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle /** * Segmented Button with Title. @@ -70,7 +70,7 @@ fun SegmentedButtonWithTitle( .fillMaxWidth() .padding(horizontal = horizontalPadding, vertical = verticalPadding) ) { - SettingsCategoryTitle(title = title, padding = 0.dp) + SettingsSubcategoryTitle(title = title, padding = 0.dp) Spacer(modifier = Modifier.height(8.dp)) @@ -182,10 +182,4 @@ private fun SegmentedButton( else colors.inactiveContentColor ) } -} - - - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SliderWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SliderWithTitle.kt index 32b81524..97f49867 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SliderWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/settings/SliderWithTitle.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle import kotlin.math.roundToInt /** @@ -70,7 +70,7 @@ fun SliderWithTitle( verticalAlignment = Alignment.CenterVertically ) { Column(Modifier.fillMaxWidth(0.2f)) { - SettingsCategoryTitle(title = title, padding = 0.dp) + SettingsSubcategoryTitle(title = title, padding = 0.dp) Spacer(modifier = Modifier.height(4.dp)) Text( text = placeholder.value, @@ -158,7 +158,7 @@ fun SliderWithTitle( verticalAlignment = Alignment.CenterVertically ) { Column(Modifier.fillMaxWidth(0.2f)) { - SettingsCategoryTitle(title = title, padding = 0.dp) + SettingsSubcategoryTitle(title = title, padding = 0.dp) Spacer(modifier = Modifier.height(4.dp)) Text( text = placeholder.value, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/AboutBadgeConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/AboutBadgeConstants.kt index b7e7a824..0ed3ba69 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/AboutBadgeConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/AboutBadgeConstants.kt @@ -3,7 +3,7 @@ package ua.acclorite.book_story.presentation.core.constants import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Badge +import ua.acclorite.book_story.domain.about.Badge fun Constants.provideAboutBadges() = listOf( Badge( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt index 2826441e..5fc71896 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt @@ -3,10 +3,10 @@ package ua.acclorite.book_story.presentation.core.constants import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.ColorPreset -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.reader.ColorPreset +import ua.acclorite.book_story.domain.ui.UIText // Main State fun Constants.provideMainState() = "main_state" diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CreditConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CreditConstants.kt index 4fa42d62..b6ce9fac 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CreditConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CreditConstants.kt @@ -1,8 +1,8 @@ package ua.acclorite.book_story.presentation.core.constants import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Credit -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.about.Credit +import ua.acclorite.book_story.domain.ui.UIText fun Constants.provideCredits() = listOf( Credit( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/FontConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/FontConstants.kt index f05009a2..b23dc234 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/FontConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/FontConstants.kt @@ -5,8 +5,8 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.FontWithName -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.reader.FontWithName +import ua.acclorite.book_story.domain.ui.UIText fun Constants.provideFonts(withRandom: Boolean): List { return mutableListOf( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/HelpTipConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/HelpTipConstants.kt index e87eb7c8..c7602fc2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/HelpTipConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/HelpTipConstants.kt @@ -2,40 +2,23 @@ package ua.acclorite.book_story.presentation.core.constants import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.HelpTip -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.domain.help.HelpTip +import ua.acclorite.book_story.presentation.help.HelpAnnotation fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_add_books, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_add_books_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Browse, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_add_books_2)) } append(". ") append(stringResource(id = R.string.help_desc_how_to_add_books_3) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Library, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_add_books_4)) } append(".") @@ -44,18 +27,10 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_customize_app, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_customize_app_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Settings, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_customize_app_2)) } append(". ") @@ -66,18 +41,10 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_move_or_delete_books, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Library, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_2)) } append(". ") @@ -88,18 +55,10 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_edit_book, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_edit_book_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Library, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_edit_book_2)) } append(" ") @@ -110,18 +69,10 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_read_book, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_read_book_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Library, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_read_book_2)) } append(". ") @@ -132,18 +83,10 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_customize_reader, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_customize_reader_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Settings, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_customize_reader_2)) } append(" ") @@ -154,18 +97,10 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_update_book, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_update_book_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.Library, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_update_book_2)) } append(" ") @@ -176,18 +111,10 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_manage_history, - description = { onNavigate, fromStart -> + description = { append(stringResource(id = R.string.help_desc_how_to_manage_history_1) + " ") - HelpAnnotation( - onClick = { - if (!fromStart) { - onNavigate { - navigate(Screen.History, useBackAnimation = true) - } - } - } - ) { + HelpAnnotation { append(stringResource(id = R.string.help_desc_how_to_manage_history_2)) } append(". ") @@ -198,35 +125,35 @@ fun Constants.provideHelpTips() = listOf( HelpTip( title = R.string.help_title_how_to_use_tooltip, - description = { _, _ -> + description = { append(stringResource(id = R.string.help_desc_how_to_use_tooltip_1)) } ), HelpTip( title = R.string.help_title_how_to_use_double_click_translation, - description = { _, _ -> + description = { append(stringResource(id = R.string.help_desc_how_to_use_double_click_translation_1)) } ), HelpTip( title = R.string.help_title_how_to_create_color_presets, - description = { _, _ -> + description = { append(stringResource(id = R.string.help_desc_how_to_create_color_presets_1)) } ), HelpTip( title = R.string.help_title_how_to_use_color_presets, - description = { _, _ -> + description = { append(stringResource(id = R.string.help_desc_how_to_use_color_presets_1)) } ), HelpTip( title = R.string.help_title_how_to_use_perception_expander, - description = { _, _ -> + description = { append(stringResource(id = R.string.help_title_how_to_use_perception_expander_1)) } ), diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/NavigationItemConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/NavigationItemConstants.kt deleted file mode 100644 index 26d16e38..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/NavigationItemConstants.kt +++ /dev/null @@ -1,29 +0,0 @@ -package ua.acclorite.book_story.presentation.core.constants - -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.NavigationItem -import ua.acclorite.book_story.presentation.core.navigation.Screen - -fun Constants.provideNavigationItems() = listOf( - NavigationItem( - screen = Screen.Library, - title = R.string.library_screen, - tooltip = R.string.library_content_desc, - selectedIcon = R.drawable.library_screen_filled, - unselectedIcon = R.drawable.library_screen_outlined - ), - NavigationItem( - screen = Screen.History, - title = R.string.history_screen, - tooltip = R.string.history_content_desc, - selectedIcon = R.drawable.history_screen_filled, - unselectedIcon = R.drawable.history_screen_outlined - ), - NavigationItem( - screen = Screen.Browse, - title = R.string.browse_screen, - tooltip = R.string.browse_content_desc, - selectedIcon = R.drawable.browse_screen_filled, - unselectedIcon = R.drawable.browse_screen_outlined - ) -) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/ScrollbarConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/ScrollbarConstants.kt index 468cd4ac..31a4ddea 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/ScrollbarConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/ScrollbarConstants.kt @@ -8,12 +8,12 @@ import my.nanihadesuka.compose.ScrollbarSelectionMode import my.nanihadesuka.compose.ScrollbarSettings @Composable -fun Constants.providePrimaryScrollbar() = ScrollbarSettings( +fun Constants.providePrimaryScrollbar(canSelect: Boolean = true) = ScrollbarSettings( thumbUnselectedColor = MaterialTheme.colorScheme.primary, thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(0.8f), hideDelayMillis = 2000, durationAnimationMillis = 300, - selectionMode = ScrollbarSelectionMode.Thumb, + selectionMode = if (canSelect) ScrollbarSelectionMode.Thumb else ScrollbarSelectionMode.Disabled, thumbThickness = 8.dp, scrollbarPadding = 4.dp ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/Navigation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/Navigation.kt deleted file mode 100644 index 9cf13e19..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/Navigation.kt +++ /dev/null @@ -1,474 +0,0 @@ -package ua.acclorite.book_story.presentation.core.navigation - -import android.annotation.SuppressLint -import android.os.Parcelable -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.activity.viewModels -import androidx.annotation.Keep -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.lifecycle.withCreationCallback -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable -import ua.acclorite.book_story.domain.util.OnNavigate -import ua.acclorite.book_story.domain.util.Route -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.ui.Transitions - -private const val CURRENT_SCREEN = "current_screen" -private const val BACKSTACK = "back_stack" -private const val USE_BACK_ANIM = "use_back_animation" -private const val SCREENS = "screens" - -val LocalNavigatorInstance = compositionLocalOf { - error("Cannot initialize Navigator.") -} -val LocalNavigator = compositionLocalOf { - error("Cannot initialize Navigator.") -} - -/** - * Screens. Should be [Serializable] to be able to pass them inside [SavedStateHandle]. - * Each screen should have unique name. - */ -@Immutable -@Parcelize -@Keep -sealed class Screen : Parcelable { - - @Parcelize - @Keep - data object Library : Screen() - - @Parcelize - @Keep - data object History : Screen() - - @Parcelize - @Keep - data object Browse : Screen() - - @Parcelize - @Keep - data class BookInfo( - val bookId: Int, - val startUpdate: Boolean - ) : Screen() - - @Parcelize - @Keep - data class Reader( - val bookId: Int - ) : Screen() - - @Parcelize - @Keep - data object Settings : Screen() { - @Parcelize - @Keep - data object General : Screen() - - @Parcelize - @Keep - data object Appearance : Screen() - - @Parcelize - @Keep - data object ReaderSettings : Screen() - - @Parcelize - @Keep - data object BrowseSettings : Screen() - } - - @Parcelize - @Keep - data object About : Screen() { - @Parcelize - @Keep - data object Licenses : Screen() - - @Parcelize - @Keep - data class LicenseInfo(val licenseId: String) : Screen() - - @Parcelize - @Keep - data object Credits : Screen() - } - - @Parcelize - @Keep - data class Help( - val fromStart: Boolean - ) : Screen() - - @Parcelize - @Keep - data object Start : Screen() -} - -/** - * Navigator. Used to navigate between screens. - * - * [Navigator.currentScreen] param represents current [Screen]. - * [Navigator.navigate] navigates to [Screen] passed as param. - */ -@HiltViewModel(assistedFactory = Navigator.Factory::class) -class Navigator @AssistedInject constructor( - private val savedStateHandle: SavedStateHandle, - @Assisted private val startScreen: Screen -) : ViewModel() { - - val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen.getRoute()) - val useBackAnimation = savedStateHandle.getStateFlow(USE_BACK_ANIM, false) - private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf()) - val screens = savedStateHandle.getStateFlow(SCREENS, mutableListOf()) - - private val _showNavigationBottomSheet = MutableStateFlow(false) - private val showNavigationBottomSheet = _showNavigationBottomSheet.asStateFlow() - - init { - putScreen(startScreen) - } - - /** - * Saves screen into [screens]. Later can be retrieved via [retrieveScreen]. - * - * @param screen [Screen]. - */ - fun putScreen(screen: Screen) = viewModelScope.launch(Dispatchers.Main.immediate) { - var found = false - - for ((index, arg) in screens.value.withIndex()) { - if (arg.getRoute() == screen.getRoute()) { - screens.value[index] = screen - found = true - break - } - } - - if (!found) { - screens.value.add(screen) - } - } - - /** - * Retrieves screen that was put via [putScreen]. - * - * @exception ClassCastException throws an exception if could not cast saved screen as [S]. - * @exception Exception if there is no such screen saved that specified in [S], throws an [Exception]. - */ - inline fun retrieveScreen(): S { - for (arg in screens.value) { - if (arg.getRoute() == getRoute()) { - val screen = arg as? S - ?: throw ClassCastException("Cannot cast ${arg::class} as ${S::class}") - return screen - } - } - - throw Exception("Screen was not found.") - } - - /** - * Navigates to the desired screen. Ignored if [currentScreen] is already [screen]. - * - * @param screen [Screen] to navigate to. - * @param useBackAnimation Whether back animation should be used(as when user goes back). - * @param saveInBackStack Whether this screen should be saved in [backStack] (basically history of all opened screens). - */ - fun navigate( - screen: Screen, - useBackAnimation: Boolean = false, - saveInBackStack: Boolean = true - ) = viewModelScope.launch(Dispatchers.Main.immediate) { - if (screen.getRoute() == currentScreen.value) { - return@launch - } - - if (saveInBackStack) { - backStack.value.add(currentScreen.value) - } - - putScreen(screen) - - savedStateHandle[USE_BACK_ANIM] = useBackAnimation - savedStateHandle[CURRENT_SCREEN] = screen.getRoute() - } - - /** - * Navigates user to the previous screen, if there is. - * If there is nowhere to go, this call is ignored. - * - * @param useBackAnimation Whether user should see back animation when he goes to the previous screen. - */ - fun navigateBack( - useBackAnimation: Boolean = true - ) = viewModelScope.launch(Dispatchers.Main.immediate) { - if (canGoBack()) { - savedStateHandle[USE_BACK_ANIM] = useBackAnimation - savedStateHandle[CURRENT_SCREEN] = backStack.value.last() - - backStack.value.removeAt(backStack.value.lastIndex) - } - } - - /** - * Clears the whole [backStack]. - */ - fun clearBackStack() { - backStack.value.clear() - } - - /** - * Whether there is screen to go back. - */ - fun canGoBack(): Boolean { - return backStack.value.isNotEmpty() - } - - /** - * Gets route of specified [S]. - */ - inline fun getRoute(): Route { - return S::class.simpleName!! - } - - /** - * Gets route of this screen. - */ - fun Screen.getRoute(): Route { - return this::class.simpleName!! - } - - /** - * Shows navigation bottom sheet. - */ - fun showNavigationBottomSheet() { - _showNavigationBottomSheet.update { true } - } - - /** - * Hides navigation bottom sheet. - */ - fun hideNavigationBottomSheet() { - _showNavigationBottomSheet.update { false } - } - - /** - * Animated Screen. Used in [NavigationHost]. - * Each [composable] should have unique [Screen]. - * - * @param enterAnim Enter Animation. - * @param backEnterAnim Enter Animation for navigating back. - * @param exitAnim Exit Animation. - * @param backExitAnim Exit Animation for navigating back. - * @param content The Screen content to show when [Navigator.currentScreen] equals [S]. - */ - @SuppressLint("ComposableNaming") - @Composable - inline fun composable( - enterAnim: EnterTransition = Transitions.SlidingTransitionIn, - backEnterAnim: EnterTransition = Transitions.BackSlidingTransitionIn, - exitAnim: ExitTransition = Transitions.SlidingTransitionOut, - backExitAnim: ExitTransition = Transitions.BackSlidingTransitionOut, - noinline content: @Composable (screen: S) -> Unit - ) { - val currentRoute = currentScreen.collectAsState() - val useBackAnimation by useBackAnimation.collectAsState() - val shouldShow by remember { - derivedStateOf { - currentRoute.value == getRoute() - } - } - - AnimatedVisibility( - visible = shouldShow, - enter = if (!useBackAnimation) enterAnim else backEnterAnim, - exit = if (!useBackAnimation) exitAnim else backExitAnim - ) { - val screen = remember { retrieveScreen() } - content(screen) - } - } - - /** - * Navigator's navigation. - * Shows BottomBar or Navigation Rail when any of the [screens] are currently showing. - * - * @param screens [Screen]s where navigation shows. - * @param enterBarAnim Enter animation for navigation. - * @param backEnterBarAnim Back enter animation for navigation. - * @param exitBarAnim Exit animation for navigation. - * @param backExitBarAnim Back exit animation for navigation. - * @param bottomBar Bottom bar, sticks to the bottom. - * @param navigationRail Navigation rail, sticks to the sides. - * @param content Content, all [screens] should be inside as [composable]. - */ - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) - @SuppressLint("ComposableNaming") - @Composable - fun navigation( - vararg screens: Route, - enterBarAnim: EnterTransition = Transitions.SlidingTransitionIn, - backEnterBarAnim: EnterTransition = Transitions.BackSlidingTransitionIn, - exitBarAnim: ExitTransition = Transitions.SlidingTransitionOut, - backExitBarAnim: ExitTransition = Transitions.BackSlidingTransitionOut, - bottomBar: @Composable () -> Unit, - navigationRail: @Composable () -> Unit, - content: @Composable () -> Unit - ) { - val activity = LocalContext.current as ComponentActivity - val windowClass = calculateWindowSizeClass(activity = activity) - val tabletUI = remember(windowClass) { - windowClass.widthSizeClass != WindowWidthSizeClass.Compact - } - val layoutDirection = LocalLayoutDirection.current - - val currentScreen = currentScreen.collectAsState() - val useBackAnimation by useBackAnimation.collectAsState() - val showNavigation = remember { - derivedStateOf { - screens.any { it == currentScreen.value } - } - } - val showNavigationBottomSheet = showNavigationBottomSheet.collectAsState() - - AnimatedVisibility( - visible = showNavigation.value, - enter = if (useBackAnimation) backEnterBarAnim - else enterBarAnim, - exit = if (useBackAnimation) backExitBarAnim - else exitBarAnim - ) { - if (showNavigationBottomSheet.value) { - NavigationBottomSheet( - onDismissRequest = { - hideNavigationBottomSheet() - } - ) - } - - Scaffold( - bottomBar = { - if (!tabletUI) { - bottomBar() - } - }, - containerColor = MaterialTheme.colorScheme.surface - ) { - Row( - modifier = Modifier - .fillMaxSize() - ) { - if (tabletUI) { - navigationRail() - } - - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize() - .padding( - end = it.calculateEndPadding(layoutDirection), - bottom = it.calculateBottomPadding(), - start = it.calculateStartPadding(layoutDirection) - ) - ) { - content() - } - } - } - } - } - - @AssistedFactory - interface Factory { - fun create(startScreen: Screen): Navigator - } -} - -/** - * Custom Navigation Host. Contains [Navigator.composable]s in [content]. - * Based on [AnimatedVisibility]. - * - * @param startScreen Start Screen. Be sure to pass [Screen] that uses in one of your [Navigator.composable]s. - * @param colorBetweenAnimations The color, that using between animations, recommended to set this to background or navigation bar color. - * @param content Content of the [NavigationHost]. Highly recommended to use [Navigator.composable]. - */ -@Composable -fun NavigationHost( - startScreen: Screen, - colorBetweenAnimations: Color = MaterialTheme.colorScheme.surface, - content: @Composable Navigator.() -> Unit -) { - val activity = LocalContext.current as ComponentActivity - val navigator by activity.viewModels( - extrasProducer = { - activity - .defaultViewModelCreationExtras - .withCreationCallback { factory -> - factory.create(startScreen) - } - } - ) - - BackHandler { - if (navigator.canGoBack()) { - navigator.navigateBack() - } else { - activity.finishAffinity() - } - } - - Box( - Modifier - .fillMaxSize() - .background(colorBetweenAnimations) - ) - - CompositionLocalProvider( - LocalNavigatorInstance provides navigator, - LocalNavigator provides { navigator.it() } - ) { - content(navigator) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/util/LifecycleExtensions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/util/LifecycleExtensions.kt index 1e26a511..abc5ab0b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/util/LifecycleExtensions.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/util/LifecycleExtensions.kt @@ -3,12 +3,17 @@ package ua.acclorite.book_story.presentation.core.util import android.content.Intent import android.view.WindowManager import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.platform.LocalContext -fun Intent.launchActivity( +inline fun Intent.launchActivity( activity: ComponentActivity, createChooser: Boolean = false, openInNewWindow: Boolean = true, - error: () -> Unit + success: () -> Unit = {}, + error: () -> Unit = {} ) { val intent = if (createChooser) Intent.createChooser(this, "") else this if (openInNewWindow) { @@ -22,6 +27,8 @@ fun Intent.launchActivity( error() return } + + success() } fun ComponentActivity.setBrightness(brightness: Float?) { @@ -29,4 +36,11 @@ fun ComponentActivity.setBrightness(brightness: Float?) { screenBrightness = brightness ?: WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE window.attributes = this } -} \ No newline at end of file +} + +val LocalActivity: ProvidableCompositionLocal + @Composable get() { + (LocalContext.current as ComponentActivity).let { activity -> + return compositionLocalOf { activity } + } + } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/credits/components/CreditItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditItem.kt similarity index 88% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/credits/components/CreditItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditItem.kt index b1020517..24c2eb02 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/credits/components/CreditItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.credits.components +package ua.acclorite.book_story.presentation.credits import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -19,16 +19,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.Credit +import ua.acclorite.book_story.domain.about.Credit -/** - * Credit Item. - * - * @param credit [Credit] item. - * @param horizontalPadding Horizontal item padding. - * @param verticalPadding Vertical item padding. - * @param onClick OnClick callback. - */ @OptIn(ExperimentalLayoutApi::class) @Composable fun CreditItem( @@ -81,11 +73,4 @@ fun CreditItem( } } } -} - - - - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsContent.kt new file mode 100644 index 00000000..644ea84e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsContent.kt @@ -0,0 +1,23 @@ +package ua.acclorite.book_story.presentation.credits + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.ui.about.AboutEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreditsContent( + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + navigateBack: () -> Unit +) { + CreditsScaffold( + scrollBehavior = scrollBehavior, + listState = listState, + navigateToBrowserPage = navigateToBrowserPage, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsLayout.kt new file mode 100644 index 00000000..6e1eb3b6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsLayout.kt @@ -0,0 +1,45 @@ +package ua.acclorite.book_story.presentation.credits + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideCredits +import ua.acclorite.book_story.ui.about.AboutEvent + +@Composable +fun CreditsLayout( + paddingValues: PaddingValues, + listState: LazyListState, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit +) { + val context = LocalContext.current + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + items( + Constants.provideCredits(), + key = { it.name } + ) { credit -> + CreditItem(credit = credit) { + credit.website?.let { website -> + navigateToBrowserPage( + AboutEvent.OnNavigateToBrowserPage( + page = website, + context = context + ) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsScaffold.kt new file mode 100644 index 00000000..8fbbad71 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsScaffold.kt @@ -0,0 +1,44 @@ +package ua.acclorite.book_story.presentation.credits + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import ua.acclorite.book_story.ui.about.AboutEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreditsScaffold( + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + CreditsTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + CreditsLayout( + paddingValues = paddingValues, + listState = listState, + navigateToBrowserPage = navigateToBrowserPage + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsTopBar.kt new file mode 100644 index 00000000..05f270eb --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/credits/CreditsTopBar.kt @@ -0,0 +1,33 @@ +package ua.acclorite.book_story.presentation.credits + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreditsTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.credits_option)) + }, + navigationIcon = { + NavigatorBackIconButton(navigateBack = navigateBack) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpAnnotation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpAnnotation.kt new file mode 100644 index 00000000..239e16b5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpAnnotation.kt @@ -0,0 +1,22 @@ +package ua.acclorite.book_story.presentation.help + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle + +@Composable +fun AnnotatedString.Builder.HelpAnnotation( + block: @Composable AnnotatedString.Builder.() -> Unit +) { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.tertiary, + fontWeight = FontWeight.Medium + ) + ) { + block() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpBottomBar.kt new file mode 100644 index 00000000..7033571f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpBottomBar.kt @@ -0,0 +1,41 @@ +package ua.acclorite.book_story.presentation.help + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.ui.browse.BrowseScreen +import ua.acclorite.book_story.ui.main.MainEvent + +@Composable +fun HelpBottomBar( + changeShowStartScreen: (MainEvent.OnChangeShowStartScreen) -> Unit, + navigateToBrowse: () -> Unit +) { + Column(Modifier.fillMaxWidth()) { + Spacer(modifier = Modifier.height(18.dp)) + Button( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 8.dp) + .padding(horizontal = 18.dp) + .fillMaxWidth(), + onClick = { + BrowseScreen.refreshListChannel.trySend(Unit) + changeShowStartScreen(MainEvent.OnChangeShowStartScreen(false)) + navigateToBrowse() + } + ) { + Text(text = stringResource(id = R.string.done)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpContent.kt new file mode 100644 index 00000000..250c99bb --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpContent.kt @@ -0,0 +1,29 @@ +package ua.acclorite.book_story.presentation.help + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.ui.main.MainEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HelpContent( + fromStart: Boolean, + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + changeShowStartScreen: (MainEvent.OnChangeShowStartScreen) -> Unit, + navigateToBrowse: () -> Unit, + navigateToStart: () -> Unit, + navigateBack: () -> Unit +) { + HelpScaffold( + fromStart = fromStart, + scrollBehavior = scrollBehavior, + listState = listState, + changeShowStartScreen = changeShowStartScreen, + navigateToBrowse = navigateToBrowse, + navigateToStart = navigateToStart, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpItem.kt similarity index 87% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/help/HelpItem.kt index 5be844d3..b801f422 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.help.components +package ua.acclorite.book_story.presentation.help import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState @@ -33,27 +33,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.HelpTip +import ua.acclorite.book_story.domain.help.HelpTip import ua.acclorite.book_story.domain.util.Position -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.ui.ExpandingTransition +import ua.acclorite.book_story.ui.theme.ExpandingTransition -/** - * Help Item. - * Can be Expanded or Collapsed. - * - * @param helpTip [HelpTip] to be shown. - * @param position Position of the [helpTip](top/center/bottom). - * @param fromStart Whether user came from Start screen. - */ @Composable fun HelpItem( helpTip: HelpTip, - position: Position, - fromStart: Boolean + position: Position ) { - val onNavigate = LocalNavigator.current val showDescription = rememberSaveable { mutableStateOf(false) } val animatedArrowRotation = animateFloatAsState( @@ -149,11 +138,7 @@ fun HelpItem( Spacer(modifier = Modifier.height(9.dp)) Text( text = buildAnnotatedString { - helpTip.description.invoke( - this@buildAnnotatedString, - onNavigate, - fromStart - ) + helpTip.description.invoke(this@buildAnnotatedString) }, style = MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurfaceVariant diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpLayout.kt new file mode 100644 index 00000000..e81df040 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpLayout.kt @@ -0,0 +1,42 @@ +package ua.acclorite.book_story.presentation.help + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.domain.util.Position +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideHelpTips + +@Composable +fun HelpLayout( + paddingValues: PaddingValues, + listState: LazyListState +) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(paddingValues), + state = listState, + contentPadding = PaddingValues(vertical = 16.dp) + ) { + itemsIndexed( + Constants.provideHelpTips(), + key = { _, helpTip -> helpTip.title } + ) { index, helpTip -> + HelpItem( + helpTip = helpTip, + position = when (index) { + 0 -> Position.TOP + Constants.provideHelpTips().lastIndex -> Position.BOTTOM + else -> Position.CENTER + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpScaffold.kt new file mode 100644 index 00000000..85212f6b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpScaffold.kt @@ -0,0 +1,54 @@ +package ua.acclorite.book_story.presentation.help + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import ua.acclorite.book_story.ui.main.MainEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HelpScaffold( + fromStart: Boolean, + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + changeShowStartScreen: (MainEvent.OnChangeShowStartScreen) -> Unit, + navigateToBrowse: () -> Unit, + navigateToStart: () -> Unit, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + HelpTopBar( + fromStart = fromStart, + scrollBehavior = scrollBehavior, + navigateToStart = navigateToStart, + navigateBack = navigateBack + ) + }, + bottomBar = { + if (fromStart) HelpBottomBar( + navigateToBrowse = navigateToBrowse, + changeShowStartScreen = changeShowStartScreen + ) + } + ) { paddingValues -> + HelpLayout( + paddingValues = paddingValues, + listState = listState + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpTopBar.kt new file mode 100644 index 00000000..b6d96a09 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/help/HelpTopBar.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.help + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.IconButton +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HelpTopBar( + fromStart: Boolean, + scrollBehavior: TopAppBarScrollBehavior, + navigateToStart: () -> Unit, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.help_screen)) + }, + navigationIcon = { + if (!fromStart) NavigatorBackIconButton(navigateBack = navigateBack) + }, + actions = { + if (!fromStart) { + IconButton( + icon = Icons.Outlined.RestartAlt, + contentDescription = R.string.reset_start_content_desc, + disableOnClick = false + ) { + navigateToStart() + } + } + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryBackHandler.kt new file mode 100644 index 00000000..0b4429ec --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryBackHandler.kt @@ -0,0 +1,21 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.ui.history.HistoryEvent + +@Composable +fun HistoryBackHandler( + showSearch: Boolean, + searchVisibility: (HistoryEvent.OnSearchVisibility) -> Unit, + navigateToLibrary: () -> Unit +) { + BackHandler { + if (showSearch) { + searchVisibility(HistoryEvent.OnSearchVisibility(false)) + return@BackHandler + } + + navigateToLibrary() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryContent.kt new file mode 100644 index 00000000..73c32ee2 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryContent.kt @@ -0,0 +1,73 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.history.GroupedHistory +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.history.HistoryEvent + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HistoryContent( + refreshState: PullRefreshState, + snackbarState: SnackbarHostState, + history: List, + dialog: Dialog?, + listState: LazyListState, + canScrollBackward: Boolean, + showSearch: Boolean, + isLoading: Boolean, + isRefreshing: Boolean, + historyIsEmpty: Boolean, + focusRequester: FocusRequester, + searchQuery: String, + searchVisibility: (HistoryEvent.OnSearchVisibility) -> Unit, + requestFocus: (HistoryEvent.OnRequestFocus) -> Unit, + searchQueryChange: (HistoryEvent.OnSearchQueryChange) -> Unit, + search: (HistoryEvent.OnSearch) -> Unit, + deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit, + showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit, + actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit, + dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit, + navigateToLibrary: () -> Unit, + navigateToBookInfo: (Int) -> Unit, + navigateToReader: (Int) -> Unit +) { + HistoryDialog( + dialog = dialog, + actionDeleteWholeHistoryDialog = actionDeleteWholeHistoryDialog, + dismissDialog = dismissDialog + ) + + HistoryScaffold( + refreshState = refreshState, + snackbarState = snackbarState, + history = history, + listState = listState, + canScrollBackward = canScrollBackward, + showSearch = showSearch, + isLoading = isLoading, + isRefreshing = isRefreshing, + historyIsEmpty = historyIsEmpty, + focusRequester = focusRequester, + searchQuery = searchQuery, + searchVisibility = searchVisibility, + requestFocus = requestFocus, + searchQueryChange = searchQueryChange, + search = search, + deleteHistoryEntry = deleteHistoryEntry, + showDeleteWholeHistoryDialog = showDeleteWholeHistoryDialog, + navigateToBookInfo = navigateToBookInfo, + navigateToReader = navigateToReader + ) + + HistoryBackHandler( + showSearch = showSearch, + searchVisibility = searchVisibility, + navigateToLibrary = navigateToLibrary + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryDeleteWholeHistoryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryDeleteWholeHistoryDialog.kt new file mode 100644 index 00000000..40f272d1 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryDeleteWholeHistoryDialog.kt @@ -0,0 +1,36 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.ui.history.HistoryEvent + +@Composable +fun HistoryDeleteWholeHistoryDialog( + actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit, + dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit +) { + val context = LocalContext.current + Dialog( + title = stringResource(id = R.string.delete_history), + imageVectorIcon = Icons.Outlined.DeleteOutline, + description = stringResource(id = R.string.delete_history_description), + actionText = stringResource(id = R.string.delete), + actionEnabled = true, + onDismiss = { + dismissDialog(HistoryEvent.OnDismissDialog) + }, + onAction = { + actionDeleteWholeHistoryDialog( + HistoryEvent.OnActionDeleteWholeHistoryDialog( + context = context + ) + ) + }, + withDivider = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryDialog.kt new file mode 100644 index 00000000..9d287565 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryDialog.kt @@ -0,0 +1,22 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.history.HistoryEvent +import ua.acclorite.book_story.ui.history.HistoryScreen + +@Composable +fun HistoryDialog( + dialog: Dialog?, + actionDeleteWholeHistoryDialog: (HistoryEvent.OnActionDeleteWholeHistoryDialog) -> Unit, + dismissDialog: (HistoryEvent.OnDismissDialog) -> Unit +) { + when (dialog) { + HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG -> { + HistoryDeleteWholeHistoryDialog( + actionDeleteWholeHistoryDialog = actionDeleteWholeHistoryDialog, + dismissDialog = dismissDialog + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEmptyPlaceholder.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEmptyPlaceholder.kt new file mode 100644 index 00000000..d6f9ef5e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryEmptyPlaceholder.kt @@ -0,0 +1,36 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility +import ua.acclorite.book_story.presentation.core.components.placeholder.EmptyPlaceholder +import ua.acclorite.book_story.ui.theme.Transitions + +@Composable +fun BoxScope.HistoryEmptyPlaceholder( + isLoading: Boolean, + isRefreshing: Boolean, + isHistoryEmpty: Boolean +) { + AnimatedVisibility( + visible = !isLoading + && isHistoryEmpty + && !isRefreshing, + modifier = Modifier.align(Alignment.Center), + enter = Transitions.DefaultTransitionIn, + exit = fadeOut(tween(0)) + ) { + EmptyPlaceholder( + modifier = Modifier.align(Alignment.Center), + message = stringResource(id = R.string.history_empty), + icon = painterResource(id = R.drawable.empty_history) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryItem.kt similarity index 73% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryItem.kt index cbe9f2d5..c663eca2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.history.components +package ua.acclorite.book_story.presentation.history import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -14,6 +14,7 @@ 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.lazy.LazyItemScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image @@ -22,7 +23,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -31,7 +32,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.presentation.core.components.common.AsyncImage import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.util.noRippleClickable @@ -39,44 +40,30 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * History list element item. - * - * @param modifier Modifier. - * @param history [History] item. - * @param isOnClickEnabled Whether OnClick is enabled. - * @param onBodyClick OnBodyClick callback. - * @param onTitleClick OnTitleClick callback. - * @param isDeleteEnabled Whether Delete button is enabled. - * @param onDeleteClick OnDeleteClick callback. - */ @Composable -fun HistoryItem( - modifier: Modifier = Modifier, - history: History, - isOnClickEnabled: Boolean, +fun LazyItemScope.HistoryItem( + historyEntry: History, + isRefreshing: Boolean, onBodyClick: () -> Unit, onTitleClick: () -> Unit, - isDeleteEnabled: Boolean, onDeleteClick: () -> Unit ) { - val date = remember(history) { Date(history.time) } - val pattern = remember { - SimpleDateFormat("HH:mm", Locale.getDefault()) - } + val date = rememberSaveable(historyEntry) { Date(historyEntry.time) } + val pattern = rememberSaveable { SimpleDateFormat("HH:mm", Locale.getDefault()) } Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = Modifier + .animateItem() .fillMaxWidth() - .clickable(enabled = isOnClickEnabled) { + .clickable(enabled = !isRefreshing) { onBodyClick() } .padding(top = 6.dp, bottom = 6.dp, start = 16.dp, end = 10.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(0.89f) + modifier = Modifier.weight(1f) ) { Box( modifier = Modifier @@ -87,9 +74,9 @@ fun HistoryItem( RoundedCornerShape(10.dp) ) ) { - if (history.book?.coverImage != null) { + if (historyEntry.book?.coverImage != null) { AsyncImage( - uri = history.book.coverImage, + uri = historyEntry.book.coverImage, modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(10.dp)) @@ -116,14 +103,14 @@ fun HistoryItem( modifier = Modifier.fillMaxHeight() ) { Text( - history.book?.title ?: return, + historyEntry.book?.title ?: return, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge, lineHeight = 21.sp, modifier = Modifier .fillMaxWidth() .noRippleClickable( - enabled = isOnClickEnabled, + enabled = !isRefreshing, onClick = onTitleClick ), maxLines = 2, @@ -140,16 +127,16 @@ fun HistoryItem( ) } } - Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) { - IconButton( - icon = Icons.Outlined.Delete, - contentDescription = R.string.delete_history_element_content_desc, - disableOnClick = true, - enabled = isDeleteEnabled, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) { - onDeleteClick() - } + + Spacer(Modifier.width(10.dp)) + IconButton( + icon = Icons.Outlined.Delete, + contentDescription = R.string.delete_history_element_content_desc, + disableOnClick = true, + enabled = !isRefreshing, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) { + onDeleteClick() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryLayout.kt new file mode 100644 index 00000000..d0e7788e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryLayout.kt @@ -0,0 +1,96 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.history.GroupedHistory +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle +import ua.acclorite.book_story.ui.history.HistoryEvent +import ua.acclorite.book_story.ui.theme.DefaultTransition + +@Composable +fun HistoryLayout( + listState: LazyListState, + history: List, + snackbarState: SnackbarHostState, + isLoading: Boolean, + isRefreshing: Boolean, + deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit, + navigateToBookInfo: (Int) -> Unit, + navigateToReader: (Int) -> Unit, +) { + val context = LocalActivity.current + + DefaultTransition(visible = !isLoading) { + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + state = listState, + scrollbarSettings = Constants.providePrimaryScrollbar(false) + ) { + item { + Spacer(modifier = Modifier.height(12.dp)) + } + + history.forEachIndexed { index, groupedHistory -> + item(key = groupedHistory.title) { + if (index > 0) { + Spacer(modifier = Modifier.height(8.dp)) + } + + SettingsSubcategoryTitle( + modifier = Modifier.animateItem(), + title = when (groupedHistory.title) { + "today" -> stringResource(id = R.string.today) + "yesterday" -> stringResource(id = R.string.yesterday) + else -> groupedHistory.title + }, + padding = 16.dp + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + + items( + groupedHistory.history, + key = { it.id } + ) { historyEntry -> + HistoryItem( + historyEntry = historyEntry, + isRefreshing = isRefreshing, + onBodyClick = { + navigateToBookInfo(historyEntry.bookId) + }, + onTitleClick = { + navigateToReader(historyEntry.bookId) + }, + onDeleteClick = { + deleteHistoryEntry( + HistoryEvent.OnDeleteHistoryEntry( + history = historyEntry, + snackbarState = snackbarState, + context = context, + ) + ) + } + ) + } + } + + item { + Spacer(modifier = Modifier.height(12.dp)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryRefreshIndicator.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryRefreshIndicator.kt new file mode 100644 index 00000000..4fa34d26 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryRefreshIndicator.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BoxScope.HistoryRefreshIndicator( + isRefreshing: Boolean, + refreshState: PullRefreshState +) { + PullRefreshIndicator( + isRefreshing, + refreshState, + Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryScaffold.kt new file mode 100644 index 00000000..1bed3785 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryScaffold.kt @@ -0,0 +1,96 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.history.GroupedHistory +import ua.acclorite.book_story.presentation.core.components.common.Snackbar +import ua.acclorite.book_story.ui.history.HistoryEvent + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HistoryScaffold( + refreshState: PullRefreshState, + snackbarState: SnackbarHostState, + history: List, + listState: LazyListState, + canScrollBackward: Boolean, + showSearch: Boolean, + isLoading: Boolean, + isRefreshing: Boolean, + historyIsEmpty: Boolean, + focusRequester: FocusRequester, + searchQuery: String, + searchVisibility: (HistoryEvent.OnSearchVisibility) -> Unit, + requestFocus: (HistoryEvent.OnRequestFocus) -> Unit, + searchQueryChange: (HistoryEvent.OnSearchQueryChange) -> Unit, + deleteHistoryEntry: (HistoryEvent.OnDeleteHistoryEntry) -> Unit, + showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit, + search: (HistoryEvent.OnSearch) -> Unit, + navigateToBookInfo: (Int) -> Unit, + navigateToReader: (Int) -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .pullRefresh(refreshState), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + HistoryTopBar( + canScrollBackward = canScrollBackward, + showSearch = showSearch, + isLoading = isLoading, + isRefreshing = isRefreshing, + historyIsNotEmpty = !historyIsEmpty, + focusRequester = focusRequester, + searchQuery = searchQuery, + searchVisibility = searchVisibility, + requestFocus = requestFocus, + searchQueryChange = searchQueryChange, + showDeleteWholeHistoryDialog = showDeleteWholeHistoryDialog, + search = search + ) + }, + bottomBar = { + Snackbar(snackbarState = snackbarState) + } + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()) + ) { + HistoryLayout( + listState = listState, + history = history, + snackbarState = snackbarState, + isLoading = isLoading, + isRefreshing = isRefreshing, + deleteHistoryEntry = deleteHistoryEntry, + navigateToBookInfo = navigateToBookInfo, + navigateToReader = navigateToReader + ) + + HistoryEmptyPlaceholder( + isLoading = isLoading, + isRefreshing = isRefreshing, + isHistoryEmpty = historyIsEmpty + ) + + HistoryRefreshIndicator( + isRefreshing = isRefreshing, + refreshState = refreshState + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryTopBar.kt new file mode 100644 index 00000000..77ee73b5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/history/HistoryTopBar.kt @@ -0,0 +1,112 @@ +package ua.acclorite.book_story.presentation.history + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.DeleteSweep +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.IconButton +import ua.acclorite.book_story.presentation.core.components.common.SearchTextField +import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBar +import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBarData +import ua.acclorite.book_story.presentation.navigator.NavigatorIconButton +import ua.acclorite.book_story.ui.history.HistoryEvent + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HistoryTopBar( + canScrollBackward: Boolean, + showSearch: Boolean, + isLoading: Boolean, + isRefreshing: Boolean, + historyIsNotEmpty: Boolean, + focusRequester: FocusRequester, + searchQuery: String, + searchVisibility: (HistoryEvent.OnSearchVisibility) -> Unit, + requestFocus: (HistoryEvent.OnRequestFocus) -> Unit, + searchQueryChange: (HistoryEvent.OnSearchQueryChange) -> Unit, + showDeleteWholeHistoryDialog: (HistoryEvent.OnShowDeleteWholeHistoryDialog) -> Unit, + search: (HistoryEvent.OnSearch) -> Unit, +) { + TopAppBar( + scrollBehavior = null, + isTopBarScrolled = canScrollBackward, + + shownTopBar = when { + showSearch -> 1 + else -> 0 + }, + topBars = listOf( + TopAppBarData( + contentID = 0, + contentNavigationIcon = {}, + contentTitle = { + Text(stringResource(id = R.string.history_screen)) + }, + contentActions = { + IconButton( + icon = Icons.Default.Search, + contentDescription = R.string.search_content_desc, + disableOnClick = true, + ) { + searchVisibility(HistoryEvent.OnSearchVisibility(true)) + } + IconButton( + icon = Icons.Outlined.DeleteSweep, + contentDescription = R.string.delete_whole_history_content_desc, + disableOnClick = false, + enabled = !isLoading + && !isRefreshing + && historyIsNotEmpty + ) { + showDeleteWholeHistoryDialog( + HistoryEvent.OnShowDeleteWholeHistoryDialog + ) + } + NavigatorIconButton() + } + ), + + TopAppBarData( + contentID = 1, + contentNavigationIcon = { + IconButton( + icon = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = R.string.exit_search_content_desc, + disableOnClick = true + ) { + searchVisibility(HistoryEvent.OnSearchVisibility(false)) + } + }, + contentTitle = { + SearchTextField( + modifier = Modifier + .focusRequester(focusRequester) + .onGloballyPositioned { + requestFocus(HistoryEvent.OnRequestFocus(focusRequester)) + }, + query = searchQuery, + onQueryChange = { + searchQueryChange(HistoryEvent.OnSearchQueryChange(it)) + }, + onSearch = { + search(HistoryEvent.OnSearch) + } + ) + }, + contentActions = { + NavigatorIconButton() + } + ) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryBackHandler.kt new file mode 100644 index 00000000..8c3098cb --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryBackHandler.kt @@ -0,0 +1,61 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.library.LibraryEvent + +@Composable +fun LibraryBackHandler( + hasSelectedItems: Boolean, + showSearch: Boolean, + pagerState: PagerState, + doublePressExit: Boolean, + clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit, + searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit +) { + val activity = LocalActivity.current + val scope = rememberCoroutineScope() + var shouldExit = rememberSaveable { false } + + BackHandler { + if (hasSelectedItems) { + clearSelectedBooks(LibraryEvent.OnClearSelectedBooks) + return@BackHandler + } + + if (showSearch) { + searchVisibility(LibraryEvent.OnSearchVisibility(false)) + return@BackHandler + } + + if (pagerState.currentPage > 0) { + scope.launch { + pagerState.animateScrollToPage(0) + } + return@BackHandler + } + + if (shouldExit || !doublePressExit) { + activity.finish() + return@BackHandler + } + + activity + .getString(R.string.press_again_toast) + .showToast(context = activity, longToast = false) + shouldExit = true + + scope.launch { + delay(1500) + shouldExit = false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryContent.kt new file mode 100644 index 00000000..36f6b558 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryContent.kt @@ -0,0 +1,88 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.library.LibraryEvent + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun LibraryContent( + books: List, + selectedItemsCount: Int, + hasSelectedItems: Boolean, + showSearch: Boolean, + searchQuery: String, + bookCount: Int, + focusRequester: FocusRequester, + pagerState: PagerState, + isLoading: Boolean, + isRefreshing: Boolean, + doublePressExit: Boolean, + categories: List, + refreshState: PullRefreshState, + dialog: Dialog?, + selectBook: (LibraryEvent.OnSelectBook) -> Unit, + searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit, + requestFocus: (LibraryEvent.OnRequestFocus) -> Unit, + searchQueryChange: (LibraryEvent.OnSearchQueryChange) -> Unit, + search: (LibraryEvent.OnSearch) -> Unit, + clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit, + showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit, + showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit, + actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit, + actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit, + dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit, + navigateToBrowse: () -> Unit, + navigateToBookInfo: (id: Int) -> Unit, + navigateToReader: (id: Int) -> Unit +) { + LibraryDialog( + dialog = dialog, + books = books, + categories = categories, + selectedItemsCount = selectedItemsCount, + actionMoveDialog = actionMoveDialog, + actionDeleteDialog = actionDeleteDialog, + dismissDialog = dismissDialog + ) + + LibraryScaffold( + selectedItemsCount = selectedItemsCount, + hasSelectedItems = hasSelectedItems, + showSearch = showSearch, + searchQuery = searchQuery, + bookCount = bookCount, + focusRequester = focusRequester, + pagerState = pagerState, + isLoading = isLoading, + isRefreshing = isRefreshing, + categories = categories, + searchVisibility = searchVisibility, + requestFocus = requestFocus, + searchQueryChange = searchQueryChange, + search = search, + selectBook = selectBook, + clearSelectedBooks = clearSelectedBooks, + showMoveDialog = showMoveDialog, + showDeleteDialog = showDeleteDialog, + refreshState = refreshState, + navigateToBrowse = navigateToBrowse, + navigateToBookInfo = navigateToBookInfo, + navigateToReader = navigateToReader + ) + + LibraryBackHandler( + hasSelectedItems = hasSelectedItems, + showSearch = showSearch, + pagerState = pagerState, + doublePressExit = doublePressExit, + clearSelectedBooks = clearSelectedBooks, + searchVisibility = searchVisibility, + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDeleteDialog.kt new file mode 100644 index 00000000..b67852ad --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDeleteDialog.kt @@ -0,0 +1,41 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.ui.library.LibraryEvent + +@Composable +fun LibraryDeleteDialog( + selectedItemsCount: Int, + actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit, + dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit +) { + val context = LocalContext.current + + Dialog( + title = stringResource(id = R.string.delete_books), + imageVectorIcon = Icons.Outlined.DeleteOutline, + description = stringResource( + id = R.string.delete_books_description, + selectedItemsCount + ), + actionText = stringResource(id = R.string.delete), + actionEnabled = true, + onDismiss = { + dismissDialog(LibraryEvent.OnDismissDialog) + }, + onAction = { + actionDeleteDialog( + LibraryEvent.OnActionDeleteDialog( + context = context + ) + ) + }, + withDivider = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDialog.kt new file mode 100644 index 00000000..1a179d88 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDialog.kt @@ -0,0 +1,39 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.library.LibraryEvent +import ua.acclorite.book_story.ui.library.LibraryScreen + +@Composable +fun LibraryDialog( + dialog: Dialog?, + books: List, + categories: List, + selectedItemsCount: Int, + actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit, + actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit, + dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit +) { + when (dialog) { + LibraryScreen.MOVE_DIALOG -> { + LibraryMoveDialog( + books = books, + categories = categories, + selectedItemsCount = selectedItemsCount, + actionMoveDialog = actionMoveDialog, + dismissDialog = dismissDialog + ) + } + + LibraryScreen.DELETE_DIALOG -> { + LibraryDeleteDialog( + selectedItemsCount = selectedItemsCount, + actionDeleteDialog = actionDeleteDialog, + dismissDialog = dismissDialog + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryEmptyPlaceholder.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryEmptyPlaceholder.kt new file mode 100644 index 00000000..fed276af --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryEmptyPlaceholder.kt @@ -0,0 +1,40 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.placeholder.EmptyPlaceholder +import ua.acclorite.book_story.ui.theme.Transitions + +@Composable +fun BoxScope.LibraryEmptyPlaceholder( + isLoading: Boolean, + isRefreshing: Boolean, + isBooksEmpty: Boolean, + navigateToBrowse: () -> Unit +) { + AnimatedVisibility( + visible = !isLoading + && !isRefreshing + && isBooksEmpty, + modifier = Modifier.align(Alignment.Center), + enter = Transitions.DefaultTransitionIn, + exit = fadeOut(tween(0)) + ) { + EmptyPlaceholder( + message = stringResource(id = R.string.library_empty), + icon = painterResource(id = R.drawable.empty_library), + modifier = Modifier.align(Alignment.Center), + actionTitle = stringResource(id = R.string.add_book) + ) { + navigateToBrowse() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryItem.kt similarity index 80% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryItem.kt index a7dca315..20b7a592 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.library.components +package ua.acclorite.book_story.presentation.library import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -12,6 +12,7 @@ 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.lazy.grid.LazyGridItemScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.PlayArrow @@ -31,40 +32,31 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.util.Selected +import ua.acclorite.book_story.domain.library.book.SelectableBook import ua.acclorite.book_story.presentation.core.components.common.AsyncImage import ua.acclorite.book_story.presentation.core.util.calculateProgress -/** - * Library list element item. - * - * @param book [Pair] of [Book] and [Selected] (Boolean). - * @param modifier Modifier. - * @param onCoverImageClick OnCoverImageClick callback. - * @param onLongClick OnLongClick callback. - * @param onButtonClick OnButtonClick callback. - */ @OptIn(ExperimentalFoundationApi::class) @Composable -fun LibraryBookItem( - book: Pair, - modifier: Modifier = Modifier, - onCoverImageClick: () -> Unit, - onLongClick: () -> Unit, - onButtonClick: () -> Unit +fun LazyGridItemScope.LibraryItem( + book: SelectableBook, + hasSelectedItems: Boolean, + selectBook: (select: Boolean?) -> Unit, + navigateToBookInfo: () -> Unit, + navigateToReader: () -> Unit ) { - val backgroundColor = if (book.second) MaterialTheme.colorScheme.primary + val backgroundColor = if (book.selected) MaterialTheme.colorScheme.primary else Color.Transparent - val fontColor = if (book.second) MaterialTheme.colorScheme.onPrimary + val fontColor = if (book.selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface - val progress = rememberSaveable(book.first.progress) { - "${book.first.progress.calculateProgress(1)}%" + val progress = rememberSaveable(book.data.progress) { + "${book.data.progress.calculateProgress(1)}%" } Column( - modifier + Modifier + .animateItem() .padding(3.dp) .background( backgroundColor, @@ -84,16 +76,17 @@ fun LibraryBookItem( ) .combinedClickable( onClick = { - onCoverImageClick() + if (hasSelectedItems) selectBook(null) + else navigateToBookInfo() }, onLongClick = { - onLongClick() + if (!hasSelectedItems) selectBook(true) } ) ) { - if (book.first.coverImage != null) { + if (book.data.coverImage != null) { AsyncImage( - uri = book.first.coverImage!!, + uri = book.data.coverImage, modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.medium) @@ -125,7 +118,7 @@ fun LibraryBookItem( ) FilledIconButton( - onClick = { onButtonClick() }, + onClick = { navigateToReader() }, modifier = Modifier .align(Alignment.BottomEnd) .padding(6.dp) @@ -147,7 +140,7 @@ fun LibraryBookItem( } Spacer(modifier = Modifier.height(6.dp)) Text( - book.first.title, + book.data.title, color = fontColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryLayout.kt new file mode 100644 index 00000000..9401f7e8 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryLayout.kt @@ -0,0 +1,26 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.presentation.core.components.common.LazyVerticalGridWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar + +@Composable +fun LibraryLayout( + items: LazyGridScope.() -> Unit +) { + LazyVerticalGridWithScrollbar( + columns = GridCells.Adaptive(120.dp), + modifier = Modifier.fillMaxSize(), + scrollbarSettings = Constants.providePrimaryScrollbar(false), + contentPadding = PaddingValues(8.dp) + ) { + items() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryMoveDialog.kt new file mode 100644 index 00000000..7e503021 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryMoveDialog.kt @@ -0,0 +1,80 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.DriveFileMove +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem +import ua.acclorite.book_story.ui.library.LibraryEvent + +@Composable +fun LibraryMoveDialog( + books: List, + categories: List, + selectedItemsCount: Int, + actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit, + dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit +) { + val context = LocalContext.current + val selectedBooks = remember { + derivedStateOf { + books.filter { it.selected } + } + } + val moveCategories = remember { + derivedStateOf { + categories.mapNotNull { category -> + if (!selectedBooks.value.all { it.data.category == category.category }) { + return@mapNotNull category + } + return@mapNotNull null + } + } + } + var selectedCategory = remember { + mutableStateOf(moveCategories.value[0]) + } + + Dialog( + title = stringResource(id = R.string.move_books), + imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove, + description = stringResource( + id = R.string.move_books_description, + selectedItemsCount + ), + actionText = stringResource(id = R.string.move), + actionEnabled = true, + onDismiss = { + dismissDialog(LibraryEvent.OnDismissDialog) + }, + onAction = { + actionMoveDialog( + LibraryEvent.OnActionMoveDialog( + selectedCategory = selectedCategory.value.category, + categories = categories, + context = context + ) + ) + }, + withDivider = false, + items = { + items(moveCategories.value) { category -> + SelectableDialogItem( + selected = category == selectedCategory.value, + title = category.title.asString() + ) { + selectedCategory.value = category + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryPager.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryPager.kt new file mode 100644 index 00000000..db207c30 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryPager.kt @@ -0,0 +1,68 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.ui.library.LibraryEvent +import ua.acclorite.book_story.ui.theme.DefaultTransition + +@Composable +fun LibraryPager( + pagerState: PagerState, + categories: List, + hasSelectedItems: Boolean, + isLoading: Boolean, + isRefreshing: Boolean, + selectBook: (LibraryEvent.OnSelectBook) -> Unit, + navigateToBrowse: () -> Unit, + navigateToBookInfo: (id: Int) -> Unit, + navigateToReader: (id: Int) -> Unit, +) { + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { index -> + val category = remember(categories, index) { + derivedStateOf { + categories[index] + } + } + + Box(modifier = Modifier.fillMaxSize()) { + DefaultTransition(visible = !isLoading) { + LibraryLayout { + items( + category.value.books, + key = { it.data.id } + ) { book -> + LibraryItem( + book = book, + hasSelectedItems = hasSelectedItems, + selectBook = { select -> + selectBook( + LibraryEvent.OnSelectBook( + id = book.data.id, + select = select + ) + ) + }, + navigateToBookInfo = { navigateToBookInfo(book.data.id) }, + navigateToReader = { navigateToReader(book.data.id) }, + ) + } + } + } + + LibraryEmptyPlaceholder( + isLoading = isLoading, + isRefreshing = isRefreshing, + isBooksEmpty = category.value.books.isEmpty(), + navigateToBrowse = navigateToBrowse + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryRefreshIndicator.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryRefreshIndicator.kt new file mode 100644 index 00000000..7059874a --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryRefreshIndicator.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun BoxScope.LibraryRefreshIndicator( + isRefreshing: Boolean, + refreshState: PullRefreshState +) { + PullRefreshIndicator( + isRefreshing, + refreshState, + Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryScaffold.kt new file mode 100644 index 00000000..6627778b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryScaffold.kt @@ -0,0 +1,94 @@ +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.ui.library.LibraryEvent + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun LibraryScaffold( + selectedItemsCount: Int, + hasSelectedItems: Boolean, + showSearch: Boolean, + searchQuery: String, + bookCount: Int, + focusRequester: FocusRequester, + pagerState: PagerState, + isLoading: Boolean, + isRefreshing: Boolean, + refreshState: PullRefreshState, + categories: List, + searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit, + selectBook: (LibraryEvent.OnSelectBook) -> Unit, + requestFocus: (LibraryEvent.OnRequestFocus) -> Unit, + searchQueryChange: (LibraryEvent.OnSearchQueryChange) -> Unit, + search: (LibraryEvent.OnSearch) -> Unit, + clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit, + showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit, + showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit, + navigateToBrowse: () -> Unit, + navigateToBookInfo: (id: Int) -> Unit, + navigateToReader: (id: Int) -> Unit, +) { + Scaffold( + Modifier + .fillMaxSize() + .pullRefresh(refreshState), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + LibraryTopBar( + selectedItemsCount = selectedItemsCount, + hasSelectedItems = hasSelectedItems, + showSearch = showSearch, + searchQuery = searchQuery, + bookCount = bookCount, + focusRequester = focusRequester, + pagerState = pagerState, + isLoading = isLoading, + isRefreshing = isRefreshing, + categories = categories, + searchVisibility = searchVisibility, + requestFocus = requestFocus, + searchQueryChange = searchQueryChange, + search = search, + clearSelectedBooks = clearSelectedBooks, + showMoveDialog = showMoveDialog, + showDeleteDialog = showDeleteDialog + ) + } + ) { paddingValues -> + Box( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()) + ) { + LibraryPager( + pagerState = pagerState, + categories = categories, + hasSelectedItems = hasSelectedItems, + isLoading = isLoading, + isRefreshing = isRefreshing, + selectBook = selectBook, + navigateToBrowse = navigateToBrowse, + navigateToReader = navigateToReader, + navigateToBookInfo = navigateToBookInfo, + ) + + LibraryRefreshIndicator( + isRefreshing = isRefreshing, + refreshState = refreshState + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTabs.kt similarity index 65% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTabs.kt index 886b4793..e33ef2d4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTabs.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.library.components +package ua.acclorite.book_story.presentation.library import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background @@ -18,53 +18,21 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.CategorizedBooks -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent +import kotlinx.coroutines.launch +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks -/** - * Tab row, either scrollable or static, depends on screen width. - * - * @param onEvent Event used to Scroll to Page. - * @param itemCountBackgroundColor Item count background color. - * @param books The list of all books. - * @param pagerState [PagerState]. - */ @Composable -fun LibraryTabRow( - onEvent: (LibraryEvent) -> Unit, +fun LibraryTabs( + categories: List, + pagerState: PagerState, itemCountBackgroundColor: Color, - books: List, - pagerState: PagerState ) { - val context = LocalContext.current - val tabItems = remember(books) { - listOf( - Pair( - context.getString(R.string.reading_tab), - books.find { it.category == Category.READING }?.books?.size ?: 0 - ), - Pair( - context.getString(R.string.already_read_tab), - books.find { it.category == Category.ALREADY_READ }?.books?.size ?: 0 - ), - Pair( - context.getString(R.string.planning_tab), - books.find { it.category == Category.PLANNING }?.books?.size ?: 0 - ), - Pair( - context.getString(R.string.dropped_tab), - books.find { it.category == Category.DROPPED }?.books?.size ?: 0 - ) - ) - } + val scope = rememberCoroutineScope() Box(Modifier.fillMaxWidth()) { HorizontalDivider( @@ -95,24 +63,26 @@ fun LibraryTabRow( } } ) { - tabItems.forEachIndexed { index, tabItem -> + categories.forEachIndexed { index, tabItem -> Tab( selected = pagerState.currentPage == index, onClick = { - onEvent(LibraryEvent.OnScrollToPage(index, pagerState)) + scope.launch { + pagerState.animateScrollToPage(index) + } }, selectedContentColor = MaterialTheme.colorScheme.primary, unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, text = { Row(verticalAlignment = Alignment.CenterVertically) { Text( - tabItem.first, + tabItem.title.asString(), style = MaterialTheme.typography.bodyLarge, maxLines = 1, ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = tabItem.second.toString(), + text = tabItem.books.count().toString(), modifier = Modifier .background( itemCountBackgroundColor, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTopBar.kt similarity index 67% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTopBar.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTopBar.kt index eed56e0e..0aa3fad4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTopBar.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.library.components +package ua.acclorite.book_story.presentation.library import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring @@ -20,7 +20,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.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.focus.FocusRequester @@ -31,34 +30,48 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.components.common.SearchTextField import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBar import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBarData -import ua.acclorite.book_story.presentation.core.navigation.NavigationIconButton -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +import ua.acclorite.book_story.presentation.navigator.NavigatorIconButton +import ua.acclorite.book_story.ui.library.LibraryEvent -/** - * Library Top Bar. - * Contains [TopAppBar] and [LibraryTabRow]. - * - * @param pagerState [PagerState]. - */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LibraryTopBar(pagerState: PagerState) { - val state = LibraryViewModel.getState() - val onEvent = LibraryViewModel.getEvent() - val focusRequester = remember { FocusRequester() } +fun LibraryTopBar( + selectedItemsCount: Int, + hasSelectedItems: Boolean, + showSearch: Boolean, + searchQuery: String, + bookCount: Int, + focusRequester: FocusRequester, + pagerState: PagerState, + isLoading: Boolean, + isRefreshing: Boolean, + categories: List, + searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit, + requestFocus: (LibraryEvent.OnRequestFocus) -> Unit, + searchQueryChange: (LibraryEvent.OnSearchQueryChange) -> Unit, + search: (LibraryEvent.OnSearch) -> Unit, + clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit, + showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit, + showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit +) { + val animatedItemCountBackgroundColor = animateColorAsState( + if (hasSelectedItems) MaterialTheme.colorScheme.surfaceContainerHighest + else MaterialTheme.colorScheme.surfaceContainer, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow) + ) TopAppBar( scrollBehavior = null, - isTopBarScrolled = state.value.hasSelectedItems, + isTopBarScrolled = hasSelectedItems, shownTopBar = when { - state.value.hasSelectedItems -> 2 - state.value.showSearch -> 1 + hasSelectedItems -> 2 + showSearch -> 1 else -> 0 }, topBars = listOf( @@ -70,7 +83,7 @@ fun LibraryTopBar(pagerState: PagerState) { Text(stringResource(id = R.string.library_screen)) Spacer(modifier = Modifier.width(6.dp)) Text( - text = state.value.books.size.toString(), + text = bookCount.toString(), modifier = Modifier .background( MaterialTheme.colorScheme.surfaceContainer, @@ -88,9 +101,9 @@ fun LibraryTopBar(pagerState: PagerState) { contentDescription = R.string.search_content_desc, disableOnClick = true, ) { - onEvent(LibraryEvent.OnSearchShowHide) + searchVisibility(LibraryEvent.OnSearchVisibility(true)) } - NavigationIconButton() + NavigatorIconButton() } ), @@ -102,9 +115,7 @@ fun LibraryTopBar(pagerState: PagerState) { contentDescription = R.string.exit_search_content_desc, disableOnClick = true ) { - onEvent( - LibraryEvent.OnSearchShowHide - ) + searchVisibility(LibraryEvent.OnSearchVisibility(false)) } }, contentTitle = { @@ -112,19 +123,19 @@ fun LibraryTopBar(pagerState: PagerState) { modifier = Modifier .focusRequester(focusRequester) .onGloballyPositioned { - onEvent(LibraryEvent.OnRequestFocus(focusRequester)) + requestFocus(LibraryEvent.OnRequestFocus(focusRequester)) }, - query = state.value.searchQuery, + query = searchQuery, onQueryChange = { - onEvent(LibraryEvent.OnSearchQueryChange(it)) + searchQueryChange(LibraryEvent.OnSearchQueryChange(it)) }, onSearch = { - onEvent(LibraryEvent.OnSearch) + search(LibraryEvent.OnSearch) } ) }, contentActions = { - NavigationIconButton() + NavigatorIconButton() }, ), @@ -136,14 +147,14 @@ fun LibraryTopBar(pagerState: PagerState) { contentDescription = R.string.clear_selected_items_content_desc, disableOnClick = true ) { - onEvent(LibraryEvent.OnClearSelectedBooks) + clearSelectedBooks(LibraryEvent.OnClearSelectedBooks) } }, contentTitle = { Text( stringResource( id = R.string.selected_items_count_query, - state.value.selectedItemsCount.coerceAtLeast(1) + selectedItemsCount.coerceAtLeast(1) ), overflow = TextOverflow.Ellipsis, maxLines = 1 @@ -153,37 +164,27 @@ fun LibraryTopBar(pagerState: PagerState) { IconButton( icon = Icons.AutoMirrored.Outlined.DriveFileMove, contentDescription = R.string.move_books_content_desc, - enabled = !state.value.isLoading - && !state.value.isRefreshing - && !state.value.showMoveDialog, + enabled = !isLoading && !isRefreshing, disableOnClick = false, - - ) { - onEvent(LibraryEvent.OnShowHideMoveDialog) + ) { + showMoveDialog(LibraryEvent.OnShowMoveDialog) } IconButton( icon = Icons.Outlined.Delete, contentDescription = R.string.delete_books_content_desc, - enabled = !state.value.isLoading - && !state.value.isRefreshing - && !state.value.showDeleteDialog, + enabled = !isLoading && !isRefreshing, disableOnClick = false ) { - onEvent(LibraryEvent.OnShowHideDeleteDialog) + showDeleteDialog(LibraryEvent.OnShowDeleteDialog) } } ), ), customContent = { - LibraryTabRow( - onEvent = onEvent, - books = state.value.categorizedBooks, - itemCountBackgroundColor = animateColorAsState( - if (state.value.hasSelectedItems) MaterialTheme.colorScheme.surfaceContainerHighest - else MaterialTheme.colorScheme.surfaceContainer, - animationSpec = spring(stiffness = Spring.StiffnessMediumLow) - ).value, - pagerState = pagerState + LibraryTabs( + categories = categories, + pagerState = pagerState, + itemCountBackgroundColor = animatedItemCountBackgroundColor.value ) } ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoContent.kt new file mode 100644 index 00000000..b8d4cefc --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoContent.kt @@ -0,0 +1,26 @@ +package ua.acclorite.book_story.presentation.license_info + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import com.mikepenz.aboutlibraries.entity.Library +import ua.acclorite.book_story.ui.about.AboutEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicenseInfoContent( + library: Library?, + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + navigateBack: () -> Unit +) { + LicenseInfoScaffold( + library = library, + scrollBehavior = scrollBehavior, + listState = listState, + navigateToBrowserPage = navigateToBrowserPage, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoItem.kt new file mode 100644 index 00000000..0ebca679 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoItem.kt @@ -0,0 +1,73 @@ +package ua.acclorite.book_story.presentation.license_info + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.mikepenz.aboutlibraries.entity.License +import ua.acclorite.book_story.ui.theme.SlidingTransition + +@Composable +fun LazyItemScope.LicenseInfoItem( + license: License +) { + val showed = rememberSaveable { + mutableStateOf(false) + } + + Column( + modifier = Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null + ) + ) { + Text( + text = license.name, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .clip(RoundedCornerShape(100)) + .clickable(enabled = license.licenseContent?.isNotBlank() == true) { + showed.value = !showed.value + } + .background( + MaterialTheme.colorScheme.primary, + RoundedCornerShape(100) + ) + .padding(vertical = 6.dp, horizontal = 12.dp) + ) + + SlidingTransition( + visible = showed.value && license.licenseContent?.isNotBlank() == true, + modifier = Modifier.padding(horizontal = 8.dp) + ) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = license.licenseContent!! + .lines() + .joinToString(separator = "\n") { + it.trim() + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoLayout.kt new file mode 100644 index 00000000..1c69d079 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoLayout.kt @@ -0,0 +1,36 @@ +package ua.acclorite.book_story.presentation.license_info + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mikepenz.aboutlibraries.entity.Library +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun LicenseInfoLayout( + library: Library, + paddingValues: PaddingValues, + listState: LazyListState +) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items( + library.licenses.toList(), + key = { it.name } + ) { license -> + LicenseInfoItem(license = license) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoScaffold.kt new file mode 100644 index 00000000..e3b9d220 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoScaffold.kt @@ -0,0 +1,53 @@ +package ua.acclorite.book_story.presentation.license_info + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import com.mikepenz.aboutlibraries.entity.Library +import ua.acclorite.book_story.ui.about.AboutEvent +import ua.acclorite.book_story.ui.theme.DefaultTransition + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicenseInfoScaffold( + library: Library?, + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + DefaultTransition(library != null) { + LicenseInfoTopBar( + library = library!!, + scrollBehavior = scrollBehavior, + navigateToBrowserPage = navigateToBrowserPage, + navigateBack = navigateBack + ) + } + } + ) { paddingValues -> + DefaultTransition(library != null) { + LicenseInfoLayout( + library = library!!, + paddingValues = paddingValues, + listState = listState + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoTopBar.kt new file mode 100644 index 00000000..1d16de45 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/license_info/LicenseInfoTopBar.kt @@ -0,0 +1,63 @@ +package ua.acclorite.book_story.presentation.license_info + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Language +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import com.mikepenz.aboutlibraries.entity.Library +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.IconButton +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton +import ua.acclorite.book_story.ui.about.AboutEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicenseInfoTopBar( + library: Library, + scrollBehavior: TopAppBarScrollBehavior, + navigateToBrowserPage: (AboutEvent.OnNavigateToBrowserPage) -> Unit, + navigateBack: () -> Unit +) { + val context = LocalContext.current + + LargeTopAppBar( + title = { + Text( + library.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + NavigatorBackIconButton(navigateBack = navigateBack) + }, + actions = { + if (library.website?.isNotBlank() == true) { + IconButton( + icon = Icons.Outlined.Language, + contentDescription = R.string.open_in_web_content_desc, + disableOnClick = false + ) { + navigateToBrowserPage( + AboutEvent.OnNavigateToBrowserPage( + page = library.website!!, + context = context + ) + ) + } + } + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesContent.kt new file mode 100644 index 00000000..19764a8d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesContent.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.licenses + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import com.mikepenz.aboutlibraries.entity.Library + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicensesContent( + scrollBehavior: TopAppBarScrollBehavior, + licenses: List, + listState: LazyListState, + navigateToLicenseInfo: (Library) -> Unit, + navigateBack: () -> Unit +) { + LicensesScaffold( + scrollBehavior = scrollBehavior, + licenses = licenses, + listState = listState, + navigateToLicenseInfo = navigateToLicenseInfo, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/components/LicenseItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesItem.kt similarity index 88% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/components/LicenseItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesItem.kt index 8f45a0a4..8a724cc6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/components/LicenseItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.licenses.components +package ua.acclorite.book_story.presentation.licenses import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -22,34 +22,22 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.ui.compose.m3.util.author import ua.acclorite.book_story.R -/** - * License Item. - * - * @param modifier Modifier. - * @param library [Library] item. - * @param horizontalPadding Horizontal item padding. - * @param verticalPadding Vertical item padding. - */ @OptIn(ExperimentalLayoutApi::class) @Composable -fun LicenseItem( - modifier: Modifier = Modifier, +fun LicensesItem( library: Library, - horizontalPadding: Dp = 18.dp, - verticalPadding: Dp = 12.dp, onClick: () -> Unit ) { Column( - modifier = modifier + modifier = Modifier .fillMaxWidth() .clickable { onClick.invoke() } - .padding(vertical = verticalPadding, horizontal = horizontalPadding) + .padding(vertical = 12.dp, horizontal = 18.dp) ) { Row { Text( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesLayout.kt new file mode 100644 index 00000000..494a5946 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesLayout.kt @@ -0,0 +1,46 @@ +package ua.acclorite.book_story.presentation.licenses + +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mikepenz.aboutlibraries.entity.Library +import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun LicensesLayout( + licenses: List, + paddingValues: PaddingValues, + listState: LazyListState, + navigateToLicenseInfo: (Library) -> Unit +) { + AnimatedVisibility( + visible = licenses.isNotEmpty(), + enter = fadeIn(), + exit = fadeOut() + ) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState, + contentPadding = PaddingValues(vertical = 12.dp) + ) { + items( + licenses, + key = { it.uniqueId } + ) { library -> + LicensesItem(library = library) { + navigateToLicenseInfo(library) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesScaffold.kt new file mode 100644 index 00000000..fac0e52c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesScaffold.kt @@ -0,0 +1,46 @@ +package ua.acclorite.book_story.presentation.licenses + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import com.mikepenz.aboutlibraries.entity.Library + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicensesScaffold( + scrollBehavior: TopAppBarScrollBehavior, + licenses: List, + listState: LazyListState, + navigateToLicenseInfo: (Library) -> Unit, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + LicensesTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + LicensesLayout( + licenses = licenses, + paddingValues = paddingValues, + listState = listState, + navigateToLicenseInfo = navigateToLicenseInfo + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesTopBar.kt new file mode 100644 index 00000000..286e5cd4 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/licenses/LicensesTopBar.kt @@ -0,0 +1,33 @@ +package ua.acclorite.book_story.presentation.licenses + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicensesTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.licenses_option)) + }, + navigationIcon = { + NavigatorBackIconButton(navigateBack = navigateBack) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/main/MainActivityKeyboardManager.kt b/app/src/main/java/ua/acclorite/book_story/presentation/main/MainActivityKeyboardManager.kt new file mode 100644 index 00000000..9569ea83 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/main/MainActivityKeyboardManager.kt @@ -0,0 +1,29 @@ +package ua.acclorite.book_story.presentation.main + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager + +@Composable +fun MainActivityKeyboardManager() { + val density = LocalDensity.current + val imeInsets = WindowInsets.ime + val focusManager = LocalFocusManager.current + val isKeyboardVisible by remember { + derivedStateOf { + imeInsets.getBottom(density) > 0 + } + } + + LaunchedEffect(isKeyboardVisible) { + if (!isKeyboardVisible) { + focusManager.clearFocus() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/navigator/Navigator.kt b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/Navigator.kt new file mode 100644 index 00000000..2e9cba16 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/Navigator.kt @@ -0,0 +1,64 @@ +package ua.acclorite.book_story.presentation.navigator + +import androidx.activity.compose.BackHandler +import androidx.activity.viewModels +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dagger.hilt.android.lifecycle.withCreationCallback +import ua.acclorite.book_story.domain.navigator.Navigator +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.domain.navigator.StackEvent +import ua.acclorite.book_story.presentation.core.util.LocalActivity + +@Composable +fun rememberNavigator(initialScreen: Screen): Navigator { + val activity = LocalActivity.current + return activity.viewModels( + extrasProducer = { + activity.defaultViewModelCreationExtras + .withCreationCallback { factory -> + factory.create(initialScreen) + } + } + ).value +} + +@Composable +fun Navigator( + modifier: Modifier = Modifier, + initialScreen: Screen, + transitionSpec: AnimatedContentTransitionScope.(lastEvent: StackEvent) -> ContentTransform, + contentKey: (Screen) -> Any? = { it }, + backHandlerEnabled: (Screen) -> Boolean = { true }, + content: @Composable (currentScreen: Screen) -> Unit +) { + val navigator = rememberNavigator(initialScreen = initialScreen) + val currentScreen = navigator.lastItem.collectAsStateWithLifecycle() + val lastEvent = navigator.lastEvent.collectAsStateWithLifecycle() + + CompositionLocalProvider(LocalNavigator provides navigator) { + AnimatedContent( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface), + targetState = currentScreen.value, + transitionSpec = { + transitionSpec(this, lastEvent.value) + }, + contentKey = contentKey, + content = { content(it) } + ) + } + + BackHandler(enabled = backHandlerEnabled.invoke(currentScreen.value)) { + navigator.pop() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBackIconButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBackIconButton.kt new file mode 100644 index 00000000..361e054e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBackIconButton.kt @@ -0,0 +1,22 @@ +package ua.acclorite.book_story.presentation.navigator + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.IconButton + +@Composable +fun NavigatorBackIconButton( + enabled: Boolean = true, + navigateBack: () -> Unit, +) { + IconButton( + icon = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = R.string.go_back_content_desc, + disableOnClick = true, + enabled = enabled + ) { + navigateBack() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBottomSheet.kt similarity index 61% rename from app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBottomSheet.kt index f41b1657..87cef7a4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBottomSheet.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.core.navigation +package ua.acclorite.book_story.presentation.navigator import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -8,50 +8,45 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import kotlinx.coroutines.channels.Channel import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.util.Position import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet +import ua.acclorite.book_story.ui.about.AboutScreen +import ua.acclorite.book_story.ui.help.HelpScreen +import ua.acclorite.book_story.ui.settings.SettingsScreen + +val navigatorBottomSheetChannel = Channel(Channel.CONFLATED) -/** - * Navigation Bottom Sheet. - * Previously known as More Drop-Down. - * Bottom sheet allows to have more options and it generally looks better. - * - * @param onDismissRequest Dismiss callback. - */ @Composable -fun NavigationBottomSheet(onDismissRequest: () -> Unit) { - val onNavigate = LocalNavigator.current +fun NavigatorBottomSheet() { + val navigator = LocalNavigator.current ModalBottomSheet( modifier = Modifier.fillMaxWidth(), - onDismissRequest = onDismissRequest, + onDismissRequest = { navigatorBottomSheetChannel.trySend(false) }, sheetGesturesEnabled = true ) { LazyColumn(modifier = Modifier.fillMaxWidth()) { item { - NavigationItem( + NavigatorBottomSheetItem( title = stringResource(id = R.string.about_screen), primary = false, position = Position.TOP ) { - onNavigate { - navigate(screen = Screen.About) - hideNavigationBottomSheet() - } + navigator.push(AboutScreen) + navigatorBottomSheetChannel.trySend(false) } } item { - NavigationItem( + NavigatorBottomSheetItem( title = stringResource(id = R.string.help_screen), primary = false, position = Position.BOTTOM ) { - onNavigate { - navigate(screen = Screen.Help(fromStart = false)) - hideNavigationBottomSheet() - } + navigator.push(HelpScreen(fromStart = false)) + navigatorBottomSheetChannel.trySend(false) } } @@ -60,15 +55,13 @@ fun NavigationBottomSheet(onDismissRequest: () -> Unit) { } item { - NavigationItem( + NavigatorBottomSheetItem( title = stringResource(id = R.string.settings_screen), primary = true, position = Position.SOLO ) { - onNavigate { - navigate(screen = Screen.Settings) - hideNavigationBottomSheet() - } + navigator.push(SettingsScreen) + navigatorBottomSheetChannel.trySend(false) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBottomSheetItem.kt similarity index 90% rename from app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBottomSheetItem.kt index 27ea3d40..f42574cd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorBottomSheetItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.core.navigation +package ua.acclorite.book_story.presentation.navigator import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -19,16 +19,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.domain.util.Position -/** - * Navigation item. - * - * @param title Title. - * @param primary Whether this item is primary. - * @param position Position of the item. - * @param onClick OnClick callback. - */ @Composable -fun NavigationItem( +fun NavigatorBottomSheetItem( title: String, primary: Boolean, position: Position, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorComposition.kt b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorComposition.kt new file mode 100644 index 00000000..884476e4 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorComposition.kt @@ -0,0 +1,6 @@ +package ua.acclorite.book_story.presentation.navigator + +import androidx.compose.runtime.compositionLocalOf +import ua.acclorite.book_story.domain.navigator.Navigator + +val LocalNavigator = compositionLocalOf { error("Navigator was not passed.") } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationIconButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorIconButton.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationIconButton.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorIconButton.kt index e305a560..dc7ffba0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/NavigationIconButton.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorIconButton.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.core.navigation +package ua.acclorite.book_story.presentation.navigator import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert @@ -6,20 +6,15 @@ import androidx.compose.runtime.Composable import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.common.IconButton -/** - * Navigation Icon Button. - * Used to display [NavigationBottomSheet]. - */ @Composable -fun NavigationIconButton( - onClick: Navigator.() -> Unit = { showNavigationBottomSheet() } +fun NavigatorIconButton( + onClick: () -> Unit = { navigatorBottomSheetChannel.trySend(true) } ) { - val navigator = LocalNavigatorInstance.current IconButton( icon = Icons.Default.MoreVert, contentDescription = R.string.more_content_desc, - disableOnClick = false, + disableOnClick = false ) { - navigator.onClick() + onClick() } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorTabs.kt b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorTabs.kt new file mode 100644 index 00000000..4dc6ca19 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/navigator/NavigatorTabs.kt @@ -0,0 +1,81 @@ +package ua.acclorite.book_story.presentation.navigator + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.receiveAsFlow +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.util.LocalActivity + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +fun NavigatorTabs( + currentTab: Screen, + transitionSpec: AnimatedContentTransitionScope.() -> ContentTransform, + navigationBar: @Composable () -> Unit, + navigationRail: @Composable () -> Unit, + content: @Composable (Screen) -> Unit +) { + val windowClass = calculateWindowSizeClass(activity = LocalActivity.current) + val tabletUI = remember(windowClass) { + windowClass.widthSizeClass != WindowWidthSizeClass.Compact + } + val layoutDirection = LocalLayoutDirection.current + val showNavigatorBottomSheet = navigatorBottomSheetChannel + .receiveAsFlow() + .collectAsStateWithLifecycle(false) + + if (showNavigatorBottomSheet.value) { + NavigatorBottomSheet() + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + if (!tabletUI) navigationBar() + }, + containerColor = MaterialTheme.colorScheme.surface + ) { + Row( + modifier = Modifier + .fillMaxSize() + ) { + if (tabletUI) navigationRail() + + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .padding( + end = it.calculateEndPadding(layoutDirection), + bottom = it.calculateBottomPadding(), + start = it.calculateStartPadding(layoutDirection) + ) + ) { + AnimatedContent( + targetState = currentTab, + transitionSpec = transitionSpec, + content = { + content(it) + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBackHandler.kt new file mode 100644 index 00000000..6e5b2ab8 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBackHandler.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.ui.reader.ReaderEvent + +@Composable +fun ReaderBackHandler( + leave: (ReaderEvent.OnLeave) -> Unit, + navigateBack: () -> Unit +) { + val activity = LocalActivity.current + + BackHandler { + leave( + ReaderEvent.OnLeave( + activity = activity, + navigate = { + navigateBack() + } + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBar.kt new file mode 100644 index 00000000..ab96bdc4 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBar.kt @@ -0,0 +1,154 @@ +package ua.acclorite.book_story.presentation.reader + +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.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.Checkpoint +import ua.acclorite.book_story.domain.util.Direction +import ua.acclorite.book_story.presentation.core.components.common.IconButton +import ua.acclorite.book_story.presentation.core.util.calculateProgress +import ua.acclorite.book_story.presentation.core.util.noRippleClickable +import ua.acclorite.book_story.ui.reader.ReaderEvent +import ua.acclorite.book_story.ui.theme.Colors +import ua.acclorite.book_story.ui.theme.HorizontalExpandingTransition + +@Composable +fun ReaderBottomBar( + book: Book, + text: List, + listState: LazyListState, + lockMenu: Boolean, + currentChapter: Chapter?, + currentChapterProgress: Float, + checkpoint: Checkpoint, + restoreCheckpoint: (ReaderEvent.OnRestoreCheckpoint) -> Unit, + scroll: (ReaderEvent.OnScroll) -> Unit, + changeProgress: (ReaderEvent.OnChangeProgress) -> Unit +) { + val bookProgress = remember(book.progress) { + derivedStateOf { + "${book.progress.calculateProgress(2)}%" + } + } + val chapterProgress = remember(currentChapter, currentChapterProgress) { + derivedStateOf { + if (currentChapter == null) return@derivedStateOf "" + " (${currentChapterProgress.calculateProgress(2)}%)" + } + } + val progress = remember(bookProgress.value, chapterProgress.value) { + derivedStateOf { + "${bookProgress.value}${chapterProgress.value}" + } + } + + val arrowDirection by remember { + derivedStateOf { + val checkpointIndex = checkpoint.index + val index = listState.firstVisibleItemIndex + + when { + checkpointIndex > index -> Direction.END + checkpointIndex < index -> Direction.START + else -> Direction.NEUTRAL + } + } + } + val checkpointProgress by remember { + derivedStateOf { + (checkpoint.index / text.lastIndex.toFloat()) * 0.987f + } + } + + Column( + Modifier + .fillMaxWidth() + .background(Colors.readerSystemBarsColor) + .noRippleClickable(onClick = {}) + .navigationBarsPadding() + .padding(horizontal = 18.dp) + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = progress.value, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge + ) + Row( + modifier = Modifier.padding(top = 3.dp, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + HorizontalExpandingTransition( + visible = arrowDirection == Direction.START, + startDirection = true + ) { + IconButton( + icon = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = R.string.checkpoint_back_content_desc, + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.primary, + disableOnClick = false + ) { + restoreCheckpoint(ReaderEvent.OnRestoreCheckpoint) + } + } + + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterStart + ) { + ReaderBottomBarSlider( + book = book, + lockMenu = lockMenu, + listState = listState, + scroll = scroll, + changeProgress = changeProgress + ) + + if (arrowDirection != Direction.NEUTRAL) { + ReaderBottomBarSliderIndicator(progress = checkpointProgress) + } + } + + HorizontalExpandingTransition( + visible = arrowDirection == Direction.END, + startDirection = false + ) { + IconButton( + icon = Icons.AutoMirrored.Default.ArrowForward, + contentDescription = R.string.checkpoint_forward_content_desc, + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.primary, + disableOnClick = false + ) { + restoreCheckpoint(ReaderEvent.OnRestoreCheckpoint) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBarSlider.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBarSlider.kt new file mode 100644 index 00000000..c66e30f8 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBarSlider.kt @@ -0,0 +1,41 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.ui.reader.ReaderEvent + +@Composable +fun ReaderBottomBarSlider( + book: Book, + lockMenu: Boolean, + listState: LazyListState, + scroll: (ReaderEvent.OnScroll) -> Unit, + changeProgress: (ReaderEvent.OnChangeProgress) -> Unit +) { + Slider( + value = book.progress, + enabled = !lockMenu, + onValueChange = { + if (listState.layoutInfo.totalItemsCount > 0) { + scroll(ReaderEvent.OnScroll(it)) + changeProgress( + ReaderEvent.OnChangeProgress( + progress = it, + firstVisibleItemIndex = listState.firstVisibleItemIndex, + firstVisibleItemOffset = 0 + ) + ) + } + }, + colors = SliderDefaults.colors( + inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f), + disabledActiveTrackColor = MaterialTheme.colorScheme.primary, + disabledThumbColor = MaterialTheme.colorScheme.primary, + disabledInactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f), + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBarSliderIndicator.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBarSliderIndicator.kt new file mode 100644 index 00000000..f562f058 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomBarSliderIndicator.kt @@ -0,0 +1,31 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +@Composable +fun ReaderBottomBarSliderIndicator(progress: Float) { + Row(Modifier.fillMaxWidth()) { + Spacer( + modifier = Modifier.fillMaxWidth(progress) + ) + Box( + Modifier + .width(4.dp) + .height(16.dp) + .clip(RoundedCornerShape(0.5.dp)) + .background(MaterialTheme.colorScheme.onPrimary.copy(0.6f)) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomSheet.kt new file mode 100644 index 00000000..cd30cc2d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderBottomSheet.kt @@ -0,0 +1,24 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.ui.reader.ReaderEvent +import ua.acclorite.book_story.ui.reader.ReaderScreen + +@Composable +fun ReaderBottomSheet( + bottomSheet: BottomSheet?, + fullscreenMode: Boolean, + menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit, + dismissBottomSheet: (ReaderEvent.OnDismissBottomSheet) -> Unit +) { + when (bottomSheet) { + ReaderScreen.SETTINGS_BOTTOM_SHEET -> { + ReaderSettingsBottomSheet( + fullscreenMode = fullscreenMode, + menuVisibility = menuVisibility, + dismissBottomSheet = dismissBottomSheet + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderChaptersDrawer.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderChaptersDrawer.kt similarity index 50% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderChaptersDrawer.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderChaptersDrawer.kt index ab82aa19..db9a2534 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderChaptersDrawer.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderChaptersDrawer.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.reader import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width @@ -11,56 +11,46 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawer import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawerSelectableItem import ua.acclorite.book_story.presentation.core.components.modal_drawer.ModalDrawerTitleItem import ua.acclorite.book_story.presentation.core.util.calculateProgress -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel +import ua.acclorite.book_story.ui.reader.ReaderEvent -/** - * Reader Chapters Drawer. - * Shows the list of all chapter, current chapter and lets user to go to specific chapter. - */ @Composable -fun ReaderChaptersDrawer() { - val state = ReaderViewModel.getState() - val onEvent = ReaderViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - +fun ReaderChaptersDrawer( + show: Boolean, + chapters: List, + currentChapter: Chapter?, + currentChapterProgress: Float, + scrollToChapter: (ReaderEvent.OnScrollToChapter) -> Unit, + dismissDrawer: (ReaderEvent.OnDismissDrawer) -> Unit +) { ModalDrawer( - show = state.value.showChaptersDrawer, - startIndex = state.value.currentChapter?.index ?: 0, - onDismissRequest = { onEvent(ReaderEvent.OnShowHideChaptersDrawer(false)) }, + show = show, + startIndex = currentChapter?.index ?: 0, + onDismissRequest = { dismissDrawer(ReaderEvent.OnDismissDrawer) }, header = { ModalDrawerTitleItem( title = stringResource(id = R.string.chapters) ) } ) { - items(state.value.book.chapters, key = { it.index }) { chapter -> - val selected = rememberSaveable(state.value.currentChapter) { - chapter.index == state.value.currentChapter?.index + items(chapters, key = { it.index }) { chapter -> + val selected = rememberSaveable(currentChapter) { + chapter.index == currentChapter?.index } ModalDrawerSelectableItem( selected = selected, onClick = { - onEvent( + scrollToChapter( ReaderEvent.OnScrollToChapter( - chapterStartIndex = chapter.startIndex, - refreshList = { book -> - onLibraryEvent(LibraryEvent.OnUpdateBook(book)) - onHistoryEvent(HistoryEvent.OnUpdateBook(book)) - } + chapterStartIndex = chapter.startIndex ) ) - onEvent(ReaderEvent.OnShowHideChaptersDrawer(false)) + dismissDrawer(ReaderEvent.OnDismissDrawer) } ) { Text( @@ -72,7 +62,7 @@ fun ReaderChaptersDrawer() { if (selected) { Spacer(modifier = Modifier.width(18.dp)) Text( - text = "${state.value.currentChapterProgress.calculateProgress(1)}%" + text = "${currentChapterProgress.calculateProgress(1)}%" ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderFastColorPresetChange.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderColorPresetChange.kt similarity index 51% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderFastColorPresetChange.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderColorPresetChange.kt index df196fef..558e3625 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderFastColorPresetChange.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderColorPresetChange.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.reader import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.runtime.Composable @@ -6,47 +6,38 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput -import ua.acclorite.book_story.domain.util.UIText -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel +import androidx.compose.ui.platform.LocalContext +import ua.acclorite.book_story.ui.settings.SettingsEvent -/** - * Reader Fast Color Preset Change. - * Detects horizontal swipe gestures. - * - * @param fastColorPresetChangeEnabled Whether this is enabled. - * @param isLoading Whether Reader is loading. - * @param presetChanged Callback when Color Preset was changed. - */ @Composable -fun Modifier.readerFastColorPresetChange( - fastColorPresetChangeEnabled: Boolean, +fun Modifier.readerColorPresetChange( + colorPresetChangeEnabled: Boolean, isLoading: Boolean, - presetChanged: (UIText) -> Unit + selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit, + selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit ): Modifier { - val onSettingsEvent = SettingsViewModel.getEvent() - + val context = LocalContext.current val offset = remember { mutableFloatStateOf(0f) } return this.then( - if (fastColorPresetChangeEnabled && !isLoading) { + if (colorPresetChangeEnabled && !isLoading) { Modifier.pointerInput(Unit) { detectHorizontalDragGestures( onDragStart = { offset.floatValue = 0f }, onDragEnd = { when { offset.floatValue > 200 -> { - onSettingsEvent( - SettingsEvent.OnSelectPreviousPreset { - presetChanged(it) - } + selectPreviousPreset( + SettingsEvent.OnSelectPreviousPreset( + context = context + ) ) } offset.floatValue < -200 -> { - onSettingsEvent( - SettingsEvent.OnSelectNextPreset { - presetChanged(it) - } + selectNextPreset( + SettingsEvent.OnSelectNextPreset( + context = context + ) ) } } @@ -55,8 +46,6 @@ fun Modifier.readerFastColorPresetChange( offset.floatValue += dragAmount } } - } else { - Modifier - } + } else Modifier ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderContent.kt new file mode 100644 index 00000000..9e944c47 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderContent.kt @@ -0,0 +1,165 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.Checkpoint +import ua.acclorite.book_story.domain.reader.FontWithName +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment +import ua.acclorite.book_story.domain.ui.UIText +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.domain.util.Drawer +import ua.acclorite.book_story.ui.reader.ReaderEvent +import ua.acclorite.book_story.ui.settings.SettingsEvent + +@Composable +fun ReaderContent( + book: Book, + text: List, + dialog: Dialog?, + bottomSheet: BottomSheet?, + drawer: Drawer?, + listState: LazyListState, + currentChapter: Chapter?, + nestedScrollConnection: NestedScrollConnection, + fastColorPresetChange: Boolean, + perceptionExpander: Boolean, + perceptionExpanderPadding: Dp, + perceptionExpanderThickness: Dp, + currentChapterProgress: Float, + isLoading: Boolean, + errorMessage: UIText?, + checkpoint: Checkpoint, + checkingForUpdate: Boolean, + updateFound: Boolean, + showMenu: Boolean, + lockMenu: Boolean, + chapters: Map, + contentPadding: PaddingValues, + verticalPadding: Dp, + paragraphHeight: Dp, + sidePadding: Dp, + backgroundColor: Color, + fontColor: Color, + fontFamily: FontWithName, + lineHeight: TextUnit, + fontStyle: FontStyle, + textAlignment: ReaderTextAlignment, + fontSize: TextUnit, + letterSpacing: TextUnit, + paragraphIndentation: TextUnit, + doubleClickTranslation: Boolean, + fullscreenMode: Boolean, + selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit, + selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit, + menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit, + leave: (ReaderEvent.OnLeave) -> Unit, + restoreCheckpoint: (ReaderEvent.OnRestoreCheckpoint) -> Unit, + scroll: (ReaderEvent.OnScroll) -> Unit, + changeProgress: (ReaderEvent.OnChangeProgress) -> Unit, + openShareApp: (ReaderEvent.OnOpenShareApp) -> Unit, + openWebBrowser: (ReaderEvent.OnOpenWebBrowser) -> Unit, + openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit, + openDictionary: (ReaderEvent.OnOpenDictionary) -> Unit, + scrollToChapter: (ReaderEvent.OnScrollToChapter) -> Unit, + updateText: (ReaderEvent.OnUpdateText) -> Unit, + dismissDialog: (ReaderEvent.OnDismissDialog) -> Unit, + cancelCheckForTextUpdate: (ReaderEvent.OnCancelCheckForTextUpdate) -> Unit, + showUpdateDialog: (ReaderEvent.OnShowUpdateDialog) -> Unit, + showSettingsBottomSheet: (ReaderEvent.OnShowSettingsBottomSheet) -> Unit, + dismissBottomSheet: (ReaderEvent.OnDismissBottomSheet) -> Unit, + showChaptersDrawer: (ReaderEvent.OnShowChaptersDrawer) -> Unit, + dismissDrawer: (ReaderEvent.OnDismissDrawer) -> Unit, + navigateBack: () -> Unit, + navigateToBookInfo: (startUpdate: Boolean) -> Unit +) { + ReaderDialog( + dialog = dialog, + updateText = updateText, + dismissDialog = dismissDialog, + navigateToBookInfo = navigateToBookInfo + ) + + ReaderBottomSheet( + bottomSheet = bottomSheet, + fullscreenMode = fullscreenMode, + menuVisibility = menuVisibility, + dismissBottomSheet = dismissBottomSheet + ) + + ReaderScaffold( + book = book, + text = text, + listState = listState, + currentChapter = currentChapter, + nestedScrollConnection = nestedScrollConnection, + fastColorPresetChange = fastColorPresetChange, + perceptionExpander = perceptionExpander, + perceptionExpanderPadding = perceptionExpanderPadding, + perceptionExpanderThickness = perceptionExpanderThickness, + currentChapterProgress = currentChapterProgress, + isLoading = isLoading, + errorMessage = errorMessage, + checkpoint = checkpoint, + checkingForUpdate = checkingForUpdate, + updateFound = updateFound, + showMenu = showMenu, + lockMenu = lockMenu, + chapters = chapters, + contentPadding = contentPadding, + verticalPadding = verticalPadding, + paragraphHeight = paragraphHeight, + sidePadding = sidePadding, + backgroundColor = backgroundColor, + fontColor = fontColor, + fontFamily = fontFamily, + lineHeight = lineHeight, + fontStyle = fontStyle, + textAlignment = textAlignment, + fontSize = fontSize, + letterSpacing = letterSpacing, + paragraphIndentation = paragraphIndentation, + doubleClickTranslation = doubleClickTranslation, + fullscreenMode = fullscreenMode, + selectPreviousPreset = selectPreviousPreset, + selectNextPreset = selectNextPreset, + menuVisibility = menuVisibility, + leave = leave, + restoreCheckpoint = restoreCheckpoint, + scroll = scroll, + changeProgress = changeProgress, + openShareApp = openShareApp, + openWebBrowser = openWebBrowser, + openTranslator = openTranslator, + openDictionary = openDictionary, + cancelCheckForTextUpdate = cancelCheckForTextUpdate, + showUpdateDialog = showUpdateDialog, + showSettingsBottomSheet = showSettingsBottomSheet, + showChaptersDrawer = showChaptersDrawer, + navigateBack = navigateBack, + navigateToBookInfo = navigateToBookInfo + ) + + ReaderDrawer( + drawer = drawer, + chapters = chapters.values.toList(), + currentChapter = currentChapter, + currentChapterProgress = currentChapterProgress, + scrollToChapter = scrollToChapter, + dismissDrawer = dismissDrawer + ) + + ReaderBackHandler( + leave = leave, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderDialog.kt new file mode 100644 index 00000000..98dba5a2 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderDialog.kt @@ -0,0 +1,24 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.ui.reader.ReaderEvent +import ua.acclorite.book_story.ui.reader.ReaderScreen + +@Composable +fun ReaderDialog( + dialog: Dialog?, + updateText: (ReaderEvent.OnUpdateText) -> Unit, + dismissDialog: (ReaderEvent.OnDismissDialog) -> Unit, + navigateToBookInfo: (startUpdate: Boolean) -> Unit +) { + when (dialog) { + ReaderScreen.UPDATE_DIALOG -> { + ReaderUpdateDialog( + updateText = updateText, + dismissDialog = dismissDialog, + navigateToBookInfo = navigateToBookInfo + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderDrawer.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderDrawer.kt new file mode 100644 index 00000000..2174df19 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderDrawer.kt @@ -0,0 +1,26 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.util.Drawer +import ua.acclorite.book_story.ui.reader.ReaderEvent +import ua.acclorite.book_story.ui.reader.ReaderScreen + +@Composable +fun ReaderDrawer( + drawer: Drawer?, + chapters: List, + currentChapter: Chapter?, + currentChapterProgress: Float, + scrollToChapter: (ReaderEvent.OnScrollToChapter) -> Unit, + dismissDrawer: (ReaderEvent.OnDismissDrawer) -> Unit +) { + ReaderChaptersDrawer( + show = drawer == ReaderScreen.CHAPTERS_DRAWER, + chapters = chapters, + currentChapter = currentChapter, + currentChapterProgress = currentChapterProgress, + scrollToChapter = scrollToChapter, + dismissDrawer = dismissDrawer + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayout.kt new file mode 100644 index 00000000..60b64c51 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayout.kt @@ -0,0 +1,169 @@ +package ua.acclorite.book_story.presentation.reader + +import android.os.Build +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.coerceAtLeast +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.FontWithName +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.components.common.SelectionContainer +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.presentation.core.util.noRippleClickable +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.reader.ReaderEvent + +@Composable +fun ReaderLayout( + text: List, + chapters: Map, + listState: LazyListState, + contentPadding: PaddingValues, + verticalPadding: Dp, + paragraphHeight: Dp, + sidePadding: Dp, + backgroundColor: Color, + fontColor: Color, + fontFamily: FontWithName, + lineHeight: TextUnit, + fontStyle: FontStyle, + textAlignment: ReaderTextAlignment, + fontSize: TextUnit, + letterSpacing: TextUnit, + paragraphIndentation: TextUnit, + doubleClickTranslation: Boolean, + fullscreenMode: Boolean, + isLoading: Boolean, + showMenu: Boolean, + menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit, + openShareApp: (ReaderEvent.OnOpenShareApp) -> Unit, + openWebBrowser: (ReaderEvent.OnOpenWebBrowser) -> Unit, + openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit, + openDictionary: (ReaderEvent.OnOpenDictionary) -> Unit +) { + val activity = LocalActivity.current + SelectionContainer( + onCopyRequested = { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + activity.getString(R.string.copied) + .showToast(context = activity, longToast = false) + } + }, + onShareRequested = { textToShare -> + openShareApp( + ReaderEvent.OnOpenShareApp( + textToShare = textToShare, + activity = activity + ) + ) + }, + onWebSearchRequested = { textToSearch -> + openWebBrowser( + ReaderEvent.OnOpenWebBrowser( + textToSearch = textToSearch, + activity = activity + ) + ) + }, + onTranslateRequested = { textToTranslate -> + openTranslator( + ReaderEvent.OnOpenTranslator( + textToTranslate = textToTranslate, + translateWholeParagraph = false, + activity = activity + ) + ) + }, + onDictionaryRequested = { textToDefine -> + openDictionary( + ReaderEvent.OnOpenDictionary( + textToDefine, + activity = activity + ) + ) + } + ) { toolbarHidden -> + LazyColumnWithScrollbar( + state = listState, + enableScrollbar = false, + modifier = Modifier + .fillMaxSize() + .background(backgroundColor) + .then( + if (!isLoading && toolbarHidden) { + Modifier.noRippleClickable( + onClick = { + menuVisibility( + ReaderEvent.OnMenuVisibility( + show = !showMenu, + fullscreenMode = fullscreenMode, + saveCheckpoint = true, + activity = activity + ) + ) + } + ) + } else Modifier + ) + .padding(contentPadding) + .padding(vertical = verticalPadding), + verticalArrangement = Arrangement.spacedBy(paragraphHeight), + contentPadding = PaddingValues( + top = (WindowInsets.displayCutout.asPaddingValues() + .calculateTopPadding() + paragraphHeight) + .coerceAtLeast(18.dp), + bottom = (WindowInsets.displayCutout.asPaddingValues() + .calculateBottomPadding() + paragraphHeight) + .coerceAtLeast(18.dp), + ) + ) { + itemsIndexed( + text, key = { index, _ -> index } + ) { index, line -> + ReaderLayoutChapter( + chapter = chapters[index], // Shows only when matching startIndex of chapter. + fontColor = fontColor, + sidePadding = sidePadding + ) + + ReaderLayoutParagraph( + activity = activity, + showMenu = showMenu, + line = line, + fontFamily = fontFamily, + fontColor = fontColor, + lineHeight = lineHeight, + fontStyle = fontStyle, + textAlignment = textAlignment, + fontSize = fontSize, + letterSpacing = letterSpacing, + sidePadding = sidePadding, + paragraphIndentation = paragraphIndentation, + fullscreenMode = fullscreenMode, + doubleClickTranslation = doubleClickTranslation, + toolbarHidden = toolbarHidden, + openTranslator = openTranslator, + menuVisibility = menuVisibility + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderChapter.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutChapter.kt similarity index 68% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderChapter.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutChapter.kt index 3aa0d7f0..c6daa9f2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderChapter.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutChapter.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.reader import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -11,18 +11,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.reader.Chapter -/** - * Reader Chapter. - * Displays chapter title. Null if no chapter should be shown. - * - * @param chapter [Chapter]. - * @param fontColor Font color to use for title and divider. - * @param sidePadding Side padding to apply to title. - */ @Composable -fun ReaderChapter(chapter: Chapter?, fontColor: Color, sidePadding: Dp) { +fun ReaderLayoutChapter( + chapter: Chapter?, + fontColor: Color, + sidePadding: Dp +) { chapter?.let { Spacer(modifier = Modifier.height(22.dp)) Text( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTextParagraph.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutParagraph.kt similarity index 64% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTextParagraph.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutParagraph.kt index 3b718d7d..4ba4700c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderTextParagraph.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutParagraph.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.reader import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement @@ -14,7 +14,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.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle @@ -24,33 +23,15 @@ import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.FontWithName +import ua.acclorite.book_story.domain.reader.FontWithName +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.ReaderTextAlignment +import ua.acclorite.book_story.ui.reader.ReaderEvent -/** - * Reader Text Paragraph item. - * - * @param line Current line. - * @param fontFamily Line's font family. - * @param fontColor Line's font color. - * @param lineHeight Line's line height. - * @param fontStyle Line's font style. - * @param textAlignment Line's text alignment. - * @param fontSize Line's font size. - * @param letterSpacing Letter spacing. - * @param sidePadding Line's side padding. - * @param paragraphIndentation Paragraph Indentation for this line. - * @param fullscreenMode Whether Reader is in fullscreen mode. - * @param doubleClickTranslationEnabled Whether Double Click Translation is enabled. - * @param toolbarHidden Whether selection toolBar is hidden. - */ @Composable -fun LazyItemScope.ReaderTextParagraph( +fun LazyItemScope.ReaderLayoutParagraph( + activity: ComponentActivity, + showMenu: Boolean, line: AnnotatedString, fontFamily: FontWithName, fontColor: Color, @@ -62,12 +43,11 @@ fun LazyItemScope.ReaderTextParagraph( sidePadding: Dp, paragraphIndentation: TextUnit, fullscreenMode: Boolean, - doubleClickTranslationEnabled: Boolean, - toolbarHidden: Boolean + doubleClickTranslation: Boolean, + toolbarHidden: Boolean, + openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit, + menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit ) { - val onEvent = ReaderViewModel.getEvent() - val context = LocalContext.current - Column( modifier = Modifier .animateItem(fadeInSpec = null, fadeOutSpec = null) @@ -93,37 +73,29 @@ fun LazyItemScope.ReaderTextParagraph( BasicText( text = line, modifier = Modifier.then( - if ( - doubleClickTranslationEnabled && - toolbarHidden - ) { + if (doubleClickTranslation && toolbarHidden) { Modifier.noRippleClickable( onDoubleClick = { - onEvent( + openTranslator( ReaderEvent.OnOpenTranslator( textToTranslate = line.text, translateWholeParagraph = true, - context = context as ComponentActivity, - noAppsFound = { - context.getString(R.string.error_no_translator) - .showToast(context = context, longToast = false) - } + activity = activity ) ) }, onClick = { - onEvent( - ReaderEvent.OnShowHideMenu( + menuVisibility( + ReaderEvent.OnMenuVisibility( + show = !showMenu, fullscreenMode = fullscreenMode, saveCheckpoint = true, - activity = context as ComponentActivity + activity = activity ) ) } ) - } else { - Modifier - } + } else Modifier ), style = TextStyle( fontFamily = fontFamily.font, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLoadingIndicator.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLoadingIndicator.kt new file mode 100644 index 00000000..dc76a014 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLoadingIndicator.kt @@ -0,0 +1,61 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.ui.UIText +import ua.acclorite.book_story.presentation.core.components.placeholder.ErrorPlaceholder +import ua.acclorite.book_story.presentation.core.components.progress_indicator.CircularProgressIndicator +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.ui.reader.ReaderEvent + +@Composable +fun ReaderLoadingIndicator( + isLoading: Boolean, + errorMessage: UIText?, + leave: (ReaderEvent.OnLeave) -> Unit, + navigateBack: () -> Unit +) { + val activity = LocalActivity.current + + if (isLoading || errorMessage != null) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + if (!isLoading) { + ErrorPlaceholder( + errorMessage = errorMessage!!.asString(), + icon = painterResource(id = R.drawable.error), + actionTitle = stringResource(id = R.string.go_back), + action = { + leave( + ReaderEvent.OnLeave( + activity = activity, + navigate = { + navigateBack() + } + ) + ) + } + ) + } else { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + strokeWidth = 4.5.dp + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderPerceptionExpander.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderPerceptionExpander.kt similarity index 62% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderPerceptionExpander.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderPerceptionExpander.kt index 48399010..68c151be 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderPerceptionExpander.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderPerceptionExpander.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.reader.components +package ua.acclorite.book_story.presentation.reader import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -13,29 +13,19 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp -/** - * Reader Perception Expander. - * Shows two vertical lines on both sides which separate text. - * This can help you improve reading speed by focusing only on central side. - * - * @param perceptionExpander Whether perception expander is enabled. - * @param sidePadding Side padding applied on top of side text padding. - * @param thickness Thickness of the line. - * @param lineColor Color of the line. - */ @Composable fun ReaderPerceptionExpander( perceptionExpander: Boolean, - sidePadding: Dp, - thickness: Dp, - lineColor: Color + perceptionExpanderPadding: Dp, + perceptionExpanderThickness: Dp, + perceptionExpanderColor: Color ) { if (perceptionExpander) { Row( modifier = Modifier .fillMaxSize() .padding( - horizontal = sidePadding.coerceAtMost( + horizontal = perceptionExpanderPadding.coerceAtMost( LocalConfiguration.current.screenWidthDp.run { this / 2f - (this * 0.1f) }.dp @@ -45,8 +35,8 @@ fun ReaderPerceptionExpander( ) { repeat(2) { VerticalDivider( - color = lineColor, - thickness = thickness + color = perceptionExpanderColor, + thickness = perceptionExpanderThickness ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderScaffold.kt new file mode 100644 index 00000000..00dbdd57 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderScaffold.kt @@ -0,0 +1,181 @@ +package ua.acclorite.book_story.presentation.reader + +import android.annotation.SuppressLint +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.Checkpoint +import ua.acclorite.book_story.domain.reader.FontWithName +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment +import ua.acclorite.book_story.domain.ui.UIText +import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility +import ua.acclorite.book_story.ui.reader.ReaderEvent +import ua.acclorite.book_story.ui.settings.SettingsEvent + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun ReaderScaffold( + book: Book, + text: List, + listState: LazyListState, + currentChapter: Chapter?, + nestedScrollConnection: NestedScrollConnection, + fastColorPresetChange: Boolean, + perceptionExpander: Boolean, + perceptionExpanderPadding: Dp, + perceptionExpanderThickness: Dp, + currentChapterProgress: Float, + isLoading: Boolean, + errorMessage: UIText?, + checkpoint: Checkpoint, + checkingForUpdate: Boolean, + updateFound: Boolean, + showMenu: Boolean, + lockMenu: Boolean, + chapters: Map, + contentPadding: PaddingValues, + verticalPadding: Dp, + paragraphHeight: Dp, + sidePadding: Dp, + backgroundColor: Color, + fontColor: Color, + fontFamily: FontWithName, + lineHeight: TextUnit, + fontStyle: FontStyle, + textAlignment: ReaderTextAlignment, + fontSize: TextUnit, + letterSpacing: TextUnit, + paragraphIndentation: TextUnit, + doubleClickTranslation: Boolean, + fullscreenMode: Boolean, + selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit, + selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit, + menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit, + leave: (ReaderEvent.OnLeave) -> Unit, + restoreCheckpoint: (ReaderEvent.OnRestoreCheckpoint) -> Unit, + scroll: (ReaderEvent.OnScroll) -> Unit, + changeProgress: (ReaderEvent.OnChangeProgress) -> Unit, + openShareApp: (ReaderEvent.OnOpenShareApp) -> Unit, + openWebBrowser: (ReaderEvent.OnOpenWebBrowser) -> Unit, + openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit, + openDictionary: (ReaderEvent.OnOpenDictionary) -> Unit, + cancelCheckForTextUpdate: (ReaderEvent.OnCancelCheckForTextUpdate) -> Unit, + showUpdateDialog: (ReaderEvent.OnShowUpdateDialog) -> Unit, + showSettingsBottomSheet: (ReaderEvent.OnShowSettingsBottomSheet) -> Unit, + showChaptersDrawer: (ReaderEvent.OnShowChaptersDrawer) -> Unit, + navigateBack: () -> Unit, + navigateToBookInfo: (startUpdate: Boolean) -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(nestedScrollConnection), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + AnimatedVisibility( + visible = showMenu, + enter = slideInVertically { -it }, + exit = slideOutVertically { -it } + ) { + ReaderTopBar( + book = book, + currentChapter = currentChapter, + fastColorPresetChange = fastColorPresetChange, + currentChapterProgress = currentChapterProgress, + isLoading = isLoading, + checkingForUpdate = checkingForUpdate, + updateFound = updateFound, + lockMenu = lockMenu, + leave = leave, + selectPreviousPreset = selectPreviousPreset, + selectNextPreset = selectNextPreset, + cancelCheckForTextUpdate = cancelCheckForTextUpdate, + showUpdateDialog = showUpdateDialog, + showSettingsBottomSheet = showSettingsBottomSheet, + showChaptersDrawer = showChaptersDrawer, + navigateBack = navigateBack, + navigateToBookInfo = navigateToBookInfo + ) + } + }, + bottomBar = { + AnimatedVisibility( + modifier = Modifier.fillMaxWidth(), + visible = showMenu, + enter = slideInVertically { it }, + exit = slideOutVertically { it } + ) { + ReaderBottomBar( + book = book, + text = text, + listState = listState, + lockMenu = lockMenu, + currentChapter = currentChapter, + currentChapterProgress = currentChapterProgress, + checkpoint = checkpoint, + restoreCheckpoint = restoreCheckpoint, + scroll = scroll, + changeProgress = changeProgress + ) + } + } + ) { + ReaderLayout( + text = text, + chapters = chapters, + listState = listState, + contentPadding = contentPadding, + verticalPadding = verticalPadding, + paragraphHeight = paragraphHeight, + sidePadding = sidePadding, + backgroundColor = backgroundColor, + fontColor = fontColor, + fontFamily = fontFamily, + lineHeight = lineHeight, + fontStyle = fontStyle, + textAlignment = textAlignment, + fontSize = fontSize, + letterSpacing = letterSpacing, + paragraphIndentation = paragraphIndentation, + doubleClickTranslation = doubleClickTranslation, + fullscreenMode = fullscreenMode, + isLoading = isLoading, + showMenu = showMenu, + menuVisibility = menuVisibility, + openShareApp = openShareApp, + openWebBrowser = openWebBrowser, + openTranslator = openTranslator, + openDictionary = openDictionary + ) + + ReaderPerceptionExpander( + perceptionExpander = perceptionExpander, + perceptionExpanderPadding = perceptionExpanderPadding, + perceptionExpanderThickness = perceptionExpanderThickness, + perceptionExpanderColor = fontColor + ) + + ReaderLoadingIndicator( + isLoading = isLoading, + errorMessage = errorMessage, + leave = leave, + navigateBack = navigateBack + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderSettingsBottomSheet.kt similarity index 68% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderSettingsBottomSheet.kt index 473bf54b..30764df6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderSettingsBottomSheet.kt @@ -1,6 +1,5 @@ -package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet +package ua.acclorite.book_story.presentation.reader -import androidx.activity.ComponentActivity import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -13,38 +12,40 @@ import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ColorsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.FontSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.MiscSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.PaddingSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.ReadingSpeedSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.SystemSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.TextSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.TranslatorSubcategory +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.presentation.settings.appearance.colors.ColorsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.font.FontSubcategory +import ua.acclorite.book_story.presentation.settings.reader.misc.MiscSubcategory +import ua.acclorite.book_story.presentation.settings.reader.padding.PaddingSubcategory +import ua.acclorite.book_story.presentation.settings.reader.reading_speed.ReadingSpeedSubcategory +import ua.acclorite.book_story.presentation.settings.reader.system.SystemSubcategory +import ua.acclorite.book_story.presentation.settings.reader.text.TextSubcategory +import ua.acclorite.book_story.presentation.settings.reader.translator.TranslatorSubcategory +import ua.acclorite.book_story.ui.reader.ReaderEvent + +private var initialPage = 0 -/** - * Settings bottom sheet. Has General and Colors categories. - */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ReaderSettingsBottomSheet() { - val state = ReaderViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = ReaderViewModel.getEvent() - val context = LocalContext.current - - val pagerState = rememberPagerState(state.value.currentPage) { 3 } +fun ReaderSettingsBottomSheet( + fullscreenMode: Boolean, + menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit, + dismissBottomSheet: (ReaderEvent.OnDismissBottomSheet) -> Unit +) { + val activity = LocalActivity.current + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState(initialPage) { 3 } + DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } } val animatedScrimColor by animateColorAsState( targetValue = if (pagerState.currentPage == 2) Color.Transparent @@ -57,18 +58,12 @@ fun ReaderSettingsBottomSheet() { ) LaunchedEffect(pagerState.currentPage) { - onEvent( - ReaderEvent.OnScrollToSettingsPage( - page = pagerState.currentPage, - pagerState = null - ) - ) - onEvent( - ReaderEvent.OnShowHideMenu( + menuVisibility( + ReaderEvent.OnMenuVisibility( show = pagerState.currentPage != 2, - fullscreenMode = mainState.value.fullscreen, + fullscreenMode = fullscreenMode, saveCheckpoint = false, - activity = context as ComponentActivity + activity = activity ) ) } @@ -81,11 +76,18 @@ fun ReaderSettingsBottomSheet() { .fillMaxHeight(animatedHeight), dragHandle = {}, onDismissRequest = { - onEvent(ReaderEvent.OnShowHideSettingsBottomSheet(false)) + dismissBottomSheet(ReaderEvent.OnDismissBottomSheet) }, sheetGesturesEnabled = false ) { - ReaderSettingsBottomSheetTabRow(pagerState = pagerState) + ReaderSettingsBottomSheetTabRow( + currentPage = pagerState.currentPage, + scrollToPage = { + scope.launch { + pagerState.animateScrollToPage(it) + } + } + ) HorizontalPager(state = pagerState) { page -> when (page) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderSettingsBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderSettingsBottomSheetTabRow.kt new file mode 100644 index 00000000..29d55b3f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderSettingsBottomSheetTabRow.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheetTabRow + +@Composable +fun ReaderSettingsBottomSheetTabRow( + currentPage: Int, + scrollToPage: (Int) -> Unit +) { + val tabItems = listOf( + stringResource(id = R.string.general_tab), + stringResource(id = R.string.reader_tab), + stringResource(id = R.string.color_tab) + ) + + ModalBottomSheetTabRow( + selectedTabIndex = currentPage, + tabs = tabItems + ) { index -> + scrollToPage(index) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderTopBar.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderTopBar.kt index c7daaad6..a7798bb4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderTopBar.kt @@ -1,6 +1,5 @@ -package ua.acclorite.book_story.presentation.screens.reader.components.app_bar +package ua.acclorite.book_story.presentation.reader -import androidx.activity.ComponentActivity import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -28,63 +27,56 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.reader.Chapter import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.components.progress_indicator.CircularProgressIndicator -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen +import ua.acclorite.book_story.presentation.core.util.LocalActivity import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.screens.reader.components.readerFastColorPresetChange -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel -import ua.acclorite.book_story.presentation.ui.Colors +import ua.acclorite.book_story.ui.reader.ReaderEvent +import ua.acclorite.book_story.ui.settings.SettingsEvent +import ua.acclorite.book_story.ui.theme.Colors -/** - * Reader top bar. Displays title of the book. - */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -fun ReaderTopBar() { - val state = ReaderViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = ReaderViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - val context = LocalContext.current as ComponentActivity - val onNavigate = LocalNavigator.current - +fun ReaderTopBar( + book: Book, + currentChapter: Chapter?, + fastColorPresetChange: Boolean, + currentChapterProgress: Float, + isLoading: Boolean, + checkingForUpdate: Boolean, + updateFound: Boolean, + lockMenu: Boolean, + leave: (ReaderEvent.OnLeave) -> Unit, + selectPreviousPreset: (SettingsEvent.OnSelectPreviousPreset) -> Unit, + selectNextPreset: (SettingsEvent.OnSelectNextPreset) -> Unit, + cancelCheckForTextUpdate: (ReaderEvent.OnCancelCheckForTextUpdate) -> Unit, + showUpdateDialog: (ReaderEvent.OnShowUpdateDialog) -> Unit, + showSettingsBottomSheet: (ReaderEvent.OnShowSettingsBottomSheet) -> Unit, + showChaptersDrawer: (ReaderEvent.OnShowChaptersDrawer) -> Unit, + navigateBack: () -> Unit, + navigateToBookInfo: (startUpdate: Boolean) -> Unit +) { + val activity = LocalActivity.current val animatedChapterProgress = animateFloatAsState( - targetValue = state.value.currentChapterProgress + targetValue = currentChapterProgress ) Column( Modifier .fillMaxWidth() .background(Colors.readerSystemBarsColor) - .readerFastColorPresetChange( - fastColorPresetChangeEnabled = mainState.value.fastColorPresetChange, - isLoading = state.value.loading, - presetChanged = { - context - .getString( - R.string.color_preset_selected_query, - it - .asString(context) - .trim() - ) - .showToast(context = context, longToast = false) - } + .readerColorPresetChange( + colorPresetChangeEnabled = fastColorPresetChange, + isLoading = isLoading, + selectPreviousPreset = selectPreviousPreset, + selectNextPreset = selectNextPreset ) ) { TopAppBar( @@ -94,17 +86,11 @@ fun ReaderTopBar() { contentDescription = R.string.go_back_content_desc, disableOnClick = true ) { - onEvent( - ReaderEvent.OnGoBack( - context = context, - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - }, + leave( + ReaderEvent.OnLeave( + activity = activity, navigate = { - onNavigate { - navigateBack() - } + navigateBack() } ) ) @@ -112,7 +98,7 @@ fun ReaderTopBar() { }, title = { Text( - text = state.value.book.title, + text = book.title, fontSize = 20.sp, lineHeight = 20.sp, color = MaterialTheme.colorScheme.onSurface, @@ -121,41 +107,28 @@ fun ReaderTopBar() { modifier = Modifier .padding(end = 8.dp) .noRippleClickable( - enabled = !state.value.lockMenu, + enabled = !lockMenu, onClick = { - onEvent( - ReaderEvent.OnGoBack( - context = context, - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - }, + leave( + ReaderEvent.OnLeave( + activity = activity, navigate = { - onNavigate { - navigate( - Screen.BookInfo( - bookId = state.value.book.id, - startUpdate = false - ), - useBackAnimation = true, - saveInBackStack = false - ) - } + navigateToBookInfo(false) } ) ) } ) .then( - if (state.value.checkingForUpdate || state.value.updateFound) Modifier + if (checkingForUpdate || updateFound) Modifier else Modifier.basicMarquee(iterations = Int.MAX_VALUE) ) ) }, subtitle = { Text( - text = state.value.currentChapter?.title - ?: context.getString(R.string.no_chapters), + text = currentChapter?.title + ?: activity.getString(R.string.no_chapters), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge, maxLines = 1, @@ -164,7 +137,7 @@ fun ReaderTopBar() { }, actions = { AnimatedVisibility( - visible = state.value.checkingForUpdate || state.value.updateFound, + visible = checkingForUpdate || updateFound, enter = fadeIn(), exit = shrinkHorizontally() + fadeOut() ) { @@ -172,24 +145,22 @@ fun ReaderTopBar() { modifier = Modifier.size(40.dp), contentAlignment = Alignment.Center ) { - if (state.value.updateFound) { + if (updateFound) { IconButton( icon = Icons.Rounded.Upgrade, contentDescription = R.string.update_text_content_desc, color = MaterialTheme.colorScheme.primary, disableOnClick = false, - enabled = !state.value.lockMenu && - state.value.updateFound && - !state.value.checkingForUpdate + enabled = !lockMenu && !checkingForUpdate ) { - onEvent(ReaderEvent.OnShowHideUpdateDialog(true)) + showUpdateDialog(ReaderEvent.OnShowUpdateDialog) } } else { CircularProgressIndicator( modifier = Modifier .size(19.dp) .noRippleClickable { - onEvent(ReaderEvent.OnCancelCheckTextForUpdate) + cancelCheckForTextUpdate(ReaderEvent.OnCancelCheckForTextUpdate) }, strokeWidth = 2.4.dp ) @@ -197,16 +168,14 @@ fun ReaderTopBar() { } } - if (state.value.currentChapter != null) { + if (currentChapter != null) { IconButton( icon = Icons.Rounded.Menu, contentDescription = R.string.chapters_content_desc, disableOnClick = false, - enabled = !state.value.lockMenu && - !state.value.showChaptersDrawer && - !state.value.showSettingsBottomSheet + enabled = !lockMenu ) { - onEvent(ReaderEvent.OnShowHideChaptersDrawer(true)) + showChaptersDrawer(ReaderEvent.OnShowChaptersDrawer) } } @@ -214,11 +183,9 @@ fun ReaderTopBar() { icon = Icons.Default.Settings, contentDescription = R.string.open_reader_settings_content_desc, disableOnClick = false, - enabled = !state.value.lockMenu && - !state.value.showChaptersDrawer && - !state.value.showSettingsBottomSheet + enabled = !lockMenu ) { - onEvent(ReaderEvent.OnShowHideSettingsBottomSheet(true)) + showSettingsBottomSheet(ReaderEvent.OnShowSettingsBottomSheet) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -226,7 +193,7 @@ fun ReaderTopBar() { ) ) - if (state.value.currentChapter != null) { + if (currentChapter != null) { LinearProgressIndicator( progress = { animatedChapterProgress.value }, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderUpdateDialog.kt new file mode 100644 index 00000000..460df298 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderUpdateDialog.kt @@ -0,0 +1,41 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Upgrade +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.ui.reader.ReaderEvent + +@Composable +fun ReaderUpdateDialog( + updateText: (ReaderEvent.OnUpdateText) -> Unit, + dismissDialog: (ReaderEvent.OnDismissDialog) -> Unit, + navigateToBookInfo: (startUpdate: Boolean) -> Unit +) { + val activity = LocalActivity.current + + Dialog( + title = stringResource(id = R.string.update_book), + imageVectorIcon = Icons.Rounded.Upgrade, + description = stringResource( + id = R.string.update_book_description + ), + actionText = stringResource(id = R.string.proceed), + actionEnabled = true, + onDismiss = { dismissDialog(ReaderEvent.OnDismissDialog) }, + onAction = { + updateText( + ReaderEvent.OnUpdateText( + activity = activity, + navigateToBookInfo = { + navigateToBookInfo(true) + } + ) + ) + }, + withDivider = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt deleted file mode 100644 index 709fab5e..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt +++ /dev/null @@ -1,259 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideContributorsPage -import ua.acclorite.book_story.presentation.core.constants.provideIssuesPage -import ua.acclorite.book_story.presentation.core.constants.provideReleasesPage -import ua.acclorite.book_story.presentation.core.constants.provideTranslationPage -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.about.components.AboutItem -import ua.acclorite.book_story.presentation.screens.about.components.AboutUpdateDialog -import ua.acclorite.book_story.presentation.screens.about.components.badges.AboutBadges -import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent -import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel - -@Composable -fun AboutScreenRoot() { - AboutScreen() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AboutScreen() { - val state = AboutViewModel.getState() - val onEvent = AboutViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - if (state.value.showUpdateDialog) { - AboutUpdateDialog() - } - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.about_screen)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState - ) { - item { - Spacer(modifier = Modifier.height(16.dp)) - } - - item { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - Icon( - painterResource(id = R.drawable.app_icon), - contentDescription = stringResource(id = R.string.app_icon_content_desc), - modifier = Modifier - .padding(14.dp) - .size(120.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - item { - Spacer(modifier = Modifier.height(36.dp)) - HorizontalDivider( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.outlineVariant - ) - } - - item { - AboutItem( - title = stringResource(id = R.string.app_version_option), - description = buildAnnotatedString { - append( - stringResource( - id = R.string.app_version_option_desc_1, - context.getString(R.string.app_version) - ) - ) - append("\n") - append(stringResource(id = R.string.app_version_option_desc_2)) - }, - showLoading = state.value.updateLoading - ) { - if (!state.value.alreadyCheckedForUpdates) { - onEvent( - AboutEvent.OnCheckForUpdates( - context = context, - noUpdatesFound = { - context.getString(R.string.no_updates) - .showToast(context = context) - }, - error = { - context.getString(R.string.error_check_internet) - .showToast(context = context) - } - ) - ) - } else { - context.getString(R.string.no_updates) - .showToast(context = context) - } - } - } - - item { - AboutItem( - title = stringResource(id = R.string.report_bug_option), - description = null - ) { - onEvent( - AboutEvent.OnNavigateToBrowserPage( - Constants.provideIssuesPage(), - context, - noAppsFound = { - context.getString(R.string.error_no_browser) - .showToast(context = context, longToast = false) - } - ) - ) - } - } - - - item { - AboutItem( - title = stringResource(id = R.string.contributors_option), - description = null - ) { - onEvent( - AboutEvent.OnNavigateToBrowserPage( - Constants.provideContributorsPage(), - context, - noAppsFound = { - context.getString(R.string.error_no_browser) - .showToast(context = context, longToast = false) - } - ) - ) - } - } - - item { - AboutItem( - title = stringResource(id = R.string.whats_new_option), - description = null - ) { - onEvent( - AboutEvent.OnNavigateToBrowserPage( - Constants.provideReleasesPage(), - context, - noAppsFound = { - context.getString(R.string.error_no_browser) - .showToast(context = context, longToast = false) - } - ) - ) - } - } - - item { - AboutItem( - title = stringResource(id = R.string.licenses_option), - description = null - ) { - onNavigate { - navigate(Screen.About.Licenses) - } - } - } - - item { - AboutItem( - title = stringResource(id = R.string.credits_option), - description = null - ) { - onNavigate { - navigate(Screen.About.Credits) - } - } - } - - item { - AboutItem( - title = stringResource(id = R.string.help_translate_option), - description = null - ) { - onEvent( - AboutEvent.OnNavigateToBrowserPage( - Constants.provideTranslationPage(), - context, - noAppsFound = { - context.getString(R.string.error_no_browser) - .showToast(context = context, longToast = false) - } - ) - ) - } - } - - item { - AboutBadges() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutUpdateDialog.kt deleted file mode 100644 index a65d1412..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutUpdateDialog.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.components - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Update -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideReleasesPage -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent -import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel - -/** - * Update dialog. If there is app update transfers to the download page. - */ -@Composable -fun AboutUpdateDialog() { - val state = AboutViewModel.getState() - val onEvent = AboutViewModel.getEvent() - val context = LocalContext.current - - val update = remember(state.value.showUpdateDialog) { - state.value.updateInfo!! - } - - DialogWithContent( - title = stringResource(id = R.string.update_query, update.tagName), - imageVectorIcon = Icons.Default.Update, - description = stringResource( - id = R.string.update_app_description - ), - actionText = stringResource(id = R.string.proceed), - isActionEnabled = true, - disableOnClick = false, - onDismiss = { onEvent(AboutEvent.OnDismissUpdateDialog) }, - onAction = { - onEvent( - AboutEvent.OnNavigateToBrowserPage( - page = Constants.provideReleasesPage(), - context = context, - noAppsFound = { - context.getString(R.string.error_no_browser).showToast( - context = context, - longToast = false - ) - } - ) - ) - onEvent(AboutEvent.OnDismissUpdateDialog) - }, - withDivider = false, - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt deleted file mode 100644 index d00c5fc2..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.data - -import android.content.Context -import androidx.compose.runtime.Immutable - -@Immutable -sealed class AboutEvent { - data class OnNavigateToBrowserPage( - val page: String, - val context: Context, - val noAppsFound: () -> Unit - ) : AboutEvent() - - data class OnCheckForUpdates( - val context: Context, - val noUpdatesFound: () -> Unit, - val error: () -> Unit - ) : AboutEvent() - - data object OnDismissUpdateDialog : AboutEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/credits/CreditsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/credits/CreditsScreen.kt deleted file mode 100644 index 93c5fe18..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/credits/CreditsScreen.kt +++ /dev/null @@ -1,99 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.credits - -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideCredits -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent -import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel -import ua.acclorite.book_story.presentation.screens.about.nested.credits.components.CreditItem - -@Composable -fun CreditsScreenRoot() { - CreditsScreen() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun CreditsScreen() { - val onEvent = AboutViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.credits_option)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState - ) { - items(Constants.provideCredits(), key = { it.name }) { - CreditItem(credit = it) { - it.website?.let { website -> - onEvent( - AboutEvent.OnNavigateToBrowserPage( - page = website, - context = context, - noAppsFound = { - context.getString(R.string.error_no_browser) - .showToast(context = context, longToast = false) - } - ) - ) - } - } - } - } - } -} - - - - - - - diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/LicenseInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/LicenseInfoScreen.kt deleted file mode 100644 index 2a5742ca..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/LicenseInfoScreen.kt +++ /dev/null @@ -1,214 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.license_info - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Language -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.IconButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.about.nested.license_info.data.LicenseInfoEvent -import ua.acclorite.book_story.presentation.screens.about.nested.license_info.data.LicenseInfoViewModel -import ua.acclorite.book_story.presentation.ui.DefaultTransition -import ua.acclorite.book_story.presentation.ui.SlidingTransition - -@Composable -fun LicenseInfoScreenRoot(screen: Screen.About.LicenseInfo) { - val onEvent = LicenseInfoViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - LaunchedEffect(Unit) { - onEvent( - LicenseInfoEvent.OnInit( - screen = screen, - navigateBack = { - onNavigate { - navigateBack() - } - }, - context = context - ) - ) - } - - LicenseInfoScreen() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun LicenseInfoScreen() { - val state = LicenseInfoViewModel.getState() - val onEvent = LicenseInfoViewModel.getEvent() - val context = LocalContext.current - val onNavigate = LocalNavigator.current - - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - val licenses = remember(state.value.license?.licenses) { - state.value.license?.licenses?.toList() - } - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - DefaultTransition( - visible = state.value.license?.name != null - ) { - Text( - state.value.license?.name ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - actions = { - if (state.value.license?.website?.isNotBlank() == true) { - IconButton( - icon = Icons.Outlined.Language, - contentDescription = R.string.open_in_web_content_desc, - disableOnClick = false - ) { - var url = state.value.license?.website!! - if (url.startsWith("http://") || !url.startsWith("https://")) { - url = "https://$url" - } - - onEvent( - LicenseInfoEvent.OnOpenLicensePage( - page = url, - context = context, - noAppsFound = { - context.getString(R.string.error_no_browser).showToast( - context = context, - longToast = false - ) - } - ) - ) - } - } - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - DefaultTransition( - visible = state.value.license != null - ) { - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState, - contentPadding = PaddingValues(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - item { - Spacer(modifier = Modifier.height(12.dp)) - } - - items( - licenses ?: emptyList(), - key = { it.name } - ) { license -> - val showed = remember { - mutableStateOf(licenses?.size == 1) - } - - Column( - modifier = Modifier.animateItem( - fadeInSpec = null, - fadeOutSpec = null - ) - ) { - Text( - text = license.name, - color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .clip(RoundedCornerShape(100)) - .clickable(enabled = license.licenseContent?.isNotBlank() == true) { - showed.value = !showed.value - } - .background( - MaterialTheme.colorScheme.primary, - RoundedCornerShape(100) - ) - .padding(vertical = 6.dp, horizontal = 12.dp) - ) - - SlidingTransition( - visible = showed.value && license.licenseContent?.isNotBlank() == true, - modifier = Modifier.padding(horizontal = 8.dp) - ) { - Column { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = license.licenseContent!! - .lines() - .joinToString(separator = "\n") { - it.trim() - }, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - item { - Spacer(modifier = Modifier.height(12.dp)) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoEvent.kt deleted file mode 100644 index 0ffd354d..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoEvent.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.license_info.data - -import android.content.Context -import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.presentation.core.navigation.Screen - -@Immutable -sealed class LicenseInfoEvent { - data class OnInit( - val screen: Screen.About.LicenseInfo, - val navigateBack: () -> Unit, - val context: Context - ) : LicenseInfoEvent() - - data class OnOpenLicensePage( - val page: String, - val context: Context, - val noAppsFound: () -> Unit - ) : LicenseInfoEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoState.kt deleted file mode 100644 index 1961e2ab..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.license_info.data - -import androidx.compose.runtime.Immutable -import com.mikepenz.aboutlibraries.entity.Library - -@Immutable -data class LicenseInfoState( - val license: Library? = null, -) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoViewModel.kt deleted file mode 100644 index 6ab654c7..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/license_info/data/LicenseInfoViewModel.kt +++ /dev/null @@ -1,75 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.license_info.data - -import android.content.Intent -import android.net.Uri -import androidx.activity.ComponentActivity -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewModelScope -import com.mikepenz.aboutlibraries.Libs -import com.mikepenz.aboutlibraries.util.withContext -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ua.acclorite.book_story.domain.util.UIViewModel -import ua.acclorite.book_story.presentation.core.util.launchActivity -import javax.inject.Inject - -@HiltViewModel -class LicenseInfoViewModel @Inject constructor( - -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } - - private val _state = MutableStateFlow(LicenseInfoState()) - override val state = _state.asStateFlow() - - override fun onEvent(event: LicenseInfoEvent) { - when (event) { - is LicenseInfoEvent.OnInit -> init(event) - - is LicenseInfoEvent.OnOpenLicensePage -> { - val intent = Intent( - Intent.ACTION_VIEW, - Uri.parse(event.page) - ) - - intent.launchActivity(event.context as ComponentActivity) { - event.noAppsFound() - } - } - } - } - - private fun init(event: LicenseInfoEvent.OnInit) { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy(license = null) - } - - val license = Libs.Builder().withContext(event.context).build().libraries.find { - it.uniqueId == event.screen.licenseId - } - - if (license == null) { - event.navigateBack() - return@launch - } - - _state.update { - it.copy( - license = license - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/LicensesScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/LicensesScreen.kt deleted file mode 100644 index 92b1f1c6..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/LicensesScreen.kt +++ /dev/null @@ -1,111 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.licenses - -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.screens.about.nested.licenses.components.LicenseItem -import ua.acclorite.book_story.presentation.screens.about.nested.licenses.data.LicensesEvent -import ua.acclorite.book_story.presentation.screens.about.nested.licenses.data.LicensesViewModel - -@Composable -fun LicensesScreenRoot() { - val onEvent = LicensesViewModel.getEvent() - val context = LocalContext.current - - LaunchedEffect(Unit) { - onEvent(LicensesEvent.OnInit(context = context)) - } - - LicensesScreen() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun LicensesScreen() { - val state = LicensesViewModel.getState() - val onNavigate = LocalNavigator.current - - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior( - listState = state.value.listState - ) - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.licenses_option)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - AnimatedVisibility( - visible = state.value.licenses.isNotEmpty(), - enter = fadeIn(tween(300)), - exit = fadeOut(tween(300)) - ) { - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState - ) { - item { - Spacer(modifier = Modifier.height(12.dp)) - } - - items(state.value.licenses, key = { it.uniqueId }) { - LicenseItem(library = it) { - onNavigate { - navigate(Screen.About.LicenseInfo(it.uniqueId)) - } - } - } - - item { - Spacer(modifier = Modifier.height(12.dp)) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesEvent.kt deleted file mode 100644 index 0d71db92..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.licenses.data - -import android.content.Context -import androidx.compose.runtime.Immutable - -@Immutable -sealed class LicensesEvent { - data class OnInit( - val context: Context - ) : LicensesEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesState.kt deleted file mode 100644 index 0265ee26..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.licenses.data - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Immutable -import com.mikepenz.aboutlibraries.entity.Library - -@Immutable -data class LicensesState( - val licenses: List = emptyList(), - val listState: LazyListState = LazyListState() -) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesViewModel.kt deleted file mode 100644 index 3a0ed54b..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/nested/licenses/data/LicensesViewModel.kt +++ /dev/null @@ -1,61 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.about.nested.licenses.data - -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewModelScope -import com.mikepenz.aboutlibraries.Libs -import com.mikepenz.aboutlibraries.util.withJson -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.util.UIViewModel -import javax.inject.Inject - -@HiltViewModel -class LicensesViewModel @Inject constructor( - -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } - - private val _state = MutableStateFlow(LicensesState()) - override val state = _state.asStateFlow() - - override fun onEvent(event: LicensesEvent) { - viewModelScope.launch(Dispatchers.Main) { - when (event) { - is LicensesEvent.OnInit -> init(event) - } - } - } - - private fun init(event: LicensesEvent.OnInit) { - viewModelScope.launch(Dispatchers.IO) { - if (_state.value.licenses.isNotEmpty()) { - return@launch - } - - val licenses = Libs - .Builder() - .withJson(event.context, R.raw.aboutlibraries) - .build() - .libraries - .toList() - - _state.update { - it.copy( - licenses = licenses.sortedBy { library -> library.openSource } - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt deleted file mode 100644 index f9c7cb7e..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt +++ /dev/null @@ -1,276 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.book_info - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkHorizontally -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.core.components.common.Snackbar -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoreBottomSheet -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection -import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoTopBar -import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet -import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet -import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoConfirmUpdateDialog -import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoDeleteDialog -import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel - -@Composable -fun BookInfoScreenRoot(screen: Screen.BookInfo) { - val onEvent = BookInfoViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - val snackbarState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - onEvent( - BookInfoEvent.OnInit( - screen = screen, - snackbarState = snackbarState, - onNavigate = onNavigate, - context = context - ) - ) - } - - BookInfoScreen( - snackbarState = snackbarState - ) - - DisposableEffect(Unit) { - onDispose { - onEvent(BookInfoEvent.OnClearViewModel) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun BookInfoScreen(snackbarState: SnackbarHostState) { - val context = LocalContext.current - val state = BookInfoViewModel.getState() - val onEvent = BookInfoViewModel.getEvent() - val onNavigate = LocalNavigator.current - - val listState = rememberLazyListState() - val refreshState = rememberPullRefreshState( - refreshing = state.value.updating || state.value.checkingForUpdate, - onRefresh = { - onEvent( - BookInfoEvent.OnCheckForTextUpdate( - snackbarState, - context - ) - ) - } - ) - - if (state.value.showChangeCoverBottomSheet) { - BookInfoChangeCoverBottomSheet() - } - if (state.value.showDetailsBottomSheet) { - BookInfoDetailsBottomSheet() - } - if (state.value.showDeleteDialog) { - BookInfoDeleteDialog() - } - if (state.value.showMoveDialog) { - BookInfoMoveDialog() - } - if (state.value.showConfirmTextUpdateDialog) { - BookInfoConfirmUpdateDialog(snackbarHostState = snackbarState) - } - if (state.value.showMoreBottomSheet) { - BookInfoMoreBottomSheet(snackbarState = snackbarState) - } - - Scaffold( - Modifier - .fillMaxSize() - .imePadding() - .windowInsetsPadding(WindowInsets.navigationBars) - .pullRefresh(refreshState), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - BookInfoTopBar( - listState = listState, - snackbarState = snackbarState - ) - }, - bottomBar = { - Snackbar( - modifier = Modifier.padding(bottom = 70.dp), - snackbarState = snackbarState - ) - } - ) { paddingValues -> - Box( - Modifier - .fillMaxSize() - ) { - LazyColumn( - Modifier - .fillMaxSize(), - state = listState - ) { - item { - Box(modifier = Modifier.fillMaxWidth()) { - if (state.value.book.coverImage != null) { - BookInfoBackground( - height = paddingValues.calculateTopPadding() + 12.dp + 195.dp, - image = state.value.book.coverImage!! - ) - } - - Column(Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding() + 12.dp)) - // Info - BookInfoInfoSection() - } - } - } - - item { - Spacer(modifier = Modifier.height(24.dp)) - // Statistic - BookInfoStatisticSection() - } - - item { - Spacer(modifier = Modifier.height(24.dp)) - // Description - BookInfoDescriptionSection() - } - } - - FloatingActionButton( - onClick = { - if (!state.value.updating) { - onEvent(BookInfoEvent.OnNavigateToReaderScreen(onNavigate = onNavigate)) - } - }, - shape = MaterialTheme.shapes.large, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - elevation = FloatingActionButtonDefaults.elevation(), - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - content = { - Row(verticalAlignment = Alignment.CenterVertically) { - Spacer(modifier = Modifier.width(13.dp)) - Icon( - imageVector = Icons.Filled.PlayArrow, - contentDescription = stringResource(id = R.string.continue_reading_content_desc), - Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = !listState.canScrollBackward, - enter = fadeIn() + expandHorizontally(), - exit = fadeOut() + shrinkHorizontally() - ) { - Text( - text = stringResource( - if (state.value.book.progress == 0f) R.string.start - else R.string.continue_read - ), - style = MaterialTheme.typography.labelLarge, - maxLines = 1, - modifier = Modifier.padding(start = 8.dp) - ) - } - Spacer(modifier = Modifier.width(16.dp)) - } - } - ) - - PullRefreshIndicator( - state.value.updating || state.value.checkingForUpdate, - refreshState, - Modifier - .align(Alignment.TopCenter) - .padding(top = paddingValues.calculateTopPadding()), - backgroundColor = MaterialTheme.colorScheme.inverseSurface, - contentColor = MaterialTheme.colorScheme.inverseOnSurface - ) - } - } - - BackHandler { - var exitedEditMode = false - - if (state.value.editTitle) { - onEvent(BookInfoEvent.OnShowHideEditTitle) - exitedEditMode = true - } - - if (state.value.editAuthor) { - onEvent(BookInfoEvent.OnShowHideEditAuthor) - exitedEditMode = true - } - - if (state.value.editDescription) { - onEvent(BookInfoEvent.OnShowHideEditDescription) - exitedEditMode = true - } - - if (exitedEditMode) { - return@BackHandler - } - - if (!state.value.updating) { - onEvent(BookInfoEvent.OnCancelTextUpdate) - onNavigate { - navigateBack() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoTopBar.kt deleted file mode 100644 index 26c6d43e..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoTopBar.kt +++ /dev/null @@ -1,188 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components - -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.material.icons.outlined.Done -import androidx.compose.material.icons.outlined.Refresh -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.IconButton -import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBar -import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBarData -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.NavigationIconButton -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.ui.DefaultTransition -import ua.acclorite.book_story.presentation.ui.Transitions - -/** - * Book Info Top Bar. - * - * @param listState [LazyListState]. - * @param snackbarState [SnackbarHostState]. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BookInfoTopBar( - listState: LazyListState, - snackbarState: SnackbarHostState -) { - val state = BookInfoViewModel.getState() - val onEvent = BookInfoViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } } - - TopAppBar( - containerColor = MaterialTheme.colorScheme.surface.copy(0f), - scrollBehavior = null, - isTopBarScrolled = listState.canScrollBackward, - - shownTopBar = 0, - topBars = listOf( - TopAppBarData( - contentID = 0, - contentNavigationIcon = { - if ( - state.value.editTitle || - state.value.editAuthor || - state.value.editDescription - ) { - IconButton( - icon = Icons.AutoMirrored.Outlined.ArrowBack, - contentDescription = R.string.exit_editing_content_desc, - disableOnClick = true - ) { - if (state.value.editTitle) { - onEvent(BookInfoEvent.OnShowHideEditTitle) - } - - if (state.value.editAuthor) { - onEvent(BookInfoEvent.OnShowHideEditAuthor) - } - - if (state.value.editDescription) { - onEvent(BookInfoEvent.OnShowHideEditDescription) - } - } - } else { - GoBackButton(onNavigate = onNavigate, enabled = !state.value.updating) { - onEvent(BookInfoEvent.OnCancelTextUpdate) - } - } - }, - contentTitle = { - DefaultTransition( - firstVisibleItemIndex > 0 && !state.value.editTitle - ) { - Text( - state.value.book.title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - } - }, - contentActions = { - IconButton( - icon = Icons.Outlined.Refresh, - contentDescription = R.string.refresh_book_content_desc, - disableOnClick = false, - enabled = !state.value.updating && !state.value.checkingForUpdate - ) { - onEvent( - BookInfoEvent.OnCheckForTextUpdate( - snackbarState, - context - ) - ) - } - - Box { - DefaultTransition( - visible = !state.value.editTitle && - !state.value.editAuthor && - !state.value.editDescription - ) { - NavigationIconButton { - onEvent(BookInfoEvent.OnShowHideMoreBottomSheet(true)) - } - } - AnimatedVisibility( - visible = state.value.editTitle || - state.value.editAuthor || - state.value.editDescription, - enter = Transitions.DefaultTransitionIn, - exit = fadeOut(tween(200)) - ) { - IconButton( - icon = Icons.Outlined.Done, - contentDescription = R.string.apply_changes_content_desc, - disableOnClick = true, - enabled = !state.value.updating && - (state.value.titleValue.isNotBlank() && - state.value.titleValue.trim() != - state.value.book.title.trim()) || - - (state.value.authorValue.isNotBlank() && - state.value.authorValue.trim() != - state.value.book.author.getAsString()?.trim()) || - - (state.value.descriptionValue.isNotBlank() && - state.value.descriptionValue.trim() != - state.value.book.description?.trim()), - - color = if ((state.value.titleValue.isNotBlank() && - state.value.titleValue.trim() != - state.value.book.title.trim()) || - - (state.value.authorValue.isNotBlank() && - state.value.authorValue.trim() != - state.value.book.author.getAsString()?.trim()) || - - (state.value.descriptionValue.isNotBlank() && - state.value.descriptionValue.trim() != - state.value.book.description?.trim()) - ) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurfaceVariant - ) { - onEvent( - BookInfoEvent.OnUpdateData( - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - } - ) - ) - } - } - } - } - ) - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoConfirmUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoConfirmUpdateDialog.kt deleted file mode 100644 index fe03d856..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoConfirmUpdateDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components.dialog - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Update -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel - -/** - * Confirm update dialog. - * Updates book if action clicked. - * - * @param snackbarHostState [SnackbarHostState]. - */ -@Composable -fun BookInfoConfirmUpdateDialog(snackbarHostState: SnackbarHostState) { - val onEvent = BookInfoViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - val context = LocalContext.current - - DialogWithContent( - title = stringResource(id = R.string.confirm_update), - imageVectorIcon = Icons.Default.Update, - description = stringResource( - id = R.string.confirm_update_description - ), - actionText = stringResource(id = R.string.confirm), - isActionEnabled = true, - onDismiss = { onEvent(BookInfoEvent.OnDismissConfirmTextUpdateDialog) }, - onAction = { - onEvent( - BookInfoEvent.OnConfirmTextUpdate( - snackbarState = snackbarHostState, - context = context, - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - } - ) - ) - }, - withDivider = false - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt deleted file mode 100644 index 47a88a4b..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoDeleteDialog.kt +++ /dev/null @@ -1,59 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components.dialog - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel - -/** - * Delete dialog. Deletes current book. - */ -@Composable -fun BookInfoDeleteDialog() { - val onEvent = BookInfoViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - val onBrowseEvent = BrowseViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - DialogWithContent( - title = stringResource(id = R.string.delete_book), - imageVectorIcon = Icons.Outlined.DeleteOutline, - description = stringResource( - id = R.string.delete_book_description - ), - actionText = stringResource(id = R.string.delete), - onDismiss = { onEvent(BookInfoEvent.OnShowHideDeleteDialog) }, - withDivider = false, - isActionEnabled = true, - onAction = { - onEvent( - BookInfoEvent.OnDeleteBook( - onNavigate = onNavigate, - refreshList = { - onLibraryEvent(LibraryEvent.OnLoadList) - onBrowseEvent(BrowseEvent.OnLoadList) - onHistoryEvent(HistoryEvent.OnLoadList) - } - ) - ) - onEvent(BookInfoEvent.OnShowHideMoreBottomSheet(false)) - context.getString(R.string.book_deleted) - .showToast(context = context) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt deleted file mode 100644 index 4db2273c..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt +++ /dev/null @@ -1,90 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.book_info.components.dialog - -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.DriveFileMove -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithLazyColumn -import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent -import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel - -/** - * Move dialog. - * Moves current book to the selected category. - */ -@Composable -fun BookInfoMoveDialog() { - val state = BookInfoViewModel.getState() - val onEvent = BookInfoViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - val categories = remember { - Category.entries.filter { state.value.book.category != it } - } - - LaunchedEffect(Unit) { - onEvent(BookInfoEvent.OnSelectCategory(categories[0])) - } - - DialogWithLazyColumn( - title = stringResource(id = R.string.move_book), - imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove, - description = stringResource( - id = R.string.move_book_description - ), - actionText = stringResource(id = R.string.move), - isActionEnabled = true, - onDismiss = { onEvent(BookInfoEvent.OnShowHideMoveDialog) }, - onAction = { - onEvent( - BookInfoEvent.OnMoveBook( - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - }, - updatePage = { - onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it)) - }, - onNavigate = onNavigate - ) - ) - onEvent(BookInfoEvent.OnShowHideMoreBottomSheet(false)) - context.getString(R.string.book_moved) - .showToast(context = context) - }, - withDivider = false, - items = { - items(categories, key = { it.name }) { - val category = when (it) { - Category.READING -> stringResource(id = R.string.reading_tab) - Category.ALREADY_READ -> stringResource(id = R.string.already_read_tab) - Category.PLANNING -> stringResource(id = R.string.planning_tab) - Category.DROPPED -> stringResource(id = R.string.dropped_tab) - } - - SelectableDialogItem( - selected = it == state.value.selectedCategory, - title = category - ) { - onEvent(BookInfoEvent.OnSelectCategory(it)) - } - } - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt deleted file mode 100644 index 1f97739e..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt +++ /dev/null @@ -1,139 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.book_info.data - -import android.content.Context -import android.net.Uri -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Immutable -import androidx.compose.ui.focus.FocusRequester -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.domain.util.OnNavigate -import ua.acclorite.book_story.domain.util.UIText -import ua.acclorite.book_story.presentation.core.navigation.Screen - -@Immutable -sealed class BookInfoEvent { - data class OnInit( - val screen: Screen.BookInfo, - val snackbarState: SnackbarHostState, - val context: Context, - val onNavigate: OnNavigate - ) : BookInfoEvent() - - data object OnClearViewModel : BookInfoEvent() - - data object OnShowHideChangeCoverBottomSheet : BookInfoEvent() - - data object OnShowHideDetailsBottomSheet : BookInfoEvent() - - data class OnDeleteCover( - val refreshList: (Book) -> Unit - ) : BookInfoEvent() - - data class OnChangeCover( - val uri: Uri, - val context: Context, - val refreshList: (Book) -> Unit - ) : BookInfoEvent() - - data object OnCheckCoverReset : BookInfoEvent() - - data class OnResetCoverImage( - val refreshList: (Book) -> Unit, - val showResult: (UIText) -> Unit - ) : BookInfoEvent() - - data object OnShowHideEditTitle : BookInfoEvent() - - data class OnTitleRequestFocus( - val focusRequester: FocusRequester - ) : BookInfoEvent() - - data class OnTitleValueChange( - val value: String - ) : BookInfoEvent() - - data object OnShowHideEditAuthor : BookInfoEvent() - - data class OnAuthorRequestFocus( - val focusRequester: FocusRequester - ) : BookInfoEvent() - - data class OnAuthorValueChange( - val value: String - ) : BookInfoEvent() - - data object OnShowHideEditDescription : BookInfoEvent() - - data class OnDescriptionRequestFocus( - val focusRequester: FocusRequester - ) : BookInfoEvent() - - data class OnDescriptionValueChange( - val value: String - ) : BookInfoEvent() - - data class OnUpdateData( - val refreshList: (Book) -> Unit - ) : BookInfoEvent() - - data object OnShowHideDeleteDialog : BookInfoEvent() - - data object OnShowHideMoveDialog : BookInfoEvent() - - data class OnDeleteBook( - val refreshList: () -> Unit, - val onNavigate: OnNavigate - ) : BookInfoEvent() - - data class OnMoveBook( - val refreshList: (Book) -> Unit, - val updatePage: (Int) -> Unit, - val onNavigate: OnNavigate - ) : BookInfoEvent() - - data class OnSelectCategory( - val category: Category - ) : BookInfoEvent() - - data class OnCopyToClipboard( - val context: Context, - val text: String, - val success: () -> Unit - ) : BookInfoEvent() - - data class OnShowSnackbar( - val text: String, - val action: String?, - val onAction: () -> Unit = {}, - val durationMillis: Long, - val snackbarState: SnackbarHostState - ) : BookInfoEvent() - - data class OnNavigateToReaderScreen( - val onNavigate: OnNavigate - ) : BookInfoEvent() - - data class OnCheckForTextUpdate( - val snackbarState: SnackbarHostState, - val context: Context - ) : BookInfoEvent() - - data object OnDismissConfirmTextUpdateDialog : BookInfoEvent() - - data class OnShowConfirmTextUpdateDialog( - val updatedText: List, - val updatedChapters: List - ) : BookInfoEvent() - - data object OnCancelTextUpdate : BookInfoEvent() - - data class OnConfirmTextUpdate( - val snackbarState: SnackbarHostState, - val context: Context, - val refreshList: (Book) -> Unit - ) : BookInfoEvent() - - data class OnShowHideMoreBottomSheet(val show: Boolean) : BookInfoEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt deleted file mode 100644 index 7d6580fc..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt +++ /dev/null @@ -1,228 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.browse - -import android.Manifest -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import com.google.accompanist.permissions.rememberPermissionState -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.SelectableFile -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.browse.components.BrowseEmptyPlaceholder -import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog -import ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog.BrowseAddingDialog -import ua.acclorite.book_story.presentation.screens.browse.components.filter_bottom_sheet.BrowseFilterBottomSheet -import ua.acclorite.book_story.presentation.screens.browse.components.layout.BrowseLayout -import ua.acclorite.book_story.presentation.screens.browse.components.top_bar.BrowseTopBar -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure -import ua.acclorite.book_story.presentation.ui.DefaultTransition - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun BrowseScreenRoot() { - val state = BrowseViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = BrowseViewModel.getEvent() - - val permissionState = rememberPermissionState( - permission = Manifest.permission.READ_EXTERNAL_STORAGE - ) - val filteredFiles = remember( - state.value.selectableFiles, - state.value.selectedDirectory - ) { - derivedStateOf { - BrowseViewModel.filterList( - browseState = state.value, - mainState = mainState.value - ) - } - } - - LaunchedEffect(Unit) { - onEvent(BrowseEvent.OnUpdateScrollOffset) - onEvent(BrowseEvent.OnPermissionCheck(permissionState)) - } - - - BrowseScreen( - permissionState = permissionState, - filteredFiles = filteredFiles.value, - ) - - DisposableEffect(Unit) { - onDispose { - onEvent(BrowseEvent.OnClearViewModel) - } - } -} - -@OptIn( - ExperimentalPermissionsApi::class, - ExperimentalMaterialApi::class -) -@Composable -private fun BrowseScreen( - permissionState: PermissionState, - filteredFiles: List -) { - val state = BrowseViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = BrowseViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - val refreshState = rememberPullRefreshState( - refreshing = state.value.isRefreshing, - onRefresh = { - onEvent(BrowseEvent.OnRefreshList) - } - ) - - if (state.value.requestPermissionDialog) { - BrowseStoragePermissionDialog(permissionState) - } - if (state.value.showAddingDialog) { - BrowseAddingDialog() - } - if (state.value.showFilterBottomSheet) { - BrowseFilterBottomSheet() - } - - Scaffold( - modifier = Modifier - .fillMaxSize() - .pullRefresh(refreshState), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - BrowseTopBar(filteredFiles = filteredFiles) - } - ) { padding -> - Box( - Modifier - .fillMaxSize() - .padding(top = padding.calculateTopPadding()) - ) { - DefaultTransition(visible = !state.value.isLoading) { - BrowseLayout( - filteredFiles = filteredFiles, - onLongItemClick = { selectableFile -> - when (selectableFile.isDirectory) { - false -> { - context.getString( - R.string.file_path_query, - selectableFile.fileOrDirectory.path - ).showToast(context = context) - } - - true -> { - onEvent( - BrowseEvent.OnSelectFile( - includedFileFormats = mainState.value.browseIncludedFilterItems, - file = selectableFile - ) - ) - } - } - }, - onFavoriteItemClick = { - onEvent( - BrowseEvent.OnUpdateFavoriteDirectory( - it.fileOrDirectory.path - ) - ) - }, - onItemClick = { selectableFile -> - when (selectableFile.isDirectory) { - false -> { - onEvent( - BrowseEvent.OnSelectFile( - includedFileFormats = mainState.value.browseIncludedFilterItems, - file = selectableFile - ) - ) - } - - true -> { - if (!state.value.hasSelectedItems) { - onEvent( - BrowseEvent.OnChangeDirectory( - selectableFile.fileOrDirectory, - savePreviousDirectory = true - ) - ) - } else { - onEvent( - BrowseEvent.OnSelectFile( - includedFileFormats = mainState.value.browseIncludedFilterItems, - file = selectableFile - ) - ) - } - } - } - } - ) - } - - BrowseEmptyPlaceholder( - isFilesEmpty = filteredFiles.isEmpty(), - storagePermissionState = permissionState, - ) - - PullRefreshIndicator( - state.value.isRefreshing, - refreshState, - Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.colorScheme.inverseSurface, - contentColor = MaterialTheme.colorScheme.inverseOnSurface - ) - } - } - - BackHandler { - if (state.value.hasSelectedItems) { - onEvent(BrowseEvent.OnClearSelectedFiles) - return@BackHandler - } - - if (state.value.showSearch) { - onEvent(BrowseEvent.OnSearchShowHide) - return@BackHandler - } - - if ( - state.value.inNestedDirectory - && mainState.value.browseFilesStructure != BrowseFilesStructure.ALL_FILES - ) { - onEvent(BrowseEvent.OnGoBackDirectory) - return@BackHandler - } - - onNavigate { - navigate(Screen.Library) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt deleted file mode 100644 index 7a8448e3..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt +++ /dev/null @@ -1,53 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.browse.components - -import androidx.activity.ComponentActivity -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.SdStorage -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel - -/** - * Storage permission dialog. - * - * @param permissionState Storage [PermissionState]. - */ -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun BrowseStoragePermissionDialog( - permissionState: PermissionState -) { - val activity = LocalContext.current as ComponentActivity - val onEvent = BrowseViewModel.getEvent() - - DialogWithContent( - title = stringResource(id = R.string.storage_permission), - description = stringResource(id = R.string.storage_permission_description), - actionText = stringResource(id = R.string.grant), - imageVectorIcon = Icons.Default.SdStorage, - onDismiss = { - onEvent( - BrowseEvent.OnStoragePermissionDismiss( - permissionState = permissionState - ) - ) - }, - isActionEnabled = true, - withDivider = false, - disableOnClick = false, - onAction = { - onEvent( - BrowseEvent.OnStoragePermissionRequest( - activity = activity, - storagePermissionState = permissionState - ) - ) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/filter_bottom_sheet/BrowseFilterBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/filter_bottom_sheet/BrowseFilterBottomSheetTabRow.kt deleted file mode 100644 index f93fb417..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/filter_bottom_sheet/BrowseFilterBottomSheetTabRow.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.filter_bottom_sheet - -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheetTabRow -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent - -/** - * Browse Filter Bottom Sheet Tab Row. - * Lets user switch tabs in bottom sheet. - * - * @param onEvent [BrowseEvent] callback. - * @param pagerState [PagerState]. - */ -@Composable -fun BrowseFilterBottomSheetTabRow( - onEvent: (BrowseEvent) -> Unit, - pagerState: PagerState -) { - val tabItems = listOf( - stringResource(id = R.string.general_tab), - stringResource(id = R.string.filter_tab), - stringResource(id = R.string.sort_tab) - ) - - ModalBottomSheetTabRow( - selectedTabIndex = pagerState.currentPage, - tabs = tabItems - ) { index -> - onEvent(BrowseEvent.OnScrollToFilterPage(index, pagerState)) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/BrowseLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/BrowseLayout.kt deleted file mode 100644 index 6e5af4bd..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/layout/BrowseLayout.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.browse.components.layout - -import androidx.compose.runtime.Composable -import ua.acclorite.book_story.domain.model.SelectableFile -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.browse.components.layout.grid.BrowseGridLayout -import ua.acclorite.book_story.presentation.screens.browse.components.layout.list.BrowseListLayout -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout - -/** - * Browse Layout. - * Shows Grid or List. - * - * @param filteredFiles Filtered items. - * @param onLongItemClick OnLongItemClick callback. - * @param onFavoriteItemClick OnFavoriteItemClick callback. - * @param onItemClick OnItemClick callback. - */ -@Composable -fun BrowseLayout( - filteredFiles: List, - onLongItemClick: (SelectableFile) -> Unit, - onFavoriteItemClick: (SelectableFile) -> Unit, - onItemClick: (SelectableFile) -> Unit -) { - val mainState = MainViewModel.getState() - - when (mainState.value.browseLayout) { - BrowseLayout.LIST -> { - BrowseListLayout( - filteredFiles = filteredFiles, - onLongItemClick = onLongItemClick, - onFavoriteItemClick = onFavoriteItemClick, - onItemClick = onItemClick - ) - } - - BrowseLayout.GRID -> { - BrowseGridLayout( - gridSize = mainState.value.browseGridSize, - autoGridSize = mainState.value.browseAutoGridSize, - filteredFiles = filteredFiles, - onLongItemClick = onLongItemClick, - onFavoriteItemClick = onFavoriteItemClick, - onItemClick = onItemClick - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt deleted file mode 100644 index 631a438c..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt +++ /dev/null @@ -1,70 +0,0 @@ -@file:OptIn(ExperimentalPermissionsApi::class) - -package ua.acclorite.book_story.presentation.screens.browse.data - -import androidx.activity.ComponentActivity -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.Immutable -import androidx.compose.ui.focus.FocusRequester -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.domain.model.SelectableFile -import ua.acclorite.book_story.domain.util.OnNavigate -import ua.acclorite.book_story.domain.util.Selected -import java.io.File - -@Immutable -sealed class BrowseEvent { - data object OnClearViewModel : BrowseEvent() - - data class OnStoragePermissionRequest( - val activity: ComponentActivity, - val storagePermissionState: PermissionState - ) : BrowseEvent() - - data class OnStoragePermissionDismiss( - val permissionState: PermissionState - ) : BrowseEvent() - - data object OnRefreshList : BrowseEvent() - data class OnPermissionCheck( - val permissionState: PermissionState - ) : BrowseEvent() - - data class OnSelectFile( - val includedFileFormats: List, - val file: SelectableFile - ) : BrowseEvent() - - data class OnSelectFiles( - val includedFileFormats: List, - val files: List - ) : BrowseEvent() - - data class OnSelectBook(val book: Pair) : BrowseEvent() - data object OnSearchShowHide : BrowseEvent() - data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent() - data object OnClearSelectedFiles : BrowseEvent() - data class OnSearchQueryChange(val query: String) : BrowseEvent() - data object OnSearch : BrowseEvent() - data object OnAddingDialogRequest : BrowseEvent() - data object OnAddingDialogDismiss : BrowseEvent() - data object OnGetBooksFromFiles : BrowseEvent() - data class OnAddBooks( - val onNavigate: OnNavigate, - val resetScroll: () -> Unit, - val onFailed: () -> Unit, - val onSuccess: () -> Unit - ) : BrowseEvent() - - data object OnLoadList : BrowseEvent() - data object OnUpdateScrollOffset : BrowseEvent() - data class OnChangeDirectory(val directory: File, val savePreviousDirectory: Boolean) : - BrowseEvent() - - data object OnGoBackDirectory : BrowseEvent() - data object OnShowHideFilterBottomSheet : BrowseEvent() - data class OnScrollToFilterPage(val page: Int, val pagerState: PagerState?) : BrowseEvent() - data class OnUpdateFavoriteDirectory(val path: String) : BrowseEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt deleted file mode 100644 index ec7cce7f..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.browse.data - -import android.os.Environment -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.domain.model.SelectableFile -import ua.acclorite.book_story.domain.util.Selected -import java.io.File - -@Immutable -data class BrowseState( - val selectableFiles: List = emptyList(), - val listState: LazyListState = LazyListState(), - val gridState: LazyGridState = LazyGridState(), - - val selectedDirectory: File = Environment.getExternalStorageDirectory(), - val previousDirectory: File? = null, - val inNestedDirectory: Boolean = false, - val showFilterBottomSheet: Boolean = false, - val currentPage: Int = 1, - - val isLoading: Boolean = true, - val isRefreshing: Boolean = false, - val isError: Boolean = false, - val requestPermissionDialog: Boolean = false, - - val selectedItemsCount: Int = 0, - val hasSelectedItems: Boolean = false, - - val showSearch: Boolean = false, - val searchQuery: String = "", - val hasSearched: Boolean = false, - val hasFocused: Boolean = false, - - val showAddingDialog: Boolean = false, - val selectedBooks: List> = emptyList(), - val isBooksLoading: Boolean = false -) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt deleted file mode 100644 index d780556e..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt +++ /dev/null @@ -1,743 +0,0 @@ -@file:Suppress("LABEL_NAME_CLASH") - -package ua.acclorite.book_story.presentation.screens.browse.data - -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.Settings -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewModelScope -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.shouldShowRationale -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.domain.model.SelectableFile -import ua.acclorite.book_story.domain.use_case.book.InsertBook -import ua.acclorite.book_story.domain.use_case.favorite_directory.UpdateFavoriteDirectory -import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFile -import ua.acclorite.book_story.domain.use_case.file_system.GetFilesFromDevice -import ua.acclorite.book_story.domain.util.UIViewModel -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.core.util.launchActivity -import ua.acclorite.book_story.presentation.data.MainState -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseSortOrder -import javax.inject.Inject - -@OptIn(ExperimentalPermissionsApi::class) -@HiltViewModel -class BrowseViewModel @Inject constructor( - private val getBookFromFile: GetBookFromFile, - private val getFilesFromDevice: GetFilesFromDevice, - private val insertBook: InsertBook, - private val updateFavoriteDirectory: UpdateFavoriteDirectory, -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - - fun filterList(browseState: BrowseState, mainState: MainState): List { - fun thenCompareBy( - selector: (T) -> Comparable<*>? - ): Comparator { - return if (mainState.browseSortOrderDescending) { - compareByDescending(selector) - } else { - compareBy(selector) - } - } - - fun List.filterFiles(): List { - if (mainState.browseIncludedFilterItems.isEmpty()) { - return this - } - - return filter { file -> - when (file.isDirectory) { - true -> { - return@filter this.filter { - if (file == it) { - return@filter false - } - - it.fileOrDirectory.path.startsWith(file.fileOrDirectory.path) - }.filterFiles().isNotEmpty() - } - - false -> { - return@filter mainState.browseIncludedFilterItems.any { - file.fileOrDirectory.path.endsWith( - it, ignoreCase = true - ) - } - } - } - } - } - - return browseState.selectableFiles - .filterFiles() - .filter { - if ( - browseState.hasSearched - || mainState.browseFilesStructure == BrowseFilesStructure.ALL_FILES - ) { - return@filter !it.isDirectory - } - - if ( - Environment.getExternalStorageDirectory() == browseState.selectedDirectory - && it.isFavorite - && mainState.browsePinFavoriteDirectories - ) { - return@filter true - } - - it.parentDirectory == browseState.selectedDirectory - } - .sortedWith( - compareByDescending { - when (mainState.browsePinFavoriteDirectories) { - true -> it.isFavorite - false -> true - } - }.then( - compareByDescending { - when (mainState.browseSortOrder != BrowseSortOrder.FILE_TYPE) { - true -> it.isDirectory - false -> true - } - } - ).then( - thenCompareBy { - when (mainState.browseSortOrder) { - BrowseSortOrder.NAME -> { - it.fileOrDirectory.name.lowercase().trim() - } - - BrowseSortOrder.FILE_TYPE -> { - it.isDirectory - } - - BrowseSortOrder.FILE_FORMAT -> { - it.fileOrDirectory.extension - } - - BrowseSortOrder.FILE_SIZE -> { - it.fileOrDirectory.length() - } - - BrowseSortOrder.LAST_MODIFIED -> { - it.fileOrDirectory.lastModified() - } - } - } - ) - ) - } - } - - private val _state = MutableStateFlow(BrowseState()) - override val state = _state.asStateFlow() - - private var searchQueryJob: Job? = null - private var refreshListJob: Job? = null - private var getBooksJob: Job? = null - private var storagePermissionJob: Job? = null - private var changeDirectoryJob: Job? = null - - init { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isLoading = true, - selectedDirectory = Environment.getExternalStorageDirectory(), - inNestedDirectory = false - ) - } - getFilesFromDownloads() - } - } - - override fun onEvent(event: BrowseEvent) { - when (event) { - is BrowseEvent.OnClearViewModel -> clearViewModel() - - is BrowseEvent.OnStoragePermissionRequest -> { - val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - - val isPermissionGranted = if (legacyStoragePermission) { - event.storagePermissionState.status.isGranted - } else { - Environment.isExternalStorageManager() - } - - if (isPermissionGranted) { - _state.update { - it.copy( - requestPermissionDialog = false, - isError = false - ) - } - onEvent(BrowseEvent.OnRefreshList) - return - } - - if (legacyStoragePermission) { - if (!event.storagePermissionState.status.shouldShowRationale) { - event.storagePermissionState.launchPermissionRequest() - } else { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) - - var failure = false - intent.launchActivity(event.activity) { - failure = true - } - if (failure) { - return - } - } - } - - if (!legacyStoragePermission) { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent( - Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, - uri - ) - - var failure = false - intent.launchActivity(event.activity) { - failure = true - } - if (failure) { - return - } - } - - storagePermissionJob?.cancel() - storagePermissionJob = viewModelScope.launch { - while (true) { - val granted = if (legacyStoragePermission) { - event.storagePermissionState.status.isGranted - } else { - Environment.isExternalStorageManager() - } - - if (!granted) { - delay(1000) - yield() - continue - } - - yield() - - _state.update { - it.copy( - requestPermissionDialog = false, - isError = false - ) - } - onEvent(BrowseEvent.OnRefreshList) - break - } - } - } - - is BrowseEvent.OnStoragePermissionDismiss -> { - val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - val isPermissionGranted = - if (!legacyPermission) Environment.isExternalStorageManager() - else event.permissionState.status.isGranted - - storagePermissionJob?.cancel() - _state.update { - it.copy( - requestPermissionDialog = false - ) - } - - if (isPermissionGranted) { - viewModelScope.launch(Dispatchers.IO) { - getFilesFromDownloads() - } - } else { - _state.update { - it.copy( - isError = true - ) - } - } - } - - is BrowseEvent.OnRefreshList -> { - refreshListJob?.cancel() - refreshListJob = viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isRefreshing = true, - hasSelectedItems = false, - showSearch = false, - listState = LazyListState(), - gridState = LazyGridState(), - ) - } - yield() - getFilesFromDownloads("") - delay(500) - _state.update { - it.copy( - isRefreshing = false - ) - } - } - } - - is BrowseEvent.OnPermissionCheck -> { - viewModelScope.launch(Dispatchers.IO) { - val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - val isPermissionGranted = - if (!legacyPermission) Environment.isExternalStorageManager() - else event.permissionState.status.isGranted - - if (isPermissionGranted) { - return@launch - } - - _state.update { - it.copy( - requestPermissionDialog = true, - isError = false - ) - } - } - } - - is BrowseEvent.OnSelectFile -> { - viewModelScope.launch(Dispatchers.IO) { - val editedList = _state.value.selectableFiles.map { file -> - when (event.file.isDirectory) { - false -> { - if (event.file.fileOrDirectory.path == file.fileOrDirectory.path) { - file.copy( - isSelected = !file.isSelected - ) - } else { - file - } - } - - true -> { - if ( - file.fileOrDirectory.path.startsWith( - event.file.fileOrDirectory.path - ) && event.includedFileFormats.run { - if (isEmpty()) return@run true - any { - file.fileOrDirectory.path.endsWith( - it, ignoreCase = true - ) || file.isDirectory - } - } - ) { - file.copy( - isSelected = !event.file.isSelected - ) - } else { - file - } - } - } - } - - _state.update { - it.copy( - selectableFiles = editedList, - selectedItemsCount = editedList.filter { file -> - file.isSelected && !file.isDirectory - }.size.run { - if (this == 0) return@run it.selectedItemsCount - this - }, - hasSelectedItems = editedList.any { file -> - file.isSelected && !file.isDirectory - } - ) - } - } - } - - is BrowseEvent.OnSelectFiles -> { - viewModelScope.launch(Dispatchers.IO) { - val editedList = _state.value.selectableFiles.map { file -> - if ( - event.files.any { - file.fileOrDirectory.path.startsWith(it.fileOrDirectory.path) - } && event.includedFileFormats.run { - if (isEmpty()) return@run true - any { - file.fileOrDirectory.path.endsWith( - it, ignoreCase = true - ) || file.isDirectory - } - } - ) { - file.copy(isSelected = true) - } else { - file - } - } - - _state.update { - it.copy( - selectableFiles = editedList, - selectedItemsCount = editedList.filter { file -> - file.isSelected && !file.isDirectory - }.size.run { - if (this == 0) return@run it.selectedItemsCount - this - }, - hasSelectedItems = editedList.any { file -> - file.isSelected && !file.isDirectory - } - ) - } - } - } - - is BrowseEvent.OnSelectBook -> { - viewModelScope.launch(Dispatchers.IO) { - val indexOfFile = _state.value.selectedBooks.indexOf(event.book) - - if (indexOfFile == -1) { - return@launch - } - - val editedList = _state.value.selectedBooks.toMutableList() - editedList[indexOfFile] = editedList[indexOfFile].first to - !editedList[indexOfFile].second - - if (!editedList.filter { it.first is NullableBook.NotNull } - .any { it.second } - ) { - return@launch - } - - _state.update { - it.copy( - selectedBooks = editedList - ) - } - } - } - - is BrowseEvent.OnSearchShowHide -> { - viewModelScope.launch(Dispatchers.IO) { - val shouldHide = _state.value.showSearch - - if (shouldHide) { - getFilesFromDownloads("") - } else { - _state.update { - it.copy( - searchQuery = "", - hasSearched = false, - hasFocused = false - ) - } - } - _state.update { - it.copy( - showSearch = !shouldHide - ) - } - } - } - - is BrowseEvent.OnRequestFocus -> { - if (!_state.value.hasFocused) { - event.focusRequester.requestFocus() - _state.update { - it.copy( - hasFocused = true - ) - } - } - } - - is BrowseEvent.OnClearSelectedFiles -> { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - selectableFiles = it.selectableFiles.map { file -> - file.copy( - isSelected = false - ) - }, - hasSelectedItems = false - ) - } - } - } - - is BrowseEvent.OnSearchQueryChange -> { - _state.update { - it.copy( - searchQuery = event.query - ) - } - searchQueryJob?.cancel() - searchQueryJob = viewModelScope.launch(Dispatchers.IO) { - delay(500) - yield() - onEvent(BrowseEvent.OnSearch) - } - } - - is BrowseEvent.OnSearch -> { - viewModelScope.launch(Dispatchers.IO) { - getFilesFromDownloads() - } - } - - is BrowseEvent.OnAddingDialogDismiss -> { - _state.update { - it.copy( - showAddingDialog = false - ) - } - getBooksJob?.cancel() - } - - is BrowseEvent.OnAddingDialogRequest -> { - _state.update { - it.copy( - showAddingDialog = true, - selectedBooks = emptyList() - ) - } - onEvent(BrowseEvent.OnGetBooksFromFiles) - } - - is BrowseEvent.OnGetBooksFromFiles -> { - getBooksJob = viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isBooksLoading = true - ) - } - - yield() - - val books = mutableListOf() - _state.value.selectableFiles - .filter { it.isSelected && !it.isDirectory } - .ifEmpty { - _state.update { - it.copy( - isBooksLoading = false, - showAddingDialog = false - ) - } - return@launch - } - .map { it.fileOrDirectory } - .forEach { - yield() - books.add(getBookFromFile.execute(it)) - } - - yield() - - _state.update { - it.copy( - selectedBooks = books.map { book -> book to true }, - isBooksLoading = false - ) - } - } - } - - is BrowseEvent.OnAddBooks -> { - viewModelScope.launch { - val booksToInsert = _state.value.selectedBooks - .filter { it.first is NullableBook.NotNull } - .filter { it.second } - .map { it.first } - - if (booksToInsert.isEmpty()) { - return@launch - } - - var failed = false - booksToInsert.forEach { - if (!insertBook.execute(it.bookWithTextAndCover!!)) { - failed = true - } - } - - if (failed) { - _state.update { - it.copy( - showAddingDialog = false - ) - } - onEvent(BrowseEvent.OnLoadList) - onEvent(BrowseEvent.OnClearSelectedFiles) - event.onFailed() - return@launch - } - - event.resetScroll() - event.onNavigate { - navigate(Screen.Library) - } - event.onSuccess() - - _state.update { - it.copy( - showAddingDialog = false - ) - } - onEvent(BrowseEvent.OnLoadList) - onEvent(BrowseEvent.OnClearSelectedFiles) - } - } - - is BrowseEvent.OnLoadList -> { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isLoading = true, - hasSelectedItems = false, - listState = LazyListState(), - gridState = LazyGridState() - ) - } - getFilesFromDownloads() - } - } - - is BrowseEvent.OnUpdateScrollOffset -> { - _state.update { - it.copy( - listState = LazyListState( - it.listState.firstVisibleItemIndex, - 0 - ), - gridState = LazyGridState( - it.gridState.firstVisibleItemIndex, - 0 - ) - ) - } - } - - is BrowseEvent.OnChangeDirectory -> { - viewModelScope.launch { - changeDirectoryJob?.cancel() - searchQueryJob?.cancel() - - changeDirectoryJob = launch(Dispatchers.IO) { - yield() - _state.update { - it.copy( - listState = LazyListState(), - gridState = LazyGridState(), - selectedDirectory = event.directory, - previousDirectory = if (event.savePreviousDirectory) it.selectedDirectory - else event.directory.parentFile, - inNestedDirectory = event.directory != Environment.getExternalStorageDirectory() - ) - } - } - } - } - - is BrowseEvent.OnGoBackDirectory -> { - onEvent( - BrowseEvent.OnChangeDirectory( - _state.value.previousDirectory - ?: Environment.getExternalStorageDirectory(), - savePreviousDirectory = false - ) - ) - } - - is BrowseEvent.OnShowHideFilterBottomSheet -> { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - showFilterBottomSheet = !it.showFilterBottomSheet - ) - } - } - } - - is BrowseEvent.OnScrollToFilterPage -> { - viewModelScope.launch { - _state.update { - event.pagerState?.scrollToPage(event.page) - - it.copy( - currentPage = event.page - ) - } - } - } - - is BrowseEvent.OnUpdateFavoriteDirectory -> { - viewModelScope.launch { - _state.update { - it.copy(isLoading = true) - } - updateFavoriteDirectory.execute(event.path) - getFilesFromDownloads() - } - } - } - } - - private fun clearViewModel() { - viewModelScope.launch(Dispatchers.Main) { - _state.update { - it.copy(isError = false) - } - } - } - - private suspend fun getFilesFromDownloads( - query: String = if (_state.value.showSearch) _state.value.searchQuery else "" - ) { - getFilesFromDevice.execute(query).apply { - yield() - _state.update { - it.copy( - selectableFiles = this, - selectedItemsCount = 0, - hasSearched = query.isNotBlank(), - hasSelectedItems = false, - isLoading = false - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt deleted file mode 100644 index 9aa73dd4..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt +++ /dev/null @@ -1,181 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.help - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.RestartAlt -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.util.Position -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.IconButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideHelpTips -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.help.components.HelpItem -import ua.acclorite.book_story.presentation.screens.help.components.items.HelpClickMeNoteItem -import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent -import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel -import ua.acclorite.book_story.presentation.screens.start.data.StartEvent -import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel - -@Composable -fun HelpScreenRoot(screen: Screen.Help) { - val onEvent = HelpViewModel.getEvent() - - LaunchedEffect(Unit) { - onEvent( - HelpEvent.OnInit( - screen = screen - ) - ) - } - - HelpScreen() -} - -@OptIn( - ExperimentalMaterial3Api::class -) -@Composable -private fun HelpScreen() { - val state = HelpViewModel.getState() - val onMainEvent = MainViewModel.getEvent() - val onBrowseEvent = BrowseViewModel.getEvent() - val onStartEvent = StartViewModel.getEvent() - val onNavigate = LocalNavigator.current - - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.help_screen)) - }, - navigationIcon = { - if (!state.value.fromStart) { - GoBackButton(onNavigate = onNavigate) - } - }, - actions = { - if (!state.value.fromStart) { - IconButton( - icon = Icons.Outlined.RestartAlt, - contentDescription = R.string.reset_start_content_desc, - disableOnClick = false - ) { - onStartEvent(StartEvent.OnResetStartScreen) - onMainEvent(MainEvent.OnChangeShowStartScreen(true)) - onNavigate { - navigate(Screen.Start, saveInBackStack = false) - clearBackStack() - } - } - } - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - }, - bottomBar = { - if (state.value.fromStart) { - Column { - Spacer(modifier = Modifier.height(18.dp)) - Button( - modifier = Modifier - .navigationBarsPadding() - .padding(bottom = 8.dp) - .padding(horizontal = 18.dp) - .fillMaxWidth(), - shape = RoundedCornerShape(100), - onClick = { - onBrowseEvent(BrowseEvent.OnLoadList) - onMainEvent(MainEvent.OnChangeShowStartScreen(false)) - onNavigate { - navigate(Screen.Browse, saveInBackStack = false) - } - } - ) { - Text(text = stringResource(id = R.string.done)) - } - } - } - } - ) { paddingValues -> - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(paddingValues), - state = lazyListState - ) { - item { - Spacer(modifier = Modifier.height(16.dp)) - } - - item { - HelpClickMeNoteItem() - } - - item { - Spacer(modifier = Modifier.height(8.dp)) - } - - itemsIndexed( - Constants.provideHelpTips(), - key = { _, helpTip -> helpTip.title } - ) { index, helpTip -> - HelpItem( - helpTip = helpTip, - position = when (index) { - 0 -> Position.TOP - Constants.provideHelpTips().lastIndex -> Position.BOTTOM - else -> Position.CENTER - }, - fromStart = state.value.fromStart - ) - } - - item { - Spacer(modifier = Modifier.height(48.dp)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt deleted file mode 100644 index 1fdd2999..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.help.components - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withLink -import androidx.compose.ui.text.withStyle - -/** - * Help clickable annotation. - * Mainly used for highlighting screens. - * - * @param onClick OnClick callback, to, for example navigate to another screen. - * @param block Lambda to append all text to be highlighted. - */ -@Composable -fun AnnotatedString.Builder.HelpAnnotation( - onClick: () -> Unit, - block: @Composable AnnotatedString.Builder.() -> Unit -) { - withLink( - LinkAnnotation.Url("", styles = null) { - onClick() - } - ) { - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.tertiary, - fontWeight = FontWeight.Medium - ) - ) { - block() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpClickableNote.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpClickableNote.kt deleted file mode 100644 index 9edfcd11..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpClickableNote.kt +++ /dev/null @@ -1,61 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.help.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.Tooltip -import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.ui.ExpandingTransition - -/** - * Help Clickable Note. - * Collapsible simple note to user. - * - * @param text [AnnotatedString]. - */ -@Composable -fun HelpClickableNote(text: AnnotatedString) { - val showNote = rememberSaveable { mutableStateOf(true) } - - Column(Modifier.padding(horizontal = 18.dp)) { - Tooltip(text = stringResource(R.string.note_content_desc)) { - Icon( - imageVector = Icons.Outlined.Info, - modifier = Modifier - .size(20.dp) - .noRippleClickable { - showNote.value = !showNote.value - }, - contentDescription = stringResource(R.string.note_content_desc), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - ExpandingTransition(visible = showNote.value) { - Column { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = text, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt deleted file mode 100644 index 039932ec..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.help.components.items - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.buildAnnotatedString -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation -import ua.acclorite.book_story.presentation.screens.help.components.HelpClickableNote - -/** - * Help Click Me Note Item. - * Shows an example to user on how to interact with highlighted text. - */ -@Composable -fun HelpClickMeNoteItem() { - val context = LocalContext.current - - HelpClickableNote( - text = buildAnnotatedString { - append(stringResource(id = R.string.help_clickable_note_1) + " ") - - HelpAnnotation( - onClick = { - context.getString(R.string.help_clickable_note_action) - .showToast(context = context, longToast = false) - } - ) { - append(stringResource(id = R.string.help_clickable_note_2)) - } - - append(" " + stringResource(id = R.string.help_clickable_note_3)) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt deleted file mode 100644 index 286059f6..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.help.data - -import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.presentation.core.navigation.Screen - -@Immutable -sealed class HelpEvent { - data class OnInit( - val screen: Screen.Help - ) : HelpEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt deleted file mode 100644 index 9bf2bf88..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.help.data - -import androidx.compose.runtime.Immutable - -@Immutable -data class HelpState( - val fromStart: Boolean = false -) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt deleted file mode 100644 index b460f05c..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.help.data - -import androidx.compose.runtime.Composable -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import ua.acclorite.book_story.domain.util.UIViewModel -import javax.inject.Inject - -@HiltViewModel -class HelpViewModel @Inject constructor( - -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } - - private val _state = MutableStateFlow(HelpState()) - override val state = _state.asStateFlow() - - override fun onEvent(event: HelpEvent) { - when (event) { - is HelpEvent.OnInit -> init(event) - } - } - - private fun init(event: HelpEvent.OnInit) { - _state.update { - it.copy( - fromStart = event.screen.fromStart - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt deleted file mode 100644 index ea4271cc..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt +++ /dev/null @@ -1,297 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.history - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.outlined.DeleteSweep -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.core.components.common.IconButton -import ua.acclorite.book_story.presentation.core.components.common.SearchTextField -import ua.acclorite.book_story.presentation.core.components.common.Snackbar -import ua.acclorite.book_story.presentation.core.components.placeholder.EmptyPlaceholder -import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBar -import ua.acclorite.book_story.presentation.core.components.top_bar.TopAppBarData -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.NavigationIconButton -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.screens.history.components.HistoryDeleteWholeHistoryDialog -import ua.acclorite.book_story.presentation.screens.history.components.HistoryItem -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle -import ua.acclorite.book_story.presentation.ui.DefaultTransition -import ua.acclorite.book_story.presentation.ui.Transitions - -@Composable -fun HistoryScreenRoot() { - val onEvent = HistoryViewModel.getEvent() - - LaunchedEffect(Unit) { - onEvent(HistoryEvent.OnUpdateScrollOffset) - } - - HistoryScreen() -} - -@OptIn( - ExperimentalMaterial3Api::class, - ExperimentalMaterialApi::class -) -@Composable -private fun HistoryScreen() { - val context = LocalContext.current - val state = HistoryViewModel.getState() - val onEvent = HistoryViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onNavigate = LocalNavigator.current - - val refreshState = rememberPullRefreshState( - refreshing = state.value.isRefreshing, - onRefresh = { - onEvent(HistoryEvent.OnRefreshList) - } - ) - val focusRequester = remember { FocusRequester() } - val snackbarState = remember { SnackbarHostState() } - - if (state.value.showDeleteWholeHistoryDialog) { - HistoryDeleteWholeHistoryDialog() - } - - Scaffold( - Modifier - .fillMaxSize() - .pullRefresh(refreshState), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - TopAppBar( - scrollBehavior = null, - isTopBarScrolled = state.value.listState.canScrollBackward, - - shownTopBar = when { - state.value.showSearch -> 1 - else -> 0 - }, - topBars = listOf( - TopAppBarData( - contentID = 0, - contentNavigationIcon = {}, - contentTitle = { - Text(stringResource(id = R.string.history_screen)) - }, - contentActions = { - IconButton( - icon = Icons.Default.Search, - contentDescription = R.string.search_content_desc, - disableOnClick = true, - ) { - onEvent(HistoryEvent.OnSearchShowHide) - } - IconButton( - icon = Icons.Outlined.DeleteSweep, - contentDescription = R.string.delete_whole_history_content_desc, - disableOnClick = false, - enabled = !state.value.isLoading - && !state.value.isRefreshing - && state.value.history.isNotEmpty() - && !state.value.showDeleteWholeHistoryDialog - ) { - onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) - } - NavigationIconButton() - } - ), - - TopAppBarData( - contentID = 1, - contentNavigationIcon = { - IconButton( - icon = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = R.string.exit_search_content_desc, - disableOnClick = true - ) { - onEvent(HistoryEvent.OnSearchShowHide) - } - }, - contentTitle = { - SearchTextField( - modifier = Modifier - .focusRequester(focusRequester) - .onGloballyPositioned { - onEvent(HistoryEvent.OnRequestFocus(focusRequester)) - }, - query = state.value.searchQuery, - onQueryChange = { - onEvent(HistoryEvent.OnSearchQueryChange(it)) - }, - onSearch = { - onEvent(HistoryEvent.OnSearch) - } - ) - }, - contentActions = { - NavigationIconButton() - }, - ) - ) - ) - }, - bottomBar = { - Snackbar(snackbarState = snackbarState) - } - ) { paddingValues -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) - ) { - DefaultTransition(visible = !state.value.isLoading) { - LazyColumn( - Modifier - .fillMaxSize(), - state = state.value.listState, - ) { - item { - Spacer(modifier = Modifier.height(12.dp)) - } - - state.value.history.forEachIndexed { index, groupedHistory -> - item(key = groupedHistory.title) { - if (index > 0) { - Spacer(modifier = Modifier.height(8.dp)) - } - - SettingsCategoryTitle( - modifier = Modifier.animateItem(), - title = when (groupedHistory.title) { - "today" -> stringResource(id = R.string.today) - "yesterday" -> stringResource(id = R.string.yesterday) - else -> groupedHistory.title - }, - padding = 16.dp - ) - - Spacer(modifier = Modifier.height(8.dp)) - } - - items( - groupedHistory.history, - key = { it.id } - ) { - HistoryItem( - modifier = Modifier.animateItem(), - history = it, - isOnClickEnabled = !state.value.isRefreshing, - onBodyClick = { - onNavigate { - navigate( - Screen.BookInfo( - bookId = it.bookId, - startUpdate = false - ) - ) - } - }, - onTitleClick = { - onEvent( - HistoryEvent.OnNavigateToReaderScreen( - onNavigate = onNavigate, - book = it.book!! - ) - ) - }, - isDeleteEnabled = !state.value.isRefreshing, - onDeleteClick = { - onEvent( - HistoryEvent.OnDeleteHistoryElement( - historyToDelete = it, - snackbarState = snackbarState, - context = context, - refreshList = { - onLibraryEvent( - LibraryEvent.OnLoadList - ) - } - ) - ) - } - ) - } - } - - item { - Spacer(modifier = Modifier.height(12.dp)) - } - } - } - - AnimatedVisibility( - visible = !state.value.isLoading - && state.value.history.isEmpty() - && !state.value.isRefreshing, - modifier = Modifier.align(Alignment.Center), - enter = Transitions.DefaultTransitionIn, - exit = fadeOut(tween(0)) - ) { - EmptyPlaceholder( - modifier = Modifier.align(Alignment.Center), - message = stringResource(id = R.string.history_empty), - icon = painterResource(id = R.drawable.empty_history) - ) - } - - PullRefreshIndicator( - state.value.isRefreshing, - refreshState, - Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.colorScheme.inverseSurface, - contentColor = MaterialTheme.colorScheme.inverseOnSurface - ) - } - } - - BackHandler { - if (state.value.showSearch) { - onEvent(HistoryEvent.OnSearchShowHide) - return@BackHandler - } - - onNavigate { - navigate(Screen.Library) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt deleted file mode 100644 index 61eedbd5..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryDeleteWholeHistoryDialog.kt +++ /dev/null @@ -1,45 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.history.components - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel - -/** - * Delete whole history dialog. - */ -@Composable -fun HistoryDeleteWholeHistoryDialog() { - val context = LocalContext.current - val onEvent = HistoryViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - - DialogWithContent( - title = stringResource(id = R.string.delete_history), - imageVectorIcon = Icons.Outlined.DeleteOutline, - description = stringResource( - id = R.string.delete_history_description - ), - actionText = stringResource(id = R.string.delete), - onDismiss = { onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) }, - withDivider = false, - isActionEnabled = true, - onAction = { - onEvent( - HistoryEvent.OnDeleteWholeHistory { - onLibraryEvent(LibraryEvent.OnLoadList) - } - ) - context.getString(R.string.history_deleted) - .showToast(context = context) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt deleted file mode 100644 index 83ea5a3c..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.history.data - -import android.content.Context -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Immutable -import androidx.compose.ui.focus.FocusRequester -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.History -import ua.acclorite.book_story.domain.util.OnNavigate - -@Immutable -sealed class HistoryEvent { - data object OnRefreshList : HistoryEvent() - data object OnLoadList : HistoryEvent() - data object OnShowHideDeleteWholeHistoryDialog : HistoryEvent() - data class OnDeleteWholeHistory(val refreshList: () -> Unit) : HistoryEvent() - data class OnDeleteHistoryElement( - val historyToDelete: History, - val snackbarState: SnackbarHostState, - val refreshList: () -> Unit, - val context: Context - ) : HistoryEvent() - - data class OnSearchQueryChange(val query: String) : HistoryEvent() - data object OnSearch : HistoryEvent() - data object OnSearchShowHide : HistoryEvent() - data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent() - data class OnUpdateBook(val book: Book) : HistoryEvent() - data class OnNavigateToReaderScreen( - val onNavigate: OnNavigate, - val book: Book - ) : HistoryEvent() - - data object OnUpdateScrollOffset : HistoryEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt deleted file mode 100644 index 87c0ac75..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt +++ /dev/null @@ -1,383 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.history.data - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.material3.SnackbarResult -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.GroupedHistory -import ua.acclorite.book_story.domain.model.History -import ua.acclorite.book_story.domain.use_case.book.GetBooksById -import ua.acclorite.book_story.domain.use_case.history.DeleteHistory -import ua.acclorite.book_story.domain.use_case.history.DeleteWholeHistory -import ua.acclorite.book_story.domain.use_case.history.GetHistory -import ua.acclorite.book_story.domain.use_case.history.InsertHistory -import ua.acclorite.book_story.domain.util.UIViewModel -import ua.acclorite.book_story.presentation.core.navigation.Screen -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale -import javax.inject.Inject - -@HiltViewModel -class HistoryViewModel @Inject constructor( - private val insertHistory: InsertHistory, - private val getHistory: GetHistory, - private val deleteWholeHistory: DeleteWholeHistory, - private val deleteHistory: DeleteHistory, - private val getBooksById: GetBooksById -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } - - private val _state = MutableStateFlow(HistoryState()) - override val state = _state.asStateFlow() - - private var job: Job? = null - private var job2: Job? = null - private var job3: Job? = null - - init { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isLoading = true - ) - } - getHistoryFromDatabase() - } - } - - override fun onEvent(event: HistoryEvent) { - when (event) { - is HistoryEvent.OnRefreshList -> { - job3?.cancel() - job3 = viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isRefreshing = true, - showSearch = false, - listState = LazyListState(0, 0), - ) - } - - yield() - getHistoryFromDatabase("") - - delay(500) - _state.update { - it.copy( - isRefreshing = false - ) - } - } - } - - is HistoryEvent.OnLoadList -> { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isLoading = true, - listState = LazyListState(0, 0), - ) - } - getHistoryFromDatabase() - } - } - - is HistoryEvent.OnDeleteWholeHistory -> { - viewModelScope.launch { - _state.update { - it.copy( - showDeleteWholeHistoryDialog = false, - isLoading = true - ) - } - - deleteWholeHistory.execute() - event.refreshList() - getHistoryFromDatabase("") - } - - } - - is HistoryEvent.OnShowHideDeleteWholeHistoryDialog -> { - _state.update { - it.copy( - showDeleteWholeHistoryDialog = !it.showDeleteWholeHistoryDialog - ) - } - } - - is HistoryEvent.OnDeleteHistoryElement -> { - viewModelScope.launch(Dispatchers.IO) { - deleteHistory.execute(event.historyToDelete) - - _state.update { - it.copy( - isRefreshing = true - ) - } - getHistoryFromDatabase() - _state.update { - it.copy( - isRefreshing = false - ) - } - - event.refreshList() - - job2?.cancel() - event.snackbarState.currentSnackbarData?.dismiss() - - job2 = launch(Dispatchers.IO) { - yield() - delay(10000) - yield() - event.snackbarState.currentSnackbarData?.dismiss() - } - val snackbar = event.snackbarState.showSnackbar( - event.context.getString(R.string.history_element_deleted), - event.context.getString(R.string.undo) - ) - - when (snackbar) { - SnackbarResult.Dismissed -> Unit - SnackbarResult.ActionPerformed -> { - insertHistory.execute(event.historyToDelete) - event.refreshList() - _state.update { - it.copy( - isRefreshing = true - ) - } - getHistoryFromDatabase() - delay(500) - _state.update { - it.copy( - isRefreshing = false - ) - } - } - } - } - } - - is HistoryEvent.OnSearchShowHide -> { - viewModelScope.launch(Dispatchers.IO) { - val shouldHide = _state.value.showSearch - - if (shouldHide) { - getHistoryFromDatabase("") - } else { - _state.update { - it.copy( - searchQuery = "", - hasFocused = false - ) - } - } - _state.update { - it.copy( - showSearch = !shouldHide - ) - } - } - } - - is HistoryEvent.OnSearchQueryChange -> { - _state.update { - it.copy( - searchQuery = event.query - ) - } - job?.cancel() - job = viewModelScope.launch(Dispatchers.IO) { - delay(500) - yield() - onEvent(HistoryEvent.OnSearch) - } - } - - is HistoryEvent.OnSearch -> { - viewModelScope.launch(Dispatchers.IO) { - getHistoryFromDatabase() - } - } - - is HistoryEvent.OnRequestFocus -> { - if (!_state.value.hasFocused) { - event.focusRequester.requestFocus() - _state.update { - it.copy( - hasFocused = true - ) - } - } - } - - is HistoryEvent.OnUpdateBook -> { - viewModelScope.launch(Dispatchers.IO) { - val history = _state.value.history.map { - it.copy( - history = it.history.map { history -> - if (history.bookId == event.book.id) { - history.copy( - book = event.book - ) - } else { - history - } - } - ) - } - - _state.update { - it.copy( - history = history - ) - } - } - } - - is HistoryEvent.OnNavigateToReaderScreen -> { - viewModelScope.launch { - event.book.id.let { - insertHistory.execute( - History( - bookId = it, - book = null, - time = Date().time - ) - ) - } - event.onNavigate { - navigate(Screen.Reader(event.book.id)) - } - } - } - - is HistoryEvent.OnUpdateScrollOffset -> { - _state.update { - it.copy( - listState = LazyListState( - it.listState.firstVisibleItemIndex, - 0 - ) - ) - } - } - } - } - - private suspend fun getHistoryFromDatabase( - query: String = if (_state.value.showSearch) _state.value.searchQuery else "" - ) { - fun isSameDay(cal1: Calendar, cal2: Calendar): Boolean { - return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && - cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) - } - - fun filterMaxElementsById(elements: List): List { - val groupedById = elements.groupBy { it.bookId } - val maxElementsById = groupedById.map { (_, values) -> - values.maxByOrNull { it.time } - } - return maxElementsById.filterNotNull() - } - - val historyWithoutBooks = getHistory.execute() - .sortedByDescending { it.time } - - if (historyWithoutBooks.isEmpty()) { - _state.update { - it.copy( - history = emptyList(), - isLoading = false - ) - } - return - } - - val books = getBooksById.execute( - historyWithoutBooks.map { it.bookId }.distinct() - ) - - if (books.isEmpty()) { - _state.update { - it.copy( - history = emptyList(), - isLoading = false - ) - } - return - } - - val history = historyWithoutBooks.mapNotNull { - val book = books.find { book -> book.id == it.bookId } ?: return@mapNotNull null - it.copy( - book = book - ) - } - - val groupedHistory = mutableListOf() - - history - .filter { - val book = books.find { book -> book.id == it.bookId } - book?.title?.lowercase()?.trim()?.contains(query.lowercase().trim()) == true - }.groupBy { item -> - val calendar = Calendar.getInstance().apply { - timeInMillis = item.time - } - val now = Calendar.getInstance() - - when { - isSameDay(calendar, now) -> "today" - isSameDay( - calendar, - now.apply { - add( - Calendar.DAY_OF_YEAR, - -1 - ) - }) -> "yesterday" - - else -> SimpleDateFormat( - "dd.MM.yy", - Locale.getDefault() - ).format(item.time) - } - }.forEach { (key, value) -> - groupedHistory.add( - GroupedHistory( - key, - filterMaxElementsById(value) - ) - ) - } - - _state.update { - it.copy( - history = groupedHistory, - isLoading = false - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt deleted file mode 100644 index cdfe43f5..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt +++ /dev/null @@ -1,276 +0,0 @@ -@file:OptIn(ExperimentalMaterialApi::class) - -package ua.acclorite.book_story.presentation.screens.library - -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.PullRefreshState -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -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.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.Selected -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.core.components.common.header -import ua.acclorite.book_story.presentation.core.components.placeholder.EmptyPlaceholder -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.library.components.LibraryBookItem -import ua.acclorite.book_story.presentation.screens.library.components.LibraryTopBar -import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryDeleteDialog -import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.ui.DefaultTransition -import ua.acclorite.book_story.presentation.ui.Transitions - -@Composable -fun LibraryScreenRoot() { - val state = LibraryViewModel.getState() - val onEvent = LibraryViewModel.getEvent() - - val pagerState = rememberPagerState( - initialPage = state.value.currentPage, - pageCount = { Category.entries.size } - ) - val refreshState = rememberPullRefreshState( - refreshing = state.value.isRefreshing, - onRefresh = { - onEvent(LibraryEvent.OnRefreshList) - } - ) - - LaunchedEffect(Unit) { - onEvent(LibraryEvent.OnScrollToPage(state.value.currentPage, pagerState)) - } - LaunchedEffect(pagerState.currentPage) { - if (pagerState.currentPage != state.value.currentPage) { - onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage)) - } - } - - LibraryScreen( - pagerState = pagerState, - refreshState = refreshState - ) -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun LibraryScreen( - pagerState: PagerState, - refreshState: PullRefreshState -) { - val state = LibraryViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = LibraryViewModel.getEvent() - val onNavigate = LocalNavigator.current - - var isScrollInProgress by remember { mutableStateOf(false) } - - if (state.value.showMoveDialog) { - LibraryMoveDialog(pagerState = pagerState) - } - if (state.value.showDeleteDialog) { - LibraryDeleteDialog() - } - - Scaffold( - Modifier - .fillMaxSize() - .pullRefresh(refreshState), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LibraryTopBar(pagerState = pagerState) - } - ) { paddingValues -> - Box( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) - ) { - HorizontalPager( - state = pagerState, - userScrollEnabled = !isScrollInProgress - ) { index -> - val category = remember { Category.entries[index] } - val books = remember(state.value.categorizedBooks) { - state.value.categorizedBooks.find { - it.category == category - }?.books?.sortedWith( - compareByDescending> { it.first.lastOpened } - .thenBy { it.first.title } - ) ?: emptyList() - } - val listState = rememberLazyGridState() - - LaunchedEffect(listState.isScrollInProgress, pagerState.currentPage) { - if (listState.isScrollInProgress != isScrollInProgress - && pagerState.currentPage == state.value.currentPage - ) { - isScrollInProgress = listState.isScrollInProgress - } - } - - Box(modifier = Modifier.fillMaxSize()) { - DefaultTransition(visible = !state.value.isLoading) { - LazyVerticalGrid( - columns = GridCells.Adaptive(120.dp), - modifier = Modifier.fillMaxSize(), - state = listState, - contentPadding = PaddingValues(horizontal = 8.dp) - ) { - header { - Spacer(modifier = Modifier.height(8.dp)) - } - - items( - books, - key = { it.first.id } - ) { - LibraryBookItem( - book = it, - modifier = Modifier.animateItem( - fadeInSpec = null, - fadeOutSpec = null - ), - onCoverImageClick = { - if (state.value.hasSelectedItems) { - onEvent(LibraryEvent.OnSelectBook(it)) - } else { - onNavigate { - navigate( - Screen.BookInfo( - bookId = it.first.id, - startUpdate = false - ) - ) - } - } - }, - onLongClick = { - if (!it.second) { - onEvent(LibraryEvent.OnSelectBook(it, true)) - } - }, - onButtonClick = { - onEvent( - LibraryEvent.OnNavigateToReaderScreen( - onNavigate = onNavigate, - book = it.first - ) - ) - } - ) - } - - header { - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - - AnimatedVisibility( - visible = !state.value.isLoading - && !state.value.isRefreshing - && books.isEmpty(), - modifier = Modifier.align(Alignment.Center), - enter = Transitions.DefaultTransitionIn, - exit = fadeOut(tween(0)) - ) { - EmptyPlaceholder( - message = stringResource(id = R.string.library_empty), - icon = painterResource(id = R.drawable.empty_library), - modifier = Modifier.align(Alignment.Center), - actionTitle = stringResource(id = R.string.add_book) - ) { - onNavigate { - navigate(Screen.Browse) - } - } - } - } - } - - PullRefreshIndicator( - state.value.isRefreshing, - refreshState, - Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.colorScheme.inverseSurface, - contentColor = MaterialTheme.colorScheme.inverseOnSurface - ) - } - } - - val activity = LocalContext.current as ComponentActivity - val scope = rememberCoroutineScope() - var shouldExit = rememberSaveable { false } - BackHandler { - if (state.value.hasSelectedItems) { - onEvent(LibraryEvent.OnClearSelectedBooks) - return@BackHandler - } - - if (state.value.showSearch) { - onEvent(LibraryEvent.OnSearchShowHide) - return@BackHandler - } - - if (state.value.currentPage > 0) { - onEvent(LibraryEvent.OnScrollToPage(0, pagerState)) - return@BackHandler - } - - if (shouldExit || !mainState.value.doublePressExit) { - activity.finish() - return@BackHandler - } - - activity.getString(R.string.press_again_toast) - .showToast(context = activity, longToast = false) - shouldExit = true - - scope.launch { - delay(1500) - shouldExit = false - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt deleted file mode 100644 index ae2dae40..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryDeleteDialog.kt +++ /dev/null @@ -1,51 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.library.components.dialog - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel - -/** - * Delete dialog. Deletes all selected books. - */ -@Composable -fun LibraryDeleteDialog() { - val context = LocalContext.current - val state = LibraryViewModel.getState() - val onEvent = LibraryViewModel.getEvent() - val onBrowseEvent = BrowseViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - - DialogWithContent( - title = stringResource(id = R.string.delete_books), - imageVectorIcon = Icons.Outlined.DeleteOutline, - description = stringResource( - id = R.string.delete_books_description, - state.value.books.filter { it.second }.size - ), - actionText = stringResource(id = R.string.delete), - onDismiss = { onEvent(LibraryEvent.OnShowHideDeleteDialog) }, - withDivider = false, - isActionEnabled = true, - onAction = { - onEvent( - LibraryEvent.OnDeleteBooks { - onBrowseEvent(BrowseEvent.OnLoadList) - onHistoryEvent(HistoryEvent.OnLoadList) - } - ) - context.getString(R.string.books_deleted) - .showToast(context = context) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt deleted file mode 100644 index 583565af..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt +++ /dev/null @@ -1,71 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.library.components.dialog - -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.pager.PagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.DriveFileMove -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithLazyColumn -import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel - -/** - * Move dialog. Moves all selected books to the selected category. - */ -@Composable -fun LibraryMoveDialog(pagerState: PagerState) { - val context = LocalContext.current - val state = LibraryViewModel.getState() - val onEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - - DialogWithLazyColumn( - title = stringResource(id = R.string.move_books), - imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove, - description = stringResource( - id = R.string.move_books_description, - state.value.books.filter { it.second }.size - ), - actionText = stringResource(id = R.string.move), - isActionEnabled = true, - onDismiss = { onEvent(LibraryEvent.OnShowHideMoveDialog) }, - onAction = { - onEvent( - LibraryEvent.OnMoveBooks( - pagerState, - refreshList = { - onHistoryEvent(HistoryEvent.OnLoadList) - } - ) - ) - context.getString(R.string.books_moved) - .showToast(context = context) - }, - withDivider = false, - items = { - items(state.value.categories, key = { it.name }) { - val category = when (it) { - Category.READING -> stringResource(id = R.string.reading_tab) - Category.ALREADY_READ -> stringResource(id = R.string.already_read_tab) - Category.PLANNING -> stringResource(id = R.string.planning_tab) - Category.DROPPED -> stringResource(id = R.string.dropped_tab) - } - - SelectableDialogItem( - selected = it == state.value.selectedCategory, - title = category - ) { - onEvent(LibraryEvent.OnSelectCategory(it)) - } - } - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt deleted file mode 100644 index cbfb5571..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.library.data - -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.Immutable -import androidx.compose.ui.focus.FocusRequester -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.OnNavigate -import ua.acclorite.book_story.domain.util.Selected - -@Immutable -sealed class LibraryEvent { - data object OnRefreshList : LibraryEvent() - data object OnLoadList : LibraryEvent() - data class OnRecalculateCategories(val books: List>) : LibraryEvent() - data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent() - data class OnUpdateCurrentPage(val page: Int) : LibraryEvent() - data object OnSearchShowHide : LibraryEvent() - data class OnRequestFocus(val focusRequester: FocusRequester) : LibraryEvent() - data class OnSearchQueryChange(val query: String) : LibraryEvent() - data object OnSearch : LibraryEvent() - data class OnSelectBook(val book: Pair, val select: Boolean? = null) : - LibraryEvent() - - data object OnClearSelectedBooks : LibraryEvent() - data object OnShowHideMoveDialog : LibraryEvent() - data class OnSelectCategory(val category: Category) : LibraryEvent() - data class OnMoveBooks(val pagerState: PagerState, val refreshList: () -> Unit) : LibraryEvent() - data object OnShowHideDeleteDialog : LibraryEvent() - data class OnDeleteBooks(val refreshList: () -> Unit) : LibraryEvent() - data class OnUpdateBook(val book: Book) : LibraryEvent() - data class OnNavigateToReaderScreen( - val onNavigate: OnNavigate, - val book: Book - ) : LibraryEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt deleted file mode 100644 index e28a76ef..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.library.data - -import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.CategorizedBooks -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.util.Selected - -@Immutable -data class LibraryState( - val books: List> = emptyList(), - val categorizedBooks: List = emptyList(), - - val isLoading: Boolean = true, - val isRefreshing: Boolean = false, - - val currentPage: Int = 0, - - val selectedItemsCount: Int = 0, - val hasSelectedItems: Boolean = false, - - val showSearch: Boolean = false, - val searchQuery: String = "", - val hasFocused: Boolean = false, - - val showMoveDialog: Boolean = false, - val categories: List = emptyList(), - val selectedCategory: Category = Category.READING, - - val showDeleteDialog: Boolean = false, -) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt deleted file mode 100644 index 58f019ed..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt +++ /dev/null @@ -1,393 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.library.data - -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import ua.acclorite.book_story.domain.model.CategorizedBooks -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.History -import ua.acclorite.book_story.domain.use_case.book.DeleteBooks -import ua.acclorite.book_story.domain.use_case.book.GetBooks -import ua.acclorite.book_story.domain.use_case.book.UpdateBook -import ua.acclorite.book_story.domain.use_case.history.InsertHistory -import ua.acclorite.book_story.domain.util.UIViewModel -import ua.acclorite.book_story.presentation.core.navigation.Screen -import java.util.Date -import javax.inject.Inject - -@HiltViewModel -class LibraryViewModel @Inject constructor( - private val getBooks: GetBooks, - private val updateBook: UpdateBook, - private val deleteBooks: DeleteBooks, - private val insertHistory: InsertHistory -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } - - private val _state = MutableStateFlow(LibraryState()) - override val state = _state.asStateFlow() - - private val _isReady = MutableStateFlow(false) - val isReady = _isReady.asStateFlow() - - private var job: Job? = null - private var job2: Job? = null - - init { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isLoading = true - ) - } - getBooksFromDatabase() - _isReady.update { - true - } - } - } - - override fun onEvent(event: LibraryEvent) { - when (event) { - is LibraryEvent.OnScrollToPage -> { - viewModelScope.launch { - event.pagerState.scrollToPage(event.index) - _state.update { - it.copy( - currentPage = event.index - ) - } - } - } - - is LibraryEvent.OnUpdateCurrentPage -> { - _state.update { - it.copy( - currentPage = event.page - ) - } - } - - is LibraryEvent.OnRefreshList -> { - job2?.cancel() - job2 = viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isRefreshing = true, - hasSelectedItems = false, - showSearch = false - ) - } - - yield() - getBooksFromDatabase("") - - delay(500) - _state.update { - it.copy( - isRefreshing = false - ) - } - } - } - - is LibraryEvent.OnLoadList -> { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - isLoading = true, - hasSelectedItems = false - ) - } - getBooksFromDatabase() - } - } - - is LibraryEvent.OnSearchShowHide -> { - viewModelScope.launch(Dispatchers.IO) { - val shouldHide = _state.value.showSearch - - if (shouldHide) { - getBooksFromDatabase("") - } else { - _state.update { - it.copy( - searchQuery = "", - hasFocused = false - ) - } - } - - _state.update { - it.copy( - showSearch = !shouldHide - ) - } - } - } - - is LibraryEvent.OnRequestFocus -> { - if (!_state.value.hasFocused) { - event.focusRequester.requestFocus() - _state.update { - it.copy( - hasFocused = true - ) - } - } - } - - is LibraryEvent.OnSearchQueryChange -> { - _state.update { - it.copy( - searchQuery = event.query - ) - } - job?.cancel() - job = viewModelScope.launch(Dispatchers.IO) { - delay(500) - yield() - onEvent(LibraryEvent.OnSearch) - } - } - - is LibraryEvent.OnSearch -> { - viewModelScope.launch(Dispatchers.IO) { - getBooksFromDatabase() - } - } - - is LibraryEvent.OnSelectBook -> { - viewModelScope.launch(Dispatchers.IO) { - val editedList = _state.value.books.map { - if (it.first.id == event.book.first.id) { - it.copy( - second = event.select ?: !it.second - ) - } else { - it - } - } - - onEvent(LibraryEvent.OnRecalculateCategories(editedList)) - _state.update { - it.copy( - books = editedList, - selectedItemsCount = editedList.filter { book -> book.second }.size, - hasSelectedItems = editedList.any { book -> book.second } - ) - } - } - } - - is LibraryEvent.OnClearSelectedBooks -> { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - books = it.books.map { book -> book.copy(second = false) }, - hasSelectedItems = false - ) - } - onEvent(LibraryEvent.OnRecalculateCategories(_state.value.books)) - } - } - - is LibraryEvent.OnShowHideMoveDialog -> { - val categories = mutableListOf() - - Category.entries.forEach { category -> - if (!_state.value.books - .filter { it.second } - .all { it.first.category == category } - ) { - categories.add( - category - ) - } - } - - _state.update { - it.copy( - categories = categories, - selectedCategory = categories[0], - showMoveDialog = !it.showMoveDialog - ) - } - } - - is LibraryEvent.OnSelectCategory -> { - _state.update { - it.copy( - selectedCategory = event.category - ) - } - } - - is LibraryEvent.OnMoveBooks -> { - viewModelScope.launch { - _state.update { - it.copy( - showMoveDialog = false - ) - } - - val books = _state.value.books.filter { - it.second - }.map { it.first.copy(category = _state.value.selectedCategory) } - books.forEach { updateBook.execute(it) } - - _state.update { - it.copy( - books = it.books.map { book -> - if (book.second) { - book.copy( - first = book.first.copy(category = _state.value.selectedCategory) - ) - } else { - book - } - } - ) - } - onEvent(LibraryEvent.OnRecalculateCategories(_state.value.books)) - event.refreshList() - - onEvent(LibraryEvent.OnClearSelectedBooks) - onEvent( - LibraryEvent.OnScrollToPage( - Category.entries.dropLastWhile { - it != _state.value.selectedCategory - }.size - 1, - event.pagerState - ) - ) - } - } - - is LibraryEvent.OnShowHideDeleteDialog -> { - _state.update { - it.copy( - showDeleteDialog = !it.showDeleteDialog - ) - } - } - - is LibraryEvent.OnDeleteBooks -> { - viewModelScope.launch { - _state.update { - it.copy( - showDeleteDialog = false - ) - } - - val books = _state.value.books.filter { - it.second - }.map { it.first } - deleteBooks.execute(books) - - _state.update { - it.copy( - books = it.books.filter { book -> !book.second } - ) - } - onEvent(LibraryEvent.OnRecalculateCategories(_state.value.books)) - event.refreshList() - - onEvent(LibraryEvent.OnClearSelectedBooks) - } - } - - is LibraryEvent.OnUpdateBook -> { - viewModelScope.launch(Dispatchers.IO) { - val books = _state.value.books.map { - if (it.first.id == event.book.id) { - it.copy( - first = event.book - ) - } else { - it - } - } - - onEvent(LibraryEvent.OnRecalculateCategories(books)) - _state.update { - it.copy( - books = books - ) - } - } - } - - is LibraryEvent.OnNavigateToReaderScreen -> { - viewModelScope.launch { - event.book.id.let { - insertHistory.execute( - History( - bookId = it, - book = null, - time = Date().time - ) - ) - } - event.onNavigate { - navigate(Screen.Reader(event.book.id)) - } - } - } - - is LibraryEvent.OnRecalculateCategories -> { - val categorizedBooks = event.books.groupBy { - it.first.category - }.toList().map { - CategorizedBooks( - it.first, - it.second - ) - } - _state.update { - it.copy( - categorizedBooks = categorizedBooks - ) - } - } - } - } - - private suspend fun getBooksFromDatabase( - query: String = if (_state.value.showSearch) _state.value.searchQuery else "" - ) { - val books = getBooks.execute(query).map { book -> Pair(book, false) } - - val categorizedBooks = books.groupBy { - it.first.category - }.toList().map { - CategorizedBooks( - it.first, - it.second - ) - } - - _state.update { - it.copy( - books = books, - categorizedBooks = categorizedBooks, - isLoading = false - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt deleted file mode 100644 index 7442ffaf..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt +++ /dev/null @@ -1,602 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.reader - -import android.annotation.SuppressLint -import android.content.pm.ActivityInfo -import android.os.Build -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.displayCutout -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBarsIgnoringVisibility -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.unit.coerceAtLeast -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.em -import androidx.compose.ui.unit.sp -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.core.components.common.SelectionContainer -import ua.acclorite.book_story.presentation.core.components.placeholder.ErrorPlaceholder -import ua.acclorite.book_story.presentation.core.components.progress_indicator.CircularProgressIndicator -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideFonts -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.core.util.setBrightness -import ua.acclorite.book_story.presentation.core.util.showToast -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderChapter -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderChaptersDrawer -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderPerceptionExpander -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderTextParagraph -import ua.acclorite.book_story.presentation.screens.reader.components.ReaderUpdateDialog -import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar -import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar -import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.ReaderTextAlignment - -@Composable -fun ReaderScreenRoot(screen: Screen.Reader) { - val state = ReaderViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = ReaderViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - val context = LocalContext.current as ComponentActivity - val onNavigate = LocalNavigator.current - - - val lazyListState = rememberSaveable( - state.value.listState, - saver = LazyListState.Saver - ) { - state.value.listState - } - val canScroll by remember { - derivedStateOf { - state.value.listState.canScrollBackward && state.value.listState.canScrollForward - } - } - - LaunchedEffect(Unit) { - onEvent( - ReaderEvent.OnInit( - screen = screen, - navigateBack = { - onNavigate { - navigateBack() - } - }, - fullscreenMode = mainState.value.fullscreen, - checkForTextUpdate = mainState.value.checkForTextUpdate, - checkForTextUpdateToast = { - if (mainState.value.checkForTextUpdateToast) { - context.getString(R.string.nothing_changed).showToast( - context = context, - longToast = false - ) - } - }, - activity = context, - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnLoadList) - }, - onError = { - it.asString(context) - .showToast(context = context) - } - ) - ) - } - LaunchedEffect(canScroll) { - if (!canScroll && !state.value.showMenu && !state.value.loading) { - onEvent( - ReaderEvent.OnShowHideMenu( - fullscreenMode = mainState.value.fullscreen, - saveCheckpoint = true, - activity = context - ) - ) - } - } - LaunchedEffect(mainState.value.fullscreen) { - onEvent( - ReaderEvent.OnShowHideMenu( - show = state.value.showMenu, - fullscreenMode = mainState.value.fullscreen, - saveCheckpoint = false, - activity = context - ) - ) - } - LaunchedEffect(lazyListState) { - onEvent( - ReaderEvent.OnUpdateProgress( - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - } - ) - ) - } - DisposableEffect(mainState.value.screenOrientation) { - context.requestedOrientation = mainState.value.screenOrientation.code - onDispose { - context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - } - DisposableEffect( - mainState.value.screenBrightness, - mainState.value.customScreenBrightness - ) { - when (mainState.value.customScreenBrightness) { - true -> context.setBrightness(brightness = mainState.value.screenBrightness) - false -> context.setBrightness(brightness = null) - } - - onDispose { - context.setBrightness(brightness = null) - } - } - DisposableEffect(mainState.value.keepScreenOn) { - when (mainState.value.keepScreenOn) { - true -> context.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - false -> context.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - onDispose { - context.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - } - - ReaderScreen(lazyListState = lazyListState) - - DisposableEffect(Unit) { - onDispose { - onEvent(ReaderEvent.OnClearViewModel) - WindowCompat.getInsetsController( - context.window, - context.window.decorView - ).show(WindowInsetsCompat.Type.systemBars()) - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") -@Composable -private fun ReaderScreen(lazyListState: LazyListState) { - val state = ReaderViewModel.getState() - val mainState = MainViewModel.getState() - val settingsState = SettingsViewModel.getState() - val onEvent = ReaderViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - val context = LocalContext.current as ComponentActivity - val onNavigate = LocalNavigator.current - - val nestedScrollConnection = remember(state) { - object : NestedScrollConnection { - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - consumed.y.let { velocity -> - if (velocity in -70f..70f) return@let - if (!state.value.showMenu) return@let - if (state.value.lockMenu) return@let - if (!mainState.value.hideBarsOnFastScroll) return@let - - onEvent( - ReaderEvent.OnShowHideMenu( - show = false, - fullscreenMode = mainState.value.fullscreen, - saveCheckpoint = false, - activity = context - ) - ) - } - return super.onPostScroll(consumed, available, source) - } - } - } - - val fontFamily = remember(mainState.value.fontFamily) { - Constants.provideFonts(withRandom = true).find { - it.id == mainState.value.fontFamily - } ?: Constants.provideFonts(withRandom = false)[0] - } - val backgroundColor = animateColorAsState( - settingsState.value.selectedColorPreset.backgroundColor, - label = "" - ) - val fontColor = animateColorAsState( - settingsState.value.selectedColorPreset.fontColor, - label = "" - ) - val lineHeight = remember( - mainState.value.fontSize, - mainState.value.lineHeight - ) { - (mainState.value.fontSize + mainState.value.lineHeight).sp - } - val letterSpacing = remember(mainState.value.letterSpacing) { - (mainState.value.letterSpacing / 100f).em - } - val sidePadding = remember(mainState.value.sidePadding) { - (mainState.value.sidePadding * 3).dp - } - val verticalPadding = remember(mainState.value.verticalPadding) { - (mainState.value.verticalPadding * 4.5f).dp - } - val paragraphHeight = remember(mainState.value.paragraphHeight) { - (mainState.value.paragraphHeight * 3).dp - } - val fontStyle = remember(mainState.value.isItalic) { - when (mainState.value.isItalic) { - true -> FontStyle.Italic - false -> FontStyle.Normal - } - } - val paragraphIndentation = remember( - mainState.value.paragraphIndentation, - mainState.value.textAlignment - ) { - if ( - mainState.value.textAlignment == ReaderTextAlignment.CENTER || - mainState.value.textAlignment == ReaderTextAlignment.END - ) { - return@remember 0.sp - } - (mainState.value.paragraphIndentation * 6).sp - } - val perceptionExpanderPadding = remember( - sidePadding, - mainState.value.perceptionExpanderPadding - ) { - sidePadding + (mainState.value.perceptionExpanderPadding * 8).dp - } - val perceptionExpanderThickness = remember( - mainState.value.perceptionExpanderThickness - ) { - (mainState.value.perceptionExpanderThickness * 0.25f).dp - } - - val density = LocalDensity.current - val layoutDirection = LocalLayoutDirection.current - val cutoutInsets = WindowInsets.displayCutout - val systemBarsInsets = WindowInsets.systemBarsIgnoringVisibility - - val cutoutInsetsPadding = remember(mainState.value.cutoutPadding) { - derivedStateOf { - cutoutInsets.asPaddingValues(density = density).run { - if (mainState.value.cutoutPadding) PaddingValues( - top = calculateTopPadding(), - start = calculateStartPadding(layoutDirection), - end = calculateEndPadding(layoutDirection), - bottom = calculateBottomPadding() - ) else PaddingValues(0.dp) - } - } - } - val systemBarsInsetsPadding = remember(mainState.value.fullscreen) { - derivedStateOf { - systemBarsInsets.asPaddingValues(density = density).run { - if (!mainState.value.fullscreen) PaddingValues( - top = calculateTopPadding(), - start = calculateStartPadding(layoutDirection), - end = calculateEndPadding(layoutDirection), - bottom = calculateBottomPadding() - ) else PaddingValues(0.dp) - } - } - } - - val contentPadding = remember( - cutoutInsetsPadding.value, - systemBarsInsetsPadding.value - ) { - PaddingValues( - top = systemBarsInsetsPadding.value.calculateTopPadding().run { - if (equals(0.dp)) return@run cutoutInsetsPadding.value - .calculateTopPadding() - this - }, - start = systemBarsInsetsPadding.value.calculateStartPadding(layoutDirection).run { - if (equals(0.dp)) return@run cutoutInsetsPadding.value - .calculateStartPadding(layoutDirection) - this - }, - end = systemBarsInsetsPadding.value.calculateEndPadding(layoutDirection).run { - if (equals(0.dp)) return@run cutoutInsetsPadding.value - .calculateEndPadding(layoutDirection) - this - }, - bottom = systemBarsInsetsPadding.value.calculateBottomPadding().run { - if (equals(0.dp)) return@run cutoutInsetsPadding.value - .calculateBottomPadding() - this - } - ) - } - - val chapters = remember(state.value.book.chapters) { - val chapters = mutableMapOf() - state.value.book.chapters.forEach { - chapters[it.startIndex] = it - } - chapters - } - - // Bottom sheets & Dialogs - if (state.value.showSettingsBottomSheet) { - ReaderSettingsBottomSheet() - } - if (state.value.showUpdateDialog) { - ReaderUpdateDialog() - } - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(nestedScrollConnection), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - AnimatedVisibility( - visible = state.value.showMenu, - enter = slideInVertically { -it }, - exit = slideOutVertically { -it } - ) { - ReaderTopBar() - } - }, - bottomBar = { - AnimatedVisibility( - modifier = Modifier.fillMaxWidth(), - visible = state.value.showMenu, - enter = slideInVertically { it }, - exit = slideOutVertically { it } - ) { - ReaderBottomBar() - } - } - ) { - SelectionContainer( - onCopyRequested = { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - context.getString(R.string.copied) - .showToast(context = context, longToast = false) - } - }, - onShareRequested = { textToShare -> - onEvent( - ReaderEvent.OnOpenShareApp( - textToShare = textToShare, - context, - noAppsFound = { - context.getString(R.string.error_no_share_app) - .showToast(context = context, longToast = false) - } - ) - ) - }, - onWebSearchRequested = { textToSearch -> - onEvent( - ReaderEvent.OnOpenWebBrowser( - textToSearch = textToSearch, - context, - noAppsFound = { - context.getString(R.string.error_no_browser) - .showToast(context = context, longToast = false) - } - ) - ) - }, - onTranslateRequested = { textToTranslate -> - onEvent( - ReaderEvent.OnOpenTranslator( - textToTranslate = textToTranslate, - translateWholeParagraph = false, - context, - noAppsFound = { - context.getString(R.string.error_no_translator) - .showToast(context = context, longToast = false) - } - ) - ) - }, - onDictionaryRequested = { textToDefine -> - onEvent( - ReaderEvent.OnOpenDictionary( - textToDefine, - context, - noAppsFound = { - context.getString(R.string.error_no_dictionary) - .showToast(context = context, longToast = false) - } - ) - ) - } - ) { toolbarHidden -> - LazyColumn( - state = lazyListState, - modifier = Modifier - .fillMaxSize() - .background(backgroundColor.value) - .then( - if (!state.value.loading && toolbarHidden) { - Modifier - .noRippleClickable( - onClick = { - onEvent( - ReaderEvent.OnShowHideMenu( - fullscreenMode = mainState.value.fullscreen, - saveCheckpoint = true, - activity = context - ) - ) - } - ) - } else { - Modifier - } - ) - .padding(contentPadding) - .padding(vertical = verticalPadding), - verticalArrangement = Arrangement.spacedBy(paragraphHeight), - contentPadding = PaddingValues( - top = (WindowInsets.displayCutout.asPaddingValues() - .calculateTopPadding() + paragraphHeight) - .coerceAtLeast(18.dp), - bottom = (WindowInsets.displayCutout.asPaddingValues() - .calculateBottomPadding() + paragraphHeight) - .coerceAtLeast(18.dp), - ) - ) { - itemsIndexed( - state.value.text, key = { index, _ -> index } - ) { index, line -> - ReaderChapter( - chapter = chapters[index], // Shows only when matching startIndex of chapter. - fontColor = fontColor.value, - sidePadding = sidePadding - ) - - ReaderTextParagraph( - line = line, - fontFamily = fontFamily, - fontColor = fontColor.value, - lineHeight = lineHeight, - fontStyle = fontStyle, - textAlignment = mainState.value.textAlignment, - fontSize = mainState.value.fontSize.sp, - letterSpacing = letterSpacing, - sidePadding = sidePadding, - paragraphIndentation = paragraphIndentation, - fullscreenMode = mainState.value.fullscreen, - doubleClickTranslationEnabled = mainState.value.doubleClickTranslation, - toolbarHidden = toolbarHidden - ) - } - } - } - - ReaderPerceptionExpander( - perceptionExpander = mainState.value.perceptionExpander, - sidePadding = perceptionExpanderPadding, - thickness = perceptionExpanderThickness, - lineColor = fontColor.value - ) - } - - if (state.value.loading || state.value.errorMessage != null) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (!state.value.loading) { - ErrorPlaceholder( - errorMessage = state.value.errorMessage!!.asString(), - icon = painterResource(id = R.drawable.error), - actionTitle = stringResource(id = R.string.go_back), - action = { - onLibraryEvent(LibraryEvent.OnLoadList) - onEvent( - ReaderEvent.OnGoBack( - context = context, - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - }, - navigate = { - onNavigate { - navigateBack() - } - } - ) - ) - } - ) - } else { - CircularProgressIndicator( - modifier = Modifier.size(48.dp), - strokeWidth = 4.5.dp - ) - } - } - } - - // Drawers - ReaderChaptersDrawer() - - BackHandler { - onEvent( - ReaderEvent.OnGoBack( - context = context, - refreshList = { - onLibraryEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryEvent(HistoryEvent.OnUpdateBook(it)) - }, - navigate = { - onNavigate { - navigateBack() - } - } - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderUpdateDialog.kt deleted file mode 100644 index 9947076a..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderUpdateDialog.kt +++ /dev/null @@ -1,44 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.reader.components - -import androidx.activity.ComponentActivity -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Upgrade -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel - -/** - * Reader Update Dialog. - * Navigates user to BookInfo and starts book update. - */ -@Composable -fun ReaderUpdateDialog() { - val onEvent = ReaderViewModel.getEvent() - val onNavigate = LocalNavigator.current - val context = LocalContext.current - - DialogWithContent( - title = stringResource(id = R.string.update_book), - imageVectorIcon = Icons.Rounded.Upgrade, - description = stringResource( - id = R.string.update_book_description - ), - actionText = stringResource(id = R.string.proceed), - isActionEnabled = true, - onDismiss = { onEvent(ReaderEvent.OnShowHideUpdateDialog(false)) }, - onAction = { - onEvent( - ReaderEvent.OnUpdateText( - activity = context as ComponentActivity, - onNavigate = onNavigate - ) - ) - }, - withDivider = false - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt deleted file mode 100644 index 5ed4eda1..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt +++ /dev/null @@ -1,224 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.reader.components.app_bar - -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.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -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 -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.util.Direction -import ua.acclorite.book_story.presentation.core.components.common.IconButton -import ua.acclorite.book_story.presentation.core.util.calculateProgress -import ua.acclorite.book_story.presentation.core.util.noRippleClickable -import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent -import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel -import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent -import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel -import ua.acclorite.book_story.presentation.ui.Colors -import ua.acclorite.book_story.presentation.ui.HorizontalExpandingTransition - -/** - * Reader bottom bar. - * Has a slider to change progress. - */ -@Composable -fun ReaderBottomBar() { - val state = ReaderViewModel.getState() - val onEvent = ReaderViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - - val bookProgress by remember { - derivedStateOf { - "${state.value.book.progress.calculateProgress(2)}%" - } - } - val chapterProgress by remember { - derivedStateOf { - if (state.value.currentChapter == null) return@derivedStateOf "" - " (${state.value.currentChapterProgress.calculateProgress(2)}%)" - } - } - val progress by remember { - derivedStateOf { - "$bookProgress$chapterProgress" - } - } - - val arrowDirection by remember { - derivedStateOf { - val checkpoint = state.value.checkpoint.first - val index = state.value.listState.firstVisibleItemIndex - - when { - checkpoint > index -> Direction.END - checkpoint < index -> Direction.START - else -> Direction.NEUTRAL - } - } - } - val checkpointProgress by remember { - derivedStateOf { - (state.value.checkpoint.first / state.value.text.lastIndex.toFloat()) * 0.987f - } - } - - Column( - Modifier - .fillMaxWidth() - .background(Colors.readerSystemBarsColor) - .noRippleClickable(onClick = {}) - .navigationBarsPadding() - .padding(horizontal = 18.dp) - .padding(top = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = progress, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleLarge - ) - Row( - modifier = Modifier.padding(top = 3.dp, bottom = 5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - HorizontalExpandingTransition( - visible = arrowDirection == Direction.START, - startDirection = true - ) { - IconButton( - icon = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = R.string.checkpoint_back_content_desc, - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.primary, - disableOnClick = false - ) { - onEvent( - ReaderEvent.OnRestoreCheckpoint { book -> - onLibraryEvent(LibraryEvent.OnUpdateBook(book)) - onHistoryEvent(HistoryEvent.OnUpdateBook(book)) - } - ) - } - } - - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.CenterStart - ) { - BottomBarSlider() - - if (arrowDirection != Direction.NEUTRAL) { - SliderIndicator(progress = checkpointProgress) - } - } - - HorizontalExpandingTransition( - visible = arrowDirection == Direction.END, - startDirection = false - ) { - IconButton( - icon = Icons.AutoMirrored.Default.ArrowForward, - contentDescription = R.string.checkpoint_forward_content_desc, - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.primary, - disableOnClick = false - ) { - onEvent( - ReaderEvent.OnRestoreCheckpoint { book -> - onLibraryEvent(LibraryEvent.OnUpdateBook(book)) - onHistoryEvent(HistoryEvent.OnUpdateBook(book)) - } - ) - } - } - } - } -} - -/** - * Bottom Bar Slider. - * Has semi-transparent track color. - */ -@Composable -private fun BottomBarSlider() { - val state = ReaderViewModel.getState() - val onEvent = ReaderViewModel.getEvent() - val onLibraryEvent = LibraryViewModel.getEvent() - val onHistoryEvent = HistoryViewModel.getEvent() - - Slider( - value = state.value.book.progress, - enabled = !state.value.lockMenu, - onValueChange = { - if (state.value.listState.layoutInfo.totalItemsCount > 0) { - onEvent(ReaderEvent.OnScroll(it)) - onEvent( - ReaderEvent.OnChangeProgress( - progress = it, - firstVisibleItemIndex = state.value.listState.firstVisibleItemIndex, - firstVisibleItemOffset = 0, - refreshList = { book -> - onLibraryEvent(LibraryEvent.OnUpdateBook(book)) - onHistoryEvent(HistoryEvent.OnUpdateBook(book)) - } - ) - ) - } - }, - colors = SliderDefaults.colors( - inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f), - disabledActiveTrackColor = MaterialTheme.colorScheme.primary, - disabledThumbColor = MaterialTheme.colorScheme.primary, - disabledInactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f), - ) - ) -} - -/** - * Slider Indicator. - * Shows an indicator at desired progress. - */ -@Composable -private fun SliderIndicator(progress: Float) { - Row(Modifier.fillMaxWidth()) { - Spacer( - modifier = Modifier.fillMaxWidth(progress) - ) - Box( - Modifier - .width(4.dp) - .height(16.dp) - .clip(RoundedCornerShape(0.5.dp)) - .background( - MaterialTheme.colorScheme.onPrimary.copy(0.6f) - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt deleted file mode 100644 index 6de90271..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheetTabRow.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet - -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheetTabRow -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent -import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel - -/** - * Settings bottom sheet tab row. - * It is used to switch between categories. - * - * @param pagerState PagerState to use along with TabRow. - */ -@Composable -fun ReaderSettingsBottomSheetTabRow(pagerState: PagerState) { - val onEvent = ReaderViewModel.getEvent() - val tabItems = listOf( - stringResource(id = R.string.general_tab), - stringResource(id = R.string.reader_tab), - stringResource(id = R.string.color_tab) - ) - - ModalBottomSheetTabRow( - selectedTabIndex = pagerState.currentPage, - tabs = tabItems - ) { index -> - onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState)) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt deleted file mode 100644 index 452ed3eb..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt +++ /dev/null @@ -1,124 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.reader.data - -import androidx.activity.ComponentActivity -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.util.OnNavigate -import ua.acclorite.book_story.domain.util.UIText -import ua.acclorite.book_story.presentation.core.navigation.Screen - -@Immutable -sealed class ReaderEvent { - data class OnInit( - val screen: Screen.Reader, - val navigateBack: () -> Unit, - val fullscreenMode: Boolean, - val checkForTextUpdate: Boolean, - val checkForTextUpdateToast: () -> Unit, - val activity: ComponentActivity, - val refreshList: (Book) -> Unit, - val onError: (UIText) -> Unit - ) : ReaderEvent() - - data class OnUpdateProgress( - val refreshList: (Book) -> Unit - ) : ReaderEvent() - - data object OnClearViewModel : ReaderEvent() - - data object OnTextIsEmpty : ReaderEvent() - - data class OnLoadText( - val checkForUpdate: () -> Unit, - val refreshList: (Book) -> Unit, - val onError: (UIText) -> Unit, - val onTextIsEmpty: () -> Unit - ) : ReaderEvent() - - data class OnShowHideMenu( - val show: Boolean? = null, - val fullscreenMode: Boolean, - val saveCheckpoint: Boolean, - val activity: ComponentActivity - ) : ReaderEvent() - - data class OnRestoreCheckpoint( - val refreshList: (Book) -> Unit - ) : ReaderEvent() - - data class OnGoBack( - val context: ComponentActivity, - val navigate: () -> Unit, - val refreshList: (Book) -> Unit - ) : ReaderEvent() - - data class OnChangeProgress( - val progress: Float, - val firstVisibleItemIndex: Int, - val firstVisibleItemOffset: Int, - val refreshList: (Book) -> Unit - ) : ReaderEvent() - - data class OnScroll( - val progress: Float - ) : ReaderEvent() - - data class OnScrollToChapter( - val chapterStartIndex: Int, - val refreshList: (Book) -> Unit - ) : ReaderEvent() - - data class OnShowHideSettingsBottomSheet( - val show: Boolean - ) : ReaderEvent() - - data class OnShowHideChaptersDrawer( - val show: Boolean - ) : ReaderEvent() - - data class OnScrollToSettingsPage( - val page: Int, - val pagerState: PagerState? - ) : ReaderEvent() - - data class OnOpenTranslator( - val textToTranslate: String, - val translateWholeParagraph: Boolean, - val context: ComponentActivity, - val noAppsFound: () -> Unit - ) : ReaderEvent() - - data class OnOpenShareApp( - val textToShare: String, - val context: ComponentActivity, - val noAppsFound: () -> Unit - ) : ReaderEvent() - - data class OnOpenWebBrowser( - val textToSearch: String, - val context: ComponentActivity, - val noAppsFound: () -> Unit - ) : ReaderEvent() - - data class OnOpenDictionary( - val textToDefine: String, - val context: ComponentActivity, - val noAppsFound: () -> Unit - ) : ReaderEvent() - - data class OnCheckTextForUpdate( - val noUpdateFound: () -> Unit - ) : ReaderEvent() - - data class OnShowHideUpdateDialog( - val show: Boolean - ) : ReaderEvent() - - data object OnCancelCheckTextForUpdate : ReaderEvent() - - data class OnUpdateText( - val activity: ComponentActivity, - val onNavigate: OnNavigate - ) : ReaderEvent() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt deleted file mode 100644 index 81f6ccde..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt +++ /dev/null @@ -1,122 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.settings - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DisplaySettings -import androidx.compose.material.icons.outlined.Explore -import androidx.compose.material.icons.outlined.LocalLibrary -import androidx.compose.material.icons.outlined.Palette -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem - -@Composable -fun SettingsScreenRoot() { - SettingsScreen() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SettingsScreen() { - val onNavigate = LocalNavigator.current - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.settings_screen)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState, - contentPadding = PaddingValues(top = 16.dp, bottom = 48.dp) - ) { - item { - SettingsCategoryItem( - icon = Icons.Outlined.DisplaySettings, - title = stringResource(id = R.string.general_settings), - description = stringResource(id = R.string.general_settings_desc) - ) { - onNavigate { - navigate(Screen.Settings.General) - } - } - } - - item { - SettingsCategoryItem( - icon = Icons.Outlined.Palette, - title = stringResource(id = R.string.appearance_settings), - description = stringResource(id = R.string.appearance_settings_desc) - ) { - onNavigate { - navigate(Screen.Settings.Appearance) - } - } - } - - item { - SettingsCategoryItem( - icon = Icons.Outlined.LocalLibrary, - title = stringResource(id = R.string.reader_settings), - description = stringResource(id = R.string.reader_settings_desc) - ) { - onNavigate { - navigate(Screen.Settings.ReaderSettings) - } - } - } - - item { - SettingsCategoryItem( - icon = Icons.Outlined.Explore, - title = stringResource(id = R.string.browse_settings), - description = stringResource(id = R.string.browse_settings_desc) - ) { - onNavigate { - navigate(Screen.Settings.BrowseSettings) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt deleted file mode 100644 index 288afa4e..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt +++ /dev/null @@ -1,69 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance - -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.AppearanceSettingsCategory - -@Composable -fun AppearanceSettingsRoot() { - AppearanceSettings() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AppearanceSettings() { - val onNavigate = LocalNavigator.current - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars) - .imePadding(), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.appearance_settings)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - modifier = Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState - ) { - AppearanceSettingsCategory() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/BrowseSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/BrowseSettings.kt deleted file mode 100644 index e2bd3f9f..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/BrowseSettings.kt +++ /dev/null @@ -1,69 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse - -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.BrowseSettingsCategory - -@Composable -fun BrowseSettingsRoot() { - BrowseSettings() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun BrowseSettings() { - val onNavigate = LocalNavigator.current - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars) - .imePadding(), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.browse_settings)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState - ) { - BrowseSettingsCategory() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/BrowseSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/BrowseSettingsCategory.kt deleted file mode 100644 index 3d8b6aca..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/BrowseSettingsCategory.kt +++ /dev/null @@ -1,36 +0,0 @@ -@file:Suppress("FunctionName") - -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components - -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseFilterSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseGeneralSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseSortSubcategory - -/** - * Browse Settings Category. - * Contains all Browse settings. - * - * @param topPadding Top padding to be applied. - * @param bottomPadding Bottom padding to be applied. - */ -fun LazyListScope.BrowseSettingsCategory( - topPadding: Dp = 16.dp, - bottomPadding: Dp = 48.dp -) { - BrowseGeneralSubcategory( - topPadding = topPadding, - bottomPadding = 0.dp - ) - BrowseFilterSubcategory( - topPadding = 22.dp, - bottomPadding = 0.dp - ) - BrowseSortSubcategory( - showDivider = false, - topPadding = 22.dp, - bottomPadding = bottomPadding - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt deleted file mode 100644 index 396fde92..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt +++ /dev/null @@ -1,68 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.general - -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.screens.settings.nested.general.components.GeneralSettingsCategory - -@Composable -fun GeneralSettingsRoot() { - GeneralSettings() -} - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun GeneralSettings() { - val onNavigate = LocalNavigator.current - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.general_settings)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState - ) { - GeneralSettingsCategory() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/GeneralSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/GeneralSettingsCategory.kt deleted file mode 100644 index 82bfa681..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/GeneralSettingsCategory.kt +++ /dev/null @@ -1,46 +0,0 @@ -@file:Suppress("FunctionName") - -package ua.acclorite.book_story.presentation.screens.settings.nested.general.components - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.coerceAtLeast -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.presentation.screens.settings.nested.general.components.settings.AppLanguageSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.general.components.settings.CheckForUpdatesSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.general.components.settings.DoublePressExitSetting - -/** - * General Settings Category. - * Contains all General settings such as App Language. - * - * @param topPadding Top padding to be applied. - * @param bottomPadding Bottom padding to be applied. - */ -fun LazyListScope.GeneralSettingsCategory( - topPadding: Dp = 16.dp, - bottomPadding: Dp = 48.dp -) { - item { - Spacer(modifier = Modifier.height((topPadding - 8.dp).coerceAtLeast(0.dp))) - } - - item { - AppLanguageSetting() - } - - item { - CheckForUpdatesSetting() - } - - item { - DoublePressExitSetting() - } - - item { - Spacer(modifier = Modifier.height(bottomPadding)) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/DoublePressExitSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/DoublePressExitSetting.kt deleted file mode 100644 index f479a9d1..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/DoublePressExitSetting.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.general.components.settings - -import android.annotation.SuppressLint -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel - -/** - * Double Press Exit setting. - * If true, to close the app you need to press back twice. - */ -@SuppressLint("InlinedApi") -@Composable -fun DoublePressExitSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() - - SwitchWithTitle( - selected = state.value.doublePressExit, - title = stringResource(id = R.string.double_press_exit_option), - description = stringResource(id = R.string.double_press_exit_option_desc) - ) { - onMainEvent( - MainEvent.OnChangeDoublePressExit(!state.value.doublePressExit) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt deleted file mode 100644 index d9210413..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt +++ /dev/null @@ -1,67 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader - -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.common.GoBackButton -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.ReaderSettingsCategory - -@Composable -fun ReaderSettingsRoot() { - ReaderSettings() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ReaderSettings() { - val onNavigate = LocalNavigator.current - val (scrollBehavior, lazyListState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() - - Scaffold( - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), - containerColor = MaterialTheme.colorScheme.surface, - topBar = { - LargeTopAppBar( - title = { - Text(stringResource(id = R.string.reader_settings)) - }, - navigationIcon = { - GoBackButton(onNavigate = onNavigate) - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.largeTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()), - state = lazyListState - ) { - ReaderSettingsCategory() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/MiscSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/MiscSubcategory.kt deleted file mode 100644 index fd26a534..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/MiscSubcategory.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:Suppress("FunctionName") - -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories - -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.CheckForTextUpdateSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.CheckForTextUpdateToastSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.FullscreenSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.HideBarsOnFastScrollSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.KeepScreenOnSetting - -/** - * Misc subcategory. - * Contains all settings that cannot be categorized. - */ -fun LazyListScope.MiscSubcategory( - titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, - title: @Composable () -> String = { stringResource(id = R.string.misc_reader_settings) }, - showTitle: Boolean = true, - showDivider: Boolean = true, - topPadding: Dp, - bottomPadding: Dp -) { - SettingsSubcategory( - titleColor = titleColor, - title = title, - showTitle = showTitle, - showDivider = showDivider, - topPadding = topPadding, - bottomPadding = bottomPadding - ) { - item { - CheckForTextUpdateSetting() - } - - item { - CheckForTextUpdateToastSetting() - } - - item { - FullscreenSetting() - } - - item { - KeepScreenOnSetting() - } - - item { - HideBarsOnFastScrollSetting() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/StartScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/StartScreen.kt deleted file mode 100644 index 16d7b5a1..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/StartScreen.kt +++ /dev/null @@ -1,73 +0,0 @@ -@file:OptIn(ExperimentalPermissionsApi::class) - -package ua.acclorite.book_story.presentation.screens.start - -import android.Manifest -import android.annotation.SuppressLint -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import com.google.accompanist.permissions.rememberPermissionState -import ua.acclorite.book_story.presentation.screens.start.components.StartDone -import ua.acclorite.book_story.presentation.screens.start.components.StartSettings -import ua.acclorite.book_story.presentation.screens.start.data.StartEvent -import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel - -@SuppressLint("InlinedApi") -@Composable -fun StartScreenRoot() { - val onEvent = StartViewModel.getEvent() - - val storagePermissionState = rememberPermissionState( - permission = Manifest.permission.READ_EXTERNAL_STORAGE - ) - val notificationsPermissionState = rememberPermissionState( - permission = Manifest.permission.POST_NOTIFICATIONS - ) - - LaunchedEffect(Unit) { - onEvent( - StartEvent.OnCheckPermissions( - storagePermissionState = storagePermissionState, - notificationPermissionState = notificationsPermissionState - ) - ) - } - - StartScreen( - storagePermissionState = storagePermissionState, - notificationsPermissionState = notificationsPermissionState, - ) -} - -@OptIn(ExperimentalPermissionsApi::class) -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "InlinedApi") -@Composable -private fun StartScreen( - storagePermissionState: PermissionState, - notificationsPermissionState: PermissionState -) { - val onEvent = StartViewModel.getEvent() - val activity = LocalContext.current as ComponentActivity - - // Settings - StartSettings( - storagePermissionState = storagePermissionState, - notificationsPermissionState = notificationsPermissionState - ) - - // Done - StartDone() - - BackHandler { - onEvent( - StartEvent.OnGoBack { - activity.finish() - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartDone.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartDone.kt deleted file mode 100644 index 1cd2b441..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartDone.kt +++ /dev/null @@ -1,126 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.components - -import androidx.compose.foundation.layout.Arrangement -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.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -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.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.navigation.LocalNavigator -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent -import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel -import ua.acclorite.book_story.presentation.screens.start.data.StartEvent -import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel - -/** - * Start Done screen. - */ -@Composable -fun StartDone() { - val state = StartViewModel.getState() - val onEvent = StartViewModel.getEvent() - val onMainEvent = MainViewModel.getEvent() - val onBrowseEvent = BrowseViewModel.getEvent() - val onNavigate = LocalNavigator.current - - StartNavigationTransition( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(48.dp), - visible = state.value.isDone, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - bottomBar = { - Column { - Spacer(modifier = Modifier.height(18.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .navigationBarsPadding() - .padding(bottom = 8.dp) - .padding(horizontal = 18.dp) - .fillMaxWidth() - ) { - TextButton( - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(100), - contentPadding = ButtonDefaults.ContentPadding, - onClick = { - onEvent( - StartEvent.OnGoToBrowse( - onNavigate = onNavigate, - onCompletedStartGuide = { - onBrowseEvent(BrowseEvent.OnLoadList) - onMainEvent( - MainEvent.OnChangeShowStartScreen(false) - ) - } - ) - ) - } - ) { - Text(text = stringResource(id = R.string.no)) - } - Spacer(modifier = Modifier.width(18.dp)) - Button( - modifier = Modifier.weight(3f), - shape = RoundedCornerShape(100), - onClick = { - onEvent( - StartEvent.OnGoToHelp(onNavigate = onNavigate) - ) - } - ) { - Text(text = stringResource(id = R.string.yes_go_to_help)) - } - } - - - } - } - ) { - Icon( - painter = painterResource(id = R.drawable.start_done), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(190.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.start_done), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.headlineMedium, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(id = R.string.start_done_desc), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationScreenItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationScreenItem.kt deleted file mode 100644 index 8dcc4ff2..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationScreenItem.kt +++ /dev/null @@ -1,43 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar -import ua.acclorite.book_story.presentation.screens.start.data.StartNavigationScreen -import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel -import ua.acclorite.book_story.presentation.ui.Transitions - -/** - * Navigation item screen. Uses sliding anim. - */ -@Composable -fun StartNavigationScreenItem( - screen: StartNavigationScreen, - content: LazyListScope.() -> Unit -) { - val state = StartViewModel.getState() - val listState = rememberLazyListState() - - AnimatedVisibility( - visible = state.value.currentScreen == screen, - enter = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionIn - else Transitions.SlidingTransitionIn, - exit = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionOut - else Transitions.SlidingTransitionOut - ) { - LazyColumnWithScrollbar( - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceContainerLow), - state = listState - ) { - content() - } - } -} diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationTransition.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationTransition.kt deleted file mode 100644 index 546d39e8..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartNavigationTransition.kt +++ /dev/null @@ -1,55 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel -import ua.acclorite.book_story.presentation.ui.Transitions - -/** - * Transition between [StartSettings] and [StartDone]. - */ -@Composable -fun StartNavigationTransition( - modifier: Modifier = Modifier, - visible: Boolean, - bottomBar: @Composable () -> Unit, - horizontalAlignment: Alignment.Horizontal = Alignment.Start, - verticalArrangement: Arrangement.Vertical = Arrangement.Top, - content: @Composable ColumnScope.() -> Unit -) { - val state = StartViewModel.getState() - - AnimatedVisibility( - visible = visible, - enter = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionIn - else Transitions.SlidingTransitionIn, - exit = if (state.value.useBackAnimation) Transitions.BackSlidingTransitionOut - else Transitions.SlidingTransitionOut - ) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = bottomBar, - containerColor = MaterialTheme.colorScheme.surface - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(it) - .then(modifier), - verticalArrangement = verticalArrangement, - horizontalAlignment = horizontalAlignment - ) { - content() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt deleted file mode 100644 index 9a88e2dd..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartSettings.kt +++ /dev/null @@ -1,159 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.components - -import androidx.activity.ComponentActivity -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -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.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.GolfCourse -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.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.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem -import ua.acclorite.book_story.presentation.core.constants.Constants -import ua.acclorite.book_story.presentation.core.constants.provideLanguages -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.start.components.permissions.startPermissionsScreen -import ua.acclorite.book_story.presentation.screens.start.data.StartEvent -import ua.acclorite.book_story.presentation.screens.start.data.StartNavigationScreen -import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel - -/** - * Start Settings. - * - * @param storagePermissionState Storage [PermissionState]. - * @param notificationsPermissionState Notifications [PermissionState]. - */ -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun StartSettings( - storagePermissionState: PermissionState, - notificationsPermissionState: PermissionState -) { - val state = StartViewModel.getState() - val mainState = MainViewModel.getState() - val onEvent = StartViewModel.getEvent() - val onMainEvent = MainViewModel.getEvent() - val activity = LocalContext.current as ComponentActivity - - val languages = remember(mainState.value.language) { - Constants.provideLanguages().sortedBy { it.second }.map { - ButtonItem( - id = it.first, - title = it.second, - textStyle = TextStyle(), - selected = it.first == mainState.value.language - ) - }.sortedBy { it.title } - } - - StartNavigationTransition( - modifier = Modifier.padding(horizontal = 18.dp), - visible = !state.value.isDone, - bottomBar = { - Column { - Spacer(modifier = Modifier.height(18.dp)) - Button( - modifier = Modifier - .navigationBarsPadding() - .padding(bottom = 8.dp) - .padding(horizontal = 18.dp) - .fillMaxWidth(), - shape = RoundedCornerShape(100), - onClick = { onEvent(StartEvent.OnGoForward) }, - enabled = state.value.storagePermissionGranted || - state.value.currentScreen != StartNavigationScreen.PERMISSIONS_THIRD - ) { - Text(text = stringResource(id = R.string.next)) - } - } - } - ) { - Spacer(modifier = Modifier.height(24.dp)) - Icon( - imageVector = Icons.Default.GolfCourse, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(64.dp) - ) - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = stringResource(id = R.string.start_welcome), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.headlineSmall - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.start_welcome_desc), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.height(18.dp)) - - Box( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.large) - .background( - MaterialTheme.colorScheme.surfaceContainerLow, - MaterialTheme.shapes.large - ), - ) { - StartNavigationScreenItem( - screen = StartNavigationScreen.LANGUAGE_FIRST - ) { - startLanguageScreen(onMainEvent = onMainEvent, languages = languages) - } - - StartNavigationScreenItem( - screen = StartNavigationScreen.APPEARANCE_SECOND - ) { - startAppearanceScreen() - } - - StartNavigationScreenItem( - screen = StartNavigationScreen.PERMISSIONS_THIRD - ) { - startPermissionsScreen( - state = state, - onGrantStoragePermission = { - onEvent( - StartEvent.OnStoragePermissionRequest( - activity, - storagePermissionState - ) - ) - }, - onGrantNotificationsPermission = { - onEvent( - StartEvent.OnNotificationsPermissionRequest( - activity, - notificationsPermissionState - ) - ) - } - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionsScreen.kt deleted file mode 100644 index 6ef2740b..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionsScreen.kt +++ /dev/null @@ -1,60 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.components.permissions - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle -import ua.acclorite.book_story.presentation.screens.start.data.StartState - -/** - * Permissions settings. - */ -fun LazyListScope.startPermissionsScreen( - state: State, - onGrantStoragePermission: () -> Unit, - onGrantNotificationsPermission: () -> Unit -) { - item { - Spacer(modifier = Modifier.height(16.dp)) - } - item { - SettingsCategoryTitle( - modifier = Modifier, - title = stringResource(id = R.string.start_permissions_preferences), - color = MaterialTheme.colorScheme.primary - ) - } - item { - Spacer(modifier = Modifier.height(8.dp)) - } - - item { - StartPermissionItem( - title = stringResource(id = R.string.start_permissions_storage), - description = stringResource(id = R.string.start_permissions_storage_desc), - isOptional = false, - isGranted = state.value.storagePermissionGranted, - onGrantClick = onGrantStoragePermission - ) - } - - item { - StartPermissionItem( - title = stringResource(id = R.string.start_permissions_notifications), - description = stringResource(id = R.string.start_permissions_notifications_desc), - isOptional = true, - isGranted = state.value.notificationsPermissionGranted, - onGrantClick = onGrantNotificationsPermission - ) - } - - item { - Spacer(modifier = Modifier.height(8.dp)) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartNavigationScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartNavigationScreen.kt deleted file mode 100644 index 9bf3ffb9..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartNavigationScreen.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.data - -import androidx.compose.runtime.Immutable - -@Immutable -enum class StartNavigationScreen { - LANGUAGE_FIRST, - APPEARANCE_SECOND, - PERMISSIONS_THIRD -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartState.kt deleted file mode 100644 index a066b7fd..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.data - -import androidx.compose.runtime.Immutable - -@Immutable -data class StartState( - val isDone: Boolean = false, - - val currentScreen: StartNavigationScreen = StartNavigationScreen.entries.first(), - val useBackAnimation: Boolean = false, - - val storagePermissionGranted: Boolean = false, - val notificationsPermissionGranted: Boolean = false, -) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartViewModel.kt deleted file mode 100644 index 1d6c4f31..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartViewModel.kt +++ /dev/null @@ -1,305 +0,0 @@ -package ua.acclorite.book_story.presentation.screens.start.data - -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.Settings -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewModelScope -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.shouldShowRationale -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import ua.acclorite.book_story.domain.util.UIViewModel -import ua.acclorite.book_story.presentation.core.navigation.Screen -import ua.acclorite.book_story.presentation.core.util.launchActivity -import javax.inject.Inject - -@OptIn(ExperimentalPermissionsApi::class) -@HiltViewModel -class StartViewModel @Inject constructor( - -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } - - private val _state = MutableStateFlow(StartState()) - override val state = _state.asStateFlow() - - private var storagePermissionJob: Job? = null - private var notificationsPermissionJob: Job? = null - - override fun onEvent(event: StartEvent) { - when (event) { - is StartEvent.OnCheckPermissions -> checkPermissions(event) - - is StartEvent.OnGoBack -> { - storagePermissionJob?.cancel() - notificationsPermissionJob?.cancel() - - if (_state.value.currentScreen == StartNavigationScreen.entries.first()) { - event.onQuit() - return - } - - _state.update { - it.copy( - useBackAnimation = true - ) - } - - if (_state.value.isDone) { - _state.update { - it.copy( - isDone = false, - currentScreen = StartNavigationScreen.entries.last() - ) - } - return - } - - _state.update { - val previousScreenIndex = StartNavigationScreen.entries.indexOf( - it.currentScreen - ) - 1 - - if (previousScreenIndex == -1) { - return - } - - it.copy( - currentScreen = StartNavigationScreen.entries[previousScreenIndex] - ) - } - } - - is StartEvent.OnGoForward -> { - storagePermissionJob?.cancel() - notificationsPermissionJob?.cancel() - - _state.update { - it.copy( - useBackAnimation = false - ) - } - - if (_state.value.currentScreen == StartNavigationScreen.entries.last()) { - _state.update { - it.copy( - isDone = true - ) - } - return - } - - _state.update { - val nextScreenIndex = StartNavigationScreen.entries.indexOf( - it.currentScreen - ) + 1 - - if (nextScreenIndex == 0) { - return - } - - it.copy( - currentScreen = StartNavigationScreen.entries[nextScreenIndex] - ) - } - } - - is StartEvent.OnStoragePermissionRequest -> { - val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - - val isPermissionGranted = if (legacyStoragePermission) { - event.legacyStoragePermissionState.status.isGranted - } else { - Environment.isExternalStorageManager() - } - - if (isPermissionGranted) { - _state.update { - it.copy( - storagePermissionGranted = true - ) - } - return - } - - if (legacyStoragePermission) { - if (!event.legacyStoragePermissionState.status.shouldShowRationale) { - event.legacyStoragePermissionState.launchPermissionRequest() - } else { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) - - var failure = false - intent.launchActivity(event.activity) { - failure = true - } - if (failure) { - return - } - } - } - - if (!legacyStoragePermission) { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent( - Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, - uri - ) - - var failure = false - intent.launchActivity(event.activity) { - failure = true - } - if (failure) { - return - } - } - - storagePermissionJob?.cancel() - storagePermissionJob = viewModelScope.launch { - while (true) { - val granted = if (legacyStoragePermission) { - event.legacyStoragePermissionState.status.isGranted - } else { - Environment.isExternalStorageManager() - } - - if (!granted) { - delay(1000) - yield() - continue - } - - yield() - - _state.update { - it.copy( - storagePermissionGranted = true - ) - } - break - } - } - } - - is StartEvent.OnNotificationsPermissionRequest -> { - if (event.notificationsPermissionState.status.isGranted) { - _state.update { - it.copy( - notificationsPermissionGranted = true - ) - } - return - } - - if (!event.notificationsPermissionState.status.shouldShowRationale) { - event.notificationsPermissionState.launchPermissionRequest() - } else { - val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName) - - var failure = false - intent.launchActivity(event.activity) { - failure = true - } - if (failure) { - return - } - } - - notificationsPermissionJob?.cancel() - notificationsPermissionJob = viewModelScope.launch { - while (true) { - if (!event.notificationsPermissionState.status.isGranted) { - delay(1000) - yield() - continue - } - - yield() - - _state.update { - it.copy( - notificationsPermissionGranted = true - ) - } - - break - } - } - } - - is StartEvent.OnGoToBrowse -> { - event.onNavigate { - navigate(Screen.Browse, saveInBackStack = false) - } - event.onCompletedStartGuide() - } - - is StartEvent.OnGoToHelp -> { - event.onNavigate { - navigate(Screen.Help(true), saveInBackStack = false) - } - } - - is StartEvent.OnResetStartScreen -> { - _state.update { - StartState() - } - } - } - } - - private fun checkPermissions(event: StartEvent.OnCheckPermissions) { - viewModelScope.launch(Dispatchers.IO) { - val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - val legacyNotificationPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU - - val storagePermissionGranted = if (!legacyStoragePermission) { - Environment.isExternalStorageManager() - } else { - event.storagePermissionState.status.isGranted - } - val notificationPermissionGranted = if (!legacyNotificationPermission) { - event.notificationPermissionState.status.isGranted - } else { - true - } - - _state.update { - it.copy( - storagePermissionGranted = storagePermissionGranted, - notificationsPermissionGranted = notificationPermissionGranted - ) - } - } - } -} - - - - - - - - - - diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsContent.kt new file mode 100644 index 00000000..dbe5953b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsContent.kt @@ -0,0 +1,28 @@ +package ua.acclorite.book_story.presentation.settings + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsContent( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateToGeneralSettings: () -> Unit, + navigateToAppearanceSettings: () -> Unit, + navigateToReaderSettings: () -> Unit, + navigateToBrowseSettings: () -> Unit, + navigateBack: () -> Unit +) { + SettingsScaffold( + listState = listState, + scrollBehavior = scrollBehavior, + navigateToGeneralSettings = navigateToGeneralSettings, + navigateToAppearanceSettings = navigateToAppearanceSettings, + navigateToReaderSettings = navigateToReaderSettings, + navigateToBrowseSettings = navigateToBrowseSettings, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayout.kt new file mode 100644 index 00000000..5b3a040c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayout.kt @@ -0,0 +1,75 @@ +package ua.acclorite.book_story.presentation.settings + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DisplaySettings +import androidx.compose.material.icons.outlined.Explore +import androidx.compose.material.icons.outlined.LocalLibrary +import androidx.compose.material.icons.outlined.Palette +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun SettingsLayout( + listState: LazyListState, + paddingValues: PaddingValues, + navigateToGeneralSettings: () -> Unit, + navigateToAppearanceSettings: () -> Unit, + navigateToReaderSettings: () -> Unit, + navigateToBrowseSettings: () -> Unit +) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState, + contentPadding = PaddingValues(vertical = 16.dp) + ) { + item { + SettingsLayoutItem( + icon = Icons.Outlined.DisplaySettings, + title = stringResource(id = R.string.general_settings), + description = stringResource(id = R.string.general_settings_desc) + ) { + navigateToGeneralSettings() + } + } + + item { + SettingsLayoutItem( + icon = Icons.Outlined.Palette, + title = stringResource(id = R.string.appearance_settings), + description = stringResource(id = R.string.appearance_settings_desc) + ) { + navigateToAppearanceSettings() + } + } + + item { + SettingsLayoutItem( + icon = Icons.Outlined.LocalLibrary, + title = stringResource(id = R.string.reader_settings), + description = stringResource(id = R.string.reader_settings_desc) + ) { + navigateToReaderSettings() + } + } + + item { + SettingsLayoutItem( + icon = Icons.Outlined.Explore, + title = stringResource(id = R.string.browse_settings), + description = stringResource(id = R.string.browse_settings_desc) + ) { + navigateToBrowseSettings() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayoutItem.kt similarity index 74% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayoutItem.kt index b659f86f..1efbe82d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayoutItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.components +package ua.acclorite.book_story.presentation.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -16,36 +16,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector 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 -/** - * Settings category item. - * It is used to represent the whole category. - * - * @param icon Icon that represents category. - * @param title Title of the category. - * @param description Description of the category. - * @param verticalPadding Vertical item padding. - * @param onClick OnClick callback. Usually navigation action. - */ @Composable -fun SettingsCategoryItem( +fun SettingsLayoutItem( icon: ImageVector, title: String, description: String, - verticalPadding: Dp = 20.dp, onClick: () -> Unit ) { Row( modifier = Modifier .fillMaxWidth() .clip(MaterialTheme.shapes.extraLarge) - .clickable { - onClick() - } - .padding(vertical = verticalPadding, horizontal = 24.dp), + .clickable { onClick() } + .padding(vertical = 20.dp, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(18.dp) ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsScaffold.kt new file mode 100644 index 00000000..e3e5aaf7 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsScaffold.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.settings + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScaffold( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateToGeneralSettings: () -> Unit, + navigateToAppearanceSettings: () -> Unit, + navigateToReaderSettings: () -> Unit, + navigateToBrowseSettings: () -> Unit, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + SettingsTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + SettingsLayout( + listState = listState, + paddingValues = paddingValues, + navigateToGeneralSettings = navigateToGeneralSettings, + navigateToAppearanceSettings = navigateToAppearanceSettings, + navigateToReaderSettings = navigateToReaderSettings, + navigateToBrowseSettings = navigateToBrowseSettings + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsTopBar.kt new file mode 100644 index 00000000..0f804db7 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsTopBar.kt @@ -0,0 +1,35 @@ +package ua.acclorite.book_story.presentation.settings + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.settings_screen)) + }, + navigationIcon = { + NavigatorBackIconButton( + navigateBack = navigateBack + ) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsCategory.kt similarity index 54% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsCategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsCategory.kt index 74f32fbc..0ed73474 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsCategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsCategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components +package ua.acclorite.book_story.presentation.settings.appearance import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -8,21 +8,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ColorsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ThemePreferencesSubcategory +import ua.acclorite.book_story.presentation.settings.appearance.colors.ColorsSubcategory +import ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.ThemePreferencesSubcategory -/** - * Appearance Settings Category. - * Contains all Appearance settings such as Theme. - * - * @param titleColor Title's color. - * @param topPadding Top padding to be applied. - * @param bottomPadding Bottom padding to be applied. - */ fun LazyListScope.AppearanceSettingsCategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, topPadding: Dp = 16.dp, - bottomPadding: Dp = 48.dp + bottomPadding: Dp = 16.dp ) { ThemePreferencesSubcategory( titleColor = titleColor, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsContent.kt new file mode 100644 index 00000000..6b32ff16 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsContent.kt @@ -0,0 +1,20 @@ +package ua.acclorite.book_story.presentation.settings.appearance + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppearanceSettingsContent( + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateBack: () -> Unit +) { + AppearanceSettingsScaffold( + scrollBehavior = scrollBehavior, + listState = listState, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsLayout.kt new file mode 100644 index 00000000..8010d62e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsLayout.kt @@ -0,0 +1,24 @@ +package ua.acclorite.book_story.presentation.settings.appearance + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun AppearanceSettingsLayout( + listState: LazyListState, + paddingValues: PaddingValues +) { + LazyColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + AppearanceSettingsCategory() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsScaffold.kt new file mode 100644 index 00000000..c4e34336 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsScaffold.kt @@ -0,0 +1,43 @@ +package ua.acclorite.book_story.presentation.settings.appearance + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppearanceSettingsScaffold( + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars) + .imePadding(), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + AppearanceSettingsTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + AppearanceSettingsLayout( + listState = listState, + paddingValues = paddingValues + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsTopBar.kt new file mode 100644 index 00000000..8cb371dd --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/AppearanceSettingsTopBar.kt @@ -0,0 +1,35 @@ +package ua.acclorite.book_story.presentation.settings.appearance + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppearanceSettingsTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.appearance_settings)) + }, + navigationIcon = { + NavigatorBackIconButton( + navigateBack = navigateBack + ) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ColorsSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/ColorsSubcategory.kt similarity index 62% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ColorsSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/ColorsSubcategory.kt index 01c1be8f..afeb0ec0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ColorsSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/ColorsSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories +package ua.acclorite.book_story.presentation.settings.appearance.colors import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,14 +9,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.ColorPresetSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.FastColorPresetChangeSetting +import ua.acclorite.book_story.presentation.settings.appearance.colors.components.ColorPresetOption +import ua.acclorite.book_story.presentation.settings.appearance.colors.components.FastColorPresetChangeOption +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory -/** - * Colors subcategory. - * Contains all settings from Colors. - */ fun LazyListScope.ColorsSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.colors_appearance_settings) }, @@ -35,11 +31,13 @@ fun LazyListScope.ColorsSubcategory( bottomPadding = bottomPadding ) { item { - ColorPresetSetting(backgroundColor = backgroundColor.invoke()) + ColorPresetOption( + backgroundColor = backgroundColor.invoke() + ) } item { - FastColorPresetChangeSetting() + FastColorPresetChangeOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ColorPresetSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/components/ColorPresetOption.kt similarity index 74% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ColorPresetSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/components/ColorPresetOption.kt index 420d01a0..752aa8a7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ColorPresetSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/components/ColorPresetOption.kt @@ -1,6 +1,5 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings +package ua.acclorite.book_story.presentation.settings.appearance.colors.components -import android.content.Context import androidx.compose.animation.animateColorAsState import androidx.compose.animation.expandHorizontally import androidx.compose.animation.fadeIn @@ -10,8 +9,10 @@ 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.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -49,8 +50,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce @@ -58,36 +60,32 @@ import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.components.settings.ColorPickerWithTitle -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel -import ua.acclorite.book_story.presentation.ui.FadeTransitionPreservingSpace -import ua.acclorite.book_story.presentation.ui.Transitions +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle +import ua.acclorite.book_story.ui.settings.SettingsEvent +import ua.acclorite.book_story.ui.settings.SettingsModel +import ua.acclorite.book_story.ui.theme.FadeTransitionPreservingSpace +import ua.acclorite.book_story.ui.theme.Transitions -/** - * Color Preset setting. - * Lets user create and use custom color preset. - */ @OptIn(ExperimentalFoundationApi::class) @Composable -fun ColorPresetSetting( - backgroundColor: Color, - verticalPadding: Dp = 8.dp, - horizontalPadding: Dp = 18.dp -) { - val state = SettingsViewModel.getState() - val onEvent = SettingsViewModel.getEvent() - val context = LocalContext.current +fun ColorPresetOption(backgroundColor: Color) { + val settingsModel = hiltViewModel() + val state = settingsModel.state.collectAsStateWithLifecycle() val reorderableListState = rememberReorderableLazyListState( - lazyListState = state.value.colorPresetsListState + lazyListState = state.value.colorPresetListState ) { from, to -> - onEvent(SettingsEvent.OnReorderColorPresets(from.index, to.index)) + settingsModel.onEvent( + SettingsEvent.OnReorderColorPresets( + from = from.index, + to = to.index + ) + ) } val defaultBackgroundColor = MaterialTheme.colorScheme.surface val defaultFontColor = MaterialTheme.colorScheme.onSurfaceVariant @@ -95,17 +93,19 @@ fun ColorPresetSetting( Column( Modifier .fillMaxWidth() - .padding(vertical = verticalPadding) + .padding(vertical = 8.dp) ) { - SettingsCategoryTitle( + SettingsSubcategoryTitle( title = stringResource(id = R.string.color_preset_option), - padding = horizontalPadding + padding = 18.dp ) Spacer(modifier = Modifier.height(10.dp)) LazyRow( modifier = Modifier.fillMaxWidth(), - state = state.value.colorPresetsListState, - verticalAlignment = Alignment.CenterVertically + state = state.value.colorPresetListState, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = PaddingValues(horizontal = 18.dp) ) { itemsIndexed( state.value.colorPresets, @@ -116,46 +116,31 @@ fun ColorPresetSetting( animateItemModifier = Modifier, key = colorPreset.id ) { - Row { - if (index == 0) { - Spacer(modifier = Modifier.width(horizontalPadding)) - } else { - Spacer(modifier = Modifier.width(2.dp)) - } - - ColorPresetSettingItem( - colorPreset = colorPreset, - isSelected = remember( - colorPreset.isSelected, - state.value.colorPresets.size - ) { - if (state.value.colorPresets.size > 1) { - colorPreset.isSelected - } else true - }, - enableAnimation = state.value.animateColorPreset, - context = context, - canDrag = remember(state.value.colorPresets.size) { - state.value.colorPresets.size > 1 - }, - onDragStopped = { - onEvent(SettingsEvent.OnConfirmReorderColorPresets) - }, - onClick = { - onEvent( - SettingsEvent.OnSelectColorPreset( - id = colorPreset.id - ) + ColorPresetOptionRowItem( + colorPreset = colorPreset, + isSelected = remember( + colorPreset.isSelected, + state.value.colorPresets.size + ) { + if (state.value.colorPresets.size > 1) { + colorPreset.isSelected + } else true + }, + enableAnimation = state.value.animateColorPreset, + canDrag = remember(state.value.colorPresets.size) { + state.value.colorPresets.size > 1 + }, + onDragStopped = { + settingsModel.onEvent(SettingsEvent.OnConfirmReorderColorPresets) + }, + onClick = { + settingsModel.onEvent( + SettingsEvent.OnSelectColorPreset( + id = colorPreset.id ) - } - ) - - if (index == state.value.colorPresets.lastIndex) { - Spacer(modifier = Modifier.width(horizontalPadding)) - } else { - Spacer(modifier = Modifier.width(2.dp)) + ) } - } + ) } } } @@ -165,27 +150,24 @@ fun ColorPresetSetting( Column( Modifier .fillMaxWidth() - .padding(horizontal = horizontalPadding) + .padding(horizontal = 18.dp) .clip(MaterialTheme.shapes.large) - .background( - backgroundColor, - MaterialTheme.shapes.large - ) + .background(backgroundColor) ) { Spacer(modifier = Modifier.height(18.dp)) - ColorPresetSettingConfigurationItem( + ColorPresetOptionConfigurationItem( selectedColorPreset = state.value.selectedColorPreset, canDelete = state.value.colorPresets.size > 1, onDelete = { - onEvent( + settingsModel.onEvent( SettingsEvent.OnDeleteColorPreset( id = state.value.selectedColorPreset.id ) ) }, onTitleChange = { - onEvent( + settingsModel.onEvent( SettingsEvent.OnUpdateColorPresetTitle( id = state.value.selectedColorPreset.id, title = it @@ -193,14 +175,14 @@ fun ColorPresetSetting( ) }, onShuffle = { - onEvent( + settingsModel.onEvent( SettingsEvent.OnShuffleColorPreset( id = state.value.selectedColorPreset.id ) ) }, onAdd = { - onEvent( + settingsModel.onEvent( SettingsEvent.OnAddColorPreset( backgroundColor = defaultBackgroundColor, fontColor = defaultFontColor @@ -216,7 +198,7 @@ fun ColorPresetSetting( presetId = state.value.selectedColorPreset.id, title = stringResource(id = R.string.background_color_option), onValueChange = { - onEvent( + settingsModel.onEvent( SettingsEvent.OnUpdateColorPresetColor( id = state.value.selectedColorPreset.id, backgroundColor = it, @@ -230,7 +212,7 @@ fun ColorPresetSetting( presetId = state.value.selectedColorPreset.id, title = stringResource(id = R.string.font_color_option), onValueChange = { - onEvent( + settingsModel.onEvent( SettingsEvent.OnUpdateColorPresetColor( id = state.value.selectedColorPreset.id, backgroundColor = null, @@ -245,28 +227,16 @@ fun ColorPresetSetting( } } -/** - * Color Preset Setting Item. - * Lets user select color preset and shows little preview of the preset. - * - * @param colorPreset [ColorPreset] to select. - * @param isSelected Whether [ColorPreset] is selected. - * @param canDrag Whether user can drag [ColorPreset]. - * @param enableAnimation Whether there is appearing/disappearing animation. - * @param context [Context]. - * @param onDragStopped Callback when user stops dragging [ColorPreset]. - * @param onClick OnClick callback. - */ @Composable -private fun ReorderableCollectionItemScope.ColorPresetSettingItem( +private fun ReorderableCollectionItemScope.ColorPresetOptionRowItem( colorPreset: ColorPreset, isSelected: Selected, canDrag: Boolean, enableAnimation: Boolean, - context: Context, onDragStopped: () -> Unit, onClick: () -> Unit ) { + val context = LocalContext.current val title = remember(colorPreset) { if ((colorPreset.name ?: "").isBlank()) { return@remember context.getString( @@ -360,20 +330,9 @@ private fun ReorderableCollectionItemScope.ColorPresetSettingItem( } } -/** - * Color Preset Settings Configuration Item. - * Lets user configure Title, Shuffle colors, Delete presets and Add presets. - * - * @param selectedColorPreset Currently selected [ColorPreset]. - * @param canDelete Whether this [ColorPreset] can be deleted. - * @param onDelete Delete callback. - * @param onShuffle Shuffle callback. - * @param onTitleChange TitleChange callback. - * @param onAdd Add callback. - */ @OptIn(FlowPreview::class) @Composable -private fun ColorPresetSettingConfigurationItem( +private fun ColorPresetOptionConfigurationItem( selectedColorPreset: ColorPreset, canDelete: Boolean, onDelete: () -> Unit, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/FastColorPresetChangeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/components/FastColorPresetChangeOption.kt similarity index 57% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/FastColorPresetChangeSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/components/FastColorPresetChangeOption.kt index 64d08864..03c07f99 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/FastColorPresetChangeSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/colors/components/FastColorPresetChangeOption.kt @@ -1,27 +1,25 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings +package ua.acclorite.book_story.presentation.settings.appearance.colors.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Fast Color Preset Change setting. - * If true, user can fast change color presets in Reader with swipes. - */ @Composable -fun FastColorPresetChangeSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun FastColorPresetChangeOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.fastColorPresetChange, title = stringResource(id = R.string.fast_color_preset_change_option), description = stringResource(id = R.string.fast_color_preset_change_option_desc), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeFastColorPresetChange( !state.value.fastColorPresetChange ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ThemePreferencesSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/ThemePreferencesSubcategory.kt similarity index 50% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ThemePreferencesSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/ThemePreferencesSubcategory.kt index 661b5a64..538eca06 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ThemePreferencesSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/ThemePreferencesSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories +package ua.acclorite.book_story.presentation.settings.appearance.theme_preferences import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,17 +9,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.AbsoluteDarkSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.DarkThemeSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.PureDarkSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.ThemeContrastSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.ThemeSetting +import ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components.AbsoluteDarkOption +import ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components.AppThemeOption +import ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components.DarkThemeOption +import ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components.PureDarkOption +import ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components.ThemeContrastOption +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory -/** - * Theme Preferences subcategory. - * Contains all settings from Theme Preferences. - */ fun LazyListScope.ThemePreferencesSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.theme_appearance_settings) }, @@ -37,23 +33,23 @@ fun LazyListScope.ThemePreferencesSubcategory( bottomPadding = bottomPadding ) { item { - DarkThemeSetting() + DarkThemeOption() } item { - ThemeSetting() + AppThemeOption() } item { - ThemeContrastSetting() + ThemeContrastOption() } item { - PureDarkSetting() + PureDarkOption() } item { - AbsoluteDarkSetting() + AbsoluteDarkOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/AbsoluteDarkSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/AbsoluteDarkOption.kt similarity index 57% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/AbsoluteDarkSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/AbsoluteDarkOption.kt index abd1b616..86ab6339 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/AbsoluteDarkSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/AbsoluteDarkOption.kt @@ -1,24 +1,22 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings +package ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.ui.ExpandingTransition -import ua.acclorite.book_story.presentation.ui.isDark -import ua.acclorite.book_story.presentation.ui.isPureDark +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition +import ua.acclorite.book_story.ui.theme.isDark +import ua.acclorite.book_story.ui.theme.isPureDark -/** - * Absolute Dark setting. - * If true, changes Pure Dark (OLED) theme to have black color as background. - */ @Composable -fun AbsoluteDarkSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun AbsoluteDarkOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() val context = LocalContext.current ExpandingTransition( @@ -30,7 +28,7 @@ fun AbsoluteDarkSetting() { title = stringResource(id = R.string.absolute_dark_option), description = stringResource(id = R.string.absolute_dark_option_desc), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeAbsoluteDark( !state.value.absoluteDark ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/AppThemeOption.kt similarity index 80% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/AppThemeOption.kt index 4826e757..b6e9e963 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/AppThemeOption.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings +package ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -34,77 +34,60 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle -import ua.acclorite.book_story.presentation.ui.Theme -import ua.acclorite.book_story.presentation.ui.ThemeContrast -import ua.acclorite.book_story.presentation.ui.animatedColorScheme -import ua.acclorite.book_story.presentation.ui.isDark -import ua.acclorite.book_story.presentation.ui.isPureDark +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.Theme +import ua.acclorite.book_story.ui.theme.ThemeContrast +import ua.acclorite.book_story.ui.theme.animatedColorScheme +import ua.acclorite.book_story.ui.theme.isDark +import ua.acclorite.book_story.ui.theme.isPureDark -/** - * Theme setting. - * Lets user change app's theme from available in [Theme.entries]. - */ @Composable -fun ThemeSetting( - verticalPadding: Dp = 8.dp, - horizontalPadding: Dp = 18.dp -) { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun AppThemeOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() Column( Modifier .fillMaxWidth() - .padding(vertical = verticalPadding) + .padding(vertical = 8.dp) ) { - SettingsCategoryTitle( + SettingsSubcategoryTitle( title = stringResource(id = R.string.app_theme_option), - padding = horizontalPadding + padding = 18.dp ) Spacer(modifier = Modifier.height(10.dp)) LazyRow( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(horizontal = horizontalPadding) + contentPadding = PaddingValues(horizontal = 18.dp) ) { items( Theme.entries(), key = { theme -> theme.name } ) { theme -> - ThemeSettingItem( + AppThemeOptionItem( theme = theme, darkTheme = state.value.darkTheme.isDark(), themeContrast = state.value.themeContrast, isPureDark = state.value.pureDark.isPureDark(context = LocalContext.current), selected = state.value.theme == theme ) { - onMainEvent(MainEvent.OnChangeTheme(theme.name)) + mainModel.onEvent(MainEvent.OnChangeTheme(theme.name)) } } } } } -/** - * Theme Setting item. - * Used to switch between different themes. - * - * @param theme Target [Theme]. - * @param darkTheme Whether should be shown in Dark Theme. - * @param isPureDark Whether should be shown in Pure Dark. - * @param themeContrast [ThemeContrast]. - * @param selected True if this theme is currently selected. - * @param onClick OnClick callback. - */ @Composable -private fun ThemeSettingItem( +private fun AppThemeOptionItem( theme: Theme, darkTheme: Boolean, isPureDark: Boolean, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/DarkThemeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/DarkThemeOption.kt similarity index 59% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/DarkThemeSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/DarkThemeOption.kt index a64d1221..968f238d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/DarkThemeSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/DarkThemeOption.kt @@ -1,23 +1,21 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings +package ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.ui.DarkTheme +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.DarkTheme -/** - * Dark Theme setting. - * If true, dark theme applied to the app's theme. - */ @Composable -fun DarkThemeSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun DarkThemeOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SegmentedButtonWithTitle( title = stringResource(id = R.string.dark_theme_option), @@ -34,10 +32,6 @@ fun DarkThemeSetting() { ) } ) { - onMainEvent( - MainEvent.OnChangeDarkTheme( - it.id - ) - ) + mainModel.onEvent(MainEvent.OnChangeDarkTheme(it.id)) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/PureDarkSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/PureDarkOption.kt similarity index 63% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/PureDarkSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/PureDarkOption.kt index 1004f0b1..de12a8a6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/PureDarkSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/PureDarkOption.kt @@ -1,25 +1,23 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings +package ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.ui.ExpandingTransition -import ua.acclorite.book_story.presentation.ui.PureDark -import ua.acclorite.book_story.presentation.ui.isDark +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition +import ua.acclorite.book_story.ui.theme.PureDark +import ua.acclorite.book_story.ui.theme.isDark -/** - * Pure Dark setting. - * If true, enables Pure Dark (OLED) theme. - */ @Composable -fun PureDarkSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun PureDarkOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() ExpandingTransition(visible = state.value.darkTheme.isDark()) { SegmentedButtonWithTitle( @@ -37,7 +35,7 @@ fun PureDarkSetting() { ) } ) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangePureDark( it.id ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeContrastSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/ThemeContrastOption.kt similarity index 72% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeContrastSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/ThemeContrastOption.kt index 9b3de05c..ea9bf157 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeContrastSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/appearance/theme_preferences/components/ThemeContrastOption.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings +package ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -7,28 +7,25 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.ui.BookStoryTheme -import ua.acclorite.book_story.presentation.ui.ExpandingTransition -import ua.acclorite.book_story.presentation.ui.ThemeContrast -import ua.acclorite.book_story.presentation.ui.isDark -import ua.acclorite.book_story.presentation.ui.isPureDark +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.BookStoryTheme +import ua.acclorite.book_story.ui.theme.ExpandingTransition +import ua.acclorite.book_story.ui.theme.ThemeContrast +import ua.acclorite.book_story.ui.theme.isDark +import ua.acclorite.book_story.ui.theme.isPureDark -/** - * Theme Contrast setting. - * Lets user change theme contrast levels. - */ @Composable -fun ThemeContrastSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun ThemeContrastOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() val themeContrastTheme = remember { mutableStateOf(state.value.theme) } - LaunchedEffect(state.value.theme) { if (state.value.theme.hasThemeContrast) { themeContrastTheme.value = state.value.theme @@ -58,7 +55,7 @@ fun ThemeContrastSetting() { ) } ) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeThemeContrast( it.id ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt new file mode 100644 index 00000000..557f2853 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt @@ -0,0 +1,29 @@ +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.browse + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.presentation.settings.browse.filter.BrowseFilterSubcategory +import ua.acclorite.book_story.presentation.settings.browse.general.BrowseGeneralSubcategory +import ua.acclorite.book_story.presentation.settings.browse.sort.BrowseSortSubcategory + +fun LazyListScope.BrowseSettingsCategory( + topPadding: Dp = 16.dp, + bottomPadding: Dp = 16.dp +) { + BrowseGeneralSubcategory( + topPadding = topPadding, + bottomPadding = 0.dp + ) + BrowseFilterSubcategory( + topPadding = 22.dp, + bottomPadding = 0.dp + ) + BrowseSortSubcategory( + showDivider = false, + topPadding = 22.dp, + bottomPadding = bottomPadding + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsContent.kt new file mode 100644 index 00000000..5149f66b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsContent.kt @@ -0,0 +1,20 @@ +package ua.acclorite.book_story.presentation.settings.browse + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BrowseSettingsContent( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + BrowseSettingsScaffold( + listState = listState, + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsLayout.kt new file mode 100644 index 00000000..9f6553f0 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsLayout.kt @@ -0,0 +1,24 @@ +package ua.acclorite.book_story.presentation.settings.browse + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun BrowseSettingsLayout( + listState: LazyListState, + paddingValues: PaddingValues +) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + BrowseSettingsCategory() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsScaffold.kt new file mode 100644 index 00000000..0f2e14d8 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsScaffold.kt @@ -0,0 +1,43 @@ +package ua.acclorite.book_story.presentation.settings.browse + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BrowseSettingsScaffold( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars) + .imePadding(), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + BrowseSettingsTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + BrowseSettingsLayout( + listState = listState, + paddingValues = paddingValues + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsTopBar.kt new file mode 100644 index 00000000..3997328e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsTopBar.kt @@ -0,0 +1,35 @@ +package ua.acclorite.book_story.presentation.settings.browse + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BrowseSettingsTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.browse_settings)) + }, + navigationIcon = { + NavigatorBackIconButton( + navigateBack = navigateBack + ) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseFilterSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/filter/BrowseFilterSubcategory.kt similarity index 67% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseFilterSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/filter/BrowseFilterSubcategory.kt index 4621bd84..ceb254cd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseFilterSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/filter/BrowseFilterSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories +package ua.acclorite.book_story.presentation.settings.browse.filter import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,13 +9,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseFilterSetting +import ua.acclorite.book_story.presentation.settings.browse.filter.components.BrowseFilterOption +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory -/** - * Browse Filter subcategory. - * Contains all settings from Filter Browse settings. - */ fun LazyListScope.BrowseFilterSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.filter_browse_settings) }, @@ -32,6 +28,6 @@ fun LazyListScope.BrowseFilterSubcategory( topPadding = topPadding, bottomPadding = bottomPadding ) { - BrowseFilterSetting() + BrowseFilterOption() } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseFilterSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/filter/components/BrowseFilterOption.kt similarity index 74% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseFilterSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/filter/components/BrowseFilterOption.kt index e4ed6430..95f32c94 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseFilterSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/filter/components/BrowseFilterOption.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings +package ua.acclorite.book_story.presentation.settings.browse.filter.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row @@ -18,43 +18,33 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideSupportedExtensions -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Browse Filter setting. - * Lets user choose which items to filter in Browse. - */ -fun LazyListScope.BrowseFilterSetting() { +fun LazyListScope.BrowseFilterOption() { items(Constants.provideSupportedExtensions(), key = { it }) { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() - FilterItem( + BrowseFilterOptionItem( item = it, isSelected = state.value.browseIncludedFilterItems.any { item -> item == it } ) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeBrowseIncludedFilterItem(it) ) } } } -/** - * Filter Item. - * Can be selected. - * - * @param item Item. - * @param isSelected Whether item is selected. - * @param onClick OnClick callback. - */ @Composable -private fun FilterItem( +private fun BrowseFilterOptionItem( item: String, isSelected: Boolean, onClick: () -> Unit diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseGeneralSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/BrowseGeneralSubcategory.kt similarity index 51% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseGeneralSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/BrowseGeneralSubcategory.kt index 98499d73..4115d7b5 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseGeneralSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/BrowseGeneralSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories +package ua.acclorite.book_story.presentation.settings.browse.general import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,16 +9,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseFilesStructureSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseGridSizeSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseLayoutSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowsePinFavoriteDirectoriesSetting +import ua.acclorite.book_story.presentation.settings.browse.general.components.BrowseGridSizeOption +import ua.acclorite.book_story.presentation.settings.browse.general.components.BrowseLayoutOption +import ua.acclorite.book_story.presentation.settings.browse.general.components.FilesStructureOption +import ua.acclorite.book_story.presentation.settings.browse.general.components.PinFavoriteDirectoriesOption +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory -/** - * Browse General subcategory. - * Contains all settings from General Browse settings. - */ fun LazyListScope.BrowseGeneralSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.general_browse_settings) }, @@ -36,19 +32,19 @@ fun LazyListScope.BrowseGeneralSubcategory( bottomPadding = bottomPadding ) { item { - BrowseFilesStructureSetting() + FilesStructureOption() } item { - BrowsePinFavoriteDirectoriesSetting() + PinFavoriteDirectoriesOption() } item { - BrowseLayoutSetting() + BrowseLayoutOption() } item { - BrowseGridSizeSetting() + BrowseGridSizeOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseGridSizeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/BrowseGridSizeOption.kt similarity index 55% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseGridSizeSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/BrowseGridSizeOption.kt index 5370d671..8accaa4f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseGridSizeSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/BrowseGridSizeOption.kt @@ -1,22 +1,20 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings +package ua.acclorite.book_story.presentation.settings.browse.general.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.browse.BrowseLayout import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout -import ua.acclorite.book_story.presentation.ui.ExpandingTransition +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition -/** - * Browse Grid Size setting. - * Lets user specify grid size to be displayed. - */ @Composable -fun BrowseGridSizeSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun BrowseGridSizeOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() ExpandingTransition(visible = state.value.browseLayout == BrowseLayout.GRID) { SliderWithTitle( @@ -28,8 +26,8 @@ fun BrowseGridSizeSetting() { toValue = 10, title = stringResource(id = R.string.browse_grid_size_option), onValueChange = { - onMainEvent(MainEvent.OnChangeBrowseAutoGridSize(it == 0)) - onMainEvent( + mainModel.onEvent(MainEvent.OnChangeBrowseAutoGridSize(it == 0)) + mainModel.onEvent( MainEvent.OnChangeBrowseGridSize(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseLayoutSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/BrowseLayoutOption.kt similarity index 61% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseLayoutSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/BrowseLayoutOption.kt index 5d5523b2..f770ace7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseLayoutSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/BrowseLayoutOption.kt @@ -1,23 +1,21 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings +package ua.acclorite.book_story.presentation.settings.browse.general.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Browse Layout setting. - * Lets user choose between layouts(list, grid). - */ @Composable -fun BrowseLayoutSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun BrowseLayoutOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SegmentedButtonWithTitle( title = stringResource(id = R.string.browse_layout_option), @@ -33,7 +31,7 @@ fun BrowseLayoutSetting() { ) } ) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeBrowseLayout( it.id ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseFilesStructureSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/FilesStructureOption.kt similarity index 61% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseFilesStructureSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/FilesStructureOption.kt index 55cde3cd..e1a08ed4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseFilesStructureSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/FilesStructureOption.kt @@ -1,23 +1,21 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings +package ua.acclorite.book_story.presentation.settings.browse.general.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Browse Files Structure setting. - * Lets user choose between all and folders structure for Browse. - */ @Composable -fun BrowseFilesStructureSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun FilesStructureOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SegmentedButtonWithTitle( title = stringResource(id = R.string.browse_files_structure_option), @@ -33,7 +31,7 @@ fun BrowseFilesStructureSetting() { ) } ) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeBrowseFilesStructure( it.id ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowsePinFavoriteDirectoriesSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/PinFavoriteDirectoriesOption.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowsePinFavoriteDirectoriesSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/PinFavoriteDirectoriesOption.kt index 1e00b1cb..c5be8553 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowsePinFavoriteDirectoriesSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/general/components/PinFavoriteDirectoriesOption.kt @@ -1,22 +1,20 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings +package ua.acclorite.book_story.presentation.settings.browse.general.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure -import ua.acclorite.book_story.presentation.ui.ExpandingTransition +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition -/** - * Browse Pin Favorite Directories setting. - * If true, shows all favorite directories in root directory and moves them to the top in nested directories. - */ @Composable -fun BrowsePinFavoriteDirectoriesSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun PinFavoriteDirectoriesOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() ExpandingTransition(visible = state.value.browseFilesStructure == BrowseFilesStructure.DIRECTORIES) { SwitchWithTitle( @@ -24,7 +22,7 @@ fun BrowsePinFavoriteDirectoriesSetting() { title = stringResource(id = R.string.browse_pin_favorite_directories_option), description = stringResource(id = R.string.browse_pin_favorite_directories_option_desc) ) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeBrowsePinFavoriteDirectories( !state.value.browsePinFavoriteDirectories ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseSortSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/BrowseSortSubcategory.kt similarity index 67% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseSortSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/BrowseSortSubcategory.kt index f3a4191d..5880f048 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/subcategories/BrowseSortSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/BrowseSortSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories +package ua.acclorite.book_story.presentation.settings.browse.sort import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,13 +9,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseSortOrderSetting +import ua.acclorite.book_story.presentation.settings.browse.sort.components.BrowseSortOption +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory -/** - * Browse Sort subcategory. - * Contains all settings from Sort Browse settings. - */ fun LazyListScope.BrowseSortSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.sort_browse_settings) }, @@ -32,6 +28,6 @@ fun LazyListScope.BrowseSortSubcategory( topPadding = topPadding, bottomPadding = bottomPadding ) { - BrowseSortOrderSetting() + BrowseSortOption() } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseSortOrderSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/components/BrowseSortOption.kt similarity index 75% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseSortOrderSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/components/BrowseSortOption.kt index e966aa71..d213d6b1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/browse/components/settings/BrowseSortOrderSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/components/BrowseSortOption.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings +package ua.acclorite.book_story.presentation.settings.browse.sort.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row @@ -24,50 +24,39 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseSortOrder +import ua.acclorite.book_story.domain.browse.BrowseSortOrder +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Browse Sort Order setting. - * Lets user choose how to sort files in Browse. - */ -fun LazyListScope.BrowseSortOrderSetting() { +fun LazyListScope.BrowseSortOption() { items(BrowseSortOrder.entries, key = { it.name }) { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() - SortItem( + BrowseSortOptionItem( item = it, isSelected = state.value.browseSortOrder == it, isDescending = state.value.browseSortOrderDescending ) { if (state.value.browseSortOrder == it) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeBrowseSortOrderDescending( !state.value.browseSortOrderDescending ) ) } else { - onMainEvent(MainEvent.OnChangeBrowseSortOrderDescending(true)) - onMainEvent(MainEvent.OnChangeBrowseSortOrder(it.name)) + mainModel.onEvent(MainEvent.OnChangeBrowseSortOrderDescending(true)) + mainModel.onEvent(MainEvent.OnChangeBrowseSortOrder(it.name)) } } } } -/** - * Sort Item. - * Can sort items by descending or ascending. - * - * @param item [BrowseSortOrder]. - * @param isSelected Whether items are currently sorted by this item. - * @param isDescending Whether sorted by descending. - * @param onClick OnClick callback. - */ @Composable -private fun SortItem( +private fun BrowseSortOptionItem( item: BrowseSortOrder, isSelected: Boolean, isDescending: Boolean, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategory.kt similarity index 73% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategory.kt index 69d4893e..01910af0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.components +package ua.acclorite.book_story.presentation.settings.components import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -14,18 +14,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.dp -/** - * Settings Subcategory. - * Root for all Subcategories. - * - * @param titleColor The color of the title. - * @param title The title. - * @param showTitle Whether title should be shown. - * @param showDivider Whether divider at the end of the category should be shown. - * @param topPadding Top padding. - * @param bottomPadding Bottom padding. - * @param content Settings to be placed inside this subcategory. - */ fun LazyListScope.SettingsSubcategory( titleColor: @Composable () -> Color, title: @Composable () -> String, @@ -37,7 +25,7 @@ fun LazyListScope.SettingsSubcategory( ) { item { if (showTitle) { - SettingsCategoryTitle( + SettingsSubcategoryTitle( title = title.invoke(), color = titleColor.invoke(), modifier = Modifier diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategoryTitle.kt similarity index 86% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryTitle.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategoryTitle.kt index faace75d..cabdfa71 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SettingsCategoryTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategoryTitle.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.settings.components +package ua.acclorite.book_story.presentation.settings.components import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -14,11 +14,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** - * Settings Category Title. Used to name a category. - */ @Composable -fun SettingsCategoryTitle( +fun SettingsSubcategoryTitle( modifier: Modifier = Modifier, title: String, color: Color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsCategory.kt new file mode 100644 index 00000000..f636402a --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsCategory.kt @@ -0,0 +1,39 @@ +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.general + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.coerceAtLeast +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.presentation.settings.general.components.AppLanguageOption +import ua.acclorite.book_story.presentation.settings.general.components.CheckForUpdatesOption +import ua.acclorite.book_story.presentation.settings.general.components.DoublePressExitOption + +fun LazyListScope.GeneralSettingsCategory( + topPadding: Dp = 16.dp, + bottomPadding: Dp = 16.dp +) { + item { + Spacer(modifier = Modifier.height((topPadding - 8.dp).coerceAtLeast(0.dp))) + } + + item { + AppLanguageOption() + } + + item { + CheckForUpdatesOption() + } + + item { + DoublePressExitOption() + } + + item { + Spacer(modifier = Modifier.height(bottomPadding)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsContent.kt new file mode 100644 index 00000000..61b85054 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsContent.kt @@ -0,0 +1,20 @@ +package ua.acclorite.book_story.presentation.settings.general + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GeneralSettingsContent( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + GeneralSettingsScaffold( + listState = listState, + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsLayout.kt new file mode 100644 index 00000000..51c8a031 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsLayout.kt @@ -0,0 +1,24 @@ +package ua.acclorite.book_story.presentation.settings.general + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun GeneralSettingsLayout( + listState: LazyListState, + paddingValues: PaddingValues +) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + GeneralSettingsCategory() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsScaffold.kt new file mode 100644 index 00000000..9a41ac3a --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsScaffold.kt @@ -0,0 +1,41 @@ +package ua.acclorite.book_story.presentation.settings.general + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GeneralSettingsScaffold( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + GeneralSettingsTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + GeneralSettingsLayout( + listState = listState, + paddingValues = paddingValues + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsTopBar.kt new file mode 100644 index 00000000..744b0add --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/GeneralSettingsTopBar.kt @@ -0,0 +1,35 @@ +package ua.acclorite.book_story.presentation.settings.general + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GeneralSettingsTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.general_settings)) + }, + navigationIcon = { + NavigatorBackIconButton( + navigateBack = navigateBack + ) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/AppLanguageSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/AppLanguageOption.kt similarity index 59% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/AppLanguageSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/AppLanguageOption.kt index 53940f62..fddad5c0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/AppLanguageSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/AppLanguageOption.kt @@ -1,24 +1,22 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.general.components.settings +package ua.acclorite.book_story.presentation.settings.general.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.ChipsWithTitle import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideLanguages -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * App Language setting. - * Changes app's language. - */ @Composable -fun AppLanguageSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun AppLanguageOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() ChipsWithTitle( title = stringResource(id = R.string.language_option), @@ -31,10 +29,6 @@ fun AppLanguageSetting() { ) }.sortedBy { it.title } ) { - onMainEvent( - MainEvent.OnChangeLanguage( - it.id - ) - ) + mainModel.onEvent(MainEvent.OnChangeLanguage(it.id)) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/CheckForUpdatesSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/CheckForUpdatesOption.kt similarity index 63% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/CheckForUpdatesSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/CheckForUpdatesOption.kt index 6d2b0811..67d41234 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/components/settings/CheckForUpdatesSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/CheckForUpdatesOption.kt @@ -1,57 +1,50 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.general.components.settings +package ua.acclorite.book_story.presentation.settings.general.components import android.Manifest import android.annotation.SuppressLint -import androidx.activity.ComponentActivity import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Security import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithContent +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent -import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.settings.SettingsEvent +import ua.acclorite.book_story.ui.settings.SettingsModel -/** - * Check For Updates setting. - * If true, checks for the update on app start. - */ @OptIn(ExperimentalPermissionsApi::class) @SuppressLint("InlinedApi") @Composable -fun CheckForUpdatesSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() - val onSettingsEvent = SettingsViewModel.getEvent() - val activity = LocalContext.current as ComponentActivity +fun CheckForUpdatesOption() { + val mainModel = hiltViewModel() + val settingsModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + val activity = LocalActivity.current val notificationsPermissionState = rememberPermissionState( permission = Manifest.permission.POST_NOTIFICATIONS ) val showConfirmation = remember { mutableStateOf(false) } if (showConfirmation.value) { - DialogWithContent( + Dialog( title = stringResource(id = R.string.enable_check_for_updates), description = stringResource(id = R.string.enable_check_for_updates_description), actionText = stringResource(id = R.string.enable), imageVectorIcon = Icons.Default.Security, - isActionEnabled = true, + actionEnabled = true, onDismiss = { showConfirmation.value = false }, onAction = { - onMainEvent( - MainEvent.OnChangeCheckForUpdates( - true - ) - ) + mainModel.onEvent(MainEvent.OnChangeCheckForUpdates(true)) showConfirmation.value = false }, withDivider = false @@ -63,15 +56,15 @@ fun CheckForUpdatesSetting() { title = stringResource(id = R.string.check_for_updates_option), description = stringResource(id = R.string.check_for_updates_option_desc) ) { - onSettingsEvent( - SettingsEvent.OnGeneralChangeCheckForUpdates( + settingsModel.onEvent( + SettingsEvent.OnChangeCheckForUpdates( enable = !state.value.checkForUpdates, activity = activity, notificationsPermissionState = notificationsPermissionState, onChangeCheckForUpdates = { when (it) { true -> showConfirmation.value = true - false -> onMainEvent(MainEvent.OnChangeCheckForUpdates(false)) + false -> mainModel.onEvent(MainEvent.OnChangeCheckForUpdates(false)) } } ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/DoublePressExitOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/DoublePressExitOption.kt new file mode 100644 index 00000000..19846378 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/general/components/DoublePressExitOption.kt @@ -0,0 +1,28 @@ +package ua.acclorite.book_story.presentation.settings.general.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun DoublePressExitOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SwitchWithTitle( + selected = state.value.doublePressExit, + title = stringResource(id = R.string.double_press_exit_option), + description = stringResource(id = R.string.double_press_exit_option_desc) + ) { + mainModel.onEvent( + MainEvent.OnChangeDoublePressExit( + !state.value.doublePressExit + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/ReaderSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsCategory.kt similarity index 50% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/ReaderSettingsCategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsCategory.kt index f57c352c..24f0b08e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/ReaderSettingsCategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsCategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components +package ua.acclorite.book_story.presentation.settings.reader import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -8,27 +8,18 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.FontSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.MiscSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.PaddingSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.ReadingSpeedSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.SystemSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.TextSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories.TranslatorSubcategory +import ua.acclorite.book_story.presentation.settings.reader.font.FontSubcategory +import ua.acclorite.book_story.presentation.settings.reader.misc.MiscSubcategory +import ua.acclorite.book_story.presentation.settings.reader.padding.PaddingSubcategory +import ua.acclorite.book_story.presentation.settings.reader.reading_speed.ReadingSpeedSubcategory +import ua.acclorite.book_story.presentation.settings.reader.system.SystemSubcategory +import ua.acclorite.book_story.presentation.settings.reader.text.TextSubcategory +import ua.acclorite.book_story.presentation.settings.reader.translator.TranslatorSubcategory -/** - * Reader Settings Category. - * Contains all Reader settings. - * - * @param titleColor Color that [SettingsCategoryTitle] has. - * @param topPadding Top padding to be applied. - * @param bottomPadding Bottom padding to be applied. - */ fun LazyListScope.ReaderSettingsCategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, topPadding: Dp = 16.dp, - bottomPadding: Dp = 48.dp + bottomPadding: Dp = 16.dp ) { FontSubcategory( titleColor = titleColor, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsContent.kt new file mode 100644 index 00000000..54c1bc48 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsContent.kt @@ -0,0 +1,20 @@ +package ua.acclorite.book_story.presentation.settings.reader + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReaderSettingsContent( + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateBack: () -> Unit +) { + ReaderSettingsScaffold( + scrollBehavior = scrollBehavior, + listState = listState, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsLayout.kt new file mode 100644 index 00000000..cf3d64f9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsLayout.kt @@ -0,0 +1,24 @@ +package ua.acclorite.book_story.presentation.settings.reader + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun ReaderSettingsLayout( + listState: LazyListState, + paddingValues: PaddingValues +) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + ReaderSettingsCategory() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsScaffold.kt new file mode 100644 index 00000000..da1435ce --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsScaffold.kt @@ -0,0 +1,41 @@ +package ua.acclorite.book_story.presentation.settings.reader + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReaderSettingsScaffold( + scrollBehavior: TopAppBarScrollBehavior, + listState: LazyListState, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + ReaderSettingsTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + ReaderSettingsLayout( + listState = listState, + paddingValues = paddingValues + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsTopBar.kt new file mode 100644 index 00000000..ca38dcb3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/ReaderSettingsTopBar.kt @@ -0,0 +1,35 @@ +package ua.acclorite.book_story.presentation.settings.reader + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReaderSettingsTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.reader_settings)) + }, + navigationIcon = { + NavigatorBackIconButton( + navigateBack = navigateBack + ) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/FontSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/FontSubcategory.kt similarity index 54% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/FontSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/FontSubcategory.kt index 1fc31274..c2240e67 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/FontSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/FontSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories +package ua.acclorite.book_story.presentation.settings.reader.font import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,16 +9,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.FontFamilySetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.FontSizeSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.FontStyleSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.LetterSpacingSetting +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.font.components.FontFamilyOption +import ua.acclorite.book_story.presentation.settings.reader.font.components.FontSizeOption +import ua.acclorite.book_story.presentation.settings.reader.font.components.FontStyleOption +import ua.acclorite.book_story.presentation.settings.reader.font.components.LetterSpacingOption -/** - * Font subcategory. - * Contains all settings from Font. - */ fun LazyListScope.FontSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.font_reader_settings) }, @@ -36,19 +32,19 @@ fun LazyListScope.FontSubcategory( bottomPadding = bottomPadding ) { item { - FontFamilySetting() + FontFamilyOption() } item { - FontStyleSetting() + FontStyleOption() } item { - FontSizeSetting() + FontSizeOption() } item { - LetterSpacingSetting() + LetterSpacingOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontFamilySetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontFamilyOption.kt similarity index 72% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontFamilySetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontFamilyOption.kt index 255e35c2..8516babf 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontFamilySetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontFamilyOption.kt @@ -1,26 +1,24 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.font.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.ChipsWithTitle import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideFonts -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Font Family setting. - * Changes Reader's font, uses fonts from [provideFonts]. - */ @Composable -fun FontFamilySetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun FontFamilyOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() val fontFamily = remember(state.value.fontFamily) { Constants.provideFonts(withRandom = true).find { @@ -45,7 +43,7 @@ fun FontFamilySetting() { ) }, onClick = { - onMainEvent(MainEvent.OnChangeFontFamily(it.id)) + mainModel.onEvent(MainEvent.OnChangeFontFamily(it.id)) } ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontSizeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontSizeOption.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontSizeSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontSizeOption.kt index dae88296..516d895d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontSizeSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontSizeOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.font.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Font Size setting. - * Changes Reader's font size. - */ @Composable -fun FontSizeSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun FontSizeOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.fontSize to "pt", @@ -22,7 +20,7 @@ fun FontSizeSetting() { toValue = 35, title = stringResource(id = R.string.font_size_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeFontSize(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontStyleSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontStyleOption.kt similarity index 78% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontStyleSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontStyleOption.kt index 868d4047..5093cd2f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FontStyleSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/FontStyleOption.kt @@ -1,26 +1,24 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.font.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideFonts -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Font Style setting. - * Changes Reader's font style (Normal/Italic). - */ @Composable -fun FontStyleSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun FontStyleOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() val fontFamily = remember(state.value.fontFamily) { Constants.provideFonts(withRandom = false).find { @@ -48,10 +46,10 @@ fun FontStyleSetting() { fontStyle = FontStyle.Italic ), selected = state.value.isItalic - ), + ) ), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeFontStyle( when (it.id) { "italic" -> true diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/LetterSpacingSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/LetterSpacingOption.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/LetterSpacingSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/LetterSpacingOption.kt index 65d14056..1820f546 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/LetterSpacingSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/font/components/LetterSpacingOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.font.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Letter Spacing setting. - * Changes Reader's letter spacing. - */ @Composable -fun LetterSpacingSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun LetterSpacingOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.letterSpacing to "pt", @@ -22,7 +20,7 @@ fun LetterSpacingSetting() { toValue = 16, title = stringResource(id = R.string.letter_spacing_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeLetterSpacing(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/MiscSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/MiscSubcategory.kt new file mode 100644 index 00000000..f88bd2d6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/MiscSubcategory.kt @@ -0,0 +1,55 @@ +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.reader.misc + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.misc.components.CheckForTextUpdateOption +import ua.acclorite.book_story.presentation.settings.reader.misc.components.CheckForTextUpdateToastOption +import ua.acclorite.book_story.presentation.settings.reader.misc.components.FullscreenOption +import ua.acclorite.book_story.presentation.settings.reader.misc.components.HideBarsOnFastScrollOption +import ua.acclorite.book_story.presentation.settings.reader.misc.components.KeepScreenOnOption + +fun LazyListScope.MiscSubcategory( + titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, + title: @Composable () -> String = { stringResource(id = R.string.misc_reader_settings) }, + showTitle: Boolean = true, + showDivider: Boolean = true, + topPadding: Dp, + bottomPadding: Dp +) { + SettingsSubcategory( + titleColor = titleColor, + title = title, + showTitle = showTitle, + showDivider = showDivider, + topPadding = topPadding, + bottomPadding = bottomPadding + ) { + item { + CheckForTextUpdateOption() + } + + item { + CheckForTextUpdateToastOption() + } + + item { + FullscreenOption() + } + + item { + KeepScreenOnOption() + } + + item { + HideBarsOnFastScrollOption() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/CheckForTextUpdateOption.kt similarity index 56% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/CheckForTextUpdateOption.kt index af6efe9e..d44f956e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/CheckForTextUpdateOption.kt @@ -1,27 +1,25 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.misc.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Check for Text Update setting. - * If true, automatically checks whether there is book's text update. - */ @Composable -fun CheckForTextUpdateSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun CheckForTextUpdateOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.checkForTextUpdate, title = stringResource(id = R.string.check_for_text_update_option), description = stringResource(id = R.string.check_for_text_update_option_desc), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeCheckForTextUpdate( !state.value.checkForTextUpdate ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateToastSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/CheckForTextUpdateToastOption.kt similarity index 55% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateToastSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/CheckForTextUpdateToastOption.kt index e6c2840d..b9ff85a6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateToastSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/CheckForTextUpdateToastOption.kt @@ -1,28 +1,26 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.misc.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.ui.ExpandingTransition +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition -/** - * Check for Text Update Toast setting. - * If true, shows a toast when the text is up-to-date. - */ @Composable -fun CheckForTextUpdateToastSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun CheckForTextUpdateToastOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() ExpandingTransition(visible = state.value.checkForTextUpdate) { SwitchWithTitle( selected = state.value.checkForTextUpdateToast, title = stringResource(id = R.string.check_for_text_update_toast_option), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeCheckForTextUpdateToast( !state.value.checkForTextUpdateToast ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FullscreenSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/FullscreenOption.kt similarity index 56% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FullscreenSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/FullscreenOption.kt index 86f0530a..e0a830bc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/FullscreenSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/FullscreenOption.kt @@ -1,27 +1,25 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.misc.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Fullscreen setting. - * If true, hides system bars in Reader. - */ @Composable -fun FullscreenSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun FullscreenOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.fullscreen, title = stringResource(id = R.string.fullscreen_option), description = stringResource(id = R.string.fullscreen_option_desc), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeFullscreen( !state.value.fullscreen ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/HideBarsOnFastScrollSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/HideBarsOnFastScrollOption.kt similarity index 51% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/HideBarsOnFastScrollSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/HideBarsOnFastScrollOption.kt index b5a20cfc..a561250c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/HideBarsOnFastScrollSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/HideBarsOnFastScrollOption.kt @@ -1,26 +1,24 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.misc.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Hide Bars On Fast Scroll setting. - * If true, hides bars during fast scroll. - */ @Composable -fun HideBarsOnFastScrollSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun HideBarsOnFastScrollOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.hideBarsOnFastScroll, title = stringResource(id = R.string.hide_bars_on_fast_scroll_option) ) { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeHideBarsOnFastScroll(!state.value.hideBarsOnFastScroll) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/KeepScreenOnSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/KeepScreenOnOption.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/KeepScreenOnSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/KeepScreenOnOption.kt index 1fd98d4d..65d8b202 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/KeepScreenOnSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/misc/components/KeepScreenOnOption.kt @@ -1,26 +1,24 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.misc.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Keep Screen On setting. - * If true, keeps screen awake in Reader. - */ @Composable -fun KeepScreenOnSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun KeepScreenOnOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.keepScreenOn, title = stringResource(id = R.string.keep_screen_on_option), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeKeepScreenOn( !state.value.keepScreenOn ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/PaddingSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/PaddingSubcategory.kt similarity index 57% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/PaddingSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/PaddingSubcategory.kt index 79a1f26b..a9c77e41 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/PaddingSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/PaddingSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories +package ua.acclorite.book_story.presentation.settings.reader.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,15 +9,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.CutoutPaddingSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.SidePaddingSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.VerticalPaddingSetting +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.padding.components.CutoutPaddingOption +import ua.acclorite.book_story.presentation.settings.reader.padding.components.SidePaddingOption +import ua.acclorite.book_story.presentation.settings.reader.padding.components.VerticalPaddingOption -/** - * Padding subcategory. - * Contains all settings related to padding. - */ fun LazyListScope.PaddingSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.padding_reader_settings) }, @@ -35,15 +31,15 @@ fun LazyListScope.PaddingSubcategory( bottomPadding = bottomPadding ) { item { - SidePaddingSetting() + SidePaddingOption() } item { - VerticalPaddingSetting() + VerticalPaddingOption() } item { - CutoutPaddingSetting() + CutoutPaddingOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CutoutPaddingSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/CutoutPaddingOption.kt similarity index 56% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CutoutPaddingSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/CutoutPaddingOption.kt index 2329637b..d52829b2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CutoutPaddingSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/CutoutPaddingOption.kt @@ -1,27 +1,25 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.padding.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Cutout Padding setting. - * If true, applies padding to cutout area. - */ @Composable -fun CutoutPaddingSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun CutoutPaddingOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.cutoutPadding, title = stringResource(id = R.string.cutout_padding_option), description = stringResource(id = R.string.cutout_padding_option_desc), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeCutoutPadding( !state.value.cutoutPadding ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/SidePaddingSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/SidePaddingOption.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/SidePaddingSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/SidePaddingOption.kt index 75823e28..3b051f87 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/SidePaddingSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/SidePaddingOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.padding.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Side Padding setting. - * Changes Reader's side padding. - */ @Composable -fun SidePaddingSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun SidePaddingOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.sidePadding to "pt", @@ -22,7 +20,7 @@ fun SidePaddingSetting() { toValue = 20, title = stringResource(id = R.string.side_padding_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeSidePadding(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/VerticalPaddingSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/VerticalPaddingOption.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/VerticalPaddingSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/VerticalPaddingOption.kt index b0a93bda..c32b8ce3 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/VerticalPaddingSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/padding/components/VerticalPaddingOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.padding.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Vertical Padding setting. - * Changes Reader's vertical padding. - */ @Composable -fun VerticalPaddingSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun VerticalPaddingOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.verticalPadding to "pt", @@ -22,7 +20,7 @@ fun VerticalPaddingSetting() { toValue = 24, title = stringResource(id = R.string.vertical_padding_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeVerticalPadding(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/ReadingSpeedSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/ReadingSpeedSubcategory.kt similarity index 54% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/ReadingSpeedSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/ReadingSpeedSubcategory.kt index c737996b..ccdcdc49 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/ReadingSpeedSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/ReadingSpeedSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories +package ua.acclorite.book_story.presentation.settings.reader.reading_speed import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,15 +9,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.PerceptionExpanderPaddingSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.PerceptionExpanderSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.PerceptionExpanderThicknessSetting +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.reading_speed.components.PerceptionExpanderOption +import ua.acclorite.book_story.presentation.settings.reader.reading_speed.components.PerceptionExpanderPaddingOption +import ua.acclorite.book_story.presentation.settings.reader.reading_speed.components.PerceptionExpanderThicknessOption -/** - * Reading Speed subcategory. - * Contains all settings that can improve your reading speed. - */ fun LazyListScope.ReadingSpeedSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.reading_speed_reader_settings) }, @@ -35,15 +31,15 @@ fun LazyListScope.ReadingSpeedSubcategory( bottomPadding = bottomPadding ) { item { - PerceptionExpanderSetting() + PerceptionExpanderOption() } item { - PerceptionExpanderPaddingSetting() + PerceptionExpanderPaddingOption() } item { - PerceptionExpanderThicknessSetting() + PerceptionExpanderThicknessOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderOption.kt similarity index 56% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderOption.kt index 4dea3caf..8243bcb2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderOption.kt @@ -1,27 +1,25 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.reading_speed.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Perception Expander setting. - * If true, shows vertical lines in Reader, which makes you read faster. - */ @Composable -fun PerceptionExpanderSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun PerceptionExpanderOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.perceptionExpander, title = stringResource(id = R.string.perception_expander_option), description = stringResource(id = R.string.perception_expander_option_desc), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangePerceptionExpander( !state.value.perceptionExpander ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderPaddingSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderPaddingOption.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderPaddingSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderPaddingOption.kt index 6e63986f..5a84a917 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderPaddingSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderPaddingOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.reading_speed.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Perception Expander Padding setting. - * Changes side padding applied to lines. - */ @Composable -fun PerceptionExpanderPaddingSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun PerceptionExpanderPaddingOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.perceptionExpanderPadding to "pt", @@ -22,7 +20,7 @@ fun PerceptionExpanderPaddingSetting() { toValue = 24, title = stringResource(id = R.string.perception_expander_padding_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangePerceptionExpanderPadding(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderThicknessSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderThicknessOption.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderThicknessSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderThicknessOption.kt index ea2dc03c..24c48266 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/PerceptionExpanderThicknessSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/reading_speed/components/PerceptionExpanderThicknessOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.reading_speed.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Perception Expander Thickness setting. - * Changes thickness of the line. - */ @Composable -fun PerceptionExpanderThicknessSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun PerceptionExpanderThicknessOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.perceptionExpanderThickness to "pt", @@ -22,7 +20,7 @@ fun PerceptionExpanderThicknessSetting() { toValue = 12, title = stringResource(id = R.string.perception_expander_thickness_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangePerceptionExpanderThickness(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/SystemSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/SystemSubcategory.kt similarity index 55% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/SystemSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/SystemSubcategory.kt index 47928c76..bc898563 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/SystemSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/SystemSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories +package ua.acclorite.book_story.presentation.settings.reader.system import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,15 +9,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.CustomScreenBrightnessSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.ScreenBrightnessSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.ScreenOrientationSetting +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.system.components.CustomScreenBrightnessOption +import ua.acclorite.book_story.presentation.settings.reader.system.components.ScreenBrightnessOption +import ua.acclorite.book_story.presentation.settings.reader.system.components.ScreenOrientationOption -/** - * System subcategory. - * Contains all settings that are related to system (e.g. screen orientation). - */ fun LazyListScope.SystemSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.system_reader_settings) }, @@ -35,15 +31,15 @@ fun LazyListScope.SystemSubcategory( bottomPadding = bottomPadding ) { item { - CustomScreenBrightnessSetting() + CustomScreenBrightnessOption() } item { - ScreenBrightnessSetting() + ScreenBrightnessOption() } item { - ScreenOrientationSetting() + ScreenOrientationOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CustomScreenBrightnessSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/CustomScreenBrightnessOption.kt similarity index 54% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CustomScreenBrightnessSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/CustomScreenBrightnessOption.kt index baac5342..6fae55fd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CustomScreenBrightnessSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/CustomScreenBrightnessOption.kt @@ -1,26 +1,24 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.system.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Custom Screen Brightness setting. - * If true, applies custom brightness in Reader. - */ @Composable -fun CustomScreenBrightnessSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun CustomScreenBrightnessOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.customScreenBrightness, title = stringResource(id = R.string.custom_screen_brightness_option), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeCustomScreenBrightness( !state.value.customScreenBrightness ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ScreenBrightnessSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/ScreenBrightnessOption.kt similarity index 51% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ScreenBrightnessSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/ScreenBrightnessOption.kt index 02352cee..be4b5238 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ScreenBrightnessSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/ScreenBrightnessOption.kt @@ -1,27 +1,25 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.system.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Screen Brightness setting. - * Changes screen brightness in Reader. - */ @Composable -fun ScreenBrightnessSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun ScreenBrightnessOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.screenBrightness to "", toValue = 100, title = stringResource(id = R.string.screen_brightness_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeScreenBrightness(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ScreenOrientationSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/ScreenOrientationOption.kt similarity index 71% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ScreenOrientationSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/ScreenOrientationOption.kt index b2745e66..8f3000bc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ScreenOrientationSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/system/components/ScreenOrientationOption.kt @@ -1,23 +1,21 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.system.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.reader.ReaderScreenOrientation +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.ChipsWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.ReaderScreenOrientation +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Screen Orientation setting. - * Changes Reader's screen orientation. - */ @Composable -fun ScreenOrientationSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun ScreenOrientationOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() ChipsWithTitle( title = stringResource(id = R.string.screen_orientation_option), @@ -37,7 +35,7 @@ fun ScreenOrientationSetting() { ) }, onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeScreenOrientation( it.id ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/TextSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/TextSubcategory.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/TextSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/TextSubcategory.kt index fe9ca883..3a7128a9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/TextSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/TextSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories +package ua.acclorite.book_story.presentation.settings.reader.text import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,16 +9,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.LineHeightSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.ParagraphHeightSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.ParagraphIndentationSetting -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.TextAlignmentSetting +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.text.components.LineHeightOption +import ua.acclorite.book_story.presentation.settings.reader.text.components.ParagraphHeightOption +import ua.acclorite.book_story.presentation.settings.reader.text.components.ParagraphIndentationOption +import ua.acclorite.book_story.presentation.settings.reader.text.components.TextAlignmentOption -/** - * Text subcategory. - * Contains all settings from Text. - */ fun LazyListScope.TextSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.text_reader_settings) }, @@ -36,19 +32,19 @@ fun LazyListScope.TextSubcategory( bottomPadding = bottomPadding ) { item { - TextAlignmentSetting() + TextAlignmentOption() } item { - LineHeightSetting() + LineHeightOption() } item { - ParagraphHeightSetting() + ParagraphHeightOption() } item { - ParagraphIndentationSetting() + ParagraphIndentationOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/LineHeightSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/LineHeightOption.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/LineHeightSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/LineHeightOption.kt index bfcbe527..c5d121de 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/LineHeightSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/LineHeightOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.text.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Line Height setting. - * Changes Reader's line height. - */ @Composable -fun LineHeightSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun LineHeightOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.lineHeight to "pt", @@ -22,7 +20,7 @@ fun LineHeightSetting() { toValue = 24, title = stringResource(id = R.string.line_height_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeLineHeight(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ParagraphHeightSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/ParagraphHeightOption.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ParagraphHeightSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/ParagraphHeightOption.kt index 6e6ddc1c..6a61b8e7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ParagraphHeightSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/ParagraphHeightOption.kt @@ -1,20 +1,18 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.text.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Paragraph Height setting. - * Changes Reader's paragraph height. - */ @Composable -fun ParagraphHeightSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun ParagraphHeightOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SliderWithTitle( value = state.value.paragraphHeight to "pt", @@ -22,7 +20,7 @@ fun ParagraphHeightSetting() { toValue = 36, title = stringResource(id = R.string.paragraph_height_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeParagraphHeight(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ParagraphIndentationSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/ParagraphIndentationOption.kt similarity index 55% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ParagraphIndentationSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/ParagraphIndentationOption.kt index e873c74f..46109248 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/ParagraphIndentationSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/ParagraphIndentationOption.kt @@ -1,22 +1,20 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.text.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.ReaderTextAlignment -import ua.acclorite.book_story.presentation.ui.ExpandingTransition +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition -/** - * Paragraph Indentation setting. - * Changes Reader's paragraph indentation. - */ @Composable -fun ParagraphIndentationSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun ParagraphIndentationOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() ExpandingTransition( visible = state.value.textAlignment != ReaderTextAlignment.CENTER && @@ -28,7 +26,7 @@ fun ParagraphIndentationSetting() { toValue = 12, title = stringResource(id = R.string.paragraph_indentation_option), onValueChange = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeParagraphIndentation(it) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/TextAlignmentSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/TextAlignmentOption.kt similarity index 66% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/TextAlignmentSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/TextAlignmentOption.kt index eaeb8f75..09d92764 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/TextAlignmentSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/text/components/TextAlignmentOption.kt @@ -1,23 +1,21 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.text.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.ReaderTextAlignment +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Text Alignment setting. - * Changes Reader's text alignment (Start/Justify/Center/End). - */ @Composable -fun TextAlignmentSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun TextAlignmentOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SegmentedButtonWithTitle( title = stringResource(id = R.string.text_alignment_option), @@ -35,7 +33,7 @@ fun TextAlignmentSetting() { ) }, onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeTextAlignment( it.id ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/TranslatorSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/translator/TranslatorSubcategory.kt similarity index 67% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/TranslatorSubcategory.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/translator/TranslatorSubcategory.kt index 1402d570..617fa362 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/TranslatorSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/translator/TranslatorSubcategory.kt @@ -1,6 +1,6 @@ @file:Suppress("FunctionName") -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.subcategories +package ua.acclorite.book_story.presentation.settings.reader.translator import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme @@ -9,13 +9,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.DoubleClickTranslationSetting +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.reader.translator.components.DoubleClickTranslationOption -/** - * Translator subcategory. - * Contains all settings from Translator. - */ fun LazyListScope.TranslatorSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.translator_reader_settings) }, @@ -33,7 +29,7 @@ fun LazyListScope.TranslatorSubcategory( bottomPadding = bottomPadding ) { item { - DoubleClickTranslationSetting() + DoubleClickTranslationOption() } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/DoubleClickTranslationSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/translator/components/DoubleClickTranslationOption.kt similarity index 57% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/DoubleClickTranslationSetting.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/translator/components/DoubleClickTranslationOption.kt index 95995c04..0a59f58e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/DoubleClickTranslationSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/reader/translator/components/DoubleClickTranslationOption.kt @@ -1,27 +1,25 @@ -package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings +package ua.acclorite.book_story.presentation.settings.reader.translator.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel -/** - * Double Click Translation setting. - * Changes Reader's double click translation. - */ @Composable -fun DoubleClickTranslationSetting() { - val state = MainViewModel.getState() - val onMainEvent = MainViewModel.getEvent() +fun DoubleClickTranslationOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() SwitchWithTitle( selected = state.value.doubleClickTranslation, title = stringResource(id = R.string.double_click_translation_option), description = stringResource(id = R.string.double_click_translation_option_desc), onClick = { - onMainEvent( + mainModel.onEvent( MainEvent.OnChangeDoubleClickTranslation( !state.value.doubleClickTranslation ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartBackHandler.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartBackHandler.kt new file mode 100644 index 00000000..5a7c6a3b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartBackHandler.kt @@ -0,0 +1,13 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable + +@Composable +fun StartBackHandler( + navigateBack: () -> Unit +) { + BackHandler { + navigateBack() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContent.kt new file mode 100644 index 00000000..96ef5959 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContent.kt @@ -0,0 +1,70 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.foundation.background +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.navigator.StackEvent +import ua.acclorite.book_story.domain.ui.ButtonItem +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.start.StartEvent +import ua.acclorite.book_story.ui.start.StartScreen + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun StartContent( + currentPage: Int, + stackEvent: StackEvent, + storagePermissionGranted: Boolean, + notificationsPermissionGranted: Boolean, + storagePermissionState: PermissionState, + notificationsPermissionState: PermissionState, + languages: List, + changeLanguage: (MainEvent.OnChangeLanguage) -> Unit, + storagePermissionRequest: (StartEvent.OnStoragePermissionRequest) -> Unit, + notificationsPermissionRequest: (StartEvent.OnNotificationsPermissionRequest) -> Unit, + navigateForward: () -> Unit, + navigateBack: () -> Unit, + navigateToBrowse: () -> Unit, + navigateToHelp: () -> Unit +) { + StartContentTransition( + modifier = Modifier.background(MaterialTheme.colorScheme.surface), + targetValue = when { + currentPage in 0..2 -> StartScreen.SETTINGS + else -> StartScreen.DONE + }, + stackEvent = stackEvent + ) { page -> + when (page) { + StartScreen.SETTINGS -> { + StartSettings( + currentPage = currentPage, + stackEvent = stackEvent, + storagePermissionGranted = storagePermissionGranted, + notificationsPermissionGranted = notificationsPermissionGranted, + storagePermissionState = storagePermissionState, + notificationsPermissionState = notificationsPermissionState, + languages = languages, + changeLanguage = changeLanguage, + storagePermissionRequest = storagePermissionRequest, + notificationsPermissionRequest = notificationsPermissionRequest, + navigateForward = navigateForward + ) + } + + StartScreen.DONE -> { + StartDone( + navigateToBrowse = navigateToBrowse, + navigateToHelp = navigateToHelp + ) + } + } + } + + StartBackHandler( + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContentTransition.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContentTransition.kt new file mode 100644 index 00000000..c787342d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContentTransition.kt @@ -0,0 +1,38 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.domain.navigator.StackEvent +import ua.acclorite.book_story.ui.theme.Transitions + +@Composable +fun StartContentTransition( + modifier: Modifier = Modifier, + targetValue: String, + stackEvent: StackEvent, + content: @Composable (String) -> Unit +) { + AnimatedContent( + modifier = Modifier + .fillMaxSize() + .then(modifier), + targetState = targetValue, + transitionSpec = { + when (stackEvent) { + StackEvent.Default -> { + Transitions.SlidingTransitionIn + .togetherWith(Transitions.SlidingTransitionOut) + } + + StackEvent.Pop -> { + Transitions.BackSlidingTransitionIn + .togetherWith(Transitions.BackSlidingTransitionOut) + } + } + }, + content = { content(it) } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDone.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDone.kt new file mode 100644 index 00000000..28dc0e93 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDone.kt @@ -0,0 +1,14 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.runtime.Composable + +@Composable +fun StartDone( + navigateToBrowse: () -> Unit, + navigateToHelp: () -> Unit +) { + StartDoneScaffold( + navigateToBrowse = navigateToBrowse, + navigateToHelp = navigateToHelp + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneBottomBar.kt new file mode 100644 index 00000000..f2dde36e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneBottomBar.kt @@ -0,0 +1,60 @@ +package ua.acclorite.book_story.presentation.start + +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.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R + +@Composable +fun StartDoneBottomBar( + navigateToBrowse: () -> Unit, + navigateToHelp: () -> Unit +) { + Column { + Spacer(modifier = Modifier.height(18.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 8.dp) + .padding(horizontal = 18.dp) + .fillMaxWidth() + ) { + TextButton( + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(100), + contentPadding = ButtonDefaults.ContentPadding, + onClick = { + navigateToBrowse() + } + ) { + Text(text = stringResource(id = R.string.no)) + } + Spacer(modifier = Modifier.width(18.dp)) + Button( + modifier = Modifier.weight(3f), + shape = RoundedCornerShape(100), + onClick = { + navigateToHelp() + } + ) { + Text(text = stringResource(id = R.string.yes_go_to_help)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneLayout.kt new file mode 100644 index 00000000..c21f99bc --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneLayout.kt @@ -0,0 +1,59 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.foundation.layout.Arrangement +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R + +@Composable +fun StartDoneLayout( + paddingValues: PaddingValues +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(paddingValues) + .padding(48.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.start_done), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(190.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.start_done), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = R.string.start_done_desc), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneScaffold.kt new file mode 100644 index 00000000..1ac6d5ad --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartDoneScaffold.kt @@ -0,0 +1,29 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun StartDoneScaffold( + navigateToBrowse: () -> Unit, + navigateToHelp: () -> Unit +) { + Scaffold( + modifier = Modifier + .fillMaxSize(), + bottomBar = { + StartDoneBottomBar( + navigateToBrowse = navigateToBrowse, + navigateToHelp = navigateToHelp + ) + }, + containerColor = MaterialTheme.colorScheme.surface + ) { + StartDoneLayout( + paddingValues = it + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettings.kt new file mode 100644 index 00000000..ea6352a7 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettings.kt @@ -0,0 +1,71 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.runtime.Composable +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.navigator.StackEvent +import ua.acclorite.book_story.domain.ui.ButtonItem +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.start.StartEvent +import ua.acclorite.book_story.ui.start.StartScreen + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun StartSettings( + currentPage: Int, + stackEvent: StackEvent, + storagePermissionGranted: Boolean, + notificationsPermissionGranted: Boolean, + storagePermissionState: PermissionState, + notificationsPermissionState: PermissionState, + languages: List, + changeLanguage: (MainEvent.OnChangeLanguage) -> Unit, + storagePermissionRequest: (StartEvent.OnStoragePermissionRequest) -> Unit, + notificationsPermissionRequest: (StartEvent.OnNotificationsPermissionRequest) -> Unit, + navigateForward: () -> Unit +) { + val activity = LocalActivity.current + + StartSettingsScaffold( + currentPage = currentPage, + storagePermissionGranted = storagePermissionGranted, + navigateForward = navigateForward + ) { + StartContentTransition( + targetValue = when (currentPage) { + 0 -> StartScreen.GENERAL_SETTINGS + 1 -> StartScreen.APPEARANCE_SETTINGS + else -> StartScreen.PERMISSION_SETTINGS + }, + stackEvent = stackEvent + ) { page -> + StartSettingsLayout { + when (page) { + StartScreen.GENERAL_SETTINGS -> { + StartSettingsLayoutGeneral( + languages = languages, + changeLanguage = changeLanguage + ) + } + + StartScreen.APPEARANCE_SETTINGS -> { + StartSettingsLayoutAppearance() + } + + StartScreen.PERMISSION_SETTINGS -> { + StartSettingsLayoutPermissions( + activity = activity, + storagePermissionGranted = storagePermissionGranted, + notificationsPermissionGranted = notificationsPermissionGranted, + storagePermissionState = storagePermissionState, + notificationsPermissionState = notificationsPermissionState, + storagePermissionRequest = storagePermissionRequest, + notificationsPermissionRequest = notificationsPermissionRequest + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsBottomBar.kt new file mode 100644 index 00000000..b5ead69e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsBottomBar.kt @@ -0,0 +1,37 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R + +@Composable +fun StartSettingsBottomBar( + currentPage: Int, + storagePermissionGranted: Boolean, + navigateForward: () -> Unit +) { + Column { + Spacer(modifier = Modifier.height(18.dp)) + Button( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 8.dp) + .padding(horizontal = 18.dp) + .fillMaxWidth(), + onClick = { navigateForward() }, + enabled = storagePermissionGranted || currentPage != 2 + ) { + Text(text = stringResource(id = R.string.next)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayout.kt new file mode 100644 index 00000000..c565d028 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayout.kt @@ -0,0 +1,16 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun StartSettingsLayout( + content: LazyListScope.() -> Unit +) { + LazyColumnWithScrollbar(Modifier.fillMaxSize()) { + content() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartAppearanceScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutAppearance.kt similarity index 57% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartAppearanceScreen.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutAppearance.kt index 89146139..e79cfba9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartAppearanceScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutAppearance.kt @@ -1,15 +1,14 @@ -package ua.acclorite.book_story.presentation.screens.start.components +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.start import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ThemePreferencesSubcategory +import ua.acclorite.book_story.presentation.settings.appearance.theme_preferences.ThemePreferencesSubcategory -/** - * Appearance settings. - */ -fun LazyListScope.startAppearanceScreen() { +fun LazyListScope.StartSettingsLayoutAppearance() { ThemePreferencesSubcategory( title = { stringResource(id = R.string.start_theme_preferences) }, showDivider = false, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartLanguageScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutGeneral.kt similarity index 66% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartLanguageScreen.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutGeneral.kt index 4d780dc0..a8d00d89 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/StartLanguageScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutGeneral.kt @@ -1,4 +1,6 @@ -package ua.acclorite.book_story.presentation.screens.start.components +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.start import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -9,21 +11,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ButtonItem +import ua.acclorite.book_story.domain.ui.ButtonItem import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem -import ua.acclorite.book_story.presentation.data.MainEvent -import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryTitle +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle +import ua.acclorite.book_story.ui.main.MainEvent -/** - * Language settings. - */ -fun LazyListScope.startLanguageScreen( - onMainEvent: (MainEvent) -> Unit, - languages: List +fun LazyListScope.StartSettingsLayoutGeneral( + languages: List, + changeLanguage: (MainEvent.OnChangeLanguage) -> Unit ) { item { Spacer(modifier = Modifier.height(16.dp)) - SettingsCategoryTitle( + SettingsSubcategoryTitle( title = stringResource(id = R.string.start_language_preferences), color = MaterialTheme.colorScheme.primary ) @@ -36,7 +35,7 @@ fun LazyListScope.startLanguageScreen( title = it.title, horizontalPadding = 18.dp ) { - onMainEvent(MainEvent.OnChangeLanguage(it.id)) + changeLanguage(MainEvent.OnChangeLanguage(it.id)) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissions.kt new file mode 100644 index 00000000..ed0479b6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissions.kt @@ -0,0 +1,82 @@ +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.start + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle +import ua.acclorite.book_story.ui.start.StartEvent + +@OptIn(ExperimentalPermissionsApi::class) +fun LazyListScope.StartSettingsLayoutPermissions( + activity: ComponentActivity, + storagePermissionGranted: Boolean, + notificationsPermissionGranted: Boolean, + storagePermissionState: PermissionState, + notificationsPermissionState: PermissionState, + storagePermissionRequest: (StartEvent.OnStoragePermissionRequest) -> Unit, + notificationsPermissionRequest: (StartEvent.OnNotificationsPermissionRequest) -> Unit +) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + item { + SettingsSubcategoryTitle( + modifier = Modifier, + title = stringResource(id = R.string.start_permissions_preferences), + color = MaterialTheme.colorScheme.primary + ) + } + + item { + Spacer(modifier = Modifier.height(8.dp)) + } + + item { + StartSettingsLayoutPermissionsItem( + title = stringResource(id = R.string.start_permissions_storage), + description = stringResource(id = R.string.start_permissions_storage_desc), + isOptional = false, + isGranted = storagePermissionGranted, + onGrantClick = { + storagePermissionRequest( + StartEvent.OnStoragePermissionRequest( + activity = activity, + storagePermissionState = storagePermissionState + ) + ) + } + ) + } + + item { + StartSettingsLayoutPermissionsItem( + title = stringResource(id = R.string.start_permissions_notifications), + description = stringResource(id = R.string.start_permissions_notifications_desc), + isOptional = true, + isGranted = notificationsPermissionGranted, + onGrantClick = { + notificationsPermissionRequest( + StartEvent.OnNotificationsPermissionRequest( + activity = activity, + notificationsPermissionState = notificationsPermissionState + ) + ) + } + ) + } + + item { + Spacer(modifier = Modifier.height(8.dp)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissionsItem.kt similarity index 95% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionItem.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissionsItem.kt index b6a6a4a5..64c6c41d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/components/permissions/StartPermissionItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissionsItem.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.screens.start.components.permissions +package ua.acclorite.book_story.presentation.start import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -23,11 +23,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -/** - * Permission item. Can be optional. - */ @Composable -fun StartPermissionItem( +fun StartSettingsLayoutPermissionsItem( title: String, description: String, isOptional: Boolean, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsScaffold.kt new file mode 100644 index 00000000..8565aa91 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsScaffold.kt @@ -0,0 +1,79 @@ +package ua.acclorite.book_story.presentation.start + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Rocket +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R + +@Composable +fun StartSettingsScaffold( + currentPage: Int, + storagePermissionGranted: Boolean, + navigateForward: () -> Unit, + content: @Composable () -> Unit +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.surface, + bottomBar = { + StartSettingsBottomBar( + currentPage = currentPage, + storagePermissionGranted = storagePermissionGranted, + navigateForward = navigateForward + ) + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + .padding(horizontal = 18.dp) + ) { + Spacer(modifier = Modifier.height(24.dp)) + Icon( + imageVector = Icons.Default.Rocket, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(64.dp) + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringResource(id = R.string.start_welcome), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.start_welcome_desc), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(18.dp)) + + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + ) { + content() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/about/AboutEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutEvent.kt new file mode 100644 index 00000000..fabea12f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutEvent.kt @@ -0,0 +1,18 @@ +package ua.acclorite.book_story.ui.about + +import android.content.Context +import androidx.compose.runtime.Immutable + +@Immutable +sealed class AboutEvent { + data class OnNavigateToBrowserPage( + val page: String, + val context: Context + ) : AboutEvent() + + data class OnCheckForUpdate( + val context: Context + ) : AboutEvent() + + data object OnDismissDialog : AboutEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutModel.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt rename to app/src/main/java/ua/acclorite/book_story/ui/about/AboutModel.kt index 4edc7c4d..a3ae604c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutModel.kt @@ -1,38 +1,34 @@ -package ua.acclorite.book_story.presentation.screens.about.data +package ua.acclorite.book_story.ui.about import android.content.Intent import android.net.Uri import androidx.activity.ComponentActivity -import androidx.compose.runtime.Composable +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates -import ua.acclorite.book_story.domain.util.UIViewModel import ua.acclorite.book_story.presentation.core.util.launchActivity +import ua.acclorite.book_story.presentation.core.util.showToast import javax.inject.Inject +@OptIn(FlowPreview::class) @HiltViewModel -class AboutViewModel @Inject constructor( - private val checkForUpdates: CheckForUpdates, -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } +class AboutModel @Inject constructor( + private val checkForUpdates: CheckForUpdates +) : ViewModel() { private val _state = MutableStateFlow(AboutState()) - override val state = _state.asStateFlow() + val state = _state.asStateFlow() - override fun onEvent(event: AboutEvent) { + fun onEvent(event: AboutEvent) { when (event) { is AboutEvent.OnNavigateToBrowserPage -> { viewModelScope.launch { @@ -42,27 +38,37 @@ class AboutViewModel @Inject constructor( ) intent.launchActivity(event.context as ComponentActivity) { - event.noAppsFound() + withContext(Dispatchers.Main) { + event.context.getString(R.string.error_no_browser) + .showToast(context = event.context, longToast = false) + } } } } - is AboutEvent.OnCheckForUpdates -> { + is AboutEvent.OnCheckForUpdate -> { viewModelScope.launch { + if (_state.value.updateChecked) { + withContext(Dispatchers.Main) { + event.context.getString(R.string.no_updates) + .showToast(context = event.context) + } + return@launch + } + _state.update { - it.copy( - updateLoading = true - ) + it.copy(updateLoading = true) } val result = checkForUpdates.execute(false) if (result == null) { _state.update { - it.copy( - updateLoading = false - ) + it.copy(updateLoading = false) + } + withContext(Dispatchers.Main) { + event.context.getString(R.string.error_check_internet) + .showToast(context = event.context) } - event.error() return@launch } @@ -70,11 +76,14 @@ class AboutViewModel @Inject constructor( val currentVersion = event.context.getString(R.string.app_version) if (version == currentVersion) { - event.noUpdatesFound() + withContext(Dispatchers.Main) { + event.context.getString(R.string.no_updates) + .showToast(context = event.context) + } _state.update { it.copy( updateLoading = false, - alreadyCheckedForUpdates = true + updateChecked = true ) } return@launch @@ -82,7 +91,7 @@ class AboutViewModel @Inject constructor( _state.update { it.copy( - showUpdateDialog = true, + dialog = AboutScreen.UPDATE_DIALOG, updateLoading = false, updateInfo = result ) @@ -90,11 +99,9 @@ class AboutViewModel @Inject constructor( } } - is AboutEvent.OnDismissUpdateDialog -> { + is AboutEvent.OnDismissDialog -> { _state.update { - it.copy( - showUpdateDialog = false - ) + it.copy(dialog = null) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/about/AboutScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutScreen.kt new file mode 100644 index 00000000..994887f6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutScreen.kt @@ -0,0 +1,53 @@ +package ua.acclorite.book_story.ui.about + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.about.AboutContent +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.credits.CreditsScreen +import ua.acclorite.book_story.ui.licenses.LicensesScreen + +@Parcelize +object AboutScreen : Screen, Parcelable { + + @IgnoredOnParcel + const val UPDATE_DIALOG = "update_dialog" + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + + val state = screenModel.state.collectAsStateWithLifecycle() + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + AboutContent( + scrollBehavior = scrollBehavior, + dialog = state.value.dialog, + updateLoading = state.value.updateLoading, + updateInfo = state.value.updateInfo, + listState = listState, + checkForUpdate = screenModel::onEvent, + navigateToBrowserPage = screenModel::onEvent, + dismissDialog = screenModel::onEvent, + navigateToLicenses = { + navigator.push(LicensesScreen) + }, + navigateToCredits = { + navigator.push(CreditsScreen) + }, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutState.kt b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutState.kt similarity index 58% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutState.kt rename to app/src/main/java/ua/acclorite/book_story/ui/about/AboutState.kt index 4e52a750..3395c910 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/about/AboutState.kt @@ -1,12 +1,13 @@ -package ua.acclorite.book_story.presentation.screens.about.data +package ua.acclorite.book_story.ui.about import androidx.compose.runtime.Immutable import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo +import ua.acclorite.book_story.domain.util.Dialog @Immutable data class AboutState( - val showUpdateDialog: Boolean = false, - val alreadyCheckedForUpdates: Boolean = false, + val dialog: Dialog? = null, + val updateChecked: Boolean = false, val updateLoading: Boolean = false, val updateInfo: LatestReleaseInfo? = null -) +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEvent.kt new file mode 100644 index 00000000..bf1e19a0 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEvent.kt @@ -0,0 +1,123 @@ +package ua.acclorite.book_story.ui.book_info + +import android.content.Context +import android.net.Uri +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Immutable +import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.reader.Chapter + +@Immutable +sealed class BookInfoEvent { + + data class OnCopyToClipboard( + val text: String, + val context: Context, + ) : BookInfoEvent() + + data class OnEditTitleMode( + val edit: Boolean + ) : BookInfoEvent() + + data class OnEditTitleRequestFocus( + val focusRequester: FocusRequester + ) : BookInfoEvent() + + data class OnEditTitleValueChange( + val value: String + ) : BookInfoEvent() + + data class OnEditAuthorMode( + val edit: Boolean + ) : BookInfoEvent() + + data class OnEditAuthorRequestFocus( + val focusRequester: FocusRequester + ) : BookInfoEvent() + + data class OnEditAuthorValueChange( + val value: String + ) : BookInfoEvent() + + data class OnEditDescriptionMode( + val edit: Boolean + ) : BookInfoEvent() + + data class OnEditDescriptionRequestFocus( + val focusRequester: FocusRequester + ) : BookInfoEvent() + + data class OnEditDescriptionValueChange( + val value: String + ) : BookInfoEvent() + + data object OnUpdateData : BookInfoEvent() + + data class OnShowSnackbar( + val text: String, + val action: String?, + val onAction: () -> Unit = {}, + val durationMillis: Long, + val snackbarState: SnackbarHostState + ) : BookInfoEvent() + + data object OnShowChangeCoverBottomSheet : BookInfoEvent() + + data class OnChangeCover( + val uri: Uri, + val context: Context + ) : BookInfoEvent() + + data class OnResetCover( + val context: Context + ) : BookInfoEvent() + + data class OnDeleteCover( + val context: Context + ) : BookInfoEvent() + + data object OnCheckCoverReset : BookInfoEvent() + + data object OnShowMoreBottomSheet : BookInfoEvent() + + data object OnShowDetailsBottomSheet : BookInfoEvent() + + data object OnDismissBottomSheet : BookInfoEvent() + + data object OnShowDeleteDialog : BookInfoEvent() + + data class OnActionDeleteDialog( + val context: Context, + val navigateBack: () -> Unit + ) : BookInfoEvent() + + data object OnShowMoveDialog : BookInfoEvent() + + data class OnActionMoveDialog( + val category: Category, + val context: Context, + val navigateToLibrary: () -> Unit + ) : BookInfoEvent() + + data object OnDismissDialog : BookInfoEvent() + + data class OnCheckForTextUpdate( + val snackbarState: SnackbarHostState, + val context: Context + ) : BookInfoEvent() + + data class OnShowUpdateDialog( + val updatedText: List, + val updatedChapters: List + ) : BookInfoEvent() + + data class OnActionUpdateDialog( + val snackbarState: SnackbarHostState, + val context: Context + ) : BookInfoEvent() + + data object OnDismissUpdateDialog : BookInfoEvent() + + data object OnCancelTextUpdate : BookInfoEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoModel.kt similarity index 75% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt rename to app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoModel.kt index 5f1dfa79..d24d9fbb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoModel.kt @@ -1,17 +1,18 @@ -@file:Suppress("LABEL_NAME_CLASH") - -package ua.acclorite.book_story.presentation.screens.book_info.data +package ua.acclorite.book_story.ui.book_info import android.content.ClipData import android.content.ClipboardManager +import android.content.Context import android.content.Context.CLIPBOARD_SERVICE import android.graphics.BitmapFactory import android.os.Build +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult -import androidx.compose.runtime.Composable +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay @@ -19,11 +20,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.BookWithText -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.library.book.BookWithText +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.use_case.book.CanResetCover import ua.acclorite.book_story.domain.use_case.book.CheckForTextUpdate import ua.acclorite.book_story.domain.use_case.book.DeleteBooks @@ -32,153 +34,55 @@ import ua.acclorite.book_story.domain.use_case.book.ResetCoverImage import ua.acclorite.book_story.domain.use_case.book.UpdateBook import ua.acclorite.book_story.domain.use_case.book.UpdateBookWithText import ua.acclorite.book_story.domain.use_case.book.UpdateCoverImageOfBook -import ua.acclorite.book_story.domain.use_case.history.InsertHistory import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText -import ua.acclorite.book_story.domain.util.UIViewModel -import ua.acclorite.book_story.presentation.core.navigation.Screen -import java.util.Date +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.browse.BrowseScreen +import ua.acclorite.book_story.ui.history.HistoryScreen +import ua.acclorite.book_story.ui.library.LibraryScreen import javax.inject.Inject import kotlin.math.roundToInt - +@OptIn(FlowPreview::class) @HiltViewModel -class BookInfoViewModel @Inject constructor( - private val updateBook: UpdateBook, - private val updateBookWithText: UpdateBookWithText, - private val updateCoverImageOfBook: UpdateCoverImageOfBook, - private val insertHistory: InsertHistory, - private val deleteBooks: DeleteBooks, +class BookInfoModel @Inject constructor( private val getBookById: GetBookById, - private val canResetCover: CanResetCover, - private val resetCoverImage: ResetCoverImage, private val checkForTextUpdate: CheckForTextUpdate, -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } + private val updateBook: UpdateBook, + private val canResetCover: CanResetCover, + private val updateCoverImageOfBook: UpdateCoverImageOfBook, + private val resetCoverImage: ResetCoverImage, + private val deleteBooks: DeleteBooks, + private val updateBookWithText: UpdateBookWithText +) : ViewModel() { private val _state = MutableStateFlow(BookInfoState()) - override val state = _state.asStateFlow() + val state = _state.asStateFlow() private var eventJob = SupervisorJob() - private var snackBarJob: Job? = null private var updateJob: Job? = null + private var snackBarJob: Job? = null - override fun onEvent(event: BookInfoEvent) { + fun onEvent(event: BookInfoEvent) { viewModelScope.launch(eventJob + Dispatchers.Main) { when (event) { - is BookInfoEvent.OnInit -> init(event) + is BookInfoEvent.OnCopyToClipboard -> { + val clipboardManager = + event.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - is BookInfoEvent.OnClearViewModel -> clearViewModel() + clipboardManager.setPrimaryClip(ClipData.newPlainText(null, event.text)) - is BookInfoEvent.OnShowHideChangeCoverBottomSheet -> { - _state.update { - it.copy( - showChangeCoverBottomSheet = !it.showChangeCoverBottomSheet - ) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + withContext(Dispatchers.Main) { + event.context.getString(R.string.copied) + .showToast(context = event.context, longToast = false) + } } } - is BookInfoEvent.OnChangeCover -> { - launch { - val image = event.context.contentResolver?.openInputStream(event.uri)?.use { - BitmapFactory.decodeStream(it) - } ?: return@launch - - updateCoverImageOfBook.execute( - _state.value.book, - image - ) - - val newCoverImage = - getBookById.execute(_state.value.book.id)?.coverImage ?: return@launch - - _state.update { - it.copy( - book = it.book.copy( - coverImage = newCoverImage - ), - showChangeCoverBottomSheet = false, - canResetCover = canResetCover.execute(bookId = it.book.id) - ) - } - event.refreshList(_state.value.book) - } - } - - is BookInfoEvent.OnDeleteCover -> { - launch { - if (_state.value.book.coverImage == null) { - return@launch - } - - updateCoverImageOfBook.execute( - bookWithOldCover = _state.value.book, - newCoverImage = null - ) - _state.update { - it.copy( - book = it.book.copy( - coverImage = null - ), - showChangeCoverBottomSheet = false, - canResetCover = canResetCover.execute(bookId = it.book.id) - ) - } - event.refreshList(_state.value.book) - } - } - - is BookInfoEvent.OnCheckCoverReset -> { + is BookInfoEvent.OnEditTitleMode -> { launch(Dispatchers.IO) { - _state.update { - it.copy( - canResetCover = canResetCover.execute(_state.value.book.id) - ) - } - } - } - - is BookInfoEvent.OnResetCoverImage -> { - launch { - val result = resetCoverImage.execute(_state.value.book.id) - - if (!result) { - event.showResult(UIText.StringResource(R.string.error_could_not_reset_cover)) - return@launch - } - - val book = getBookById.execute(_state.value.book.id) - - if (book == null) { - event.showResult(UIText.StringResource(R.string.error_something_went_wrong)) - return@launch - } - - _state.update { - it.copy( - book = book, - showChangeCoverBottomSheet = false, - canResetCover = false - ) - } - event.showResult(UIText.StringResource(R.string.cover_reset)) - event.refreshList(_state.value.book) - } - } - - is BookInfoEvent.OnShowHideEditTitle -> { - launch(Dispatchers.IO) { - val shouldHide = _state.value.editTitle - - if (!shouldHide) { + if (event.edit) { _state.update { it.copy( titleValue = it.book.title, @@ -189,13 +93,13 @@ class BookInfoViewModel @Inject constructor( _state.update { it.copy( - editTitle = !shouldHide + editTitle = event.edit ) } } } - is BookInfoEvent.OnTitleRequestFocus -> { + is BookInfoEvent.OnEditTitleRequestFocus -> { if (!_state.value.hasTitleFocused) { event.focusRequester.requestFocus() _state.update { @@ -206,7 +110,7 @@ class BookInfoViewModel @Inject constructor( } } - is BookInfoEvent.OnTitleValueChange -> { + is BookInfoEvent.OnEditTitleValueChange -> { _state.update { it.copy( titleValue = event.value @@ -214,11 +118,9 @@ class BookInfoViewModel @Inject constructor( } } - is BookInfoEvent.OnShowHideEditAuthor -> { + is BookInfoEvent.OnEditAuthorMode -> { launch(Dispatchers.IO) { - val shouldHide = _state.value.editAuthor - - if (!shouldHide) { + if (event.edit) { _state.update { it.copy( authorValue = it.book.author.getAsString() ?: "", @@ -229,13 +131,13 @@ class BookInfoViewModel @Inject constructor( _state.update { it.copy( - editAuthor = !shouldHide + editAuthor = event.edit ) } } } - is BookInfoEvent.OnAuthorRequestFocus -> { + is BookInfoEvent.OnEditAuthorRequestFocus -> { if (!_state.value.hasAuthorFocused) { event.focusRequester.requestFocus() _state.update { @@ -246,7 +148,7 @@ class BookInfoViewModel @Inject constructor( } } - is BookInfoEvent.OnAuthorValueChange -> { + is BookInfoEvent.OnEditAuthorValueChange -> { _state.update { it.copy( authorValue = event.value @@ -254,11 +156,9 @@ class BookInfoViewModel @Inject constructor( } } - is BookInfoEvent.OnShowHideEditDescription -> { + is BookInfoEvent.OnEditDescriptionMode -> { launch(Dispatchers.IO) { - val shouldHide = _state.value.editDescription - - if (!shouldHide) { + if (event.edit) { _state.update { it.copy( descriptionValue = it.book.description ?: "", @@ -269,13 +169,13 @@ class BookInfoViewModel @Inject constructor( _state.update { it.copy( - editDescription = !shouldHide + editDescription = event.edit ) } } } - is BookInfoEvent.OnDescriptionRequestFocus -> { + is BookInfoEvent.OnEditDescriptionRequestFocus -> { if (!_state.value.hasDescriptionFocused) { event.focusRequester.requestFocus() _state.update { @@ -286,7 +186,7 @@ class BookInfoViewModel @Inject constructor( } } - is BookInfoEvent.OnDescriptionValueChange -> { + is BookInfoEvent.OnEditDescriptionValueChange -> { _state.update { it.copy( descriptionValue = event.value @@ -308,7 +208,6 @@ class BookInfoViewModel @Inject constructor( && author.isNotBlank() val descriptionChanged = description != _state.value.book.description && _state.value.editDescription - && description.isNotBlank() val book = _state.value.book.copy( title = if (titleChanged) title else _state.value.book.title, @@ -324,114 +223,24 @@ class BookInfoViewModel @Inject constructor( book = book ) } - event.refreshList(_state.value.book) - if (state.value.editTitle) { - onEvent(BookInfoEvent.OnShowHideEditTitle) + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) + + if (_state.value.editTitle) { + onEvent(BookInfoEvent.OnEditTitleMode(false)) } - if (state.value.editAuthor) { - onEvent(BookInfoEvent.OnShowHideEditAuthor) + if (_state.value.editAuthor) { + onEvent(BookInfoEvent.OnEditAuthorMode(false)) } - if (state.value.editDescription) { - onEvent(BookInfoEvent.OnShowHideEditDescription) + if (_state.value.editDescription) { + onEvent(BookInfoEvent.OnEditDescriptionMode(false)) } } } - is BookInfoEvent.OnShowHideDeleteDialog -> { - _state.update { - it.copy( - showDeleteDialog = !it.showDeleteDialog - ) - } - } - - is BookInfoEvent.OnDeleteBook -> { - launch { - _state.update { - it.copy( - showDeleteDialog = false - ) - } - - deleteBooks.execute(listOf(_state.value.book)) - event.refreshList() - - event.onNavigate { - navigateBack() - } - } - } - - is BookInfoEvent.OnShowHideMoveDialog -> { - _state.update { - it.copy( - showMoveDialog = !it.showMoveDialog - ) - } - } - - is BookInfoEvent.OnSelectCategory -> { - _state.update { - it.copy( - selectedCategory = event.category - ) - } - } - - is BookInfoEvent.OnMoveBook -> { - launch { - _state.update { - it.copy( - showMoveDialog = false - ) - } - - _state.update { - it.copy( - book = it.book.copy( - category = it.selectedCategory - ) - ) - } - updateBook.execute(_state.value.book) - - event.refreshList(_state.value.book) - event.updatePage( - Category.entries.dropLastWhile { - it != _state.value.selectedCategory - }.size - 1 - ) - event.onNavigate { - navigate( - Screen.Library, - useBackAnimation = true - ) - } - } - } - - is BookInfoEvent.OnShowHideDetailsBottomSheet -> { - _state.update { - it.copy( - showDetailsBottomSheet = !it.showDetailsBottomSheet - ) - } - } - - is BookInfoEvent.OnCopyToClipboard -> { - val clipboardManager = - event.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - - clipboardManager.setPrimaryClip(ClipData.newPlainText(null, event.text)) - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - event.success() - } - } - is BookInfoEvent.OnShowSnackbar -> { launch { snackBarJob?.cancel() @@ -460,12 +269,244 @@ class BookInfoViewModel @Inject constructor( } } + is BookInfoEvent.OnShowChangeCoverBottomSheet -> { + _state.update { + it.copy( + bottomSheet = BookInfoScreen.CHANGE_COVER_BOTTOM_SHEET + ) + } + } + + is BookInfoEvent.OnChangeCover -> { + launch { + val image = event.context.contentResolver?.openInputStream(event.uri)?.use { + BitmapFactory.decodeStream(it) + } ?: return@launch + + updateCoverImageOfBook.execute( + _state.value.book, + image + ) + + val newCoverImage = getBookById.execute( + _state.value.book.id + )?.coverImage ?: return@launch + + _state.update { + it.copy( + book = it.book.copy( + coverImage = newCoverImage + ), + bottomSheet = null, + canResetCover = canResetCover.execute(bookId = it.book.id) + ) + } + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) + + withContext(Dispatchers.Main) { + event.context.getString(R.string.cover_image_changed) + .showToast(context = event.context) + } + } + } + + is BookInfoEvent.OnResetCover -> { + launch { + val result = resetCoverImage.execute(_state.value.book.id) + + if (!result) { + withContext(Dispatchers.Main) { + event.context.getString(R.string.error_could_not_reset_cover) + .showToast(context = event.context) + } + return@launch + } + + val book = getBookById.execute(_state.value.book.id) + + if (book == null) { + withContext(Dispatchers.Main) { + event.context.getString(R.string.error_something_went_wrong) + .showToast(context = event.context) + } + return@launch + } + + _state.update { + it.copy( + book = book, + bottomSheet = null, + canResetCover = false + ) + } + + withContext(Dispatchers.Main) { + event.context.getString(R.string.cover_reset) + .showToast(context = event.context) + } + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) + } + } + + is BookInfoEvent.OnDeleteCover -> { + launch { + if (_state.value.book.coverImage == null) { + return@launch + } + + updateCoverImageOfBook.execute( + bookWithOldCover = _state.value.book, + newCoverImage = null + ) + _state.update { + it.copy( + book = it.book.copy( + coverImage = null + ), + bottomSheet = null, + canResetCover = canResetCover.execute(bookId = it.book.id) + ) + } + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) + + withContext(Dispatchers.Main) { + event.context.getString(R.string.cover_image_deleted) + .showToast(context = event.context) + } + } + } + + is BookInfoEvent.OnCheckCoverReset -> { + launch(Dispatchers.IO) { + _state.update { + it.copy( + canResetCover = canResetCover.execute( + _state.value.book.id + ) + ) + } + } + } + + is BookInfoEvent.OnShowMoreBottomSheet -> { + _state.update { + it.copy( + bottomSheet = BookInfoScreen.MORE_BOTTOM_SHEET + ) + } + } + + is BookInfoEvent.OnShowDetailsBottomSheet -> { + _state.update { + it.copy( + bottomSheet = BookInfoScreen.DETAILS_BOTTOM_SHEET + ) + } + } + + is BookInfoEvent.OnDismissBottomSheet -> { + _state.update { + it.copy( + bottomSheet = null + ) + } + } + + is BookInfoEvent.OnShowDeleteDialog -> { + _state.update { + it.copy( + dialog = BookInfoScreen.DELETE_DIALOG + ) + } + } + + is BookInfoEvent.OnActionDeleteDialog -> { + launch { + _state.update { + it.copy( + dialog = null, + bottomSheet = null + ) + } + + deleteBooks.execute(listOf(_state.value.book)) + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) + BrowseScreen.refreshListChannel.trySend(Unit) + + withContext(Dispatchers.Main) { + event.context.getString(R.string.book_deleted) + .showToast(context = event.context) + } + + event.navigateBack() + } + } + + is BookInfoEvent.OnShowMoveDialog -> { + _state.update { + it.copy( + dialog = BookInfoScreen.MOVE_DIALOG + ) + } + } + + is BookInfoEvent.OnActionMoveDialog -> { + launch { + _state.update { + it.copy( + dialog = null, + bottomSheet = null + ) + } + + _state.update { + it.copy( + book = it.book.copy( + category = event.category + ) + ) + } + updateBook.execute(_state.value.book) + + LibraryScreen.refreshListChannel.trySend(Unit) + LibraryScreen.scrollToPageCompositionChannel.trySend( + Category.entries.dropLastWhile { + it != event.category + }.size - 1 + ) + HistoryScreen.refreshListChannel.trySend(Unit) + + withContext(Dispatchers.Main) { + event.context.getString(R.string.book_moved) + .showToast(context = event.context) + } + + event.navigateToLibrary() + } + } + + is BookInfoEvent.OnDismissDialog -> { + _state.update { + it.copy( + dialog = null + ) + } + } + is BookInfoEvent.OnCheckForTextUpdate -> { updateJob?.cancel() updateJob = launch(Dispatchers.IO) { _state.update { it.copy( - showConfirmTextUpdateDialog = false, + dialog = null, checkingForUpdate = true, editTitle = false, editAuthor = false, @@ -496,7 +537,7 @@ class BookInfoViewModel @Inject constructor( return@launch } else { onEvent( - BookInfoEvent.OnShowConfirmTextUpdateDialog( + BookInfoEvent.OnShowUpdateDialog( updatedText = result.data.first, updatedChapters = result.data.second ) @@ -541,32 +582,22 @@ class BookInfoViewModel @Inject constructor( } } - is BookInfoEvent.OnDismissConfirmTextUpdateDialog -> { + is BookInfoEvent.OnShowUpdateDialog -> { _state.update { it.copy( - showConfirmTextUpdateDialog = false, - updatedText = null, - updatedChapters = null - ) - } - } - - is BookInfoEvent.OnShowConfirmTextUpdateDialog -> { - _state.update { - it.copy( - showConfirmTextUpdateDialog = true, + dialog = BookInfoScreen.UPDATE_DIALOG, updatedText = event.updatedText, updatedChapters = event.updatedChapters ) } } - is BookInfoEvent.OnConfirmTextUpdate -> { + is BookInfoEvent.OnActionUpdateDialog -> { launch(Dispatchers.IO) { _state.update { it.copy( - updating = true, - showConfirmTextUpdateDialog = false + isUpdating = true, + dialog = null ) } @@ -593,7 +624,7 @@ class BookInfoViewModel @Inject constructor( delay(500) _state.update { it.copy( - updating = false, + isUpdating = false, updatedText = null, updatedChapters = null ) @@ -638,7 +669,7 @@ class BookInfoViewModel @Inject constructor( delay(500) _state.update { it.copy( - updating = false, + isUpdating = false, updatedText = null, updatedChapters = null ) @@ -670,7 +701,7 @@ class BookInfoViewModel @Inject constructor( delay(500) _state.update { it.copy( - updating = false, + isUpdating = false, updatedText = null, updatedChapters = null ) @@ -685,7 +716,9 @@ class BookInfoViewModel @Inject constructor( updatedChapters = null ) } - event.refreshList(_state.value.book) + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) onEvent( BookInfoEvent.OnShowSnackbar( @@ -699,65 +732,51 @@ class BookInfoViewModel @Inject constructor( delay(500) _state.update { it.copy( - updating = false + isUpdating = false ) } } } } + is BookInfoEvent.OnDismissUpdateDialog -> { + _state.update { + it.copy( + dialog = null, + updatedText = null, + updatedChapters = null + ) + } + } + is BookInfoEvent.OnCancelTextUpdate -> { _state.update { updateJob?.cancel() it.copy( - showConfirmTextUpdateDialog = false, + dialog = null, checkingForUpdate = false, updatedText = null, updatedChapters = null ) } } - - is BookInfoEvent.OnNavigateToReaderScreen -> { - launch { - onEvent(BookInfoEvent.OnCancelTextUpdate) - _state.value.book.id.let { - insertHistory.execute( - History( - bookId = it, - book = null, - time = Date().time - ) - ) - } - event.onNavigate { - navigate( - Screen.Reader(_state.value.book.id) - ) - } - } - } - - is BookInfoEvent.OnShowHideMoreBottomSheet -> { - _state.update { - it.copy( - showMoreBottomSheet = event.show - ) - } - } } } } - private fun init(event: BookInfoEvent.OnInit) { + fun init( + bookId: Int, + startUpdate: Boolean, + snackbarState: SnackbarHostState, + context: Context, + navigateBack: () -> Unit + ) { viewModelScope.launch(Dispatchers.IO) { - val book = getBookById.execute(event.screen.bookId) + val book = getBookById.execute(bookId) if (book == null) { - event.onNavigate { - navigateBack() - } + navigateBack() return@launch } @@ -766,29 +785,24 @@ class BookInfoViewModel @Inject constructor( book = book ) } - clear() - if (event.screen.startUpdate) { + eventJob.cancel() + eventJob.join() + eventJob = SupervisorJob() + + if (startUpdate) { onEvent( BookInfoEvent.OnCheckForTextUpdate( - snackbarState = event.snackbarState, - context = event.context + snackbarState = snackbarState, + context = context ) ) - event.onNavigate { - putScreen( - Screen.BookInfo( - event.screen.bookId, - false - ) - ) - } } onEvent(BookInfoEvent.OnCheckCoverReset) } } - private fun clearViewModel() { + fun resetScreen() { viewModelScope.launch(Dispatchers.Main) { _state.update { BookInfoState() @@ -799,10 +813,4 @@ class BookInfoViewModel @Inject constructor( eventJob = SupervisorJob() } } - - private suspend fun clear() { - eventJob.cancel() - eventJob.join() - eventJob = SupervisorJob() - } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScreen.kt new file mode 100644 index 00000000..058b91f0 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScreen.kt @@ -0,0 +1,143 @@ +package ua.acclorite.book_story.ui.book_info + +import android.os.Parcelable +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.getOrElse +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.book_info.BookInfoContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.history.HistoryScreen +import ua.acclorite.book_story.ui.library.LibraryScreen +import ua.acclorite.book_story.ui.reader.ReaderScreen + +@Parcelize +data class BookInfoScreen(val bookId: Int) : Screen, Parcelable { + + companion object { + const val UPDATE_DIALOG = "update_dialog" + const val DELETE_DIALOG = "delete_dialog" + const val MOVE_DIALOG = "move_dialog" + + const val CHANGE_COVER_BOTTOM_SHEET = "change_cover_bottom_sheet" + const val MORE_BOTTOM_SHEET = "more_bottom_sheet" + const val DETAILS_BOTTOM_SHEET = "details_bottom_sheet" + + val startUpdateChannel: Channel = Channel(Channel.CONFLATED) + } + + @OptIn(ExperimentalMaterialApi::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + + val state = screenModel.state.collectAsStateWithLifecycle() + + val context = LocalContext.current + val snackbarState = remember { SnackbarHostState() } + val listState = rememberLazyListState() + val refreshState = rememberPullRefreshState( + refreshing = state.value.isUpdating || state.value.checkingForUpdate, + onRefresh = { + screenModel.onEvent( + BookInfoEvent.OnCheckForTextUpdate( + snackbarState = snackbarState, + context = context + ) + ) + } + ) + + LaunchedEffect(Unit) { + screenModel.init( + bookId = bookId, + startUpdate = startUpdateChannel.tryReceive().getOrElse { false }, + snackbarState = snackbarState, + context = context, + navigateBack = { + navigator.pop() + } + ) + } + + DisposableEffect(Unit) { + onDispose { + screenModel.resetScreen() + } + } + + BookInfoContent( + book = state.value.book, + bottomSheet = state.value.bottomSheet, + dialog = state.value.dialog, + refreshState = refreshState, + listState = listState, + snackbarState = snackbarState, + canResetCover = state.value.canResetCover, + editTitle = state.value.editTitle, + titleValue = state.value.titleValue, + editAuthor = state.value.editAuthor, + authorValue = state.value.authorValue, + editDescription = state.value.editDescription, + descriptionValue = state.value.descriptionValue, + isUpdating = state.value.isUpdating, + checkingForUpdate = state.value.checkingForUpdate, + editTitleMode = screenModel::onEvent, + editTitleValueChange = screenModel::onEvent, + editTitleRequestFocus = screenModel::onEvent, + editAuthorMode = screenModel::onEvent, + editAuthorValueChange = screenModel::onEvent, + editAuthorRequestFocus = screenModel::onEvent, + editDescriptionMode = screenModel::onEvent, + editDescriptionValueChange = screenModel::onEvent, + editDescriptionRequestFocus = screenModel::onEvent, + checkCoverReset = screenModel::onEvent, + changeCover = screenModel::onEvent, + resetCover = screenModel::onEvent, + deleteCover = screenModel::onEvent, + dismissBottomSheet = screenModel::onEvent, + dismissDialog = screenModel::onEvent, + showChangeCoverBottomSheet = screenModel::onEvent, + showDetailsBottomSheet = screenModel::onEvent, + showMoreBottomSheet = screenModel::onEvent, + showMoveDialog = screenModel::onEvent, + actionMoveDialog = screenModel::onEvent, + showDeleteDialog = screenModel::onEvent, + actionDeleteDialog = screenModel::onEvent, + actionUpdateDialog = screenModel::onEvent, + dismissUpdateDialog = screenModel::onEvent, + updateData = screenModel::onEvent, + copyToClipboard = screenModel::onEvent, + checkForTextUpdate = screenModel::onEvent, + cancelTextUpdate = screenModel::onEvent, + navigateToReader = { + if (state.value.book.id != -1) { + HistoryScreen.insertHistoryChannel.trySend(state.value.book.id) + navigator.push(ReaderScreen(state.value.book.id)) + } + }, + navigateToLibrary = { + navigator.push( + LibraryScreen, + popping = true, + saveInBackStack = false + ) + }, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoState.kt similarity index 57% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt rename to app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoState.kt index 96819e52..f74b096d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoState.kt @@ -1,9 +1,10 @@ -package ua.acclorite.book_story.presentation.screens.book_info.data +package ua.acclorite.book_story.ui.book_info import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.domain.util.Dialog import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideEmptyBook @@ -12,9 +13,7 @@ data class BookInfoState( val book: Book = Constants.provideEmptyBook(), val checkingForUpdate: Boolean = false, - val updating: Boolean = false, - - val showConfirmTextUpdateDialog: Boolean = false, + val isUpdating: Boolean = false, val updatedText: List? = null, val updatedChapters: List? = null, @@ -30,14 +29,8 @@ data class BookInfoState( val hasDescriptionFocused: Boolean = false, val descriptionValue: String = "", - val showChangeCoverBottomSheet: Boolean = false, val canResetCover: Boolean = false, - val showDetailsBottomSheet: Boolean = false, - - val showDeleteDialog: Boolean = false, - val showMoveDialog: Boolean = false, - val selectedCategory: Category = Category.READING, - - val showMoreBottomSheet: Boolean = false, -) + val dialog: Dialog? = null, + val bottomSheet: BottomSheet? = null +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseEvent.kt new file mode 100644 index 00000000..9db1b40d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseEvent.kt @@ -0,0 +1,90 @@ +@file:OptIn(ExperimentalPermissionsApi::class) + +package ua.acclorite.book_story.ui.browse + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Immutable +import androidx.compose.ui.focus.FocusRequester +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.domain.library.book.SelectableNullableBook +import java.io.File + +@Immutable +sealed class BrowseEvent { + data class OnRefreshList( + val showIndicator: Boolean, + val hideSearch: Boolean + ) : BrowseEvent() + + data class OnSearchVisibility( + val show: Boolean + ) : BrowseEvent() + + data class OnRequestFocus( + val focusRequester: FocusRequester + ) : BrowseEvent() + + data class OnSearchQueryChange( + val query: String + ) : BrowseEvent() + + data object OnSearch : BrowseEvent() + + data object OnClearSelectedFiles : BrowseEvent() + + data class OnChangeDirectory( + val directory: File, + val savePreviousDirectory: Boolean + ) : BrowseEvent() + + data object OnGoBackDirectory : BrowseEvent() + + data class OnSelectFiles( + val includedFileFormats: List, + val files: List + ) : BrowseEvent() + + data class OnSelectFile( + val includedFileFormats: List, + val file: SelectableFile + ) : BrowseEvent() + + data class OnUpdateFavoriteDirectory( + val path: String + ) : BrowseEvent() + + data object OnShowFilterBottomSheet : BrowseEvent() + + data object OnDismissBottomSheet : BrowseEvent() + + data class OnPermissionCheck( + val storagePermissionState: PermissionState + ) : BrowseEvent() + + data class OnActionPermissionDialog( + val activity: ComponentActivity, + val storagePermissionState: PermissionState + ) : BrowseEvent() + + data class OnDismissPermissionDialog( + val storagePermissionState: PermissionState + ) : BrowseEvent() + + data object OnShowAddDialog : BrowseEvent() + + data object OnDismissAddDialog : BrowseEvent() + + data class OnSelectAddDialog( + val book: SelectableNullableBook + ) : BrowseEvent() + + data class OnActionAddDialog( + val context: Context, + val navigateToLibrary: () -> Unit + ) : BrowseEvent() + + data object OnDismissDialog : BrowseEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt new file mode 100644 index 00000000..c99d6b49 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt @@ -0,0 +1,716 @@ +package ua.acclorite.book_story.ui.browse + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.shouldShowRationale +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.browse.BrowseSortOrder +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.domain.library.book.NullableBook +import ua.acclorite.book_story.domain.library.book.SelectableNullableBook +import ua.acclorite.book_story.domain.use_case.book.InsertBook +import ua.acclorite.book_story.domain.use_case.favorite_directory.UpdateFavoriteDirectory +import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFile +import ua.acclorite.book_story.domain.use_case.file_system.GetFilesFromDevice +import ua.acclorite.book_story.presentation.core.util.launchActivity +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.library.LibraryScreen +import java.io.File +import javax.inject.Inject +import kotlin.collections.map + +@OptIn(FlowPreview::class) +@HiltViewModel +class BrowseModel @Inject constructor( + private val getFilesFromDevice: GetFilesFromDevice, + private val updateFavoriteDirectory: UpdateFavoriteDirectory, + private val getBookFromFile: GetBookFromFile, + private val insertBook: InsertBook +) : ViewModel() { + + private val _state = MutableStateFlow(BrowseState()) + val state = _state.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) { + getFilesFromDownloads() + } + + /* Observe channel - - - - - - - - - - - */ + viewModelScope.launch(Dispatchers.IO) { + BrowseScreen.refreshListChannel.receiveAsFlow().debounce(200).collectLatest { + onEvent(BrowseEvent.OnRefreshList(showIndicator = false, hideSearch = false)) + } + } + /* - - - - - - - - - - - - - - - - - - - */ + } + + private var refreshJob: Job? = null + private var changeSearchQueryJob: Job? = null + private var changeDirectoryJob: Job? = null + private var storagePermissionJob: Job? = null + private var getAddDialogBooksJob: Job? = null + + @OptIn(ExperimentalPermissionsApi::class) + fun onEvent(event: BrowseEvent) { + when (event) { + is BrowseEvent.OnRefreshList -> { + refreshJob?.cancel() + refreshJob = viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isRefreshing = event.showIndicator, + isLoading = !event.showIndicator, + showSearch = if (event.hideSearch) false else it.showSearch + ) + } + + yield() + getFilesFromDownloads() + + if (event.showIndicator) delay(500) + _state.update { + it.copy( + isRefreshing = false, + isLoading = false + ) + } + } + } + + is BrowseEvent.OnSearchVisibility -> { + viewModelScope.launch(Dispatchers.IO) { + if (!event.show) { + getFilesFromDownloads("") + } else { + _state.update { + it.copy( + searchQuery = "", + hasFocused = false + ) + } + } + + _state.update { + it.copy( + showSearch = event.show + ) + } + } + } + + is BrowseEvent.OnRequestFocus -> { + if (!_state.value.hasFocused) { + event.focusRequester.requestFocus() + _state.update { + it.copy( + hasFocused = true + ) + } + } + } + + is BrowseEvent.OnSearchQueryChange -> { + _state.update { + it.copy( + searchQuery = event.query + ) + } + changeSearchQueryJob?.cancel() + changeSearchQueryJob = viewModelScope.launch(Dispatchers.IO) { + delay(500) + yield() + onEvent(BrowseEvent.OnSearch) + } + } + + is BrowseEvent.OnSearch -> { + viewModelScope.launch(Dispatchers.IO) { + getFilesFromDownloads() + } + } + + is BrowseEvent.OnClearSelectedFiles -> { + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + files = it.files.map { it.copy(isSelected = false) }, + hasSelectedItems = false + ) + } + } + } + + is BrowseEvent.OnChangeDirectory -> { + viewModelScope.launch { + changeDirectoryJob?.cancel() + changeSearchQueryJob?.cancel() + + changeDirectoryJob = launch(Dispatchers.IO) { + yield() + _state.update { + it.copy( + selectedDirectory = event.directory, + previousDirectory = if (event.savePreviousDirectory) it.selectedDirectory + else event.directory.parentFile, + inNestedDirectory = event.directory != Environment.getExternalStorageDirectory() + ) + } + BrowseScreen.resetScrollPositionCompositionChannel.trySend(Unit) + } + } + } + + is BrowseEvent.OnGoBackDirectory -> { + onEvent( + BrowseEvent.OnChangeDirectory( + _state.value.previousDirectory ?: Environment.getExternalStorageDirectory(), + savePreviousDirectory = false + ) + ) + } + + is BrowseEvent.OnSelectFiles -> { + viewModelScope.launch(Dispatchers.IO) { + val editedList = _state.value.files.map { file -> + if ( + event.files.any { + file.fileOrDirectory.path.startsWith(it.fileOrDirectory.path) + } && event.includedFileFormats.run { + if (isEmpty()) return@run true + any { + file.fileOrDirectory.path.endsWith( + it, ignoreCase = true + ) || file.isDirectory + } + } + ) { + file.copy(isSelected = true) + } else { + file + } + } + + _state.update { + it.copy( + files = editedList, + selectedItemsCount = editedList.filter { file -> + file.isSelected && !file.isDirectory + }.size.run { + if (this == 0) return@run it.selectedItemsCount + this + }, + hasSelectedItems = editedList.any { file -> + file.isSelected && !file.isDirectory + } + ) + } + } + } + + is BrowseEvent.OnSelectFile -> { + viewModelScope.launch(Dispatchers.IO) { + val editedList = _state.value.files.map { file -> + when (event.file.isDirectory) { + false -> { + if (event.file.fileOrDirectory.path == file.fileOrDirectory.path) { + file.copy( + isSelected = !file.isSelected + ) + } else { + file + } + } + + true -> { + if ( + file.fileOrDirectory.path.startsWith( + event.file.fileOrDirectory.path + ) && event.includedFileFormats.run { + if (isEmpty()) return@run true + any { + file.fileOrDirectory.path.endsWith( + it, ignoreCase = true + ) || file.isDirectory + } + } + ) { + file.copy( + isSelected = !event.file.isSelected + ) + } else { + file + } + } + } + } + + _state.update { + it.copy( + files = editedList, + selectedItemsCount = editedList.filter { file -> + file.isSelected && !file.isDirectory + }.size.run { + if (this == 0) return@run it.selectedItemsCount + this + }, + hasSelectedItems = editedList.any { file -> + file.isSelected && !file.isDirectory + } + ) + } + } + } + + is BrowseEvent.OnUpdateFavoriteDirectory -> { + viewModelScope.launch { + _state.update { + it.copy(isLoading = true) + } + updateFavoriteDirectory.execute(event.path) + getFilesFromDownloads() + } + } + + is BrowseEvent.OnShowFilterBottomSheet -> { + _state.update { + it.copy( + bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET + ) + } + } + + is BrowseEvent.OnDismissBottomSheet -> { + _state.update { + it.copy( + bottomSheet = null + ) + } + } + + is BrowseEvent.OnPermissionCheck -> { + viewModelScope.launch(Dispatchers.IO) { + val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + val isPermissionGranted = if (!legacyPermission) { + Environment.isExternalStorageManager() + } else event.storagePermissionState.status.isGranted + + if (isPermissionGranted) { + return@launch + } + + _state.update { + it.copy( + dialog = BrowseScreen.PERMISSION_DIALOG, + isError = false + ) + } + } + } + + is BrowseEvent.OnActionPermissionDialog -> { + viewModelScope.launch { + val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + val isPermissionGranted = if (legacyStoragePermission) { + event.storagePermissionState.status.isGranted + } else { + Environment.isExternalStorageManager() + } + + if (isPermissionGranted) { + _state.update { + it.copy( + dialog = null, + isError = false + ) + } + onEvent( + BrowseEvent.OnRefreshList( + showIndicator = true, + hideSearch = false + ) + ) + return@launch + } + + if (legacyStoragePermission) { + if (!event.storagePermissionState.status.shouldShowRationale) { + event.storagePermissionState.launchPermissionRequest() + } else { + val uri = Uri.parse("package:${event.activity.packageName}") + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) + + intent.launchActivity(event.activity) { + return@launch + } + } + } + + if (!legacyStoragePermission) { + val uri = Uri.parse("package:${event.activity.packageName}") + val intent = Intent( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + uri + ) + + intent.launchActivity(event.activity) { + return@launch + } + } + + storagePermissionJob?.cancel() + storagePermissionJob = launch { + while (true) { + val granted = if (legacyStoragePermission) { + event.storagePermissionState.status.isGranted + } else { + Environment.isExternalStorageManager() + } + + if (!granted) { + delay(1000) + yield() + continue + } + + yield() + + _state.update { + it.copy( + dialog = null, + isError = false + ) + } + onEvent( + BrowseEvent.OnRefreshList( + showIndicator = true, + hideSearch = false + ) + ) + break + } + } + } + } + + is BrowseEvent.OnDismissPermissionDialog -> { + val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + val isPermissionGranted = if (!legacyPermission) { + Environment.isExternalStorageManager() + } else event.storagePermissionState.status.isGranted + + storagePermissionJob?.cancel() + _state.update { it.copy(dialog = null) } + + if (isPermissionGranted) { + viewModelScope.launch(Dispatchers.IO) { + getFilesFromDownloads() + } + } else { + _state.update { it.copy(isError = true) } + } + } + + is BrowseEvent.OnShowAddDialog -> { + viewModelScope.launch { + _state.update { + it.copy( + dialog = BrowseScreen.ADD_DIALOG, + selectedBooksAddDialog = emptyList() + ) + } + + getAddDialogBooksJob = launch(Dispatchers.IO) { + _state.update { + it.copy( + loadingAddDialog = true + ) + } + + yield() + + val books = mutableListOf() + _state.value.files + .filter { it.isSelected && !it.isDirectory } + .ifEmpty { + _state.update { + it.copy( + loadingAddDialog = false, + dialog = null + ) + } + return@launch + } + .map { it.fileOrDirectory } + .forEach { + yield() + books.add(getBookFromFile.execute(it)) + } + + yield() + + _state.update { + it.copy( + selectedBooksAddDialog = books.map { book -> + SelectableNullableBook( + data = book, + selected = true + ) + }, + loadingAddDialog = false + ) + } + } + } + } + + is BrowseEvent.OnDismissAddDialog -> { + _state.update { + it.copy( + dialog = null + ) + } + getAddDialogBooksJob?.cancel() + } + + is BrowseEvent.OnActionAddDialog -> { + viewModelScope.launch { + val booksToInsert = _state.value.selectedBooksAddDialog.mapNotNull { + if (it.data is NullableBook.NotNull && it.selected) { + return@mapNotNull it.data + } + return@mapNotNull null + }.ifEmpty { return@launch } + + var failed = false + for (book in booksToInsert) { + if (!insertBook.execute(book.bookWithTextAndCover!!)) { + failed = true + break + } + } + + if (failed) { + _state.update { + it.copy( + dialog = null + ) + } + onEvent( + BrowseEvent.OnRefreshList(showIndicator = false, hideSearch = false) + ) + onEvent(BrowseEvent.OnClearSelectedFiles) + + withContext(Dispatchers.Main) { + event.context + .getString(R.string.error_something_went_wrong) + .showToast(context = event.context) + } + + return@launch + } + + LibraryScreen.refreshListChannel.trySend(Unit) + LibraryScreen.scrollToPageCompositionChannel.trySend(0) + + event.navigateToLibrary() + withContext(Dispatchers.Main) { + event.context + .getString(R.string.books_added) + .showToast(context = event.context) + } + + _state.update { + it.copy( + dialog = null + ) + } + onEvent(BrowseEvent.OnRefreshList(showIndicator = false, hideSearch = false)) + onEvent(BrowseEvent.OnClearSelectedFiles) + } + } + + is BrowseEvent.OnSelectAddDialog -> { + viewModelScope.launch(Dispatchers.IO) { + val index = _state.value.selectedBooksAddDialog.indexOf(event.book).apply { + if (this == -1) { + return@launch + } + } + + val editedList = _state.value.selectedBooksAddDialog.toMutableList() + editedList[index] = editedList[index].run { copy(selected = !selected) } + + if (!editedList.filter { it.data is NullableBook.NotNull } + .any { it.selected }) { + return@launch + } + + _state.update { + it.copy( + selectedBooksAddDialog = editedList + ) + } + } + } + + is BrowseEvent.OnDismissDialog -> { + _state.update { + it.copy( + dialog = null + ) + } + } + } + } + + private suspend fun getFilesFromDownloads( + query: String = if (_state.value.showSearch) _state.value.searchQuery else "" + ) { + getFilesFromDevice.execute(query).apply { + yield() + _state.update { + it.copy( + files = this, + selectedItemsCount = 0, + hasSearched = query.isNotBlank(), + hasSelectedItems = false, + isLoading = false + ) + } + } + } + + fun resetScreen() { + storagePermissionJob?.cancel() + _state.update { it.copy(isError = false) } + } + + fun filterList( + files: List, + hasSearched: Boolean, + selectedDirectory: File, + pinFavoriteDirectories: Boolean, + sortOrderDescending: Boolean, + includedFilterItems: List, + filesStructure: BrowseFilesStructure, + sortOrder: BrowseSortOrder + ): List { + fun thenCompareBy( + selector: (T) -> Comparable<*>? + ): Comparator { + return if (sortOrderDescending) { + compareByDescending(selector) + } else { + compareBy(selector) + } + } + + fun List.filterFiles(): List { + if (includedFilterItems.isEmpty()) { + return this + } + + return filter { file -> + when (file.isDirectory) { + true -> { + return@filter this.filter { + if (file == it) { + return@filter false + } + + it.fileOrDirectory.path.startsWith(file.fileOrDirectory.path) + }.filterFiles().isNotEmpty() + } + + false -> { + return@filter includedFilterItems.any { + file.fileOrDirectory.path.endsWith( + it, ignoreCase = true + ) + } + } + } + } + } + + return files + .filterFiles() + .filter { + if (hasSearched || filesStructure == BrowseFilesStructure.ALL_FILES) { + return@filter !it.isDirectory + } + + if ( + Environment.getExternalStorageDirectory() == selectedDirectory + && it.isFavorite + && pinFavoriteDirectories + ) { + return@filter true + } + + it.parentDirectory == selectedDirectory + } + .sortedWith( + compareByDescending { + when (pinFavoriteDirectories) { + true -> it.isFavorite + false -> true + } + }.then( + compareByDescending { + when (sortOrder != BrowseSortOrder.FILE_TYPE) { + true -> it.isDirectory + false -> true + } + } + ).then( + thenCompareBy { + when (sortOrder) { + BrowseSortOrder.NAME -> { + it.fileOrDirectory.name.lowercase().trim() + } + + BrowseSortOrder.FILE_TYPE -> { + it.isDirectory + } + + BrowseSortOrder.FILE_FORMAT -> { + it.fileOrDirectory.extension + } + + BrowseSortOrder.FILE_SIZE -> { + it.fileOrDirectory.length() + } + + BrowseSortOrder.LAST_MODIFIED -> { + it.fileOrDirectory.lastModified() + } + } + } + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt new file mode 100644 index 00000000..95b62242 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt @@ -0,0 +1,205 @@ +package ua.acclorite.book_story.ui.browse + +import android.Manifest +import android.os.Parcelable +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.browse.BrowseContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.help.HelpScreen +import ua.acclorite.book_story.ui.library.LibraryScreen +import ua.acclorite.book_story.ui.main.MainModel + +@Parcelize +object BrowseScreen : Screen, Parcelable { + + @IgnoredOnParcel + const val PERMISSION_DIALOG = "permission_dialog" + + @IgnoredOnParcel + const val ADD_DIALOG = "add_dialog" + + @IgnoredOnParcel + const val FILTER_BOTTOM_SHEET = "filter_bottom_sheet" + + @IgnoredOnParcel + val refreshListChannel: Channel = Channel(Channel.CONFLATED) + + @IgnoredOnParcel + val resetScrollPositionCompositionChannel: Channel = Channel(Channel.CONFLATED) + + @IgnoredOnParcel + private var initialListIndex = 0 + + @IgnoredOnParcel + private var initialListOffset = 0 + + @IgnoredOnParcel + private var initialGridIndex = 0 + + @IgnoredOnParcel + private var initialGridOffset = 0 + + @OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + val mainModel = hiltViewModel() + + val state = screenModel.state.collectAsStateWithLifecycle() + val mainState = mainModel.state.collectAsStateWithLifecycle() + + val listState = rememberLazyListState(initialListIndex, initialListOffset) + val gridState = rememberLazyGridState(initialGridIndex, initialGridOffset) + + val storagePermissionState = rememberPermissionState( + permission = Manifest.permission.READ_EXTERNAL_STORAGE + ) + val focusRequester = remember { FocusRequester() } + val refreshState = rememberPullRefreshState( + refreshing = state.value.isRefreshing, + onRefresh = { + screenModel.onEvent( + BrowseEvent.OnRefreshList( + showIndicator = true, + hideSearch = true + ) + ) + } + ) + + val files = remember { + derivedStateOf { + screenModel.filterList( + files = state.value.files, + hasSearched = state.value.hasSearched, + selectedDirectory = state.value.selectedDirectory, + pinFavoriteDirectories = mainState.value.browsePinFavoriteDirectories, + sortOrderDescending = mainState.value.browseSortOrderDescending, + includedFilterItems = mainState.value.browseIncludedFilterItems, + filesStructure = mainState.value.browseFilesStructure, + sortOrder = mainState.value.browseSortOrder + ) + } + } + + LaunchedEffect(Unit) { + screenModel.onEvent( + BrowseEvent.OnPermissionCheck( + storagePermissionState = storagePermissionState + ) + ) + } + + LaunchedEffect(Unit) { + resetScrollPositionCompositionChannel.receiveAsFlow().collectLatest { + initialListIndex = 0 + initialListOffset = 0 + initialGridIndex = 0 + initialGridOffset = 0 + } + } + + DisposableEffect(Unit) { + onDispose { + screenModel.resetScreen() + + initialListIndex = 0 + initialListOffset = 0 + initialGridIndex = 0 + initialGridOffset = 0 + + when (mainState.value.browseLayout) { + BrowseLayout.LIST -> { + initialListIndex = listState.firstVisibleItemIndex + initialListOffset = listState.firstVisibleItemScrollOffset + } + + BrowseLayout.GRID -> { + initialGridIndex = gridState.firstVisibleItemIndex + initialGridOffset = gridState.firstVisibleItemScrollOffset + } + } + } + } + + BrowseContent( + files = files.value, + selectedBooksAddDialog = state.value.selectedBooksAddDialog, + refreshState = refreshState, + storagePermissionState = storagePermissionState, + loadingAddDialog = state.value.loadingAddDialog, + dialog = state.value.dialog, + bottomSheet = state.value.bottomSheet, + listState = listState, + gridState = gridState, + layout = mainState.value.browseLayout, + filesStructure = mainState.value.browseFilesStructure, + gridSize = mainState.value.browseGridSize, + autoGridSize = mainState.value.browseAutoGridSize, + includedFilterItems = mainState.value.browseIncludedFilterItems, + canScrollBackList = listState.canScrollBackward, + canScrollBackGrid = gridState.canScrollBackward, + selectedDirectory = state.value.selectedDirectory, + inNestedDirectory = state.value.inNestedDirectory + && mainState.value.browseFilesStructure != BrowseFilesStructure.ALL_FILES, + hasSelectedItems = state.value.hasSelectedItems, + selectedItemsCount = state.value.selectedItemsCount, + isRefreshing = state.value.isRefreshing, + isLoading = state.value.isLoading, + isError = state.value.isError, + dialogHidden = state.value.dialog == null, + filesEmpty = files.value.isEmpty(), + showSearch = state.value.showSearch, + searchQuery = state.value.searchQuery, + hasSearched = state.value.hasSearched, + focusRequester = focusRequester, + searchVisibility = screenModel::onEvent, + searchQueryChange = screenModel::onEvent, + search = screenModel::onEvent, + requestFocus = screenModel::onEvent, + clearSelectedFiles = screenModel::onEvent, + goBackDirectory = screenModel::onEvent, + selectFiles = screenModel::onEvent, + selectFile = screenModel::onEvent, + permissionCheck = screenModel::onEvent, + changeDirectory = screenModel::onEvent, + updateFavoriteDirectory = screenModel::onEvent, + showFilterBottomSheet = screenModel::onEvent, + dismissBottomSheet = screenModel::onEvent, + actionPermissionDialog = screenModel::onEvent, + dismissPermissionDialog = screenModel::onEvent, + showAddDialog = screenModel::onEvent, + dismissAddDialog = screenModel::onEvent, + selectAddDialog = screenModel::onEvent, + actionAddDialog = screenModel::onEvent, + navigateToLibrary = { + navigator.push(LibraryScreen, saveInBackStack = false) + }, + navigateToHelp = { + navigator.push(HelpScreen(fromStart = false)) + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseState.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseState.kt new file mode 100644 index 00000000..3f5f4c89 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseState.kt @@ -0,0 +1,36 @@ +package ua.acclorite.book_story.ui.browse + +import android.os.Environment +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.browse.SelectableFile +import ua.acclorite.book_story.domain.library.book.SelectableNullableBook +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.domain.util.Dialog +import java.io.File + +@Immutable +data class BrowseState( + val files: List = emptyList(), + + val selectedDirectory: File = Environment.getExternalStorageDirectory(), + val previousDirectory: File? = null, + val inNestedDirectory: Boolean = false, + + val isLoading: Boolean = true, + val isRefreshing: Boolean = false, + val isError: Boolean = false, + + val selectedItemsCount: Int = 0, + val hasSelectedItems: Boolean = false, + + val showSearch: Boolean = false, + val searchQuery: String = "", + val hasSearched: Boolean = false, + val hasFocused: Boolean = false, + + val dialog: Dialog? = null, + val bottomSheet: BottomSheet? = null, + + val selectedBooksAddDialog: List = emptyList(), + val loadingAddDialog: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/credits/CreditsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/credits/CreditsScreen.kt new file mode 100644 index 00000000..96b16506 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/credits/CreditsScreen.kt @@ -0,0 +1,35 @@ +package ua.acclorite.book_story.ui.credits + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.credits.CreditsContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.about.AboutModel + +@Parcelize +object CreditsScreen : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + CreditsContent( + scrollBehavior = scrollBehavior, + listState = listState, + navigateToBrowserPage = screenModel::onEvent, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/help/HelpScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/help/HelpScreen.kt new file mode 100644 index 00000000..66a99b0b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/help/HelpScreen.kt @@ -0,0 +1,46 @@ +package ua.acclorite.book_story.ui.help + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.help.HelpContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.browse.BrowseScreen +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.start.StartScreen + +@Parcelize +data class HelpScreen(val fromStart: Boolean) : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val mainModel = hiltViewModel() + + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + HelpContent( + fromStart = fromStart, + scrollBehavior = scrollBehavior, + listState = listState, + changeShowStartScreen = mainModel::onEvent, + navigateToBrowse = { + navigator.push(BrowseScreen, saveInBackStack = false) + }, + navigateToStart = { + mainModel.onEvent(MainEvent.OnChangeShowStartScreen(true)) + navigator.push(StartScreen, saveInBackStack = false) + }, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEvent.kt new file mode 100644 index 00000000..59d2f2ef --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryEvent.kt @@ -0,0 +1,43 @@ +package ua.acclorite.book_story.ui.history + +import android.content.Context +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Immutable +import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.history.History + +@Immutable +sealed class HistoryEvent { + data class OnRefreshList( + val showIndicator: Boolean, + val hideSearch: Boolean + ) : HistoryEvent() + + data class OnSearchVisibility( + val show: Boolean + ) : HistoryEvent() + + data class OnRequestFocus( + val focusRequester: FocusRequester + ) : HistoryEvent() + + data class OnSearchQueryChange( + val query: String + ) : HistoryEvent() + + data object OnSearch : HistoryEvent() + + data class OnDeleteHistoryEntry( + val history: History, + val snackbarState: SnackbarHostState, + val context: Context + ) : HistoryEvent() + + data object OnShowDeleteWholeHistoryDialog : HistoryEvent() + + data class OnActionDeleteWholeHistoryDialog( + val context: Context + ) : HistoryEvent() + + data object OnDismissDialog : HistoryEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryModel.kt new file mode 100644 index 00000000..6ce4cd4a --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryModel.kt @@ -0,0 +1,315 @@ +package ua.acclorite.book_story.ui.history + +import androidx.compose.material3.SnackbarResult +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.history.GroupedHistory +import ua.acclorite.book_story.domain.history.History +import ua.acclorite.book_story.domain.use_case.book.GetBooksById +import ua.acclorite.book_story.domain.use_case.history.DeleteHistory +import ua.acclorite.book_story.domain.use_case.history.DeleteWholeHistory +import ua.acclorite.book_story.domain.use_case.history.GetHistory +import ua.acclorite.book_story.domain.use_case.history.InsertHistory +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.library.LibraryScreen +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import kotlin.collections.component1 +import kotlin.collections.component2 + +@OptIn(FlowPreview::class) +@HiltViewModel +class HistoryModel @Inject constructor( + private val getHistory: GetHistory, + private val getBooksById: GetBooksById, + private val insertHistory: InsertHistory, + private val deleteHistory: DeleteHistory, + private val deleteWholeHistory: DeleteWholeHistory +) : ViewModel() { + + private val _state = MutableStateFlow(HistoryState()) + val state = _state.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isLoading = true + ) + } + getHistoryFromDatabase() + } + + /* Observe channel - - - - - - - - - - - */ + viewModelScope.launch(Dispatchers.IO) { + HistoryScreen.refreshListChannel.receiveAsFlow().debounce(200).collectLatest { + onEvent(HistoryEvent.OnRefreshList(showIndicator = false, hideSearch = false)) + } + } + viewModelScope.launch(Dispatchers.IO) { + HistoryScreen.insertHistoryChannel.receiveAsFlow().debounce(200) + .collectLatest { + insertHistory.execute( + History( + bookId = it, + book = null, + time = Date().time + ) + ) + + getHistoryFromDatabase() + LibraryScreen.refreshListChannel.trySend(Unit) + } + } + /* - - - - - - - - - - - - - - - - - - - */ + } + + private var refreshJob: Job? = null + private var searchQueryChange: Job? = null + private var deleteHistoryEntry: Job? = null + + fun onEvent(event: HistoryEvent) { + when (event) { + is HistoryEvent.OnRefreshList -> { + refreshJob?.cancel() + refreshJob = viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isRefreshing = event.showIndicator, + isLoading = !event.showIndicator, + showSearch = if (event.hideSearch) false else it.showSearch + ) + } + + yield() + getHistoryFromDatabase() + + if (event.showIndicator) delay(500) + _state.update { + it.copy( + isRefreshing = false, + isLoading = false + ) + } + } + } + + is HistoryEvent.OnSearchVisibility -> { + viewModelScope.launch(Dispatchers.IO) { + if (!event.show) { + getHistoryFromDatabase("") + } else { + _state.update { + it.copy( + searchQuery = "", + hasFocused = false + ) + } + } + + _state.update { + it.copy( + showSearch = event.show + ) + } + } + } + + is HistoryEvent.OnRequestFocus -> { + if (!_state.value.hasFocused) { + event.focusRequester.requestFocus() + _state.update { + it.copy( + hasFocused = true + ) + } + } + } + + is HistoryEvent.OnSearchQueryChange -> { + _state.update { + it.copy( + searchQuery = event.query + ) + } + searchQueryChange?.cancel() + searchQueryChange = viewModelScope.launch(Dispatchers.IO) { + delay(500) + yield() + onEvent(HistoryEvent.OnSearch) + } + } + + is HistoryEvent.OnSearch -> { + viewModelScope.launch(Dispatchers.IO) { + getHistoryFromDatabase() + } + } + + is HistoryEvent.OnDeleteHistoryEntry -> { + viewModelScope.launch(Dispatchers.IO) { + deleteHistory.execute(event.history) + + _state.update { it.copy(isRefreshing = true) } + getHistoryFromDatabase() + _state.update { it.copy(isRefreshing = false) } + + LibraryScreen.refreshListChannel.trySend(Unit) + + deleteHistoryEntry?.cancel() + event.snackbarState.currentSnackbarData?.dismiss() + + deleteHistoryEntry = launch(Dispatchers.IO) { + repeat(10) { + yield() + delay(1000) + } + + yield() + event.snackbarState.currentSnackbarData?.dismiss() + } + val snackbarResult = event.snackbarState.showSnackbar( + event.context.getString(R.string.history_element_deleted), + event.context.getString(R.string.undo) + ) + + when (snackbarResult) { + SnackbarResult.Dismissed -> Unit + SnackbarResult.ActionPerformed -> { + insertHistory.execute(event.history) + LibraryScreen.refreshListChannel.trySend(Unit) + + _state.update { it.copy(isRefreshing = true) } + getHistoryFromDatabase() + delay(500) + _state.update { it.copy(isRefreshing = false) } + } + } + } + } + + is HistoryEvent.OnShowDeleteWholeHistoryDialog -> { + _state.update { + it.copy( + dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG + ) + } + } + + is HistoryEvent.OnActionDeleteWholeHistoryDialog -> { + viewModelScope.launch { + _state.update { + it.copy( + dialog = null, + isLoading = true + ) + } + + deleteWholeHistory.execute() + LibraryScreen.refreshListChannel.trySend(Unit) + getHistoryFromDatabase("") + + withContext(Dispatchers.Main) { + event.context + .getString(R.string.history_deleted) + .showToast(context = event.context) + } + } + } + + is HistoryEvent.OnDismissDialog -> { + _state.update { + it.copy( + dialog = null + ) + } + } + } + } + + private suspend fun getHistoryFromDatabase( + query: String = if (_state.value.showSearch) _state.value.searchQuery else "" + ) { + fun isSameDay(historyTime: Calendar, nowTime: Calendar): Boolean { + return historyTime.get(Calendar.YEAR) == nowTime.get(Calendar.YEAR) && + historyTime.get(Calendar.DAY_OF_YEAR) == nowTime.get(Calendar.DAY_OF_YEAR) + } + + fun filterMaxElementsById(elements: List): List { + val groupedById = elements.groupBy { it.bookId } + val maxElementsById = groupedById.map { (_, values) -> + values.maxByOrNull { it.time } + } + return maxElementsById.filterNotNull() + } + + val history = getHistory.execute().sortedByDescending { + it.time + }.run { + val books = getBooksById.execute( + this.map { it.bookId }.distinct() + ).toMutableList() + + mapNotNull { + val book = books.find { book -> book.id == it.bookId } ?: return@mapNotNull null + if (!book.title.lowercase().trim().contains(query.lowercase().trim())) { + return@mapNotNull null + } + + it.copy(book = book) + } + }.ifEmpty { + _state.update { + it.copy( + history = emptyList(), + isLoading = false + ) + } + return + }.groupBy { item -> + val historyTime = Calendar.getInstance().apply { + timeInMillis = item.time + } + val nowTime = Calendar.getInstance() + + return@groupBy when { + isSameDay(historyTime, nowTime) -> "today" + + isSameDay( + historyTime, + nowTime.apply { add(Calendar.DAY_OF_YEAR, -1) } + ) -> "yesterday" + + else -> SimpleDateFormat( + "dd.MM.yy", + Locale.getDefault() + ).format(item.time) + } + }.map { (day, history) -> GroupedHistory(day, filterMaxElementsById(history)) } + + _state.update { + it.copy( + history = history, + isLoading = false, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryScreen.kt new file mode 100644 index 00000000..b67e8788 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryScreen.kt @@ -0,0 +1,109 @@ +@file:OptIn(ExperimentalMaterialApi::class) + +package ua.acclorite.book_story.ui.history + +import android.os.Parcelable +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.channels.Channel +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.history.HistoryContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.book_info.BookInfoScreen +import ua.acclorite.book_story.ui.library.LibraryScreen +import ua.acclorite.book_story.ui.reader.ReaderScreen + +@Parcelize +object HistoryScreen : Screen, Parcelable { + + @IgnoredOnParcel + const val DELETE_WHOLE_HISTORY_DIALOG = "delete_whole_history_dialog" + + @IgnoredOnParcel + val refreshListChannel: Channel = Channel(Channel.CONFLATED) + + @IgnoredOnParcel + val insertHistoryChannel: Channel = Channel(Channel.CONFLATED) + + @IgnoredOnParcel + private var initialIndex = 0 + + @IgnoredOnParcel + private var initialOffset = 0 + + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + + val state = screenModel.state.collectAsStateWithLifecycle() + + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = initialIndex, + initialFirstVisibleItemScrollOffset = initialOffset + ) + DisposableEffect(Unit) { + onDispose { + initialIndex = listState.firstVisibleItemIndex + initialOffset = listState.firstVisibleItemScrollOffset + } + } + + val snackbarState = remember { SnackbarHostState() } + val focusRequester = remember { FocusRequester() } + val refreshState = rememberPullRefreshState( + refreshing = state.value.isRefreshing, + onRefresh = { + screenModel.onEvent( + HistoryEvent.OnRefreshList( + showIndicator = true, + hideSearch = true + ) + ) + } + ) + + HistoryContent( + refreshState = refreshState, + snackbarState = snackbarState, + listState = listState, + history = state.value.history, + dialog = state.value.dialog, + canScrollBackward = listState.canScrollBackward, + showSearch = state.value.showSearch, + isLoading = state.value.isLoading, + isRefreshing = state.value.isRefreshing, + historyIsEmpty = state.value.history.isEmpty(), + focusRequester = focusRequester, + searchQuery = state.value.searchQuery, + searchVisibility = screenModel::onEvent, + requestFocus = screenModel::onEvent, + searchQueryChange = screenModel::onEvent, + search = screenModel::onEvent, + deleteHistoryEntry = screenModel::onEvent, + showDeleteWholeHistoryDialog = screenModel::onEvent, + actionDeleteWholeHistoryDialog = screenModel::onEvent, + dismissDialog = screenModel::onEvent, + navigateToLibrary = { + navigator.push(LibraryScreen, saveInBackStack = false) + }, + navigateToBookInfo = { + navigator.push(BookInfoScreen(bookId = it)) + }, + navigateToReader = { + insertHistoryChannel.trySend(it) + navigator.push(ReaderScreen(it)) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryState.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt rename to app/src/main/java/ua/acclorite/book_story/ui/history/HistoryState.kt index f0dd9433..e7fc2062 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/history/HistoryState.kt @@ -1,20 +1,19 @@ -package ua.acclorite.book_story.presentation.screens.history.data +package ua.acclorite.book_story.ui.history -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.model.GroupedHistory +import ua.acclorite.book_story.domain.history.GroupedHistory +import ua.acclorite.book_story.domain.util.Dialog @Immutable data class HistoryState( val history: List = emptyList(), - val listState: LazyListState = LazyListState(0, 0), val isRefreshing: Boolean = false, val isLoading: Boolean = true, - val showDeleteWholeHistoryDialog: Boolean = false, - val showSearch: Boolean = false, val searchQuery: String = "", val hasFocused: Boolean = false, -) + + val dialog: Dialog? = null +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryEvent.kt new file mode 100644 index 00000000..cd6c7370 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryEvent.kt @@ -0,0 +1,52 @@ +package ua.acclorite.book_story.ui.library + +import android.content.Context +import androidx.compose.runtime.Immutable +import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks + +@Immutable +sealed class LibraryEvent { + data class OnRefreshList( + val showIndicator: Boolean, + val hideSearch: Boolean + ) : LibraryEvent() + + data class OnSearchVisibility( + val show: Boolean + ) : LibraryEvent() + + data class OnSearchQueryChange( + val query: String + ) : LibraryEvent() + + data object OnSearch : LibraryEvent() + + data class OnRequestFocus( + val focusRequester: FocusRequester + ) : LibraryEvent() + + data object OnClearSelectedBooks : LibraryEvent() + + data class OnSelectBook( + val id: Int, + val select: Boolean? = null + ) : LibraryEvent() + + data object OnShowMoveDialog : LibraryEvent() + + data class OnActionMoveDialog( + val selectedCategory: Category, + val categories: List, + val context: Context + ) : LibraryEvent() + + data object OnShowDeleteDialog : LibraryEvent() + + data class OnActionDeleteDialog( + val context: Context + ) : LibraryEvent() + + data object OnDismissDialog : LibraryEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryModel.kt new file mode 100644 index 00000000..5ed5de9d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryModel.kt @@ -0,0 +1,276 @@ +package ua.acclorite.book_story.ui.library + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.use_case.book.DeleteBooks +import ua.acclorite.book_story.domain.use_case.book.GetBooks +import ua.acclorite.book_story.domain.use_case.book.UpdateBook +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.browse.BrowseScreen +import ua.acclorite.book_story.ui.history.HistoryScreen +import javax.inject.Inject + +@OptIn(FlowPreview::class) +@HiltViewModel +class LibraryModel @Inject constructor( + private val getBooks: GetBooks, + private val deleteBooks: DeleteBooks, + private val moveBooks: UpdateBook +) : ViewModel() { + + private val _state = MutableStateFlow(LibraryState()) + val state = _state.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) { + getBooksFromDatabase("") + } + + /* Observe channel - - - - - - - - - - - */ + viewModelScope.launch(Dispatchers.IO) { + LibraryScreen.refreshListChannel.receiveAsFlow().debounce(200).collectLatest { + onEvent(LibraryEvent.OnRefreshList(showIndicator = false, hideSearch = false)) + } + } + /* - - - - - - - - - - - - - - - - - - - */ + } + + private var refreshJob: Job? = null + private var searchQueryChange: Job? = null + + fun onEvent(event: LibraryEvent) { + when (event) { + is LibraryEvent.OnRefreshList -> { + refreshJob?.cancel() + refreshJob = viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isRefreshing = event.showIndicator, + isLoading = !event.showIndicator, + hasSelectedItems = false, + showSearch = if (event.hideSearch) false else it.showSearch + ) + } + + yield() + getBooksFromDatabase() + + if (event.showIndicator) delay(500) + _state.update { + it.copy( + isRefreshing = false, + isLoading = false + ) + } + } + } + + is LibraryEvent.OnSearchVisibility -> { + viewModelScope.launch(Dispatchers.IO) { + if (!event.show) { + getBooksFromDatabase("") + } else { + _state.update { + it.copy( + searchQuery = "", + hasFocused = false + ) + } + } + + _state.update { + it.copy( + showSearch = event.show + ) + } + } + } + + is LibraryEvent.OnSearchQueryChange -> { + _state.update { + it.copy( + searchQuery = event.query + ) + } + searchQueryChange?.cancel() + searchQueryChange = viewModelScope.launch(Dispatchers.IO) { + delay(500) + yield() + onEvent(LibraryEvent.OnSearch) + } + } + + is LibraryEvent.OnSearch -> { + viewModelScope.launch(Dispatchers.IO) { + getBooksFromDatabase() + } + } + + is LibraryEvent.OnRequestFocus -> { + if (!_state.value.hasFocused) { + event.focusRequester.requestFocus() + _state.update { + it.copy( + hasFocused = true + ) + } + } + } + + is LibraryEvent.OnClearSelectedBooks -> { + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + books = it.books.map { book -> book.copy(selected = false) }, + hasSelectedItems = false + ) + } + } + } + + is LibraryEvent.OnSelectBook -> { + viewModelScope.launch(Dispatchers.IO) { + val editedList = _state.value.books.map { + if (it.data.id == event.id) it.copy(selected = event.select ?: !it.selected) + else it + } + + _state.update { + it.copy( + books = editedList, + selectedItemsCount = editedList.filter { book -> book.selected }.size, + hasSelectedItems = editedList.any { book -> book.selected } + ) + } + } + } + + is LibraryEvent.OnShowMoveDialog -> { + _state.update { + it.copy( + dialog = LibraryScreen.MOVE_DIALOG + ) + } + } + + is LibraryEvent.OnActionMoveDialog -> { + viewModelScope.launch { + _state.value.books.forEach { book -> + if (!book.selected) return@forEach + moveBooks.execute( + book.data.copy( + category = event.selectedCategory + ) + ) + } + + _state.update { + it.copy( + books = it.books.map { book -> + if (!book.selected) return@map book + book.copy( + data = book.data.copy( + category = event.selectedCategory + ), + selected = false + ) + }, + hasSelectedItems = false, + dialog = null + ) + } + + HistoryScreen.refreshListChannel.trySend(Unit) + LibraryScreen.scrollToPageCompositionChannel.trySend( + event.categories.dropLastWhile { + it.category != event.selectedCategory + }.lastIndex + ) + + withContext(Dispatchers.Main) { + event.context + .getString(R.string.books_moved) + .showToast(context = event.context) + } + } + } + + is LibraryEvent.OnShowDeleteDialog -> { + _state.update { + it.copy( + dialog = LibraryScreen.DELETE_DIALOG + ) + } + } + + is LibraryEvent.OnActionDeleteDialog -> { + viewModelScope.launch { + deleteBooks.execute( + _state.value.books.mapNotNull { + if (!it.selected) return@mapNotNull null + it.data + } + ) + + _state.update { + it.copy( + books = it.books.filter { book -> !book.selected }, + hasSelectedItems = false, + dialog = null + ) + } + + HistoryScreen.refreshListChannel.trySend(Unit) + BrowseScreen.refreshListChannel.trySend(Unit) + + withContext(Dispatchers.Main) { + event.context + .getString(R.string.books_deleted) + .showToast(context = event.context) + } + } + } + + is LibraryEvent.OnDismissDialog -> { + _state.update { + it.copy( + dialog = null + ) + } + } + } + } + + private suspend fun getBooksFromDatabase( + query: String = if (_state.value.showSearch) _state.value.searchQuery else "" + ) { + val books = getBooks + .execute(query) + .sortedWith(compareByDescending { it.lastOpened }.thenBy { it.title }) + .map { book -> SelectableBook(book, false) } + + _state.update { + it.copy( + books = books, + isLoading = false + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryScreen.kt new file mode 100644 index 00000000..0247a2d0 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryScreen.kt @@ -0,0 +1,151 @@ +package ua.acclorite.book_story.ui.library + +import android.os.Parcelable +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.domain.ui.UIText +import ua.acclorite.book_story.presentation.library.LibraryContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.book_info.BookInfoScreen +import ua.acclorite.book_story.ui.browse.BrowseScreen +import ua.acclorite.book_story.ui.history.HistoryScreen +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.reader.ReaderScreen + +@Parcelize +object LibraryScreen : Screen, Parcelable { + + @IgnoredOnParcel + const val MOVE_DIALOG = "move_dialog" + + @IgnoredOnParcel + const val DELETE_DIALOG = "delete_dialog" + + @IgnoredOnParcel + val refreshListChannel: Channel = Channel(Channel.CONFLATED) + + @IgnoredOnParcel + val scrollToPageCompositionChannel: Channel = Channel(Channel.CONFLATED) + + @IgnoredOnParcel + private var initialPage = 0 + + @OptIn(ExperimentalMaterialApi::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + + val screenModel = hiltViewModel() + val mainModel = hiltViewModel() + + val state = screenModel.state.collectAsStateWithLifecycle() + val mainState = mainModel.state.collectAsStateWithLifecycle() + + val categories = remember(state.value.books) { + derivedStateOf { + listOf( + CategoryWithBooks( + category = Category.READING, + title = UIText.StringResource(R.string.reading_tab), + books = state.value.books.filter { it.data.category == Category.READING } + ), + CategoryWithBooks( + category = Category.ALREADY_READ, + title = UIText.StringResource(R.string.already_read_tab), + books = state.value.books.filter { it.data.category == Category.ALREADY_READ } + ), + CategoryWithBooks( + category = Category.PLANNING, + title = UIText.StringResource(R.string.planning_tab), + books = state.value.books.filter { it.data.category == Category.PLANNING } + ), + CategoryWithBooks( + category = Category.DROPPED, + title = UIText.StringResource(R.string.dropped_tab), + books = state.value.books.filter { it.data.category == Category.DROPPED } + ) + ) + } + } + + val focusRequester = remember { FocusRequester() } + val refreshState = rememberPullRefreshState( + refreshing = state.value.isRefreshing, + onRefresh = { + screenModel.onEvent( + LibraryEvent.OnRefreshList( + showIndicator = true, + hideSearch = true + ) + ) + } + ) + + val pagerState = rememberPagerState( + initialPage = initialPage + ) { categories.value.count() } + DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } } + + LaunchedEffect(Unit) { + scrollToPageCompositionChannel.receiveAsFlow().collectLatest { + pagerState.animateScrollToPage(it) + } + } + + LibraryContent( + books = state.value.books, + selectedItemsCount = state.value.selectedItemsCount, + hasSelectedItems = state.value.hasSelectedItems, + showSearch = state.value.showSearch, + searchQuery = state.value.searchQuery, + bookCount = state.value.books.count(), + focusRequester = focusRequester, + pagerState = pagerState, + isLoading = state.value.isLoading, + isRefreshing = state.value.isRefreshing, + doublePressExit = mainState.value.doublePressExit, + categories = categories.value, + refreshState = refreshState, + dialog = state.value.dialog, + selectBook = screenModel::onEvent, + searchVisibility = screenModel::onEvent, + requestFocus = screenModel::onEvent, + searchQueryChange = screenModel::onEvent, + search = screenModel::onEvent, + clearSelectedBooks = screenModel::onEvent, + showMoveDialog = screenModel::onEvent, + actionMoveDialog = screenModel::onEvent, + actionDeleteDialog = screenModel::onEvent, + showDeleteDialog = screenModel::onEvent, + dismissDialog = screenModel::onEvent, + navigateToBrowse = { + navigator.push(BrowseScreen) + }, + navigateToReader = { + HistoryScreen.insertHistoryChannel.trySend(it) + navigator.push(ReaderScreen(it)) + }, + navigateToBookInfo = { + navigator.push(BookInfoScreen(bookId = it)) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryState.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryState.kt new file mode 100644 index 00000000..9a109eb3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryState.kt @@ -0,0 +1,22 @@ +package ua.acclorite.book_story.ui.library + +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.util.Dialog + +@Immutable +data class LibraryState( + val books: List = emptyList(), + + val isLoading: Boolean = true, + val isRefreshing: Boolean = false, + + val selectedItemsCount: Int = 0, + val hasSelectedItems: Boolean = false, + + val showSearch: Boolean = false, + val searchQuery: String = "", + val hasFocused: Boolean = false, + + val dialog: Dialog? = null +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/license_info/LicenseInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/license_info/LicenseInfoScreen.kt new file mode 100644 index 00000000..62e49bb3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/license_info/LicenseInfoScreen.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.ui.license_info + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.util.withJson +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.license_info.LicenseInfoContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.about.AboutModel + +@Parcelize +data class LicenseInfoScreen(val uniqueId: String) : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + + val context = LocalContext.current + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + val library = remember { + derivedStateOf { + Libs.Builder().withJson(context, R.raw.aboutlibraries).build() + .libraries.find { it.uniqueId == uniqueId } + } + } + + LicenseInfoContent( + library = library.value, + scrollBehavior = scrollBehavior, + listState = listState, + navigateToBrowserPage = screenModel::onEvent, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/licenses/LicensesScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/licenses/LicensesScreen.kt new file mode 100644 index 00000000..9c89018d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/licenses/LicensesScreen.kt @@ -0,0 +1,70 @@ +package ua.acclorite.book_story.ui.licenses + +import android.os.Parcelable +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.util.withJson +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.licenses.LicensesContent +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.ui.license_info.LicenseInfoScreen + +@Parcelize +object LicensesScreen : Screen, Parcelable { + + @IgnoredOnParcel + private var initialIndex = 0 + + @IgnoredOnParcel + private var initialOffset = 0 + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val context = LocalContext.current + + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior( + listState = rememberLazyListState( + initialFirstVisibleItemIndex = initialIndex, + initialFirstVisibleItemScrollOffset = initialOffset + ) + ) + val licenses = remember { + derivedStateOf { + Libs.Builder().withJson(context, R.raw.aboutlibraries).build() + .libraries.sortedBy { it.openSource } + } + } + + DisposableEffect(Unit) { + onDispose { + initialIndex = listState.firstVisibleItemIndex + initialOffset = listState.firstVisibleItemScrollOffset + } + } + + LicensesContent( + licenses = licenses.value, + scrollBehavior = scrollBehavior, + listState = listState, + navigateToLicenseInfo = { + navigator.push(LicenseInfoScreen(it.uniqueId)) + }, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainActivity.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainActivity.kt new file mode 100644 index 00000000..9953d848 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainActivity.kt @@ -0,0 +1,166 @@ +@file:Suppress("UnusedVariable", "unused") + +package ua.acclorite.book_story.ui.main + +import android.annotation.SuppressLint +import android.database.CursorWindow +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.togetherWith +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dagger.hilt.android.AndroidEntryPoint +import okhttp3.internal.immutableListOf +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.navigator.NavigatorItem +import ua.acclorite.book_story.domain.navigator.StackEvent +import ua.acclorite.book_story.presentation.core.components.navigation_bar.NavigationBar +import ua.acclorite.book_story.presentation.core.components.navigation_rail.NavigationRail +import ua.acclorite.book_story.presentation.main.MainActivityKeyboardManager +import ua.acclorite.book_story.presentation.navigator.Navigator +import ua.acclorite.book_story.presentation.navigator.NavigatorTabs +import ua.acclorite.book_story.ui.browse.BrowseModel +import ua.acclorite.book_story.ui.browse.BrowseScreen +import ua.acclorite.book_story.ui.history.HistoryModel +import ua.acclorite.book_story.ui.history.HistoryScreen +import ua.acclorite.book_story.ui.library.LibraryModel +import ua.acclorite.book_story.ui.library.LibraryScreen +import ua.acclorite.book_story.ui.settings.SettingsModel +import ua.acclorite.book_story.ui.start.StartScreen +import ua.acclorite.book_story.ui.theme.BookStoryTheme +import ua.acclorite.book_story.ui.theme.Transitions +import ua.acclorite.book_story.ui.theme.isDark +import ua.acclorite.book_story.ui.theme.isPureDark +import java.lang.reflect.Field + + +@SuppressLint("DiscouragedPrivateApi") +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + // Creating an instance of Models + private val mainModel: MainModel by viewModels() + private val settingsModel: SettingsModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + // Splash screen + installSplashScreen().apply { + setKeepOnScreenCondition { + !mainModel.isReady.value + } + } + + // Default super + super.onCreate(savedInstanceState) + + // Bigger Cursor size for Room + try { + val field: Field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize") + field.isAccessible = true + field.set(null, 100 * 1024 * 1024) + } catch (e: Exception) { + e.printStackTrace() + } + + // Initializing the MainModel + mainModel.init(settingsModel.isReady) + + // Edge to edge + enableEdgeToEdge() + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + // Initializing Screen Models + val libraryModel = hiltViewModel() + val historyModel = hiltViewModel() + val browseModel = hiltViewModel() + + val state = mainModel.state.collectAsStateWithLifecycle() + val isLoaded = mainModel.isReady.collectAsStateWithLifecycle() + + val tabs = immutableListOf( + NavigatorItem( + screen = LibraryScreen, + title = R.string.library_screen, + tooltip = R.string.library_content_desc, + selectedIcon = R.drawable.library_screen_filled, + unselectedIcon = R.drawable.library_screen_outlined + ), + NavigatorItem( + screen = HistoryScreen, + title = R.string.history_screen, + tooltip = R.string.history_content_desc, + selectedIcon = R.drawable.history_screen_filled, + unselectedIcon = R.drawable.history_screen_outlined + ), + NavigatorItem( + screen = BrowseScreen, + title = R.string.browse_screen, + tooltip = R.string.browse_content_desc, + selectedIcon = R.drawable.browse_screen_filled, + unselectedIcon = R.drawable.browse_screen_outlined + ) + ) + + MainActivityKeyboardManager() + + if (isLoaded.value) { + BookStoryTheme( + theme = state.value.theme, + isDark = state.value.darkTheme.isDark(), + isPureDark = state.value.pureDark.isPureDark(this), + themeContrast = state.value.themeContrast + ) { + Navigator( + initialScreen = if (state.value.showStartScreen) StartScreen + else LibraryScreen, + transitionSpec = { lastEvent -> + when (lastEvent) { + StackEvent.Default -> { + Transitions.SlidingTransitionIn + .togetherWith(Transitions.SlidingTransitionOut) + } + + StackEvent.Pop -> { + Transitions.BackSlidingTransitionIn + .togetherWith(Transitions.BackSlidingTransitionOut) + } + } + }, + contentKey = { + when (it) { + LibraryScreen, HistoryScreen, BrowseScreen -> "tabs" + else -> it + } + }, + backHandlerEnabled = { it != StartScreen } + ) { screen -> + when (screen) { + LibraryScreen, HistoryScreen, BrowseScreen -> { + NavigatorTabs( + currentTab = screen, + transitionSpec = { + Transitions.FadeTransitionIn + .togetherWith(Transitions.FadeTransitionOut) + }, + navigationBar = { NavigationBar(tabs = tabs) }, + navigationRail = { NavigationRail(tabs = tabs) } + ) { tab -> + tab.Content() + } + } + + else -> { + screen.Content() + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt similarity index 92% rename from app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt rename to app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt index 9eeeda76..b8339314 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt @@ -1,15 +1,9 @@ -package ua.acclorite.book_story.presentation.data +package ua.acclorite.book_story.ui.main import androidx.compose.runtime.Immutable -import kotlinx.coroutines.flow.StateFlow @Immutable sealed class MainEvent { - data class OnInit( - val libraryViewModelReady: StateFlow, - val settingsViewModelReady: StateFlow - ) : MainEvent() - data class OnChangeLanguage(val value: String) : MainEvent() data class OnChangeTheme(val value: String) : MainEvent() data class OnChangeDarkTheme(val value: String) : MainEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt similarity index 88% rename from app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt rename to app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt index 6a6c16ed..ec83bdf8 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt @@ -1,73 +1,61 @@ -package ua.acclorite.book_story.presentation.data +package ua.acclorite.book_story.ui.main -import androidx.compose.runtime.Composable import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure +import ua.acclorite.book_story.domain.browse.toBrowseLayout +import ua.acclorite.book_story.domain.browse.toBrowseSortOrder +import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation +import ua.acclorite.book_story.domain.reader.toTextAlignment import ua.acclorite.book_story.domain.use_case.data_store.ChangeLanguage import ua.acclorite.book_story.domain.use_case.data_store.GetAllSettings import ua.acclorite.book_story.domain.use_case.data_store.SetDatastore import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates -import ua.acclorite.book_story.domain.util.UIViewModel import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.DataStoreConstants import ua.acclorite.book_story.presentation.core.constants.provideFonts import ua.acclorite.book_story.presentation.core.constants.provideMainState -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseFilesStructure -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseLayout -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseSortOrder -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.toReaderScreenOrientation -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.toTextAlignment -import ua.acclorite.book_story.presentation.ui.toDarkTheme -import ua.acclorite.book_story.presentation.ui.toPureDark -import ua.acclorite.book_story.presentation.ui.toTheme -import ua.acclorite.book_story.presentation.ui.toThemeContrast +import ua.acclorite.book_story.ui.theme.toDarkTheme +import ua.acclorite.book_story.ui.theme.toPureDark +import ua.acclorite.book_story.ui.theme.toTheme +import ua.acclorite.book_story.ui.theme.toThemeContrast import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor( +class MainModel @Inject constructor( private val stateHandle: SavedStateHandle, private val setDatastore: SetDatastore, private val changeLanguage: ChangeLanguage, private val checkForUpdates: CheckForUpdates, private val getAllSettings: GetAllSettings -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } +) : ViewModel() { private val _isReady = MutableStateFlow(false) val isReady = _isReady.asStateFlow() - private val isSettingsReady = MutableStateFlow(false) - private val isViewModelReady = MutableStateFlow(false) + private val mainModelReady = MutableStateFlow(false) private val _state: MutableStateFlow = MutableStateFlow( stateHandle[Constants.provideMainState()] ?: MainState() ) - override val state = _state.asStateFlow() + val state = _state.asStateFlow() - override fun onEvent(event: MainEvent) { + fun onEvent(event: MainEvent) { when (event) { - is MainEvent.OnInit -> init(event) - is MainEvent.OnChangeLanguage -> handleLanguageUpdate(event) is MainEvent.OnChangeDarkTheme -> handleDatastoreUpdate( @@ -392,7 +380,7 @@ class MainViewModel @Inject constructor( } } - private fun init(event: MainEvent.OnInit) { + fun init(settingsModelReady: StateFlow) { viewModelScope.launch(Dispatchers.Main) { val settings = getAllSettings.execute() @@ -409,25 +397,12 @@ class MainViewModel @Inject constructor( /* - - - - - - - - - - - */ updateStateWithSavedHandle { settings } - isSettingsReady.update { true } - } - - viewModelScope.launch(Dispatchers.Main) { - combine( - event.libraryViewModelReady, - event.settingsViewModelReady - ) { (libraryViewModelReady, settingsViewModelReady) -> - libraryViewModelReady && settingsViewModelReady - }.collectLatest { ready -> - isViewModelReady.update { - ready - } - } + mainModelReady.update { true } } val isReady = combine( - isViewModelReady, - isSettingsReady + mainModelReady.asStateFlow(), + settingsModelReady ) { values -> values.all { it } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt similarity index 87% rename from app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt rename to app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt index 51114112..1a8eedec 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt @@ -1,34 +1,34 @@ @file:Suppress("UNCHECKED_CAST") -package ua.acclorite.book_story.presentation.data +package ua.acclorite.book_story.ui.main import android.os.Parcelable import androidx.annotation.Keep import androidx.compose.runtime.Immutable import androidx.datastore.preferences.core.Preferences import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.browse.BrowseFilesStructure +import ua.acclorite.book_story.domain.browse.BrowseLayout +import ua.acclorite.book_story.domain.browse.BrowseSortOrder +import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure +import ua.acclorite.book_story.domain.browse.toBrowseLayout +import ua.acclorite.book_story.domain.browse.toBrowseSortOrder +import ua.acclorite.book_story.domain.reader.ReaderScreenOrientation +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment +import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation +import ua.acclorite.book_story.domain.reader.toTextAlignment import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.DataStoreConstants import ua.acclorite.book_story.presentation.core.constants.provideFonts import ua.acclorite.book_story.presentation.core.constants.provideLanguages -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseSortOrder -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseFilesStructure -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseLayout -import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseSortOrder -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.ReaderScreenOrientation -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.ReaderTextAlignment -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.toReaderScreenOrientation -import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.toTextAlignment -import ua.acclorite.book_story.presentation.ui.DarkTheme -import ua.acclorite.book_story.presentation.ui.PureDark -import ua.acclorite.book_story.presentation.ui.Theme -import ua.acclorite.book_story.presentation.ui.ThemeContrast -import ua.acclorite.book_story.presentation.ui.toDarkTheme -import ua.acclorite.book_story.presentation.ui.toPureDark -import ua.acclorite.book_story.presentation.ui.toTheme -import ua.acclorite.book_story.presentation.ui.toThemeContrast +import ua.acclorite.book_story.ui.theme.DarkTheme +import ua.acclorite.book_story.ui.theme.PureDark +import ua.acclorite.book_story.ui.theme.Theme +import ua.acclorite.book_story.ui.theme.ThemeContrast +import ua.acclorite.book_story.ui.theme.toDarkTheme +import ua.acclorite.book_story.ui.theme.toPureDark +import ua.acclorite.book_story.ui.theme.toTheme +import ua.acclorite.book_story.ui.theme.toThemeContrast import java.util.Locale /** @@ -73,7 +73,7 @@ data class MainState( val cutoutPadding: Boolean = provideDefaultValue { false }, val fullscreen: Boolean = provideDefaultValue { true }, val keepScreenOn: Boolean = provideDefaultValue { true }, - val hideBarsOnFastScroll: Boolean = provideDefaultValue { true }, + val hideBarsOnFastScroll: Boolean = provideDefaultValue { false }, val perceptionExpander: Boolean = provideDefaultValue { false }, val perceptionExpanderPadding: Int = provideDefaultValue { 5 }, val perceptionExpanderThickness: Int = provideDefaultValue { 4 }, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderEvent.kt new file mode 100644 index 00000000..970c678b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderEvent.kt @@ -0,0 +1,88 @@ +package ua.acclorite.book_story.ui.reader + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Immutable + +@Immutable +sealed class ReaderEvent { + + data class OnLoadText( + val checkForTextUpdate: Boolean, + val checkForTextUpdateToast: Boolean, + val context: Context + ) : ReaderEvent() + + data class OnMenuVisibility( + val show: Boolean, + val fullscreenMode: Boolean, + val saveCheckpoint: Boolean, + val activity: ComponentActivity + ) : ReaderEvent() + + data class OnChangeProgress( + val progress: Float, + val firstVisibleItemIndex: Int, + val firstVisibleItemOffset: Int + ) : ReaderEvent() + + data class OnScrollToChapter( + val chapterStartIndex: Int + ) : ReaderEvent() + + data class OnScroll( + val progress: Float + ) : ReaderEvent() + + data object OnRestoreCheckpoint : ReaderEvent() + + data class OnLeave( + val activity: ComponentActivity, + val navigate: () -> Unit + ) : ReaderEvent() + + data class OnOpenTranslator( + val textToTranslate: String, + val translateWholeParagraph: Boolean, + val activity: ComponentActivity + ) : ReaderEvent() + + data class OnOpenShareApp( + val textToShare: String, + val activity: ComponentActivity + ) : ReaderEvent() + + data class OnOpenWebBrowser( + val textToSearch: String, + val activity: ComponentActivity + ) : ReaderEvent() + + data class OnOpenDictionary( + val textToDefine: String, + val activity: ComponentActivity + ) : ReaderEvent() + + data class OnCheckTextForUpdate( + val showToast: Boolean, + val context: Context + ) : ReaderEvent() + + data class OnUpdateText( + val activity: ComponentActivity, + val navigateToBookInfo: () -> Unit + ) : ReaderEvent() + + data object OnCancelCheckForTextUpdate : ReaderEvent() + + data object OnShowUpdateDialog : ReaderEvent() + + data object OnDismissDialog : ReaderEvent() + + data object OnShowSettingsBottomSheet : ReaderEvent() + + data object OnDismissBottomSheet : ReaderEvent() + + data object OnShowChaptersDrawer : ReaderEvent() + + data object OnDismissDrawer : ReaderEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderModel.kt similarity index 68% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt rename to app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderModel.kt index 22284dc0..2432c995 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderModel.kt @@ -1,14 +1,15 @@ -package ua.acclorite.book_story.presentation.screens.reader.data +package ua.acclorite.book_story.ui.reader import android.app.SearchManager import android.content.Intent import android.net.Uri import androidx.activity.ComponentActivity -import androidx.compose.runtime.Composable +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.snapshotFlow import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -26,71 +27,55 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Chapter +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.Checkpoint +import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.use_case.book.CheckForTextUpdate import ua.acclorite.book_story.domain.use_case.book.GetBookById import ua.acclorite.book_story.domain.use_case.book.GetText import ua.acclorite.book_story.domain.use_case.book.UpdateBook import ua.acclorite.book_story.domain.use_case.history.GetLatestHistory import ua.acclorite.book_story.domain.util.Resource -import ua.acclorite.book_story.domain.util.UIText -import ua.acclorite.book_story.domain.util.UIViewModel -import ua.acclorite.book_story.presentation.core.navigation.Screen import ua.acclorite.book_story.presentation.core.util.coerceAndPreventNaN import ua.acclorite.book_story.presentation.core.util.launchActivity import ua.acclorite.book_story.presentation.core.util.setBrightness +import ua.acclorite.book_story.presentation.core.util.showToast +import ua.acclorite.book_story.ui.history.HistoryScreen +import ua.acclorite.book_story.ui.library.LibraryScreen import javax.inject.Inject import kotlin.math.roundToInt -@Suppress("LABEL_NAME_CLASH") @HiltViewModel -class ReaderViewModel @Inject constructor( +class ReaderModel @Inject constructor( + private val getBookById: GetBookById, private val updateBook: UpdateBook, private val getText: GetText, private val getLatestHistory: GetLatestHistory, - private val getBookById: GetBookById, private val checkForTextUpdate: CheckForTextUpdate -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } +) : ViewModel() { private val _state = MutableStateFlow(ReaderState()) - override val state = _state.asStateFlow() + val state = _state.asStateFlow() private var eventJob = SupervisorJob() + private var scrollJob: Job? = null private var updateJob: Job? = null - override fun onEvent(event: ReaderEvent) { + fun onEvent(event: ReaderEvent) { viewModelScope.launch(eventJob + Dispatchers.Main) { when (event) { - is ReaderEvent.OnInit -> init(event) - - is ReaderEvent.OnUpdateProgress -> updateProgress(event) - - is ReaderEvent.OnClearViewModel -> clearViewModel() - - is ReaderEvent.OnTextIsEmpty -> { - _state.update { - it.copy( - loading = false, - errorMessage = UIText.StringResource(R.string.error_no_text) - ) - } - } - is ReaderEvent.OnLoadText -> { launch(Dispatchers.IO) { val text = getText.execute(_state.value.book.textPath) if (text.isEmpty()) { - event.onTextIsEmpty() + _state.update { + it.copy( + isLoading = false, + errorMessage = UIText.StringResource(R.string.error_no_text) + ) + } } _state.update { @@ -103,7 +88,9 @@ class ReaderViewModel @Inject constructor( } updateBook.execute(_state.value.book) - event.refreshList(_state.value.book) + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) launch { snapshotFlow { @@ -119,11 +106,18 @@ class ReaderViewModel @Inject constructor( updateChapter(index = scrollIndex) } - event.checkForUpdate() + if (event.checkForTextUpdate) { + onEvent( + ReaderEvent.OnCheckTextForUpdate( + showToast = event.checkForTextUpdateToast, + context = event.context + ) + ) + } _state.update { it.copy( - loading = false, + isLoading = false, errorMessage = null ) } @@ -134,17 +128,17 @@ class ReaderViewModel @Inject constructor( } } - is ReaderEvent.OnShowHideMenu -> { + is ReaderEvent.OnMenuVisibility -> { launch { if (_state.value.lockMenu) { return@launch } - val shouldShow = event.show ?: !_state.value.showMenu + val shouldShow = event.show yield() - showSystemBars( + systemBarsVisibility( show = shouldShow, fullscreenMode = event.fullscreenMode, activity = event.activity @@ -155,103 +149,13 @@ class ReaderViewModel @Inject constructor( checkpoint = _state.value.listState.run { if (!shouldShow || !event.saveCheckpoint) return@run it.checkpoint - firstVisibleItemIndex to firstVisibleItemScrollOffset + Checkpoint(firstVisibleItemIndex, firstVisibleItemScrollOffset) } ) } } } - is ReaderEvent.OnRestoreCheckpoint -> { - launch { - _state.value.listState.requestScrollToItem( - _state.value.checkpoint.first, - _state.value.checkpoint.second - ) - - onEvent( - ReaderEvent.OnChangeProgress( - progress = calculateProgress(_state.value.checkpoint.first), - firstVisibleItemIndex = _state.value.checkpoint.first, - firstVisibleItemOffset = _state.value.checkpoint.second, - refreshList = { book -> - event.refreshList(book) - } - ) - ) - } - } - - is ReaderEvent.OnGoBack -> { - launch { - yield() - - _state.update { - it.copy( - lockMenu = true - ) - } - - _state.value.listState.apply { - if ( - _state.value.loading - || layoutInfo.totalItemsCount < 1 - || _state.value.text.isEmpty() - ) return@apply - - _state.update { - it.copy( - book = it.book.copy( - progress = calculateProgress(), - scrollIndex = firstVisibleItemIndex, - scrollOffset = firstVisibleItemScrollOffset - ) - ) - } - - updateBook.execute(_state.value.book) - event.refreshList(_state.value.book) - } - - WindowCompat.getInsetsController( - event.context.window, - event.context.window.decorView - ).show(WindowInsetsCompat.Type.systemBars()) - event.context.setBrightness(brightness = null) - - event.navigate() - } - } - - is ReaderEvent.OnScroll -> { - scrollJob?.cancel() - scrollJob = launch { - delay(300) - - yield() - - val scrollTo = (_state.value.text.size * event.progress).roundToInt() - _state.value.listState.requestScrollToItem(scrollTo) - } - } - - is ReaderEvent.OnScrollToChapter -> { - launch { - _state.value.listState.requestScrollToItem( - event.chapterStartIndex - ) - updateChapter(index = event.chapterStartIndex) - onEvent( - ReaderEvent.OnChangeProgress( - progress = calculateProgress(event.chapterStartIndex), - firstVisibleItemIndex = event.chapterStartIndex, - firstVisibleItemOffset = 0, - refreshList = event.refreshList - ) - ) - } - } - is ReaderEvent.OnChangeProgress -> { launch(Dispatchers.IO) { _state.update { @@ -265,35 +169,99 @@ class ReaderViewModel @Inject constructor( } updateBook.execute(_state.value.book) - event.refreshList(_state.value.book) + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) } } - is ReaderEvent.OnShowHideSettingsBottomSheet -> { - _state.update { - it.copy( - showSettingsBottomSheet = event.show - ) - } - } - - is ReaderEvent.OnShowHideChaptersDrawer -> { - _state.update { - it.copy( - showChaptersDrawer = event.show - ) - } - } - - is ReaderEvent.OnScrollToSettingsPage -> { + is ReaderEvent.OnScrollToChapter -> { launch { - event.pagerState?.requestScrollToPage(event.page) + _state.value.listState.requestScrollToItem( + event.chapterStartIndex + ) + updateChapter(index = event.chapterStartIndex) + onEvent( + ReaderEvent.OnChangeProgress( + progress = calculateProgress(event.chapterStartIndex), + firstVisibleItemIndex = event.chapterStartIndex, + firstVisibleItemOffset = 0 + ) + ) + } + } + + is ReaderEvent.OnScroll -> { + scrollJob?.cancel() + scrollJob = launch { + delay(300) + + yield() + + val scrollTo = (_state.value.text.size * event.progress).roundToInt() + _state.value.listState.requestScrollToItem(scrollTo) + } + } + + is ReaderEvent.OnRestoreCheckpoint -> { + launch { + _state.value.apply { + listState.requestScrollToItem( + checkpoint.index, + checkpoint.offset + ) + + onEvent( + ReaderEvent.OnChangeProgress( + progress = calculateProgress(checkpoint.index), + firstVisibleItemIndex = checkpoint.index, + firstVisibleItemOffset = checkpoint.offset, + ) + ) + } + } + } + + is ReaderEvent.OnLeave -> { + launch { + yield() _state.update { it.copy( - currentPage = event.page + lockMenu = true ) } + + _state.value.listState.apply { + if ( + _state.value.isLoading + || layoutInfo.totalItemsCount < 1 + || _state.value.text.isEmpty() + ) return@apply + + _state.update { + it.copy( + book = it.book.copy( + progress = calculateProgress(), + scrollIndex = firstVisibleItemIndex, + scrollOffset = firstVisibleItemScrollOffset + ) + ) + } + + updateBook.execute(_state.value.book) + + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) + } + + WindowCompat.getInsetsController( + event.activity.window, + event.activity.window.decorView + ).show(WindowInsetsCompat.Type.systemBars()) + event.activity.setBrightness(brightness = null) + + event.navigate() } } @@ -304,7 +272,6 @@ class ReaderViewModel @Inject constructor( translatorIntent.type = "text/plain" translatorIntent.action = Intent.ACTION_PROCESS_TEXT - browserIntent.action = Intent.ACTION_WEB_SEARCH translatorIntent.putExtra( @@ -312,32 +279,29 @@ class ReaderViewModel @Inject constructor( event.textToTranslate ) translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true) - browserIntent.putExtra( SearchManager.QUERY, "translate: ${event.textToTranslate.trim()}" ) - var translatorFailure = false translatorIntent.launchActivity( - event.context, - createChooser = !event.translateWholeParagraph - ) { - translatorFailure = true - } - if (!translatorFailure) { - return@launch - } + activity = event.activity, + createChooser = !event.translateWholeParagraph, + success = { + return@launch + } + ) + browserIntent.launchActivity( + activity = event.activity, + success = { + return@launch + } + ) - var browserFailure = false - browserIntent.launchActivity(event.context) { - browserFailure = true + withContext(Dispatchers.Main) { + event.activity.getString(R.string.error_no_translator) + .showToast(context = event.activity, longToast = false) } - if (!browserFailure) { - return@launch - } - - event.noAppsFound() } } @@ -349,22 +313,25 @@ class ReaderViewModel @Inject constructor( shareIntent.type = "text/plain" shareIntent.putExtra( Intent.EXTRA_SUBJECT, - event.context.getString(R.string.app_name) + event.activity.getString(R.string.app_name) ) shareIntent.putExtra( Intent.EXTRA_TEXT, event.textToShare.trim() ) - var shareFailure = false - shareIntent.launchActivity(event.context, createChooser = true) { - shareFailure = true - } - if (!shareFailure) { - return@launch - } + shareIntent.launchActivity( + activity = event.activity, + createChooser = true, + success = { + return@launch + } + ) - event.noAppsFound() + withContext(Dispatchers.Main) { + event.activity.getString(R.string.error_no_share_app) + .showToast(context = event.activity, longToast = false) + } } } @@ -378,15 +345,17 @@ class ReaderViewModel @Inject constructor( event.textToSearch ) - var browserFailure = false - browserIntent.launchActivity(event.context) { - browserFailure = true - } - if (!browserFailure) { - return@launch - } + browserIntent.launchActivity( + activity = event.activity, + success = { + return@launch + } + ) - event.noAppsFound() + withContext(Dispatchers.Main) { + event.activity.getString(R.string.error_no_browser) + .showToast(context = event.activity, longToast = false) + } } } @@ -407,23 +376,25 @@ class ReaderViewModel @Inject constructor( val text = event.textToDefine.trim().replace(" ", "+") browserIntent.data = Uri.parse("https://www.onelook.com/?w=$text") - var dictionaryFailure = false - dictionaryIntent.launchActivity(event.context, createChooser = true) { - dictionaryFailure = true - } - if (!dictionaryFailure) { - return@launch - } + dictionaryIntent.launchActivity( + activity = event.activity, + createChooser = true, + success = { + return@launch + } + ) - var browserFailure = false - browserIntent.launchActivity(event.context) { - browserFailure = true - } - if (!browserFailure) { - return@launch - } + browserIntent.launchActivity( + activity = event.activity, + success = { + return@launch + } + ) - event.noAppsFound() + withContext(Dispatchers.Main) { + event.activity.getString(R.string.error_no_dictionary) + .showToast(context = event.activity, longToast = false) + } } } @@ -434,7 +405,7 @@ class ReaderViewModel @Inject constructor( it.copy( checkingForUpdate = true, updateFound = false, - showUpdateDialog = false + dialog = null ) } @@ -448,7 +419,13 @@ class ReaderViewModel @Inject constructor( is Resource.Success -> { if (result.data == null) { withContext(Dispatchers.Main) { - event.noUpdateFound() + if (event.showToast) { + event.context.getString(R.string.nothing_changed) + .showToast( + context = event.context, + longToast = false + ) + } } _state.update { it.copy( @@ -460,11 +437,11 @@ class ReaderViewModel @Inject constructor( } else { _state.update { it.copy( + dialog = ReaderScreen.UPDATE_DIALOG, updateFound = true, checkingForUpdate = false ) } - onEvent(ReaderEvent.OnShowHideUpdateDialog(true)) return@launch } } @@ -482,53 +459,94 @@ class ReaderViewModel @Inject constructor( } } - is ReaderEvent.OnShowHideUpdateDialog -> { - _state.update { - it.copy( - showUpdateDialog = event.show + is ReaderEvent.OnUpdateText -> { + launch { + systemBarsVisibility( + show = true, + fullscreenMode = true, + activity = event.activity ) + event.navigateToBookInfo() } } - is ReaderEvent.OnCancelCheckTextForUpdate -> { + is ReaderEvent.OnCancelCheckForTextUpdate -> { _state.update { updateJob?.cancel() it.copy( - showUpdateDialog = false, + dialog = null, checkingForUpdate = false ) } } - is ReaderEvent.OnUpdateText -> { - launch { - showSystemBars( - show = true, - activity = event.activity + is ReaderEvent.OnShowUpdateDialog -> { + _state.update { + it.copy( + dialog = ReaderScreen.UPDATE_DIALOG + ) + } + } + + is ReaderEvent.OnDismissDialog -> { + _state.update { + it.copy( + dialog = null + ) + } + } + + is ReaderEvent.OnShowSettingsBottomSheet -> { + _state.update { + it.copy( + bottomSheet = ReaderScreen.SETTINGS_BOTTOM_SHEET, + drawer = null + ) + } + } + + is ReaderEvent.OnDismissBottomSheet -> { + _state.update { + it.copy( + bottomSheet = null + ) + } + } + + is ReaderEvent.OnShowChaptersDrawer -> { + _state.update { + it.copy( + drawer = ReaderScreen.CHAPTERS_DRAWER, + bottomSheet = null + ) + } + } + + is ReaderEvent.OnDismissDrawer -> { + _state.update { + it.copy( + drawer = null ) - event.onNavigate { - navigate( - Screen.BookInfo( - bookId = _state.value.book.id, - startUpdate = true - ), - useBackAnimation = true, - saveInBackStack = false - ) - } } } } } } - private fun init(event: ReaderEvent.OnInit) { + fun init( + bookId: Int, + fullscreenMode: Boolean, + checkForTextUpdate: Boolean, + checkForTextUpdateToast: Boolean, + activity: ComponentActivity, + navigateBack: () -> Unit + ) { viewModelScope.launch(Dispatchers.IO) { - val book = getBookById.execute(event.screen.bookId) + val book = getBookById.execute(bookId) if (book == null) { - event.navigateBack() + navigateBack() return@launch } @@ -536,46 +554,32 @@ class ReaderViewModel @Inject constructor( ReaderState(book = book) } - clear() - withContext(Dispatchers.Main) { - onEvent( - ReaderEvent.OnShowHideMenu( - show = false, - fullscreenMode = event.fullscreenMode, - saveCheckpoint = false, - activity = event.activity - ) - ) - } + eventJob.cancel() + eventJob = SupervisorJob() + onEvent( + ReaderEvent.OnMenuVisibility( + show = false, + fullscreenMode = fullscreenMode, + saveCheckpoint = false, + activity = activity + ) + ) onEvent( ReaderEvent.OnLoadText( - checkForUpdate = { - if (event.checkForTextUpdate) { - onEvent( - ReaderEvent.OnCheckTextForUpdate { - event.checkForTextUpdateToast() - } - ) - } - }, - refreshList = { event.refreshList(it) }, - onError = { - event.onError(it) - }, - onTextIsEmpty = { - onEvent(ReaderEvent.OnTextIsEmpty) - } + checkForTextUpdate = checkForTextUpdate, + checkForTextUpdateToast = checkForTextUpdateToast, + context = activity ) ) } } @OptIn(FlowPreview::class) - private fun updateProgress(event: ReaderEvent.OnUpdateProgress) { + fun updateProgress(listState: LazyListState) { viewModelScope.launch(Dispatchers.Main) { snapshotFlow { - _state.value.listState.run { firstVisibleItemIndex to firstVisibleItemScrollOffset } + listState.run { firstVisibleItemIndex to firstVisibleItemScrollOffset } }.distinctUntilChanged().debounce(300).collectLatest { (index, offset) -> val progress = calculateProgress(index) if (progress == _state.value.book.progress) return@collectLatest @@ -594,19 +598,10 @@ class ReaderViewModel @Inject constructor( } updateBook.execute(_state.value.book) - event.refreshList(_state.value.book) - } - } - } - private fun clearViewModel() { - viewModelScope.launch(Dispatchers.Main) { - _state.update { - ReaderState() + LibraryScreen.refreshListChannel.trySend(Unit) + HistoryScreen.refreshListChannel.trySend(Unit) } - - eventJob.cancel() - eventJob = SupervisorJob() } } @@ -638,7 +633,7 @@ class ReaderViewModel @Inject constructor( private fun calculateProgress(firstVisibleItemIndex: Int? = null): Float { return _state.value.run { if ( - loading || + isLoading || listState.layoutInfo.totalItemsCount == 0 || text.isEmpty() ) { @@ -660,11 +655,11 @@ class ReaderViewModel @Inject constructor( } } - private fun showSystemBars( + private fun systemBarsVisibility( show: Boolean, - fullscreenMode: Boolean = true, + fullscreenMode: Boolean, activity: ComponentActivity - ): Boolean { + ) { WindowCompat.getInsetsController( activity.window, activity.window.decorView @@ -673,12 +668,16 @@ class ReaderViewModel @Inject constructor( if (show || !fullscreenMode) show(WindowInsetsCompat.Type.systemBars()) else hide(WindowInsetsCompat.Type.systemBars()) } - - return show || !fullscreenMode } - private fun clear() { - eventJob.cancel() - eventJob = SupervisorJob() + fun resetScreen() { + viewModelScope.launch(Dispatchers.Main) { + _state.update { + ReaderState() + } + + eventJob.cancel() + eventJob = SupervisorJob() + } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderScreen.kt new file mode 100644 index 00000000..9916e8ef --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderScreen.kt @@ -0,0 +1,367 @@ +package ua.acclorite.book_story.ui.reader + +import android.content.pm.ActivityInfo +import android.os.Parcelable +import android.view.WindowManager +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.systemBarsIgnoringVisibility +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.ReaderTextAlignment +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideFonts +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.presentation.core.util.setBrightness +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.reader.ReaderContent +import ua.acclorite.book_story.ui.book_info.BookInfoScreen +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.settings.SettingsModel + +@Parcelize +data class ReaderScreen(val bookId: Int) : Screen, Parcelable { + + companion object { + const val UPDATE_DIALOG = "update_dialog" + const val CHAPTERS_DRAWER = "chapters_drawer" + const val SETTINGS_BOTTOM_SHEET = "settings_bottom_sheet" + } + + @OptIn(ExperimentalLayoutApi::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + val mainModel = hiltViewModel() + val settingsModel = hiltViewModel() + + val state = screenModel.state.collectAsStateWithLifecycle() + val mainState = mainModel.state.collectAsStateWithLifecycle() + val settingsState = settingsModel.state.collectAsStateWithLifecycle() + + val activity = LocalActivity.current + val listState = rememberSaveable( + state.value.listState, + saver = LazyListState.Saver + ) { + state.value.listState + } + val nestedScrollConnection = remember { + derivedStateOf { + object : NestedScrollConnection { + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + consumed.y.let { velocity -> + if (velocity in -70f..70f) return@let + if (!state.value.showMenu) return@let + if (state.value.lockMenu) return@let + if (!mainState.value.hideBarsOnFastScroll) return@let + + screenModel.onEvent( + ReaderEvent.OnMenuVisibility( + show = false, + fullscreenMode = mainState.value.fullscreen, + saveCheckpoint = false, + activity = activity + ) + ) + } + return super.onPostScroll(consumed, available, source) + } + } + } + } + + val fontFamily = remember(mainState.value.fontFamily) { + Constants.provideFonts(withRandom = true).find { + it.id == mainState.value.fontFamily + } ?: Constants.provideFonts(withRandom = false)[0] + } + val backgroundColor = animateColorAsState( + targetValue = settingsState.value.selectedColorPreset.backgroundColor + ) + val fontColor = animateColorAsState( + targetValue = settingsState.value.selectedColorPreset.fontColor + ) + val lineHeight = remember( + mainState.value.fontSize, + mainState.value.lineHeight + ) { + (mainState.value.fontSize + mainState.value.lineHeight).sp + } + val letterSpacing = remember(mainState.value.letterSpacing) { + (mainState.value.letterSpacing / 100f).em + } + val sidePadding = remember(mainState.value.sidePadding) { + (mainState.value.sidePadding * 3).dp + } + val verticalPadding = remember(mainState.value.verticalPadding) { + (mainState.value.verticalPadding * 4.5f).dp + } + val paragraphHeight = remember(mainState.value.paragraphHeight) { + (mainState.value.paragraphHeight * 3).dp + } + val fontStyle = remember(mainState.value.isItalic) { + when (mainState.value.isItalic) { + true -> FontStyle.Italic + false -> FontStyle.Normal + } + } + val paragraphIndentation = remember( + mainState.value.paragraphIndentation, + mainState.value.textAlignment + ) { + if ( + mainState.value.textAlignment == ReaderTextAlignment.CENTER || + mainState.value.textAlignment == ReaderTextAlignment.END + ) { + return@remember 0.sp + } + (mainState.value.paragraphIndentation * 6).sp + } + val perceptionExpanderPadding = remember( + sidePadding, + mainState.value.perceptionExpanderPadding + ) { + sidePadding + (mainState.value.perceptionExpanderPadding * 8).dp + } + val perceptionExpanderThickness = remember( + mainState.value.perceptionExpanderThickness + ) { + (mainState.value.perceptionExpanderThickness * 0.25f).dp + } + + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val cutoutInsets = WindowInsets.displayCutout + val systemBarsInsets = WindowInsets.systemBarsIgnoringVisibility + + val cutoutInsetsPadding = remember(mainState.value.cutoutPadding) { + derivedStateOf { + cutoutInsets.asPaddingValues(density = density).run { + if (mainState.value.cutoutPadding) PaddingValues( + top = calculateTopPadding(), + start = calculateStartPadding(layoutDirection), + end = calculateEndPadding(layoutDirection), + bottom = calculateBottomPadding() + ) else PaddingValues(0.dp) + } + } + } + val systemBarsInsetsPadding = remember(mainState.value.fullscreen) { + derivedStateOf { + systemBarsInsets.asPaddingValues(density = density).run { + if (!mainState.value.fullscreen) PaddingValues( + top = calculateTopPadding(), + start = calculateStartPadding(layoutDirection), + end = calculateEndPadding(layoutDirection), + bottom = calculateBottomPadding() + ) else PaddingValues(0.dp) + } + } + } + val contentPadding = remember( + cutoutInsetsPadding.value, + systemBarsInsetsPadding.value + ) { + PaddingValues( + top = systemBarsInsetsPadding.value.calculateTopPadding().run { + if (equals(0.dp)) return@run cutoutInsetsPadding.value + .calculateTopPadding() + this + }, + start = systemBarsInsetsPadding.value.calculateStartPadding(layoutDirection).run { + if (equals(0.dp)) return@run cutoutInsetsPadding.value + .calculateStartPadding(layoutDirection) + this + }, + end = systemBarsInsetsPadding.value.calculateEndPadding(layoutDirection).run { + if (equals(0.dp)) return@run cutoutInsetsPadding.value + .calculateEndPadding(layoutDirection) + this + }, + bottom = systemBarsInsetsPadding.value.calculateBottomPadding().run { + if (equals(0.dp)) return@run cutoutInsetsPadding.value + .calculateBottomPadding() + this + } + ) + } + + val chapters = remember(state.value.book.chapters) { + val chapters = mutableMapOf() + state.value.book.chapters.forEach { + chapters[it.startIndex] = it + } + chapters + } + + LaunchedEffect(Unit) { + screenModel.init( + bookId = bookId, + fullscreenMode = mainState.value.fullscreen, + checkForTextUpdate = mainState.value.checkForTextUpdate, + checkForTextUpdateToast = mainState.value.checkForTextUpdateToast, + activity = activity, + navigateBack = { + navigator.pop() + } + ) + } + LaunchedEffect(mainState.value.fullscreen) { + screenModel.onEvent( + ReaderEvent.OnMenuVisibility( + show = state.value.showMenu, + fullscreenMode = mainState.value.fullscreen, + saveCheckpoint = false, + activity = activity + ) + ) + } + LaunchedEffect(listState) { + screenModel.updateProgress(listState) + } + + DisposableEffect(mainState.value.screenOrientation) { + activity.requestedOrientation = mainState.value.screenOrientation.code + onDispose { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + } + DisposableEffect( + mainState.value.screenBrightness, + mainState.value.customScreenBrightness + ) { + when (mainState.value.customScreenBrightness) { + true -> activity.setBrightness(brightness = mainState.value.screenBrightness) + false -> activity.setBrightness(brightness = null) + } + + onDispose { + activity.setBrightness(brightness = null) + } + } + DisposableEffect(mainState.value.keepScreenOn) { + when (mainState.value.keepScreenOn) { + true -> activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + false -> activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + onDispose { + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + DisposableEffect(Unit) { + onDispose { + screenModel.resetScreen() + WindowCompat.getInsetsController( + activity.window, + activity.window.decorView + ).show(WindowInsetsCompat.Type.systemBars()) + } + } + + ReaderContent( + book = state.value.book, + text = state.value.text, + dialog = state.value.dialog, + bottomSheet = state.value.bottomSheet, + drawer = state.value.drawer, + listState = listState, + currentChapter = state.value.currentChapter, + nestedScrollConnection = nestedScrollConnection.value, + fastColorPresetChange = mainState.value.fastColorPresetChange, + perceptionExpander = mainState.value.perceptionExpander, + perceptionExpanderPadding = perceptionExpanderPadding, + perceptionExpanderThickness = perceptionExpanderThickness, + currentChapterProgress = state.value.currentChapterProgress, + isLoading = state.value.isLoading, + errorMessage = state.value.errorMessage, + checkpoint = state.value.checkpoint, + checkingForUpdate = state.value.checkingForUpdate, + updateFound = state.value.updateFound, + showMenu = state.value.showMenu, + lockMenu = state.value.lockMenu, + chapters = chapters, + contentPadding = contentPadding, + verticalPadding = verticalPadding, + paragraphHeight = paragraphHeight, + sidePadding = sidePadding, + backgroundColor = backgroundColor.value, + fontColor = fontColor.value, + fontFamily = fontFamily, + lineHeight = lineHeight, + fontStyle = fontStyle, + textAlignment = mainState.value.textAlignment, + fontSize = mainState.value.fontSize.sp, + letterSpacing = letterSpacing, + paragraphIndentation = paragraphIndentation, + doubleClickTranslation = mainState.value.doubleClickTranslation, + fullscreenMode = mainState.value.fullscreen, + selectPreviousPreset = settingsModel::onEvent, + selectNextPreset = settingsModel::onEvent, + leave = screenModel::onEvent, + restoreCheckpoint = screenModel::onEvent, + scroll = screenModel::onEvent, + changeProgress = screenModel::onEvent, + menuVisibility = screenModel::onEvent, + openShareApp = screenModel::onEvent, + openWebBrowser = screenModel::onEvent, + openTranslator = screenModel::onEvent, + openDictionary = screenModel::onEvent, + scrollToChapter = screenModel::onEvent, + updateText = screenModel::onEvent, + cancelCheckForTextUpdate = screenModel::onEvent, + showUpdateDialog = screenModel::onEvent, + dismissDialog = screenModel::onEvent, + showSettingsBottomSheet = screenModel::onEvent, + dismissBottomSheet = screenModel::onEvent, + showChaptersDrawer = screenModel::onEvent, + dismissDrawer = screenModel::onEvent, + navigateBack = { + navigator.pop() + }, + navigateToBookInfo = { startUpdate -> + if (startUpdate) BookInfoScreen.startUpdateChannel.trySend(true) + navigator.push( + BookInfoScreen( + bookId = bookId, + ), + popping = true, + saveInBackStack = false + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderState.kt similarity index 55% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt rename to app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderState.kt index 057fe8d2..e72023ba 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/reader/ReaderState.kt @@ -1,11 +1,15 @@ -package ua.acclorite.book_story.presentation.screens.reader.data +package ua.acclorite.book_story.ui.reader import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Immutable import androidx.compose.ui.text.AnnotatedString -import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.Chapter -import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.reader.Chapter +import ua.acclorite.book_story.domain.reader.Checkpoint +import ua.acclorite.book_story.domain.ui.UIText +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.domain.util.Dialog +import ua.acclorite.book_story.domain.util.Drawer import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideEmptyBook @@ -17,19 +21,18 @@ data class ReaderState( val currentChapter: Chapter? = null, val currentChapterProgress: Float = 0f, - val showChaptersDrawer: Boolean = false, val errorMessage: UIText? = null, - val loading: Boolean = true, + val isLoading: Boolean = true, val showMenu: Boolean = false, - val checkpoint: Pair = 0 to 0, + val checkpoint: Checkpoint = Checkpoint(0, 0), val lockMenu: Boolean = false, val checkingForUpdate: Boolean = false, val updateFound: Boolean = false, - val showUpdateDialog: Boolean = false, - val showSettingsBottomSheet: Boolean = false, - val currentPage: Int = 0, + val dialog: Dialog? = null, + val bottomSheet: BottomSheet? = null, + val drawer: Drawer? = null ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/AppearanceSettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/AppearanceSettingsScreen.kt new file mode 100644 index 00000000..17e4f3d7 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/AppearanceSettingsScreen.kt @@ -0,0 +1,30 @@ +package ua.acclorite.book_story.ui.settings + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.settings.appearance.AppearanceSettingsContent + +@Parcelize +object AppearanceSettingsScreen : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + AppearanceSettingsContent( + scrollBehavior = scrollBehavior, + listState = listState, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/BrowseSettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/BrowseSettingsScreen.kt new file mode 100644 index 00000000..3678c66d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/BrowseSettingsScreen.kt @@ -0,0 +1,30 @@ +package ua.acclorite.book_story.ui.settings + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.settings.browse.BrowseSettingsContent + +@Parcelize +object BrowseSettingsScreen : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + BrowseSettingsContent( + listState = listState, + scrollBehavior = scrollBehavior, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/GeneralSettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/GeneralSettingsScreen.kt new file mode 100644 index 00000000..8f0ad98e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/GeneralSettingsScreen.kt @@ -0,0 +1,30 @@ +package ua.acclorite.book_story.ui.settings + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.settings.general.GeneralSettingsContent + +@Parcelize +object GeneralSettingsScreen : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + GeneralSettingsContent( + listState = listState, + scrollBehavior = scrollBehavior, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/ReaderSettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/ReaderSettingsScreen.kt new file mode 100644 index 00000000..45fc83d1 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/ReaderSettingsScreen.kt @@ -0,0 +1,30 @@ +package ua.acclorite.book_story.ui.settings + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.settings.reader.ReaderSettingsContent + +@Parcelize +object ReaderSettingsScreen : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + ReaderSettingsContent( + scrollBehavior = scrollBehavior, + listState = listState, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt similarity index 52% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt rename to app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt index 5922da77..d870e906 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt @@ -1,44 +1,69 @@ @file:OptIn(ExperimentalPermissionsApi::class) -package ua.acclorite.book_story.presentation.screens.settings.data +package ua.acclorite.book_story.ui.settings +import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import ua.acclorite.book_story.domain.util.ID -import ua.acclorite.book_story.domain.util.UIText @Immutable sealed class SettingsEvent { - data class OnGeneralChangeCheckForUpdates( + + data class OnChangeCheckForUpdates( val enable: Boolean, val activity: ComponentActivity, val notificationsPermissionState: PermissionState, val onChangeCheckForUpdates: (Boolean) -> Unit ) : SettingsEvent() + data class OnSelectColorPreset( + val id: ID + ) : SettingsEvent() + data class OnSelectPreviousPreset( - val onSelected: suspend (name: UIText) -> Unit + val context: Context ) : SettingsEvent() data class OnSelectNextPreset( - val onSelected: suspend (name: UIText) -> Unit + val context: Context + ) : SettingsEvent() + + data class OnDeleteColorPreset( + val id: ID + ) : SettingsEvent() + + data class OnUpdateColorPresetTitle( + val id: ID, + val title: String + ) : SettingsEvent() + + data class OnShuffleColorPreset( + val id: ID + ) : SettingsEvent() + + data class OnAddColorPreset( + val backgroundColor: Color, + val fontColor: Color + ) : SettingsEvent() + + data class OnReorderColorPresets( + val from: Int, + val to: Int ) : SettingsEvent() - data class OnSelectColorPreset(val id: ID) : SettingsEvent() - data class OnDeleteColorPreset(val id: ID) : SettingsEvent() - data class OnScrollToColorPreset(val scrollTo: Int) : SettingsEvent() - data class OnUpdateColorPresetTitle(val id: ID, val title: String) : SettingsEvent() - data class OnShuffleColorPreset(val id: ID) : SettingsEvent() data class OnUpdateColorPresetColor( val id: ID, val backgroundColor: Color?, val fontColor: Color? ) : SettingsEvent() - data class OnAddColorPreset(val backgroundColor: Color, val fontColor: Color) : SettingsEvent() - data class OnReorderColorPresets(val from: Int, val to: Int) : SettingsEvent() data object OnConfirmReorderColorPresets : SettingsEvent() + + data class OnScrollToColorPreset( + val index: Int + ) : SettingsEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt similarity index 76% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt rename to app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt index a834905d..58eac220 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt @@ -1,9 +1,10 @@ -package ua.acclorite.book_story.presentation.screens.settings.data +package ua.acclorite.book_story.ui.settings import android.content.Intent import android.os.Build import android.provider.Settings -import androidx.compose.runtime.Composable +import android.util.Log +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted @@ -16,43 +17,33 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.use_case.color_preset.DeleteColorPreset import ua.acclorite.book_story.domain.use_case.color_preset.GetColorPresets import ua.acclorite.book_story.domain.use_case.color_preset.ReorderColorPresets import ua.acclorite.book_story.domain.use_case.color_preset.SelectColorPreset import ua.acclorite.book_story.domain.use_case.color_preset.UpdateColorPreset -import ua.acclorite.book_story.domain.util.ID -import ua.acclorite.book_story.domain.util.UIText -import ua.acclorite.book_story.domain.util.UIViewModel import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideDefaultColorPreset import ua.acclorite.book_story.presentation.core.util.launchActivity +import ua.acclorite.book_story.presentation.core.util.showToast import javax.inject.Inject import kotlin.random.Random -@OptIn(ExperimentalPermissionsApi::class) @HiltViewModel -class SettingsViewModel @Inject constructor( +class SettingsModel @Inject constructor( private val getColorPresets: GetColorPresets, private val updateColorPreset: UpdateColorPreset, - private val deleteColorPreset: DeleteColorPreset, private val selectColorPreset: SelectColorPreset, - private val reorderColorPresets: ReorderColorPresets -) : UIViewModel() { - - companion object { - @Composable - fun getState() = getState() - - @Composable - fun getEvent() = getEvent() - } + private val reorderColorPresets: ReorderColorPresets, + private val deleteColorPreset: DeleteColorPreset +) : ViewModel() { private val _state = MutableStateFlow(SettingsState()) - override val state = _state.asStateFlow() + val state = _state.asStateFlow() private val _isReady = MutableStateFlow(false) val isReady = _isReady.asStateFlow() @@ -71,7 +62,7 @@ class SettingsViewModel @Inject constructor( if (colorPresets.isEmpty()) { updateColorPreset.execute(Constants.provideDefaultColorPreset()) - onSelectColorPreset(getColorPresets.execute().first()) + getColorPresets.execute().first().select() colorPresets = getColorPresets.execute() } @@ -81,9 +72,7 @@ class SettingsViewModel @Inject constructor( if (scrollIndex != -1) { launch(Dispatchers.Main) { try { - _state.value.colorPresetsListState.requestScrollToItem( - index = scrollIndex - ) + _state.value.colorPresetListState.requestScrollToItem(index = scrollIndex) } catch (e: Exception) { e.printStackTrace() } @@ -92,20 +81,20 @@ class SettingsViewModel @Inject constructor( _state.update { it.copy( - selectedColorPreset = selectedColorPreset(colorPresets), + selectedColorPreset = colorPresets.selected(), colorPresets = colorPresets ) } - _isReady.update { - true - } + + Log.i("SETTINGS", "SettingsModel is ready.") + _isReady.update { true } } } - @Suppress("LABEL_NAME_CLASH") - override fun onEvent(event: SettingsEvent) { + @OptIn(ExperimentalPermissionsApi::class) + fun onEvent(event: SettingsEvent) { when (event) { - is SettingsEvent.OnGeneralChangeCheckForUpdates -> { + is SettingsEvent.OnChangeCheckForUpdates -> { if (!event.enable) { event.onChangeCheckForUpdates(false) return @@ -127,12 +116,7 @@ class SettingsViewModel @Inject constructor( val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName) - var failure = false intent.launchActivity(event.activity) { - failure = true - } - - if (failure) { return } } @@ -154,6 +138,28 @@ class SettingsViewModel @Inject constructor( } } + is SettingsEvent.OnSelectColorPreset -> { + viewModelScope.launch { + cancelColorPresetJobs() + selectColorPresetJob = launch { + val colorPreset = event.id.getColorPresetById() ?: return@launch + + yield() + + colorPreset.select(animate = true) + val colorPresets = _state.value.colorPresets.map { + it.copy(isSelected = colorPreset.id == it.id) + } + _state.update { + it.copy( + selectedColorPreset = colorPresets.selected(), + colorPresets = colorPresets + ) + } + } + } + } + is SettingsEvent.OnSelectPreviousPreset -> { viewModelScope.launch { cancelColorPresetJobs() @@ -179,33 +185,35 @@ class SettingsViewModel @Inject constructor( selectedPresetIndex - 1 } } - val previousColorPreset = - colorPresets.getOrNull(previousColorPresetIndex) - ?: return@launch + val previousColorPreset = colorPresets.getOrNull(previousColorPresetIndex) + ?: return@launch yield() - onSelectColorPreset(previousColorPreset) + previousColorPreset.select() val updatedColorPresets = _state.value.colorPresets.map { it.copy(isSelected = previousColorPreset.id == it.id) } _state.update { it.copy( - selectedColorPreset = selectedColorPreset(updatedColorPresets), + selectedColorPreset = updatedColorPresets.selected(), colorPresets = updatedColorPresets ) } - event.onSelected( - if ((previousColorPreset.name ?: "").isBlank()) { - UIText.StringResource( - R.string.color_preset_query, - previousColorPreset.id.toString() - ) - } else { - UIText.StringValue(previousColorPreset.name!!) - } - ) + withContext(Dispatchers.Main) { + event.context.getString( + R.string.color_preset_selected_query, + if (previousColorPreset.name.isNullOrBlank()) { + event.context.getString( + R.string.color_preset_query, + previousColorPreset.id.toString() + ) + } else { + previousColorPreset.name + }.trim() + ).showToast(event.context, longToast = false) + } } } } @@ -240,48 +248,29 @@ class SettingsViewModel @Inject constructor( yield() - onSelectColorPreset(nextColorPreset) + nextColorPreset.select() val updatedColorPresets = _state.value.colorPresets.map { it.copy(isSelected = nextColorPreset.id == it.id) } _state.update { it.copy( - selectedColorPreset = selectedColorPreset(updatedColorPresets), + selectedColorPreset = updatedColorPresets.selected(), colorPresets = updatedColorPresets ) } - event.onSelected( - if ((nextColorPreset.name ?: "").isBlank()) { - UIText.StringResource( - R.string.color_preset_query, - nextColorPreset.id.toString() - ) - } else { - UIText.StringValue(nextColorPreset.name!!) - } - ) - } - } - } - - is SettingsEvent.OnSelectColorPreset -> { - viewModelScope.launch { - cancelColorPresetJobs() - selectColorPresetJob = launch { - val colorPreset = getColorPresetById(event.id) ?: return@launch - - yield() - - onSelectColorPreset(colorPreset, animate = true) - val colorPresets = _state.value.colorPresets.map { - it.copy(isSelected = colorPreset.id == it.id) - } - _state.update { - it.copy( - selectedColorPreset = selectedColorPreset(colorPresets), - colorPresets = colorPresets - ) + withContext(Dispatchers.Main) { + event.context.getString( + R.string.color_preset_selected_query, + if (nextColorPreset.name.isNullOrBlank()) { + event.context.getString( + R.string.color_preset_query, + nextColorPreset.id.toString() + ) + } else { + nextColorPreset.name + }.trim() + ).showToast(event.context, longToast = false) } } } @@ -292,7 +281,7 @@ class SettingsViewModel @Inject constructor( cancelColorPresetJobs() deleteColorPresetJob = launch { if (_state.value.colorPresets.size == 1) return@launch - val colorPreset = getColorPresetById(event.id) ?: return@launch + val colorPreset = event.id.getColorPresetById() ?: return@launch yield() @@ -301,12 +290,11 @@ class SettingsViewModel @Inject constructor( return@launch } - val nextPosition = - if (position == _state.value.colorPresets.lastIndex) { - position - 1 - } else { - position - } + val nextPosition = if (position == _state.value.colorPresets.lastIndex) { + position - 1 + } else { + position + } yield() @@ -314,29 +302,21 @@ class SettingsViewModel @Inject constructor( val nextColorPreset = getColorPresets.execute().getOrNull(nextPosition) ?: return@launch - onSelectColorPreset(nextColorPreset) + nextColorPreset.select() val colorPresets = getColorPresets.execute() _state.update { it.copy( - selectedColorPreset = selectedColorPreset(colorPresets), + selectedColorPreset = colorPresets.selected(), colorPresets = colorPresets ) } - onEvent(SettingsEvent.OnScrollToColorPreset(nextPosition)) - } - } - } - - is SettingsEvent.OnScrollToColorPreset -> { - viewModelScope.launch { - try { - _state.value.colorPresetsListState.requestScrollToItem( - event.scrollTo + onEvent( + SettingsEvent.OnScrollToColorPreset( + index = nextPosition + ) ) - } catch (e: Exception) { - e.printStackTrace() } } } @@ -345,7 +325,7 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { cancelColorPresetJobs() updateTitleColorPresetJob = launch { - val colorPreset = getColorPresetById(event.id) ?: return@launch + val colorPreset = event.id.getColorPresetById() ?: return@launch yield() @@ -372,7 +352,7 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { cancelColorPresetJobs() shuffleColorPresetJob = launch { - val colorPreset = getColorPresetById(event.id) ?: return@launch + val colorPreset = event.id.getColorPresetById() ?: return@launch yield() @@ -416,12 +396,12 @@ class SettingsViewModel @Inject constructor( ) updateColorPreset.execute(newColorPreset) - onSelectColorPreset(getColorPresets.execute().last()) + getColorPresets.execute().last().select() val colorPresets = getColorPresets.execute() _state.update { it.copy( - selectedColorPreset = selectedColorPreset(colorPresets), + selectedColorPreset = colorPresets.selected(), colorPresets = colorPresets ) } @@ -435,7 +415,7 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { cancelColorPresetJobs() updateColorColorPresetJob = launch { - val colorPreset = getColorPresetById(event.id) ?: return@launch + val colorPreset = event.id.getColorPresetById() ?: return@launch yield() @@ -465,8 +445,9 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { cancelColorPresetJobs() launch { - val reorderedColorPresets = - _state.value.colorPresets.toMutableList().apply { + val reorderedColorPresets = _state.value.colorPresets + .toMutableList() + .apply { add(event.to, removeAt(event.from)) } @@ -487,20 +468,23 @@ class SettingsViewModel @Inject constructor( } } } + + is SettingsEvent.OnScrollToColorPreset -> { + viewModelScope.launch { + try { + _state.value.colorPresetListState.requestScrollToItem( + index = event.index + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + } } } - private fun cancelColorPresetJobs() { - selectColorPresetJob?.cancel() - addColorPresetJob?.cancel() - updateTitleColorPresetJob?.cancel() - shuffleColorPresetJob?.cancel() - updateColorColorPresetJob?.cancel() - deleteColorPresetJob?.cancel() - } - - private suspend fun onSelectColorPreset(colorPreset: ColorPreset, animate: Boolean = false) { - selectColorPreset.execute(colorPreset) + private suspend fun ColorPreset.select(animate: Boolean = false) { + selectColorPreset.execute(this) _state.update { it.copy( animateColorPreset = animate @@ -508,14 +492,14 @@ class SettingsViewModel @Inject constructor( } } - private fun getColorPresetById(id: ID): ColorPreset? { + private fun Int.getColorPresetById(): ColorPreset? { return _state.value.colorPresets.firstOrNull { - it.id == id + it.id == this } } - private fun selectedColorPreset(colorPresets: List? = null): ColorPreset { - val presets = colorPresets ?: _state.value.colorPresets + private fun List?.selected(): ColorPreset { + val presets = this ?: _state.value.colorPresets if (presets.size == 1) { return presets.first() @@ -543,13 +527,13 @@ class SettingsViewModel @Inject constructor( } } } -} - - - - - - - - + private fun cancelColorPresetJobs() { + selectColorPresetJob?.cancel() + addColorPresetJob?.cancel() + updateTitleColorPresetJob?.cancel() + shuffleColorPresetJob?.cancel() + updateColorColorPresetJob?.cancel() + deleteColorPresetJob?.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsScreen.kt new file mode 100644 index 00000000..50f624bf --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsScreen.kt @@ -0,0 +1,42 @@ +package ua.acclorite.book_story.ui.settings + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.settings.SettingsContent + +@Parcelize +object SettingsScreen : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + SettingsContent( + listState = listState, + scrollBehavior = scrollBehavior, + navigateToGeneralSettings = { + navigator.push(GeneralSettingsScreen) + }, + navigateToAppearanceSettings = { + navigator.push(AppearanceSettingsScreen) + }, + navigateToReaderSettings = { + navigator.push(ReaderSettingsScreen) + }, + navigateToBrowseSettings = { + navigator.push(BrowseSettingsScreen) + }, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsState.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsState.kt similarity index 71% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsState.kt rename to app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsState.kt index dfb9200e..190e2d61 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsState.kt @@ -1,8 +1,8 @@ -package ua.acclorite.book_story.presentation.screens.settings.data +package ua.acclorite.book_story.ui.settings import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideDefaultColorPreset @@ -11,5 +11,5 @@ data class SettingsState( val colorPresets: List = emptyList(), val selectedColorPreset: ColorPreset = Constants.provideDefaultColorPreset(), val animateColorPreset: Boolean = false, - val colorPresetsListState: LazyListState = LazyListState() + val colorPresetListState: LazyListState = LazyListState() ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartEvent.kt similarity index 51% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartEvent.kt rename to app/src/main/java/ua/acclorite/book_story/ui/start/StartEvent.kt index 49941d90..080430dc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/start/data/StartEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/start/StartEvent.kt @@ -1,51 +1,26 @@ @file:OptIn(ExperimentalPermissionsApi::class) -package ua.acclorite.book_story.presentation.screens.start.data +package ua.acclorite.book_story.ui.start import androidx.activity.ComponentActivity import androidx.compose.runtime.Immutable import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState -import ua.acclorite.book_story.domain.util.OnNavigate @Immutable sealed class StartEvent { data class OnCheckPermissions( val storagePermissionState: PermissionState, - val notificationPermissionState: PermissionState + val notificationsPermissionState: PermissionState ) : StartEvent() - data class OnGoBack( - val onQuit: () -> Unit - ) : StartEvent() - - data object OnGoForward : StartEvent() - data class OnStoragePermissionRequest( val activity: ComponentActivity, - val legacyStoragePermissionState: PermissionState + val storagePermissionState: PermissionState ) : StartEvent() data class OnNotificationsPermissionRequest( val activity: ComponentActivity, val notificationsPermissionState: PermissionState ) : StartEvent() - - data class OnGoToBrowse( - val onNavigate: OnNavigate, - val onCompletedStartGuide: () -> Unit - ) : StartEvent() - - data class OnGoToHelp( - val onNavigate: OnNavigate - ) : StartEvent() - - data object OnResetStartScreen : StartEvent() -} - - - - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt new file mode 100644 index 00000000..fa47c304 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt @@ -0,0 +1,180 @@ +package ua.acclorite.book_story.ui.start + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.shouldShowRationale +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import ua.acclorite.book_story.presentation.core.util.launchActivity +import javax.inject.Inject + +@HiltViewModel +class StartModel @Inject constructor( + +) : ViewModel() { + + private val _state = MutableStateFlow(StartState()) + val state = _state.asStateFlow() + + private var storagePermissionJob: Job? = null + private var notificationsPermissionJob: Job? = null + + @OptIn(ExperimentalPermissionsApi::class) + fun onEvent(event: StartEvent) { + when (event) { + is StartEvent.OnCheckPermissions -> { + viewModelScope.launch(Dispatchers.IO) { + val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + val legacyNotificationPermission = + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + + val storagePermissionGranted = if (!legacyStoragePermission) { + Environment.isExternalStorageManager() + } else event.storagePermissionState.status.isGranted + val notificationPermissionGranted = if (!legacyNotificationPermission) { + event.notificationsPermissionState.status.isGranted + } else true + + _state.update { + it.copy( + storagePermissionGranted = storagePermissionGranted, + notificationsPermissionGranted = notificationPermissionGranted + ) + } + } + } + + is StartEvent.OnStoragePermissionRequest -> { + val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + + val isPermissionGranted = if (legacyStoragePermission) { + event.storagePermissionState.status.isGranted + } else Environment.isExternalStorageManager() + + if (isPermissionGranted) { + _state.update { + it.copy( + storagePermissionGranted = true + ) + } + return + } + + if (legacyStoragePermission) { + if (!event.storagePermissionState.status.shouldShowRationale) { + event.storagePermissionState.launchPermissionRequest() + } else { + val uri = Uri.parse("package:${event.activity.packageName}") + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) + + intent.launchActivity(event.activity) { + return + } + } + } + + if (!legacyStoragePermission) { + val uri = Uri.parse("package:${event.activity.packageName}") + val intent = Intent( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + uri + ) + + intent.launchActivity(event.activity) { + return + } + } + + storagePermissionJob?.cancel() + storagePermissionJob = viewModelScope.launch { + while (true) { + val granted = if (legacyStoragePermission) { + event.storagePermissionState.status.isGranted + } else Environment.isExternalStorageManager() + + if (!granted) { + delay(1000) + yield() + continue + } + + yield() + + _state.update { + it.copy( + storagePermissionGranted = true + ) + } + break + } + } + } + + is StartEvent.OnNotificationsPermissionRequest -> { + if (event.notificationsPermissionState.status.isGranted) { + _state.update { + it.copy( + notificationsPermissionGranted = true + ) + } + return + } + + if (!event.notificationsPermissionState.status.shouldShowRationale) { + event.notificationsPermissionState.launchPermissionRequest() + } else { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName) + + intent.launchActivity(event.activity) { + return + } + } + + notificationsPermissionJob?.cancel() + notificationsPermissionJob = viewModelScope.launch { + while (true) { + if (!event.notificationsPermissionState.status.isGranted) { + delay(1000) + yield() + continue + } + + yield() + + _state.update { + it.copy( + notificationsPermissionGranted = true + ) + } + + break + } + } + } + } + } + + fun resetScreen() { + _state.update { + storagePermissionJob?.cancel() + notificationsPermissionJob?.cancel() + + StartState() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartScreen.kt new file mode 100644 index 00000000..e1099499 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/start/StartScreen.kt @@ -0,0 +1,140 @@ +package ua.acclorite.book_story.ui.start + +import android.Manifest +import android.annotation.SuppressLint +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.text.TextStyle +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.domain.navigator.StackEvent +import ua.acclorite.book_story.domain.ui.ButtonItem +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideLanguages +import ua.acclorite.book_story.presentation.core.util.LocalActivity +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.start.StartContent +import ua.acclorite.book_story.ui.browse.BrowseScreen +import ua.acclorite.book_story.ui.help.HelpScreen +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Parcelize +object StartScreen : Screen, Parcelable { + + @IgnoredOnParcel + const val SETTINGS = "settings" + + @IgnoredOnParcel + const val GENERAL_SETTINGS = "general_settings" + + @IgnoredOnParcel + const val APPEARANCE_SETTINGS = "appearance_settings" + + @IgnoredOnParcel + const val PERMISSION_SETTINGS = "permission_settings" + + + @IgnoredOnParcel + const val DONE = "done" + + @SuppressLint("InlinedApi") + @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val screenModel = hiltViewModel() + val mainModel = hiltViewModel() + + val state = screenModel.state.collectAsStateWithLifecycle() + val mainState = mainModel.state.collectAsStateWithLifecycle() + + val activity = LocalActivity.current + val storagePermissionState = rememberPermissionState( + permission = Manifest.permission.READ_EXTERNAL_STORAGE + ) + val notificationsPermissionState = rememberPermissionState( + permission = Manifest.permission.POST_NOTIFICATIONS + ) + + val currentPage = remember { mutableIntStateOf(0) } + val stackEvent = remember { mutableStateOf(StackEvent.Default) } + + val languages = remember(mainState.value.language) { + Constants.provideLanguages().sortedBy { it.second }.map { + ButtonItem( + id = it.first, + title = it.second, + textStyle = TextStyle(), + selected = it.first == mainState.value.language + ) + }.sortedBy { it.title } + } + + LaunchedEffect(Unit) { + screenModel.onEvent( + StartEvent.OnCheckPermissions( + storagePermissionState = storagePermissionState, + notificationsPermissionState = notificationsPermissionState + ) + ) + } + DisposableEffect(Unit) { + onDispose { screenModel.resetScreen() } + } + + StartContent( + currentPage = currentPage.intValue, + stackEvent = stackEvent.value, + storagePermissionGranted = state.value.storagePermissionGranted, + notificationsPermissionGranted = state.value.notificationsPermissionGranted, + storagePermissionState = storagePermissionState, + notificationsPermissionState = notificationsPermissionState, + languages = languages, + changeLanguage = mainModel::onEvent, + storagePermissionRequest = screenModel::onEvent, + notificationsPermissionRequest = screenModel::onEvent, + navigateForward = { + if ((currentPage.intValue + 1 == 3) && !state.value.storagePermissionGranted) { + return@StartContent + } + + stackEvent.value = StackEvent.Default + currentPage.intValue += 1 + }, + navigateBack = { + if ((currentPage.intValue - 1) < 0) { + activity.finish() + } else { + stackEvent.value = StackEvent.Pop + currentPage.intValue -= 1 + } + }, + navigateToBrowse = { + navigator.push( + BrowseScreen, + saveInBackStack = false + ) + BrowseScreen.refreshListChannel.trySend(Unit) + mainModel.onEvent(MainEvent.OnChangeShowStartScreen(false)) + }, + navigateToHelp = { + navigator.push( + HelpScreen(fromStart = true), + saveInBackStack = false + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartState.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartState.kt new file mode 100644 index 00000000..8dbf9313 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/start/StartState.kt @@ -0,0 +1,9 @@ +package ua.acclorite.book_story.ui.start + +import androidx.compose.runtime.Immutable + +@Immutable +data class StartState( + val storagePermissionGranted: Boolean = false, + val notificationsPermissionGranted: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/Color.kt similarity index 79% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/Color.kt index fea55468..1b032d32 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.ui +package ua.acclorite.book_story.ui.theme import android.os.Build import androidx.annotation.StringRes @@ -6,19 +6,19 @@ import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import ua.acclorite.book_story.R -import ua.acclorite.book_story.presentation.ui.theme.aquaTheme -import ua.acclorite.book_story.presentation.ui.theme.blackTheme -import ua.acclorite.book_story.presentation.ui.theme.blueTheme -import ua.acclorite.book_story.presentation.ui.theme.dynamicTheme -import ua.acclorite.book_story.presentation.ui.theme.grayTheme -import ua.acclorite.book_story.presentation.ui.theme.greenTheme -import ua.acclorite.book_story.presentation.ui.theme.lavenderTheme -import ua.acclorite.book_story.presentation.ui.theme.marshTheme -import ua.acclorite.book_story.presentation.ui.theme.pink2Theme -import ua.acclorite.book_story.presentation.ui.theme.pinkTheme -import ua.acclorite.book_story.presentation.ui.theme.purpleTheme -import ua.acclorite.book_story.presentation.ui.theme.redTheme -import ua.acclorite.book_story.presentation.ui.theme.yellowTheme +import ua.acclorite.book_story.ui.theme.color.aquaTheme +import ua.acclorite.book_story.ui.theme.color.blackTheme +import ua.acclorite.book_story.ui.theme.color.blueTheme +import ua.acclorite.book_story.ui.theme.color.dynamicTheme +import ua.acclorite.book_story.ui.theme.color.grayTheme +import ua.acclorite.book_story.ui.theme.color.greenTheme +import ua.acclorite.book_story.ui.theme.color.lavenderTheme +import ua.acclorite.book_story.ui.theme.color.marshTheme +import ua.acclorite.book_story.ui.theme.color.pink2Theme +import ua.acclorite.book_story.ui.theme.color.pinkTheme +import ua.acclorite.book_story.ui.theme.color.purpleTheme +import ua.acclorite.book_story.ui.theme.color.redTheme +import ua.acclorite.book_story.ui.theme.color.yellowTheme @Immutable diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/Theme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/Theme.kt index e37ca37b..924d0ec5 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.ui +package ua.acclorite.book_story.ui.theme import android.app.Activity import android.content.Context @@ -196,8 +196,4 @@ fun animatedColorScheme( ) return animateColorScheme(targetColorScheme = colorScheme) -} - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/ThemeLogger.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/ThemeLogger.kt similarity index 98% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/ThemeLogger.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/ThemeLogger.kt index 5a52296b..871d6a57 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/ThemeLogger.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/ThemeLogger.kt @@ -1,6 +1,6 @@ @file:Suppress("unused") -package ua.acclorite.book_story.presentation.ui +package ua.acclorite.book_story.ui.theme import android.util.Log import androidx.compose.material3.ColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/Transitions.kt similarity index 95% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/Transitions.kt index 4a9f4580..2bc7032f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/Transitions.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.ui +package ua.acclorite.book_story.ui.theme import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.EaseInOut @@ -32,16 +32,16 @@ object Transitions { val DefaultTransitionOut = fadeOut(tween(300)) val FadeTransitionIn = fadeIn(tween(400)) + scaleIn(tween(400), initialScale = 0.975f) - val FadeTransitionOut = fadeOut(tween(100)) + val FadeTransitionOut = fadeOut(tween(400)) val SlidingTransitionIn = fadeIn(tween(400)) + slideInHorizontally(tween(400)) { it / 16 } val BackSlidingTransitionIn = fadeIn(tween(400)) + slideInHorizontally(tween(400)) { -it / 16 } - val SlidingTransitionOut = fadeOut(tween(300)) + + val SlidingTransitionOut = fadeOut(tween(400)) + slideOutHorizontally(tween(400)) { -it / 16 } - val BackSlidingTransitionOut = fadeOut(tween(300)) + + val BackSlidingTransitionOut = fadeOut(tween(400)) + slideOutHorizontally(tween(400)) { it / 16 } val NoEnterAnimation = fadeIn(tween(0)) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/AquaTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/AquaTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/AquaTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/AquaTheme.kt index f4506a68..36e77579 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/AquaTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/AquaTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF116682) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlackTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/BlackTheme.kt similarity index 72% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlackTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/BlackTheme.kt index c3715b1d..b7c45637 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlackTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/BlackTheme.kt @@ -1,19 +1,23 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.data.MainViewModel +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.ui.main.MainModel @Composable fun blackTheme(initialTheme: ColorScheme): ColorScheme { - val absoluteDark = MainViewModel.getState().value.absoluteDark + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + val surfaceDarker = 3f - val surfaceContainerDarker = if (absoluteDark) 3f else 1.95f + val surfaceContainerDarker = if (state.value.absoluteDark) 3f else 1.95f return initialTheme.copy( surface = initialTheme.surface.run { - if (absoluteDark) { + if (state.value.absoluteDark) { return@run Color.Black } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlueTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/BlueTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlueTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/BlueTheme.kt index 21b4ff42..ccf73f8c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlueTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/BlueTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF485D92) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/DynamicTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/DynamicTheme.kt similarity index 97% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/DynamicTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/DynamicTheme.kt index 6ddd4625..e2e57d26 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/DynamicTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/DynamicTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import android.annotation.SuppressLint import androidx.compose.material3.ColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GrayTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/GrayTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GrayTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/GrayTheme.kt index 0716d15d..c3610c4e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GrayTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/GrayTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GreenTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/GreenTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GreenTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/GreenTheme.kt index daad0f1e..1d965da0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GreenTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/GreenTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF306A42) private val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/LavenderTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/LavenderTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/LavenderTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/LavenderTheme.kt index 133a750b..a78ff3a8 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/LavenderTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/LavenderTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF824C76) private val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/MarshTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/MarshTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/MarshTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/MarshTheme.kt index cb4ba11b..44cb4ccc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/MarshTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/MarshTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF006A65) private val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/Pink2Theme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/Pink2Theme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/Pink2Theme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/Pink2Theme.kt index 78fe8536..067c44f3 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/Pink2Theme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/Pink2Theme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PinkTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/PinkTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PinkTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/PinkTheme.kt index 2e013447..19f66b98 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PinkTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/PinkTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF8C4A60) private val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PurpleTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/PurpleTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PurpleTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/PurpleTheme.kt index f747f3a2..166049ef 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PurpleTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/PurpleTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF725188) private val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/RedTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/RedTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/RedTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/RedTheme.kt index c73a7954..c140d5f5 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/RedTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/RedTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF904A42) private val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/YellowTheme.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/YellowTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/YellowTheme.kt rename to app/src/main/java/ua/acclorite/book_story/ui/theme/color/YellowTheme.kt index 2545aa01..7eb860c0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/YellowTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/color/YellowTheme.kt @@ -1,11 +1,11 @@ -package ua.acclorite.book_story.presentation.ui.theme +package ua.acclorite.book_story.ui.theme.color import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.ui.theme.ThemeContrast private val primaryLight = Color(0xFF845415) private val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 954e54ad..433a549c 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,9 +1,9 @@ - -