Merge branch 'master' into sync-part-1

This commit is contained in:
KaiserBh 2023-07-16 20:35:59 +10:00 committed by GitHub
commit abe69206dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 641 additions and 872 deletions

View File

@ -1,6 +1,7 @@
package eu.kanade.domain.base package eu.kanade.domain.base
import android.content.Context import android.content.Context
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
@ -19,7 +20,7 @@ class BasePreferences(
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType) fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
enum class ExtensionInstaller(val titleResId: Int) { enum class ExtensionInstaller(@StringRes val titleResId: Int) {
LEGACY(R.string.ext_installer_legacy), LEGACY(R.string.ext_installer_legacy),
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller), PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
SHIZUKU(R.string.ext_installer_shizuku), SHIZUKU(R.string.ext_installer_shizuku),

View File

@ -30,7 +30,5 @@ class SourcePreferences(
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet()) fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
fun searchPinnedSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
} }

View File

@ -1,8 +1,22 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -16,19 +30,25 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.VerticalDivider
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
fun GlobalSearchScreen( fun GlobalSearchScreen(
state: GlobalSearchState, state: GlobalSearchScreenModel.State,
items: Map<CatalogueSource, SearchItemResult>,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
onChangeSearchFilter: (SourceFilter) -> Unit,
onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>, getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit, onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
@ -36,6 +56,7 @@ fun GlobalSearchScreen(
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
GlobalSearchToolbar( GlobalSearchToolbar(
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
progress = state.progress, progress = state.progress,
@ -45,10 +66,70 @@ fun GlobalSearchScreen(
onSearch = onSearch, onSearch = onSearch,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
// TODO: make this UX better; it only applies when triggering a new search
FilterChip(
selected = state.sourceFilter == SourceFilter.PinnedOnly,
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.PushPin,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.pinned_sources))
},
)
FilterChip(
selected = state.sourceFilter == SourceFilter.All,
onClick = { onChangeSearchFilter(SourceFilter.All) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.DoneAll,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.all))
},
)
VerticalDivider()
FilterChip(
selected = state.onlyShowHasResults,
onClick = { onToggleResults() },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.has_results))
},
)
}
Divider()
}
}, },
) { paddingValues -> ) { paddingValues ->
GlobalSearchContent( GlobalSearchContent(
items = state.items, items = items,
contentPadding = paddingValues, contentPadding = paddingValues,
getManga = getManga, getManga = getManga,
onClickSource = onClickSource, onClickSource = onClickSource,

View File

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -25,10 +26,11 @@ import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.SettingsFlowRow
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.components.material.ChoiceChip
@Composable @Composable
fun LibrarySettingsDialog( fun LibrarySettingsDialog(
@ -167,23 +169,26 @@ private fun ColumnScope.SortPage(
} }
} }
@Composable private val displayModes = listOf(
private fun ColumnScope.DisplayPage(
screenModel: LibrarySettingsScreenModel,
) {
HeadingItem(R.string.action_display_mode)
val displayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState()
listOf(
R.string.action_display_grid to LibraryDisplayMode.CompactGrid, R.string.action_display_grid to LibraryDisplayMode.CompactGrid,
R.string.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid, R.string.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid,
R.string.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid, R.string.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid,
R.string.action_display_list to LibraryDisplayMode.List, R.string.action_display_list to LibraryDisplayMode.List,
).map { (titleRes, mode) ->
RadioItem(
label = stringResource(titleRes),
selected = displayMode == mode,
onClick = { screenModel.setDisplayMode(mode) },
) )
@Composable
private fun ColumnScope.DisplayPage(
screenModel: LibrarySettingsScreenModel,
) {
val displayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState()
SettingsFlowRow(R.string.action_display_mode) {
displayModes.map { (titleRes, mode) ->
ChoiceChip(
isSelected = displayMode == mode,
onClick = { screenModel.setDisplayMode(mode) },
content = { Text(stringResource(titleRes)) },
)
}
} }
if (displayMode != LibraryDisplayMode.List) { if (displayMode != LibraryDisplayMode.List) {

View File

@ -29,10 +29,6 @@ object SettingsBrowseScreen : SearchableSettings {
Preference.PreferenceGroup( Preference.PreferenceGroup(
title = stringResource(R.string.label_sources), title = stringResource(R.string.label_sources),
preferenceItems = listOf( preferenceItems = listOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.searchPinnedSourcesOnly(),
title = stringResource(R.string.pref_search_pinned_sources_only),
),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInLibraryItems(), pref = sourcePreferences.hideInLibraryItems(),
title = stringResource(R.string.pref_hide_in_library_items), title = stringResource(R.string.pref_hide_in_library_items),

View File

@ -7,7 +7,6 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.collectAsState import eu.kanade.presentation.util.collectAsState
@ -164,9 +163,9 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = navModePref, pref = navModePref,
title = stringResource(R.string.pref_viewer_nav), title = stringResource(R.string.pref_viewer_nav),
entries = stringArrayResource(id = R.array.pager_nav).let { entries = ReaderPreferences.TapZones
it.indices.zip(it).toMap() .mapIndexed { index, it -> index to stringResource(it) }
}, .toMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(), pref = readerPreferences.pagerNavInverted(),
@ -182,25 +181,16 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = imageScaleTypePref, pref = imageScaleTypePref,
title = stringResource(R.string.pref_image_scale_type), title = stringResource(R.string.pref_image_scale_type),
entries = mapOf( entries = ReaderPreferences.ImageScaleType
1 to stringResource(R.string.scale_type_fit_screen), .mapIndexed { index, it -> index + 1 to stringResource(it) }
2 to stringResource(R.string.scale_type_stretch), .toMap(),
3 to stringResource(R.string.scale_type_fit_width),
4 to stringResource(R.string.scale_type_fit_height),
5 to stringResource(R.string.scale_type_original_size),
6 to stringResource(R.string.scale_type_smart_fit),
),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(), pref = readerPreferences.zoomStart(),
title = stringResource(R.string.pref_zoom_start), title = stringResource(R.string.pref_zoom_start),
entries = mapOf( entries = ReaderPreferences.ZoomStart
1 to stringResource(R.string.zoom_start_automatic), .mapIndexed { index, it -> index + 1 to stringResource(it) }
2 to stringResource(R.string.zoom_start_left), .toMap(),
3 to stringResource(R.string.zoom_start_right),
4 to stringResource(R.string.zoom_start_center),
),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBorders(), pref = readerPreferences.cropBorders(),
@ -265,9 +255,9 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = navModePref, pref = navModePref,
title = stringResource(R.string.pref_viewer_nav), title = stringResource(R.string.pref_viewer_nav),
entries = stringArrayResource(id = R.array.webtoon_nav).let { entries = ReaderPreferences.TapZones
it.indices.zip(it).toMap() .mapIndexed { index, it -> index to stringResource(it) }
}, .toMap(),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(), pref = readerPreferences.webtoonNavInverted(),

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import android.os.Build import android.os.Build
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -15,8 +16,9 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.core.preference.getAndSet import tachiyomi.core.preference.getAndSet
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.SelectItem import tachiyomi.presentation.core.components.SettingsFlowRow
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.material.ChoiceChip
@Composable @Composable
internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) { internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) {
@ -122,12 +124,14 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
) )
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState() val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
SelectItem( SettingsFlowRow(R.string.pref_color_filter_mode) {
label = stringResource(R.string.pref_color_filter_mode), colorFilterModes.mapIndexed { index, it ->
options = colorFilterModes.toTypedArray(), ChoiceChip(
selectedIndex = colorFilterMode, isSelected = colorFilterMode == index,
) { onClick = { screenModel.preferences.colorFilterMode().set(index) },
screenModel.preferences.colorFilterMode().set(it) content = { Text(it) },
)
}
} }
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.reader.settings package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -9,25 +10,27 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.SettingsFlowRow
import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.material.ChoiceChip
@Composable private val themes = listOf(
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
// TODO: show this in a nicer way
HeadingItem(R.string.pref_reader_theme)
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
listOf(
R.string.black_background to 1, R.string.black_background to 1,
R.string.gray_background to 2, R.string.gray_background to 2,
R.string.white_background to 0, R.string.white_background to 0,
R.string.automatic_background to 3, R.string.automatic_background to 3,
).map { (titleRes, theme) ->
RadioItem(
label = stringResource(titleRes),
selected = readerTheme == theme,
onClick = { screenModel.preferences.readerTheme().set(theme) },
) )
@Composable
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
SettingsFlowRow(R.string.pref_reader_theme) {
themes.map { (labelRes, value) ->
ChoiceChip(
isSelected = readerTheme == value,
onClick = { screenModel.preferences.readerTheme().set(value) },
content = { Text(stringResource(labelRes)) },
)
}
} }
val showPageNumber by screenModel.preferences.showPageNumber().collectAsState() val showPageNumber by screenModel.preferences.showPageNumber().collectAsState()

View File

@ -23,9 +23,6 @@ fun ReaderSettingsDialog(
onHideMenus: () -> Unit, onHideMenus: () -> Unit,
screenModel: ReaderSettingsScreenModel, screenModel: ReaderSettingsScreenModel,
) { ) {
// TODO: undimming doesn't seem to work
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
val tabTitles = listOf( val tabTitles = listOf(
stringResource(R.string.pref_category_reading_mode), stringResource(R.string.pref_category_reading_mode),
stringResource(R.string.pref_category_general), stringResource(R.string.pref_category_general),
@ -33,16 +30,6 @@ fun ReaderSettingsDialog(
) )
val pagerState = rememberPagerState { tabTitles.size } val pagerState = rememberPagerState { tabTitles.size }
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage == 2) {
window?.setDimAmount(0f)
onHideMenus()
} else {
window?.setDimAmount(0.75f)
onShowMenus()
}
}
TabbedDialog( TabbedDialog(
onDismissRequest = { onDismissRequest = {
onDismissRequest() onDismissRequest()
@ -51,6 +38,18 @@ fun ReaderSettingsDialog(
tabTitles = tabTitles, tabTitles = tabTitles,
pagerState = pagerState, pagerState = pagerState,
) { page -> ) { page ->
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage == 2) {
window?.setDimAmount(0f)
onHideMenus()
} else {
window?.setDimAmount(0.5f)
onShowMenus()
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.padding(vertical = TabbedDialogPaddings.Vertical) .padding(vertical = TabbedDialogPaddings.Vertical)

View File

@ -2,40 +2,100 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.domain.manga.model.orientationType
import eu.kanade.domain.manga.model.readingModeType
import eu.kanade.presentation.util.collectAsState import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.SelectItem
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import java.text.NumberFormat import java.text.NumberFormat
private val readingModeOptions = ReadingModeType.values().map { it.stringRes to it }
private val orientationTypeOptions = OrientationType.values().map { it.stringRes to it }
private val tappingInvertModeOptions = ReaderPreferences.TappingInvertMode.values().map { it.titleResId to it }
@Composable @Composable
internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) { internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) {
HeadingItem(R.string.pref_category_for_this_series) HeadingItem(R.string.pref_category_for_this_series)
val manga by screenModel.mangaFlow.collectAsState()
// Reading mode val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
// Rotation type SelectItem(
label = stringResource(R.string.pref_category_reading_mode),
options = readingModeOptions.map { stringResource(it.first) }.toTypedArray(),
selectedIndex = readingModeOptions.indexOfFirst { it.second == readingMode },
) {
screenModel.onChangeReadingMode(readingModeOptions[it].second)
}
// if (pager) val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
PagerViewerSettings(screenModel) SelectItem(
label = stringResource(R.string.rotation_type),
options = orientationTypeOptions.map { stringResource(it.first) }.toTypedArray(),
selectedIndex = orientationTypeOptions.indexOfFirst { it.second == orientationType },
) {
screenModel.onChangeOrientation(orientationTypeOptions[it].second)
}
val viewer by screenModel.viewerFlow.collectAsState()
if (viewer is WebtoonViewer) {
WebtoonViewerSettings(screenModel) WebtoonViewerSettings(screenModel)
} else {
PagerViewerSettings(screenModel)
}
} }
@Composable @Composable
private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) { private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) {
HeadingItem(R.string.pager_viewer) HeadingItem(R.string.pager_viewer)
// Tap zones val navigationModePager by screenModel.preferences.navigationModePager().collectAsState()
// Invert tap zones SelectItem(
// Scale type label = stringResource(R.string.pref_viewer_nav),
// Zoom start position options = ReaderPreferences.TapZones.map { stringResource(it) }.toTypedArray(),
selectedIndex = navigationModePager,
onSelect = { screenModel.preferences.navigationModePager().set(it) },
)
if (navigationModePager != 5) {
val pagerNavInverted by screenModel.preferences.pagerNavInverted().collectAsState()
SelectItem(
label = stringResource(R.string.pref_read_with_tapping_inverted),
options = tappingInvertModeOptions.map { stringResource(it.first) }.toTypedArray(),
selectedIndex = tappingInvertModeOptions.indexOfFirst { it.second == pagerNavInverted },
onSelect = {
screenModel.preferences.pagerNavInverted().set(tappingInvertModeOptions[it].second)
},
)
}
val imageScaleType by screenModel.preferences.imageScaleType().collectAsState()
SelectItem(
label = stringResource(R.string.pref_image_scale_type),
options = ReaderPreferences.ImageScaleType.map { stringResource(it) }.toTypedArray(),
selectedIndex = imageScaleType - 1,
onSelect = { screenModel.preferences.imageScaleType().set(it + 1) },
)
val zoomStart by screenModel.preferences.zoomStart().collectAsState()
SelectItem(
label = stringResource(R.string.pref_zoom_start),
options = ReaderPreferences.ZoomStart.map { stringResource(it) }.toTypedArray(),
selectedIndex = zoomStart - 1,
onSelect = { screenModel.preferences.zoomStart().set(it + 1) },
)
val cropBorders by screenModel.preferences.cropBorders().collectAsState() val cropBorders by screenModel.preferences.cropBorders().collectAsState()
CheckboxItem( CheckboxItem(
@ -111,8 +171,25 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
HeadingItem(R.string.webtoon_viewer) HeadingItem(R.string.webtoon_viewer)
// TODO: Tap zones val navigationModeWebtoon by screenModel.preferences.navigationModeWebtoon().collectAsState()
// TODO: Invert tap zones SelectItem(
label = stringResource(R.string.pref_viewer_nav),
options = ReaderPreferences.TapZones.map { stringResource(it) }.toTypedArray(),
selectedIndex = navigationModeWebtoon,
onSelect = { screenModel.preferences.navigationModeWebtoon().set(it) },
)
if (navigationModeWebtoon != 5) {
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted().collectAsState()
SelectItem(
label = stringResource(R.string.pref_read_with_tapping_inverted),
options = tappingInvertModeOptions.map { stringResource(it.first) }.toTypedArray(),
selectedIndex = tappingInvertModeOptions.indexOfFirst { it.second == webtoonNavInverted },
onSelect = {
screenModel.preferences.webtoonNavInverted().set(tappingInvertModeOptions[it].second)
},
)
}
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState() val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem( SliderItem(

View File

@ -41,7 +41,12 @@ internal object ExtensionLoader {
const val LIB_VERSION_MIN = 1.3 const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MAX = 1.5 const val LIB_VERSION_MAX = 1.5
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES
} else {
@Suppress("DEPRECATION")
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
}
// inorichi's key // inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
@ -49,7 +54,7 @@ internal object ExtensionLoader {
/** /**
* List of the trusted signatures. * List of the trusted signatures.
*/ */
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
/** /**
* Return a list of all the installed extensions initialized concurrently. * Return a list of all the installed extensions initialized concurrently.
@ -59,7 +64,6 @@ internal object ExtensionLoader {
fun loadExtensions(context: Context): List<LoadResult> { fun loadExtensions(context: Context): List<LoadResult> {
val pkgManager = context.packageManager val pkgManager = context.packageManager
@Suppress("DEPRECATION")
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else { } else {
@ -135,7 +139,7 @@ internal object ExtensionLoader {
return LoadResult.Error return LoadResult.Error
} }
val signatureHash = getSignatureHash(pkgInfo) val signatureHash = getSignatureHash(context, pkgInfo)
if (signatureHash == null) { if (signatureHash == null) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error return LoadResult.Error
@ -220,12 +224,8 @@ internal object ExtensionLoader {
* *
* @param pkgInfo The package info of the application. * @param pkgInfo The package info of the application.
*/ */
private fun getSignatureHash(pkgInfo: PackageInfo): String? { private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName)
return if (signatures != null && signatures.isNotEmpty()) { return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) }
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
} }
} }

View File

@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
@ -313,6 +314,7 @@ internal class MigrateDialogScreenModel(
) )
} }
@Immutable
data class State( data class State(
val isMigrating: Boolean = false, val isMigrating: Boolean = false,
) )

View File

@ -10,6 +10,7 @@ import eu.kanade.presentation.browse.MigrateSearchScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
// TODO: this should probably be merged with GlobalSearchScreen somehow to dedupe logic
class MigrateSearchScreen(private val mangaId: Long) : Screen() { class MigrateSearchScreen(private val mangaId: Long) : Screen() {
@Composable @Composable

View File

@ -35,6 +35,7 @@ class GlobalSearchScreen(
var showSingleLoadingScreen by remember { var showSingleLoadingScreen by remember {
mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1) mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
} }
val filteredSources by screenModel.searchPagerFlow.collectAsState()
if (showSingleLoadingScreen) { if (showSingleLoadingScreen) {
LoadingScreen() LoadingScreen()
@ -57,10 +58,13 @@ class GlobalSearchScreen(
} else { } else {
GlobalSearchScreen( GlobalSearchScreen(
state = state, state = state,
items = filteredSources,
navigateUp = navigator::pop, navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery, onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = screenModel::search, onSearch = screenModel::search,
getManga = { screenModel.getManga(it) }, getManga = { screenModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults,
onClickSource = { onClickSource = {
if (!screenModel.incognitoMode.get()) { if (!screenModel.incognitoMode.get()) {
screenModel.lastUsedSourceId.set(it.id) screenModel.lastUsedSourceId.set(it.id)

View File

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -15,11 +20,18 @@ class GlobalSearchScreenModel(
preferences: BasePreferences = Injekt.get(), preferences: BasePreferences = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
) : SearchScreenModel<GlobalSearchState>(GlobalSearchState(searchQuery = initialQuery)) { ) : SearchScreenModel<GlobalSearchScreenModel.State>(State(searchQuery = initialQuery)) {
val incognitoMode = preferences.incognitoMode() val incognitoMode = preferences.incognitoMode()
val lastUsedSourceId = sourcePreferences.lastUsedSource() val lastUsedSourceId = sourcePreferences.lastUsedSource()
val searchPagerFlow = state.map { Pair(it.onlyShowHasResults, it.items) }
.distinctUntilChanged()
.map { (onlyShowHasResults, items) ->
items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
}
.stateIn(ioCoroutineScope, SharingStarted.Lazily, state.value.items)
init { init {
extensionFilter = initialExtensionFilter extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) { if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) {
@ -33,6 +45,7 @@ class GlobalSearchScreenModel(
val pinnedSources = sourcePreferences.pinnedSources().get() val pinnedSources = sourcePreferences.pinnedSources().get()
return sourceManager.getCatalogueSources() return sourceManager.getCatalogueSources()
.filter { mutableState.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
.filter { it.lang in enabledLanguages } .filter { it.lang in enabledLanguages }
.filterNot { "${it.id}" in disabledSources } .filterNot { "${it.id}" in disabledSources }
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" })) .sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
@ -53,15 +66,29 @@ class GlobalSearchScreenModel(
override fun getItems(): Map<CatalogueSource, SearchItemResult> { override fun getItems(): Map<CatalogueSource, SearchItemResult> {
return mutableState.value.items return mutableState.value.items
} }
fun setSourceFilter(filter: SourceFilter) {
mutableState.update { it.copy(sourceFilter = filter) }
}
fun toggleFilterResults() {
mutableState.update {
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
}
}
private fun SearchItemResult.isVisible(onlyShowHasResults: Boolean): Boolean {
return !onlyShowHasResults || (this is SearchItemResult.Success && !this.isEmpty)
} }
@Immutable @Immutable
data class GlobalSearchState( data class State(
val searchQuery: String? = null, val searchQuery: String? = null,
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: Map<CatalogueSource, SearchItemResult> = emptyMap(), val items: Map<CatalogueSource, SearchItemResult> = emptyMap(),
) { ) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading } val progress: Int = items.count { it.value !is SearchItemResult.Loading }
val total: Int = items.size val total: Int = items.size
} }
}

View File

@ -68,16 +68,7 @@ abstract class SearchScreenModel<T>(
val enabledSources = getEnabledSources() val enabledSources = getEnabledSources()
if (filter.isEmpty()) { if (filter.isEmpty()) {
val shouldSearchPinnedOnly = sourcePreferences.searchPinnedSourcesOnly().get() return enabledSources
val pinnedSources = sourcePreferences.pinnedSources().get()
return enabledSources.filter {
if (shouldSearchPinnedOnly) {
"${it.id}" in pinnedSources
} else {
true
}
}
} }
return extensionManager.installedExtensionsFlow.value return extensionManager.installedExtensionsFlow.value
@ -137,6 +128,11 @@ abstract class SearchScreenModel<T>(
} }
} }
enum class SourceFilter {
All,
PinnedOnly,
}
sealed class SearchItemResult { sealed class SearchItemResult {
object Loading : SearchItemResult() object Loading : SearchItemResult()

View File

@ -113,6 +113,10 @@ class MainActivity : BaseActivity() {
private var navigator: Navigator? = null private var navigator: Navigator? = null
init {
registerSecureActivity(this)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val isLaunch = savedInstanceState == null val isLaunch = savedInstanceState == null
@ -429,10 +433,6 @@ class MainActivity : BaseActivity() {
return true return true
} }
init {
registerSecureActivity(this)
}
companion object { companion object {
// Splash screen // Splash screen
private const val SPLASH_MIN_DURATION = 500 // ms private const val SPLASH_MIN_DURATION = 500 // ms

View File

@ -20,6 +20,7 @@ import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -265,6 +266,7 @@ data class TrackInfoDialogHomeScreen(
.filter { (it.service as? EnhancedTrackService)?.accept(source) ?: true } .filter { (it.service as? EnhancedTrackService)?.accept(source) ?: true }
} }
@Immutable
data class State( data class State(
val trackItems: List<TrackItem> = emptyList(), val trackItems: List<TrackItem> = emptyList(),
) )
@ -314,6 +316,7 @@ private data class TrackStatusSelectorScreen(
} }
} }
@Immutable
data class State( data class State(
val selection: Int, val selection: Int,
) )
@ -369,6 +372,7 @@ private data class TrackChapterSelectorScreen(
} }
} }
@Immutable
data class State( data class State(
val selection: Int, val selection: Int,
) )
@ -419,6 +423,7 @@ private data class TrackScoreSelectorScreen(
} }
} }
@Immutable
data class State( data class State(
val selection: String, val selection: String,
) )
@ -724,6 +729,7 @@ data class TrackServiceSearchScreen(
mutableState.update { it.copy(selected = selected) } mutableState.update { it.copy(selected = selected) }
} }
@Immutable
data class State( data class State(
val queryResult: Result<List<TrackSearch>>? = null, val queryResult: Result<List<TrackSearch>>? = null,
val selected: TrackSearch? = null, val selected: TrackSearch? = null,

View File

@ -69,7 +69,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
@ -391,7 +390,13 @@ class ReaderActivity : BaseActivity() {
binding.dialogRoot.setComposeContent { binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val settingsScreenModel = remember { ReaderSettingsScreenModel() } val settingsScreenModel = remember {
ReaderSettingsScreenModel(
readerState = viewModel.state,
onChangeReadingMode = viewModel::setMangaReadingMode,
onChangeOrientation = viewModel::setMangaOrientationType,
)
}
val onDismissRequest = viewModel::closeDialog val onDismissRequest = viewModel::closeDialog
when (state.dialog) { when (state.dialog) {
@ -485,7 +490,7 @@ class ReaderActivity : BaseActivity() {
) { ) {
val newReadingMode = ReadingModeType.fromPreference(itemId) val newReadingMode = ReadingModeType.fromPreference(itemId)
viewModel.setMangaReadingMode(newReadingMode.flagValue) viewModel.setMangaReadingMode(newReadingMode)
menuToggleToast?.cancel() menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) { if (!readerPreferences.showReadingMode().get()) {
@ -539,7 +544,7 @@ class ReaderActivity : BaseActivity() {
) { ) {
val newOrientation = OrientationType.fromPreference(itemId) val newOrientation = OrientationType.fromPreference(itemId)
viewModel.setMangaOrientationType(newOrientation.flagValue) viewModel.setMangaOrientationType(newOrientation)
menuToggleToast?.cancel() menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes) menuToggleToast = toast(newOrientation.stringRes)
@ -548,16 +553,6 @@ class ReaderActivity : BaseActivity() {
} }
// Settings sheet // Settings sheet
with(binding.actionSettingsLegacy) {
setTooltip(R.string.action_settings)
var readerSettingSheet: ReaderSettingsSheet? = null
setOnClickListener {
if (readerSettingSheet?.isShowing == true) return@setOnClickListener
readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity).apply { show() }
}
}
with(binding.actionSettings) { with(binding.actionSettings) {
setTooltip(R.string.action_settings) setTooltip(R.string.action_settings)

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -601,10 +602,10 @@ class ReaderViewModel(
/** /**
* Updates the viewer position for the open manga. * Updates the viewer position for the open manga.
*/ */
fun setMangaReadingMode(readingModeType: Int) { fun setMangaReadingMode(readingModeType: ReadingModeType) {
val manga = manga ?: return val manga = manga ?: return
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
setMangaViewerFlags.awaitSetMangaReadingMode(manga.id, readingModeType.toLong()) setMangaViewerFlags.awaitSetMangaReadingMode(manga.id, readingModeType.flagValue.toLong())
val currChapters = state.value.viewerChapters val currChapters = state.value.viewerChapters
if (currChapters != null) { if (currChapters != null) {
// Save current page // Save current page
@ -637,10 +638,10 @@ class ReaderViewModel(
/** /**
* Updates the orientation type for the open manga. * Updates the orientation type for the open manga.
*/ */
fun setMangaOrientationType(rotationType: Int) { fun setMangaOrientationType(rotationType: OrientationType) {
val manga = manga ?: return val manga = manga ?: return
viewModelScope.launchIO { viewModelScope.launchIO {
setMangaViewerFlags.awaitSetOrientationType(manga.id, rotationType.toLong()) setMangaViewerFlags.awaitSetOrientationType(manga.id, rotationType.flagValue.toLong())
val currChapters = state.value.viewerChapters val currChapters = state.value.viewerChapters
if (currChapters != null) { if (currChapters != null) {
// Save current page // Save current page
@ -790,16 +791,12 @@ class ReaderViewModel(
} }
} }
/**
* Results of the set as cover feature.
*/
enum class SetAsCoverResult { enum class SetAsCoverResult {
Success, AddToLibraryFirst, Error Success,
AddToLibraryFirst,
Error,
} }
/**
* Results of the save image feature.
*/
sealed class SaveImageResult { sealed class SaveImageResult {
class Success(val uri: Uri) : SaveImageResult() class Success(val uri: Uri) : SaveImageResult()
class Error(val error: Throwable) : SaveImageResult() class Error(val error: Throwable) : SaveImageResult()
@ -844,6 +841,7 @@ class ReaderViewModel(
} }
} }
@Immutable
data class State( data class State(
val manga: Manga? = null, val manga: Manga? = null,
val viewerChapters: ViewerChapters? = null, val viewerChapters: ViewerChapters? = null,

View File

@ -18,8 +18,6 @@ enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val str
companion object { companion object {
const val MASK = 0x00000038 const val MASK = 0x00000038
fun fromPreference(preference: Int?): OrientationType = values().find { it.flagValue == preference } ?: FREE fun fromPreference(preference: Int?): OrientationType = values().find { it.flagValue == preference } ?: DEFAULT
fun fromSpinner(position: Int?) = values().find { value -> value.prefValue == position } ?: DEFAULT
} }
} }

View File

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.setting package eu.kanade.tachiyomi.ui.reader.setting
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
@ -122,11 +124,15 @@ class ReaderPreferences(
// endregion // endregion
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) { enum class TappingInvertMode(
NONE, @StringRes val titleResId: Int,
HORIZONTAL(shouldInvertHorizontal = true), val shouldInvertHorizontal: Boolean = false,
VERTICAL(shouldInvertVertical = true), val shouldInvertVertical: Boolean = false,
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true), ) {
NONE(R.string.tapping_inverted_none),
HORIZONTAL(R.string.tapping_inverted_horizontal, shouldInvertHorizontal = true),
VERTICAL(R.string.tapping_inverted_vertical, shouldInvertVertical = true),
BOTH(R.string.tapping_inverted_both, shouldInvertHorizontal = true, shouldInvertVertical = true),
} }
enum class ReaderHideThreshold(val threshold: Int) { enum class ReaderHideThreshold(val threshold: Int) {
@ -139,5 +145,30 @@ class ReaderPreferences(
companion object { companion object {
const val WEBTOON_PADDING_MIN = 0 const val WEBTOON_PADDING_MIN = 0
const val WEBTOON_PADDING_MAX = 25 const val WEBTOON_PADDING_MAX = 25
val TapZones = listOf(
R.string.label_default,
R.string.l_nav,
R.string.kindlish_nav,
R.string.edge_nav,
R.string.right_and_left_nav,
R.string.disabled_nav,
)
val ImageScaleType = listOf(
R.string.scale_type_fit_screen,
R.string.scale_type_stretch,
R.string.scale_type_fit_width,
R.string.scale_type_fit_height,
R.string.scale_type_original_size,
R.string.scale_type_smart_fit,
)
val ZoomStart = listOf(
R.string.zoom_start_automatic,
R.string.zoom_start_left,
R.string.zoom_start_right,
R.string.zoom_start_center,
)
} }
} }

View File

@ -1,15 +1,35 @@
package eu.kanade.tachiyomi.ui.reader.setting package eu.kanade.tachiyomi.ui.reader.setting
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel
import eu.kanade.tachiyomi.util.preference.toggle import eu.kanade.tachiyomi.util.preference.toggle
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ReaderSettingsScreenModel( class ReaderSettingsScreenModel(
readerState: StateFlow<ReaderViewModel.State>,
val onChangeReadingMode: (ReadingModeType) -> Unit,
val onChangeOrientation: (OrientationType) -> Unit,
val preferences: ReaderPreferences = Injekt.get(), val preferences: ReaderPreferences = Injekt.get(),
) : ScreenModel { ) : ScreenModel {
val viewerFlow = readerState
.map { it.viewer }
.distinctUntilChanged()
.stateIn(ioCoroutineScope, SharingStarted.Lazily, null)
val mangaFlow = readerState
.map { it.manga }
.distinctUntilChanged()
.stateIn(ioCoroutineScope, SharingStarted.Lazily, null)
fun togglePreference(preference: (ReaderPreferences) -> Preference<Boolean>) { fun togglePreference(preference: (ReaderPreferences) -> Preference<Boolean>) {
preference(preferences).toggle() preference(preferences).toggle()
} }

View File

@ -1,90 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.os.Bundle
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetDialog
import eu.kanade.domain.manga.model.orientationType
import eu.kanade.domain.manga.model.readingModeType
import eu.kanade.tachiyomi.databinding.ReaderReadingModeSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import eu.kanade.tachiyomi.util.preference.bindToPreference
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
class ReaderSettingsSheet(
private val activity: ReaderActivity,
) : BottomSheetDialog(activity) {
private val readerPreferences: ReaderPreferences by injectLazy()
private lateinit var binding: ReaderReadingModeSettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ReaderReadingModeSettingsBinding.inflate(activity.layoutInflater)
setContentView(binding.root)
initGeneralPreferences()
when (activity.viewModel.state.value.viewer) {
is PagerViewer -> initPagerPreferences()
is WebtoonViewer -> initWebtoonPreferences()
}
}
private fun initGeneralPreferences() {
binding.viewer.onItemSelectedListener = { position ->
val readingModeType = ReadingModeType.fromSpinner(position)
activity.viewModel.setMangaReadingMode(readingModeType.flagValue)
val mangaViewer = activity.viewModel.getMangaReadingMode()
if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) {
initWebtoonPreferences()
} else {
initPagerPreferences()
}
}
binding.viewer.setSelection(activity.viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue)
binding.rotationMode.onItemSelectedListener = { position ->
val rotationType = OrientationType.fromSpinner(position)
activity.viewModel.setMangaOrientationType(rotationType.flagValue)
}
binding.rotationMode.setSelection(activity.viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue)
}
private fun initPagerPreferences() {
binding.webtoonPrefsGroup.root.isVisible = false
binding.pagerPrefsGroup.root.isVisible = true
binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), ReaderPreferences.TappingInvertMode::class.java)
binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager())
readerPreferences.navigationModePager().changes()
.onEach {
val isTappingEnabled = it != 5
binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled
}
.launchIn(activity.lifecycleScope)
binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1)
binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1)
}
private fun initWebtoonPreferences() {
binding.pagerPrefsGroup.root.isVisible = false
binding.webtoonPrefsGroup.root.isVisible = true
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), ReaderPreferences.TappingInvertMode::class.java)
binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon())
readerPreferences.navigationModeWebtoon().changes()
.onEach { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 }
.launchIn(activity.lifecycleScope)
}
}

View File

@ -29,8 +29,6 @@ enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @D
return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL
} }
fun fromSpinner(position: Int?) = values().find { value -> value.prefValue == position } ?: DEFAULT
fun toViewer(preference: Int?, activity: ReaderActivity): Viewer { fun toViewer(preference: Int?, activity: ReaderActivity): Viewer {
return when (fromPreference(preference)) { return when (fromPreference(preference)) {
LEFT_TO_RIGHT -> L2RPagerViewer(activity) LEFT_TO_RIGHT -> L2RPagerViewer(activity)

View File

@ -1,16 +1,7 @@
package eu.kanade.tachiyomi.util.preference package eu.kanade.tachiyomi.util.preference
import android.widget.CompoundButton
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
/**
* Binds a checkbox or switch view with a boolean preference.
*/
fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
isChecked = pref.get()
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
}
operator fun <T> Preference<Set<T>>.plusAssign(item: T) { operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
set(get() + item) set(get() + item)
} }

View File

@ -134,7 +134,6 @@ private fun Context.defaultBrowserPackageName(): String? {
val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.resolveActivity(browserIntent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())) packageManager.resolveActivity(browserIntent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else { } else {
@Suppress("DEPRECATION")
packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
} }
return resolveInfo return resolveInfo

View File

@ -5,10 +5,7 @@ import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.view.Display
import android.view.View import android.view.View
import android.view.WindowManager
import androidx.core.content.getSystemService
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.TabletUiMode
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -67,14 +64,6 @@ fun Context.isNightMode(): Boolean {
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
} }
val Context.displayCompat: Display?
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display
} else {
@Suppress("DEPRECATION")
getSystemService<WindowManager>()?.defaultDisplay
}
val Resources.isLTR val Resources.isLTR
get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR

View File

@ -1,12 +0,0 @@
@file:Suppress("PackageDirectoryMismatch")
package com.google.android.material.bottomsheet
import android.view.View
/**
* Returns package-private elevation value
*/
fun <T : View> BottomSheetBehavior<T>.getElevation(): Float {
return elevation.takeIf { it >= 0F } ?: 0F
}

View File

@ -1,30 +1,7 @@
package eu.kanade.tachiyomi.util.view package eu.kanade.tachiyomi.util.view
import android.content.Context
import android.graphics.Color
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import com.google.android.material.elevation.ElevationOverlayProvider
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
/**
* Sets navigation bar color to transparent if system's config_navBarNeedsScrim is false,
* otherwise it will use the theme navigationBarColor with 70% opacity.
*
* @see isNavigationBarNeedsScrim
*/
fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float = 0F) {
navigationBarColor = if (context.isNavigationBarNeedsScrim()) {
// Set navbar scrim 70% of navigationBarColor
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
context.getResourceColor(android.R.attr.navigationBarColor, 0.7F),
elevation,
)
} else {
Color.TRANSPARENT
}
}
fun Window.setSecureScreen(enabled: Boolean) { fun Window.setSecureScreen(enabled: Boolean) {
if (enabled) { if (enabled) {

View File

@ -1,154 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MenuItem
import android.widget.FrameLayout
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.withStyledAttributes
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.core.view.size
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PrefSpinnerBinding
import eu.kanade.tachiyomi.util.system.getResourceColor
import tachiyomi.core.preference.Preference
class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) {
private var entries = emptyList<String>()
private var selectedPosition = 0
private var popup: PopupMenu? = null
var onItemSelectedListener: ((Int) -> Unit)? = null
set(value) {
field = value
if (value != null) {
popup = makeSettingsPopup()
setOnTouchListener(popup?.dragToOpenListener)
setOnClickListener {
popup?.show()
}
}
}
private val emptyIcon by lazy {
AppCompatResources.getDrawable(context, R.drawable.ic_blank_24dp)
}
private val checkmarkIcon by lazy {
AppCompatResources.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
}
}
private val binding = PrefSpinnerBinding.inflate(LayoutInflater.from(context), this, false)
init {
addView(binding.root)
context.withStyledAttributes(set = attrs, attrs = R.styleable.MaterialSpinnerView) {
val title = getString(R.styleable.MaterialSpinnerView_title).orEmpty()
binding.title.text = title
val viewEntries = getTextArray(R.styleable.MaterialSpinnerView_android_entries)
.orEmpty()
.map { it.toString() }
entries = viewEntries
binding.details.text = viewEntries.firstOrNull().orEmpty()
}
}
fun setSelection(selection: Int) {
if (selectedPosition < (popup?.menu?.size ?: 0)) {
popup?.menu?.getItem(selectedPosition)?.let {
it.icon = emptyIcon
}
}
selectedPosition = selection
popup?.menu?.getItem(selectedPosition)?.let {
it.icon = checkmarkIcon
}
binding.details.text = entries.getOrNull(selection).orEmpty()
}
fun bindToPreference(pref: Preference<Int>, offset: Int = 0, block: ((Int) -> Unit)? = null) {
setSelection(pref.get() - offset)
popup = makeSettingsPopup(pref, offset, block)
setOnTouchListener(popup?.dragToOpenListener)
setOnClickListener {
popup?.show()
}
}
fun <T : Enum<T>> bindToPreference(pref: Preference<T>, clazz: Class<T>) {
val enumConstants = clazz.enumConstants
enumConstants?.indexOf(pref.get())?.let { setSelection(it) }
popup = makeSettingsPopup(pref, clazz)
setOnTouchListener(popup?.dragToOpenListener)
setOnClickListener {
popup?.show()
}
}
private fun <T : Enum<T>> makeSettingsPopup(preference: Preference<T>, clazz: Class<T>): PopupMenu {
return createPopupMenu { pos ->
onItemSelectedListener?.invoke(pos)
val enumConstants = clazz.enumConstants
enumConstants?.get(pos)?.let { enumValue -> preference.set(enumValue) }
}
}
private fun makeSettingsPopup(preference: Preference<Int>, intValues: List<Int?>, block: ((Int) -> Unit)? = null): PopupMenu {
return createPopupMenu { pos ->
preference.set(intValues[pos] ?: 0)
block?.invoke(pos)
}
}
private fun makeSettingsPopup(preference: Preference<Int>, offset: Int = 0, block: ((Int) -> Unit)? = null): PopupMenu {
return createPopupMenu { pos ->
preference.set(pos + offset)
block?.invoke(pos)
}
}
private fun makeSettingsPopup(): PopupMenu {
return createPopupMenu { pos ->
onItemSelectedListener?.invoke(pos)
}
}
private fun menuClicked(menuItem: MenuItem): Int {
val pos = menuItem.itemId
setSelection(pos)
return pos
}
@SuppressLint("RestrictedApi")
fun createPopupMenu(onItemClick: (Int) -> Unit): PopupMenu {
val popup = PopupMenu(context, this, Gravity.END, R.attr.actionOverflowMenuStyle, 0)
entries.forEachIndexed { index, entry ->
popup.menu.add(0, index, 0, entry)
}
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
popup.menu.forEach {
it.icon = emptyIcon
}
popup.menu[selectedPosition].icon = checkmarkIcon
popup.setOnMenuItemClickListener { menuItem ->
val pos = menuClicked(menuItem)
onItemClick(pos)
true
}
return popup
}
}

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in">
<translate
android:fromYDelta="100%p"
android:toYDelta="0" />
</set>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in">
<translate
android:fromYDelta="0"
android:toYDelta="100%p" />
</set>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
</vector>

View File

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingEnd="8dp"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/center_guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/dropdown_caret"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/center_guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Details" />
<ImageView
android:id="@+id/dropdown_caret"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="1dp"
android:src="@drawable/ic_expand_more_24dp"
app:layout_constraintBottom_toBottomOf="@id/details"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/details"
app:tint="?android:attr/colorControlNormal"
tools:ignore="ContentDescription" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/center_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -118,19 +118,6 @@
app:srcCompat="@drawable/ic_screen_rotation_24dp" app:srcCompat="@drawable/ic_screen_rotation_24dp"
app:tint="?attr/colorOnSurface" /> app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_settings_legacy"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@+id/action_settings"
app:layout_constraintStart_toEndOf="@id/action_rotation"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton <ImageButton
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -139,7 +126,7 @@
android:contentDescription="@string/action_settings" android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin" android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/action_settings_legacy" app:layout_constraintStart_toEndOf="@id/action_rotation"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_24dp" app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnSurface" /> app:tint="?attr/colorOnSurface" />

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/pager_prefs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/pager_viewer"
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/pager_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/pager_nav"
app:title="@string/pref_viewer_nav" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/tapping_inverted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/invert_tapping_mode"
app:title="@string/pref_read_with_tapping_inverted" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/scale_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/image_scale_type"
app:title="@string/pref_image_scale_type" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/zoom_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/zoom_start"
app:title="@string/pref_zoom_start" />
</LinearLayout>

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/for_this_series_prefs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/pref_category_for_this_series"
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/viewer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/viewers_selector"
app:title="@string/pref_category_reading_mode" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/rotation_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:entries="@array/rotation_type"
app:title="@string/rotation_type" />
<include
android:id="@+id/pager_prefs_group"
layout="@layout/reader_pager_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible" />
<include
android:id="@+id/webtoon_prefs_group"
layout="@layout/reader_webtoon_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/webtoon_prefs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@string/webtoon_viewer"
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/webtoon_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/webtoon_nav"
app:title="@string/pref_viewer_nav" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/tapping_inverted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/invert_tapping_mode"
app:title="@string/pref_read_with_tapping_inverted" />
</LinearLayout>

View File

@ -9,22 +9,6 @@
<item>@string/vertical_plus_viewer</item> <item>@string/vertical_plus_viewer</item>
</string-array> </string-array>
<string-array name="image_scale_type">
<item>@string/scale_type_fit_screen</item>
<item>@string/scale_type_stretch</item>
<item>@string/scale_type_fit_width</item>
<item>@string/scale_type_fit_height</item>
<item>@string/scale_type_original_size</item>
<item>@string/scale_type_smart_fit</item>
</string-array>
<string-array name="zoom_start">
<item>@string/zoom_start_automatic</item>
<item>@string/zoom_start_left</item>
<item>@string/zoom_start_right</item>
<item>@string/zoom_start_center</item>
</string-array>
<string-array name="rotation_type"> <string-array name="rotation_type">
<item>@string/label_default</item> <item>@string/label_default</item>
<item>@string/rotation_free</item> <item>@string/rotation_free</item>
@ -34,29 +18,4 @@
<item>@string/rotation_force_landscape</item> <item>@string/rotation_force_landscape</item>
<item>@string/rotation_reverse_portrait</item> <item>@string/rotation_reverse_portrait</item>
</string-array> </string-array>
<string-array name="invert_tapping_mode">
<item>@string/tapping_inverted_none</item>
<item>@string/tapping_inverted_horizontal</item>
<item>@string/tapping_inverted_vertical</item>
<item>@string/tapping_inverted_both</item>
</string-array>
<string-array name="pager_nav">
<item>@string/label_default</item>
<item>@string/l_nav</item>
<item>@string/kindlish_nav</item>
<item>@string/edge_nav</item>
<item>@string/right_and_left_nav</item>
<item>@string/disabled_nav</item>
</string-array>
<string-array name="webtoon_nav">
<item>@string/label_default</item>
<item>@string/l_nav</item>
<item>@string/kindlish_nav</item>
<item>@string/edge_nav</item>
<item>@string/right_and_left_nav</item>
<item>@string/disabled_nav</item>
</string-array>
</resources> </resources>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MaterialSpinnerView">
<attr name="title" format="reference|string"/>
<attr name="android:entries"/>
<attr name="summary" format="reference|string" />
</declare-styleable>
</resources>

View File

@ -1,6 +1,4 @@
<resources> <resources>
<dimen name="dialog_radius">8dp</dimen>
<dimen name="screen_edge_margin">16dp</dimen> <dimen name="screen_edge_margin">16dp</dimen>
<dimen name="appwidget_background_radius">16dp</dimen> <dimen name="appwidget_background_radius">16dp</dimen>

View File

@ -20,33 +20,6 @@
</style> </style>
<!--===========-->
<!--BottomSheet-->
<!--===========-->
<style name="ThemeOverlay.Tachiyomi.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
<item name="bottomSheetStyle">@style/Widget.Tachiyomi.BottomSheet.Modal</item>
<item name="android:windowAnimationStyle">@style/Animation.Tachiyomi.BottomSheetDialog</item>
</style>
<style name="Widget.Tachiyomi.BottomSheet.Modal" parent="Widget.Material3.BottomSheet.Modal">
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Tachiyomi.BottomSheet</item>
</style>
<style name="ShapeAppearanceOverlay.Tachiyomi.BottomSheet" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopRight">@dimen/dialog_radius</item>
<item name="cornerSizeTopLeft">@dimen/dialog_radius</item>
<item name="cornerSizeBottomRight">0dp</item>
<item name="cornerSizeBottomLeft">0dp</item>
</style>
<style name="Animation.Tachiyomi.BottomSheetDialog" parent="Animation.AppCompat.Dialog">
<item name="android:windowEnterAnimation">@anim/bottom_sheet_slide_in</item>
<item name="android:windowExitAnimation">@anim/bottom_sheet_slide_out</item>
</style>
<!--===============--> <!--===============-->
<!--Text Appearance--> <!--Text Appearance-->
<!--===============--> <!--===============-->
@ -71,18 +44,6 @@
</style> </style>
<!--=================-->
<!--Widgets.TabLayout-->
<!--=================-->
<style name="Widget.Tachiyomi.TabLayout" parent="Widget.Material3.TabLayout">
<item name="tabGravity">center</item>
<item name="tabInlineLabel">true</item>
<item name="tabMinWidth">75dp</item>
<item name="tabMode">scrollable</item>
<item name="tabRippleColor">@color/ripple_toolbar_fainter</item>
</style>
<!--==============--> <!--==============-->
<!--Widgets.Switch--> <!--Widgets.Switch-->
<!--==============--> <!--==============-->
@ -94,16 +55,6 @@
<item name="elevationOverlayEnabled">@bool/elevationOverlayEnabled</item> <item name="elevationOverlayEnabled">@bool/elevationOverlayEnabled</item>
</style> </style>
<!--==============-->
<!--Widgets.Slider-->
<!--==============-->
<style name="Widget.Tachiyomi.Slider" parent="Widget.Material3.Slider">
<item name="labelBehavior">gone</item>
<item name="tickVisible">false</item>
<item name="trackColorInactive">@color/slider_inactive_track</item>
<item name="trackColorActive">@color/slider_active_track</item>
</style>
<!--============--> <!--============-->
<!--FastScroller--> <!--FastScroller-->

View File

@ -64,17 +64,13 @@
<item name="android:backgroundDimAmount">0.32</item> <item name="android:backgroundDimAmount">0.32</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
<item name="actionOverflowButtonStyle">@style/Theme.Tachiyomi.ActionButton.Overflow</item> <item name="actionOverflowButtonStyle">@style/Theme.Tachiyomi.ActionButton.Overflow</item>
<item name="actionModeCloseDrawable">@drawable/ic_close_24dp</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Tachiyomi</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.Tachiyomi</item>
<item name="bottomSheetDialogTheme">@style/ThemeOverlay.Tachiyomi.BottomSheetDialog</item>
<item name="textInputStyle">@style/Widget.Material3.TextInputLayout.OutlinedBox</item> <item name="textInputStyle">@style/Widget.Material3.TextInputLayout.OutlinedBox</item>
<item name="appBarLayoutStyle">@style/Widget.Material3.AppBarLayout</item> <item name="appBarLayoutStyle">@style/Widget.Material3.AppBarLayout</item>
<item name="toolbarStyle">@style/Widget.Material3.Toolbar.Surface</item> <item name="toolbarStyle">@style/Widget.Material3.Toolbar.Surface</item>
<item name="tabStyle">@style/Widget.Tachiyomi.TabLayout</item>
<item name="switchStyle">@style/Widget.Tachiyomi.Switch</item> <item name="switchStyle">@style/Widget.Tachiyomi.Switch</item>
<item name="materialSwitchStyle">@style/Widget.Material3.CompoundButton.MaterialSwitch</item> <item name="materialSwitchStyle">@style/Widget.Material3.CompoundButton.MaterialSwitch</item>
<item name="switchPreferenceCompatStyle">@style/Widget.Tachiyomi.Switch</item> <item name="switchPreferenceCompatStyle">@style/Widget.Tachiyomi.Switch</item>
<item name="sliderStyle">@style/Widget.Tachiyomi.Slider</item>
<item name="materialCardViewStyle">@style/Widget.Material3.CardView.Elevated</item> <item name="materialCardViewStyle">@style/Widget.Material3.CardView.Elevated</item>
<!-- Preference text appearance --> <!-- Preference text appearance -->

View File

@ -7,6 +7,7 @@ import tachiyomi.core.util.lang.withIOContext
/** /**
* Util for evaluating JavaScript in sources. * Util for evaluating JavaScript in sources.
*/ */
@Suppress("UNUSED", "UNCHECKED_CAST")
class JavaScriptEngine(context: Context) { class JavaScriptEngine(context: Context) {
/** /**
@ -17,7 +18,6 @@ class JavaScriptEngine(context: Context) {
* @param script JavaScript to execute. * @param script JavaScript to execute.
* @return Result of JavaScript code as a primitive type. * @return Result of JavaScript code as a primitive type.
*/ */
@Suppress("UNUSED", "UNCHECKED_CAST")
suspend fun <T> evaluate(script: String): T = withIOContext { suspend fun <T> evaluate(script: String): T = withIOContext {
QuickJs.create().use { QuickJs.create().use {
it.evaluate(script) as T it.evaluate(script) as T

View File

@ -11,31 +11,26 @@ import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper( class NetworkHelper(
context: Context, private val context: Context,
private val preferences: NetworkPreferences, private val preferences: NetworkPreferences,
) { ) {
private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieJar = AndroidCookieJar() val cookieJar = AndroidCookieJar()
private val userAgentInterceptor by lazy { val client: OkHttpClient = run {
UserAgentInterceptor(::defaultUserAgentProvider)
}
private val cloudflareInterceptor by lazy {
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider)
}
private val baseClientBuilder: OkHttpClient.Builder
get() {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.cookieJar(cookieJar) .cookieJar(cookieJar)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES) .callTimeout(2, TimeUnit.MINUTES)
.cache(
Cache(
directory = File(context.cacheDir, "network_cache"),
maxSize = 5L * 1024 * 1024, // 5 MiB
),
)
.addInterceptor(UncaughtExceptionInterceptor()) .addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(userAgentInterceptor) .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
if (preferences.verboseLogging().get()) { if (preferences.verboseLogging().get()) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply { val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
@ -44,6 +39,8 @@ class NetworkHelper(
builder.addNetworkInterceptor(httpLoggingInterceptor) builder.addNetworkInterceptor(httpLoggingInterceptor)
} }
builder.addInterceptor(CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider))
when (preferences.dohProvider().get()) { when (preferences.dohProvider().get()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle() PREF_DOH_GOOGLE -> builder.dohGoogle()
@ -59,17 +56,15 @@ class NetworkHelper(
PREF_DOH_SHECAN -> builder.dohShecan() PREF_DOH_SHECAN -> builder.dohShecan()
} }
return builder builder.build()
} }
val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() } /**
* @deprecated Since extension-lib 1.5
*/
@Deprecated("The regular client handles Cloudflare by default")
@Suppress("UNUSED") @Suppress("UNUSED")
val cloudflareClient by lazy { val cloudflareClient: OkHttpClient = client
client.newBuilder()
.addInterceptor(cloudflareInterceptor)
.build()
}
fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim() fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim()
} }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -388,7 +388,7 @@
<string name="white_background">White</string> <string name="white_background">White</string>
<string name="gray_background">Gray</string> <string name="gray_background">Gray</string>
<string name="black_background">Black</string> <string name="black_background">Black</string>
<string name="automatic_background">Automatic</string> <string name="automatic_background">Auto</string>
<string name="pref_viewer_type">Default reading mode</string> <string name="pref_viewer_type">Default reading mode</string>
<string name="l_nav">L shaped</string> <string name="l_nav">L shaped</string>
<string name="kindlish_nav">Kindle-ish</string> <string name="kindlish_nav">Kindle-ish</string>
@ -485,7 +485,6 @@
<string name="action_track">Track</string> <string name="action_track">Track</string>
<!-- Browse section --> <!-- Browse section -->
<string name="pref_search_pinned_sources_only">Only search pinned sources in global search</string>
<string name="pref_hide_in_library_items">Hide entries already in library</string> <string name="pref_hide_in_library_items">Hide entries already in library</string>
<!-- Backup section --> <!-- Backup section -->
@ -660,6 +659,7 @@
<string name="latest">Latest</string> <string name="latest">Latest</string>
<string name="popular">Popular</string> <string name="popular">Popular</string>
<string name="browse">Browse</string> <string name="browse">Browse</string>
<string name="has_results">Has results</string>
<string name="local_source_help_guide">Local source guide</string> <string name="local_source_help_guide">Local source guide</string>
<string name="no_pinned_sources">You have no pinned sources</string> <string name="no_pinned_sources">You have no pinned sources</string>
<string name="chapter_not_found">Chapter not found</string> <string name="chapter_not_found">Chapter not found</string>

View File

@ -4,6 +4,8 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.FlowRowScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -61,7 +63,10 @@ fun HeadingItem(
style = MaterialTheme.typography.header, style = MaterialTheme.typography.header,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), .padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
) )
} }
@ -203,7 +208,10 @@ fun SelectItem(
modifier = Modifier modifier = Modifier
.menuAnchor() .menuAnchor()
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), .padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
label = { Text(text = label) }, label = { Text(text = label) },
value = options[selectedIndex].toString(), value = options[selectedIndex].toString(),
onValueChange = {}, onValueChange = {},
@ -259,7 +267,10 @@ fun TriStateItem(
}, },
) )
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), .padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp), horizontalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
@ -306,6 +317,26 @@ fun TextItem(
) )
} }
@Composable
fun SettingsFlowRow(
@StringRes labelRes: Int,
content: @Composable FlowRowScope.() -> Unit,
) {
Column {
HeadingItem(labelRes)
FlowRow(
modifier = Modifier.padding(
start = SettingsItemsPaddings.Horizontal,
top = 0.dp,
end = SettingsItemsPaddings.Horizontal,
bottom = SettingsItemsPaddings.Vertical,
),
horizontalArrangement = Arrangement.spacedBy(4.dp),
content = content,
)
}
}
@Composable @Composable
private fun BaseSettingsItem( private fun BaseSettingsItem(
label: String, label: String,
@ -316,7 +347,10 @@ private fun BaseSettingsItem(
modifier = Modifier modifier = Modifier
.clickable(onClick = onClick) .clickable(onClick = onClick)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), .padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp), horizontalArrangement = Arrangement.spacedBy(24.dp),
) { ) {

View File

@ -0,0 +1,60 @@
package tachiyomi.presentation.core.components.material
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
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.unit.dp
@Composable
fun Chip(
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.15f),
contentColor: Color = MaterialTheme.colorScheme.onSurface,
onClick: () -> Unit = {},
content: @Composable () -> Unit,
) {
Surface(
modifier = Modifier,
shape = CircleShape,
color = backgroundColor,
contentColor = contentColor,
onClick = {},
) {
Row(
modifier = modifier.clickable(onClick = onClick)
.widthIn(min = 56.dp)
.requiredHeight(32.dp)
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
ProvideTextStyle(MaterialTheme.typography.bodySmall, content)
}
}
}
@Composable
fun ChoiceChip(
modifier: Modifier = Modifier,
isSelected: Boolean,
onClick: () -> Unit = {},
selectedBackgroundColor: Color = MaterialTheme.colorScheme.primary,
selectedContentColor: Color = MaterialTheme.colorScheme.onPrimary,
content: @Composable () -> Unit,
) {
if (isSelected) {
Chip(modifier, selectedBackgroundColor, selectedContentColor, onClick, content)
} else {
Chip(modifier, onClick = onClick, content = content)
}
}

View File

@ -0,0 +1,90 @@
package tachiyomi.presentation.core.components.material
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
val StartItemShape = RoundedCornerShape(topStartPercent = 100, bottomStartPercent = 100)
val MiddleItemShape = RoundedCornerShape(0)
val EndItemShape = RoundedCornerShape(topEndPercent = 100, bottomEndPercent = 100)
@Composable
fun SegmentedButtons(
modifier: Modifier = Modifier,
entries: List<String>,
selectedIndex: Int,
onClick: (Int) -> Unit,
) {
Row(
modifier = modifier,
) {
entries.mapIndexed { index, label ->
val shape = remember(entries, index) {
when (index) {
0 -> StartItemShape
entries.lastIndex -> EndItemShape
else -> MiddleItemShape
}
}
if (index == selectedIndex) {
Button(
modifier = Modifier.weight(1f),
shape = shape,
onClick = { onClick(index) },
) {
Text(
text = label,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
} else {
OutlinedButton(
modifier = Modifier.weight(1f),
shape = shape,
onClick = { onClick(index) },
) {
Text(
text = label,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}
@Preview
@Composable
private fun SegmentedButtonsPreview() {
Column {
SegmentedButtons(
entries = listOf(
"Day",
"Week",
"Month",
"Year",
),
selectedIndex = 1,
onClick = {},
)
SegmentedButtons(
entries = listOf(
"Foo",
"Bar",
),
selectedIndex = 1,
onClick = {},
)
}
}

View File

@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TabPosition import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.SecondaryIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -50,8 +50,8 @@ fun TabIndicator(
currentTabPosition: TabPosition, currentTabPosition: TabPosition,
currentPageOffsetFraction: Float, currentPageOffsetFraction: Float,
) { ) {
TabRowDefaults.Indicator( SecondaryIndicator(
Modifier modifier = Modifier
.tabIndicatorOffset(currentTabPosition, currentPageOffsetFraction) .tabIndicatorOffset(currentTabPosition, currentPageOffsetFraction)
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)), .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),