1 parent
78b898dc5c
commit
52ca5a7eb8
27 files changed
+159
-206
No files matched your search
+27
-30
@@ -243,9 +243,8 @@ private class AnchoredHorizontalDraggableNode<T>(
|
||||
private fun Offset.reverseIfNeeded() = if (isReverseDirection) this * -1f else this * 1f
|
||||
}
|
||||
|
||||
internal fun <T> AnchoredDraggableState<T>.newOffsetForDelta(delta: Float) =
|
||||
((if (offset.isNaN()) 0f else offset) + delta)
|
||||
.coerceIn(anchors.minPosition(), anchors.maxPosition())
|
||||
internal fun <T> AnchoredDraggableState<T>.newOffsetForDelta(delta: Float) = ((if (offset.isNaN()) 0f else offset) + delta)
|
||||
.coerceIn(anchors.minPosition(), anchors.maxPosition())
|
||||
|
||||
/**
|
||||
* Compute the target anchor based on the [currentOffset], [velocity] and [positionalThreshold] and
|
||||
@@ -299,17 +298,16 @@ internal fun <T> anchoredDraggableFlingBehavior(
|
||||
density: Density,
|
||||
positionalThreshold: (totalDistance: Float) -> Float,
|
||||
snapAnimationSpec: AnimationSpec<Float>,
|
||||
): TargetedFlingBehavior =
|
||||
snapFlingBehavior(
|
||||
decayAnimationSpec = NoOpDecayAnimationSpec,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
snapLayoutInfoProvider =
|
||||
AnchoredDraggableLayoutInfoProvider(
|
||||
state = state,
|
||||
positionalThreshold = positionalThreshold,
|
||||
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
||||
),
|
||||
)
|
||||
): TargetedFlingBehavior = snapFlingBehavior(
|
||||
decayAnimationSpec = NoOpDecayAnimationSpec,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
snapLayoutInfoProvider =
|
||||
AnchoredDraggableLayoutInfoProvider(
|
||||
state = state,
|
||||
positionalThreshold = positionalThreshold,
|
||||
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -317,25 +315,24 @@ private fun <T> AnchoredDraggableLayoutInfoProvider(
|
||||
state: AnchoredDraggableState<T>,
|
||||
positionalThreshold: (totalDistance: Float) -> Float,
|
||||
velocityThreshold: () -> Float,
|
||||
): SnapLayoutInfoProvider =
|
||||
object : SnapLayoutInfoProvider {
|
||||
): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider {
|
||||
|
||||
// We never decay in AnchoredDraggable's fling
|
||||
override fun calculateApproachOffset(velocity: Float, decayOffset: Float) = 0f
|
||||
// We never decay in AnchoredDraggable's fling
|
||||
override fun calculateApproachOffset(velocity: Float, decayOffset: Float) = 0f
|
||||
|
||||
override fun calculateSnapOffset(velocity: Float): Float {
|
||||
val currentOffset = state.requireOffset()
|
||||
val target =
|
||||
state.anchors.computeTarget(
|
||||
currentOffset = currentOffset,
|
||||
currentValue = state.currentValue,
|
||||
velocity = velocity,
|
||||
positionalThreshold = positionalThreshold,
|
||||
velocityThreshold = velocityThreshold,
|
||||
)
|
||||
return state.anchors.positionOf(target) - currentOffset
|
||||
}
|
||||
override fun calculateSnapOffset(velocity: Float): Float {
|
||||
val currentOffset = state.requireOffset()
|
||||
val target =
|
||||
state.anchors.computeTarget(
|
||||
currentOffset = currentOffset,
|
||||
currentValue = state.currentValue,
|
||||
velocity = velocity,
|
||||
positionalThreshold = positionalThreshold,
|
||||
velocityThreshold = velocityThreshold,
|
||||
)
|
||||
return state.anchors.positionOf(target) - currentOffset
|
||||
}
|
||||
}
|
||||
|
||||
private val NoOpDecayAnimationSpec: DecayAnimationSpec<Float> =
|
||||
object : FloatDecayAnimationSpec {
|
||||
|
||||
@@ -206,11 +206,10 @@ class DrawerState2(
|
||||
/**
|
||||
* The default [Saver] implementation for [DrawerState2].
|
||||
*/
|
||||
fun Saver(confirmStateChange: (DrawerValue) -> Boolean) =
|
||||
Saver<DrawerState2, DrawerValue>(
|
||||
save = { it.currentValue },
|
||||
restore = { DrawerState2(it, confirmStateChange) },
|
||||
)
|
||||
fun Saver(confirmStateChange: (DrawerValue) -> Boolean) = Saver<DrawerState2, DrawerValue>(
|
||||
save = { it.currentValue },
|
||||
restore = { DrawerState2(it, confirmStateChange) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,16 +98,14 @@ fun rememberSystemUiController(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun findWindow(): Window? =
|
||||
(LocalView.current.parent as? DialogWindowProvider)?.window
|
||||
?: LocalView.current.context.findWindow()
|
||||
private fun findWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
|
||||
?: LocalView.current.context.findWindow()
|
||||
|
||||
private tailrec fun Context.findWindow(): Window? =
|
||||
when (this) {
|
||||
is Activity -> window
|
||||
is ContextWrapper -> baseContext.findWindow()
|
||||
else -> null
|
||||
}
|
||||
private tailrec fun Context.findWindow(): Window? = when (this) {
|
||||
is Activity -> window
|
||||
is ContextWrapper -> baseContext.findWindow()
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class for setting the navigation and status bar colors for a [View], gracefully
|
||||
|
||||
@@ -635,8 +635,7 @@ data class WebViewError(
|
||||
fun rememberWebViewState(
|
||||
url: String,
|
||||
additionalHttpHeaders: Map<String, String> = emptyMap(),
|
||||
): WebViewState =
|
||||
// Rather than using .apply {} here we will recreate the state, this prevents
|
||||
): WebViewState = // Rather than using .apply {} here we will recreate the state, this prevents
|
||||
// a recomposition loop when the webview updates the url itself.
|
||||
remember {
|
||||
WebViewState(
|
||||
|
||||
@@ -303,9 +303,8 @@ object EhEngine {
|
||||
}.executeSafely { }
|
||||
}
|
||||
|
||||
suspend fun getFavoriteNote(gid: Long, token: String) =
|
||||
ehRequest(EhUrl.getAddFavorites(gid, token), EhUrl.getGalleryDetailUrl(gid, token))
|
||||
.fetchUsingAsText(FavoritesParser::parseNote)
|
||||
suspend fun getFavoriteNote(gid: Long, token: String) = ehRequest(EhUrl.getAddFavorites(gid, token), EhUrl.getGalleryDetailUrl(gid, token))
|
||||
.fetchUsingAsText(FavoritesParser::parseNote)
|
||||
|
||||
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
||||
val url = EhUrl.getDownloadArchive(gid, token, or)
|
||||
@@ -384,54 +383,51 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(String::parseAs)
|
||||
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String? = null) =
|
||||
galleryInfoList.chunked(MAX_REQUEST_SIZE).chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk ->
|
||||
if (index != 0) {
|
||||
delay(REQUEST_INTERVAL)
|
||||
}
|
||||
chunk.parMap {
|
||||
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "gdata")
|
||||
array("gidlist") {
|
||||
it.forEach {
|
||||
addJsonArray {
|
||||
add(it.gid)
|
||||
add(it.token)
|
||||
}
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String? = null) = galleryInfoList.chunked(MAX_REQUEST_SIZE).chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk ->
|
||||
if (index != 0) {
|
||||
delay(REQUEST_INTERVAL)
|
||||
}
|
||||
chunk.parMap {
|
||||
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "gdata")
|
||||
array("gidlist") {
|
||||
it.forEach {
|
||||
addJsonArray {
|
||||
add(it.gid)
|
||||
add(it.token)
|
||||
}
|
||||
}
|
||||
put("namespace", 1)
|
||||
}
|
||||
}.fetchUsingAsText { GalleryApiParser.parse(this, it) }
|
||||
}
|
||||
put("namespace", 1)
|
||||
}
|
||||
}.fetchUsingAsText { GalleryApiParser.parse(this, it) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun voteComment(apiUid: Long, apiKey: String?, gid: Long, token: String, commentId: Long, commentVote: Int): VoteCommentResult =
|
||||
ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "votecomment")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("comment_id", commentId)
|
||||
put("comment_vote", commentVote)
|
||||
}
|
||||
}.fetchUsingAsText(String::parseAs)
|
||||
suspend fun voteComment(apiUid: Long, apiKey: String?, gid: Long, token: String, commentId: Long, commentVote: Int): VoteCommentResult = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "votecomment")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("comment_id", commentId)
|
||||
put("comment_vote", commentVote)
|
||||
}
|
||||
}.fetchUsingAsText(String::parseAs)
|
||||
|
||||
suspend fun voteTag(apiUid: Long, apiKey: String?, gid: Long, token: String, tags: String, vote: Int) =
|
||||
ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "taggallery")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("tags", tags)
|
||||
put("vote", vote)
|
||||
}
|
||||
}.fetchUsingAsText(VoteTagParser::parse)
|
||||
suspend fun voteTag(apiUid: Long, apiKey: String?, gid: Long, token: String, tags: String, vote: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "taggallery")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("tags", tags)
|
||||
put("vote", vote)
|
||||
}
|
||||
}.fetchUsingAsText(VoteTagParser::parse)
|
||||
|
||||
suspend fun getGalleryToken(gid: Long, gtoken: String, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
|
||||
@@ -125,8 +125,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
|
||||
private fun String.removeSpace(): String = replace(" ", "")
|
||||
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
|
||||
private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
|
||||
@@ -108,11 +108,10 @@ object EhUrl {
|
||||
else -> URL_IMAGE_SEARCH_E
|
||||
}
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) =
|
||||
ehUrl(listOf("g", "$gid", token)) {
|
||||
if (index != 0) addQueryParameter("p", "$index")
|
||||
if (allComment) addQueryParameter("hc", "1")
|
||||
}.buildString()
|
||||
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(listOf("g", "$gid", token)) {
|
||||
if (index != 0) addQueryParameter("p", "$index")
|
||||
if (allComment) addQueryParameter("hc", "1")
|
||||
}.buildString()
|
||||
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString()
|
||||
|
||||
|
||||
@@ -4,6 +4,5 @@ import io.ktor.client.engine.HttpClientEngine
|
||||
import io.ktor.client.engine.HttpClientEngineFactory
|
||||
|
||||
object Cronet : HttpClientEngineFactory<CronetConfig> {
|
||||
override fun create(block: CronetConfig.() -> Unit): HttpClientEngine =
|
||||
CronetEngine(CronetConfig().apply(block))
|
||||
override fun create(block: CronetConfig.() -> Unit): HttpClientEngine = CronetEngine(CronetConfig().apply(block))
|
||||
}
|
||||
@@ -315,8 +315,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
return archived
|
||||
}
|
||||
|
||||
suspend fun exportAsCbz(file: Path) =
|
||||
downloadDir!!.find(archiveName)?.sendTo(file) ?: archiveTo(file)
|
||||
suspend fun exportAsCbz(file: Path) = downloadDir!!.find(archiveName)?.sendTo(file) ?: archiveTo(file)
|
||||
|
||||
private suspend fun archiveTo(file: Path) = resourceScope {
|
||||
val comicInfo = closeable {
|
||||
|
||||
@@ -106,8 +106,7 @@ suspend fun keepNoMediaFileStatus(downloadDir: Path = downloadLocation) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getFavoriteIcon(favorited: Boolean) =
|
||||
if (favorited) Icons.Default.Favorite else Icons.Default.FavoriteBorder
|
||||
fun getFavoriteIcon(favorited: Boolean) = if (favorited) Icons.Default.Favorite else Icons.Default.FavoriteBorder
|
||||
|
||||
suspend fun DialogState.startDownload(
|
||||
context: Context,
|
||||
@@ -281,14 +280,11 @@ suspend fun removeFromFavorites(galleryInfo: BaseGalleryInfo) = doModifyFavorite
|
||||
localFavorited = EhDB.containLocalFavorites(galleryInfo.gid),
|
||||
)
|
||||
|
||||
fun DestinationsNavigator.navToReader(info: BaseGalleryInfo, page: Int = -1) =
|
||||
navToReader(ReaderScreenArgs.Gallery(info, page))
|
||||
fun DestinationsNavigator.navToReader(info: BaseGalleryInfo, page: Int = -1) = navToReader(ReaderScreenArgs.Gallery(info, page))
|
||||
|
||||
fun DestinationsNavigator.navToReader(uri: Uri) =
|
||||
navToReader(ReaderScreenArgs.Archive(uri))
|
||||
fun DestinationsNavigator.navToReader(uri: Uri) = navToReader(ReaderScreenArgs.Archive(uri))
|
||||
|
||||
private fun DestinationsNavigator.navToReader(args: ReaderScreenArgs) =
|
||||
navigate(ReaderScreenDestination(args)) { launchSingleTop = true }
|
||||
private fun DestinationsNavigator.navToReader(args: ReaderScreenArgs) = navigate(ReaderScreenDestination(args)) { launchSingleTop = true }
|
||||
|
||||
context(DialogState, Context, DestinationsNavigator)
|
||||
suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
|
||||
|
||||
@@ -94,8 +94,7 @@ class SecurityActivity : AppCompatActivity() {
|
||||
result: BiometricPrompt.AuthenticationResult,
|
||||
) = onAuthenticationSucceeded(activity, result)
|
||||
|
||||
override fun onAuthenticationFailed(activity: FragmentActivity?) =
|
||||
onAuthenticationFailed(activity)
|
||||
override fun onAuthenticationFailed(activity: FragmentActivity?) = onAuthenticationFailed(activity)
|
||||
}
|
||||
|
||||
private fun onSuccess() {
|
||||
|
||||
@@ -238,21 +238,19 @@ private fun PageContainer(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ZoomableState?.panLeft(distance: Float, bounds: Rect): Boolean =
|
||||
if (canPan { it.right - bounds.right }) {
|
||||
panBy(Offset(-distance, 0f))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
private suspend fun ZoomableState?.panLeft(distance: Float, bounds: Rect): Boolean = if (canPan { it.right - bounds.right }) {
|
||||
panBy(Offset(-distance, 0f))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
private suspend fun ZoomableState?.panRight(distance: Float, bounds: Rect): Boolean =
|
||||
if (canPan { bounds.left - it.left }) {
|
||||
panBy(Offset(distance, 0f))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
private suspend fun ZoomableState?.panRight(distance: Float, bounds: Rect): Boolean = if (canPan { bounds.left - it.left }) {
|
||||
panBy(Offset(distance, 0f))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
private inline fun ZoomableState?.canPan(getRemaining: (Rect) -> Float): Boolean {
|
||||
// TODO: Remove when K2 mode in IDE is stable
|
||||
|
||||
@@ -14,11 +14,10 @@ data class DownloadsFilterState(
|
||||
val keyword: String = "",
|
||||
) : Parcelable
|
||||
|
||||
fun DownloadsFilterState.take(info: DownloadInfo) =
|
||||
mode.take(info, label) &&
|
||||
(state == -1 || info.state == state) &&
|
||||
with(info) {
|
||||
title.containsIgnoreCase(keyword) ||
|
||||
titleJpn.containsIgnoreCase(keyword) ||
|
||||
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
|
||||
}
|
||||
fun DownloadsFilterState.take(info: DownloadInfo) = mode.take(info, label) &&
|
||||
(state == -1 || info.state == state) &&
|
||||
with(info) {
|
||||
title.containsIgnoreCase(keyword) ||
|
||||
titleJpn.containsIgnoreCase(keyword) ||
|
||||
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
|
||||
}
|
||||
@@ -95,8 +95,7 @@ abstract class Suggestion {
|
||||
open val canOpenDirectly: Boolean = false
|
||||
}
|
||||
|
||||
suspend fun SearchDao.suggestions(prefix: String, limit: Int) =
|
||||
(if (prefix.isBlank()) list(limit) else rawSuggestions(prefix, limit))
|
||||
suspend fun SearchDao.suggestions(prefix: String, limit: Int) = (if (prefix.isBlank()) list(limit) else rawSuggestions(prefix, limit))
|
||||
|
||||
context(DialogState, DestinationsNavigator)
|
||||
@Composable
|
||||
|
||||
@@ -2,5 +2,4 @@ package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
inline fun Modifier.thenIf(condition: Boolean, crossinline block: Modifier.() -> Modifier) =
|
||||
if (condition) block() else this
|
||||
inline fun Modifier.thenIf(condition: Boolean, crossinline block: Modifier.() -> Modifier) = if (condition) block() else this
|
||||
@@ -29,8 +29,7 @@ value class TransitionsVisibilityScope(val scopes: Set<AnimatedVisibilityScope>)
|
||||
|
||||
context(TransitionsVisibilityScope)
|
||||
@Composable
|
||||
inline fun <T> togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) =
|
||||
block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) })
|
||||
inline fun <T> togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) = block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) })
|
||||
|
||||
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
|
||||
@Composable
|
||||
|
||||
@@ -66,22 +66,21 @@ object AppUpdater {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun downloadUpdate(url: String, file: File) =
|
||||
ghStatement(url).executeSafely { response ->
|
||||
response.status.ensureSuccess()
|
||||
if (url.endsWith("zip")) {
|
||||
response.bodyAsChannel().toInputStream().use { stream ->
|
||||
ZipInputStream(stream).use { zip ->
|
||||
zip.nextEntry
|
||||
file.outputStream().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
suspend fun downloadUpdate(url: String, file: File) = ghStatement(url).executeSafely { response ->
|
||||
response.status.ensureSuccess()
|
||||
if (url.endsWith("zip")) {
|
||||
response.bodyAsChannel().toInputStream().use { stream ->
|
||||
ZipInputStream(stream).use { zip ->
|
||||
zip.nextEntry
|
||||
file.outputStream().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.bodyAsChannel().copyTo(file)
|
||||
}
|
||||
} else {
|
||||
response.bodyAsChannel().copyTo(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun ghStatement(url: String) = ktorClient.prepareGet(url) {
|
||||
|
||||
@@ -51,8 +51,7 @@ object AppConfig {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getDirInExternalAppDir(filename: String, create: Boolean = true) =
|
||||
externalAppDir?.run { File(this, filename).takeIf { if (create) it.ensureDirectory() else it.isDirectory } }
|
||||
private fun getDirInExternalAppDir(filename: String, create: Boolean = true) = externalAppDir?.run { File(this, filename).takeIf { if (create) it.ensureDirectory() else it.isDirectory } }
|
||||
|
||||
val defaultDownloadDir: File?
|
||||
get() = getDirInExternalAppDir(DOWNLOAD, false)
|
||||
|
||||
@@ -8,11 +8,8 @@ import kotlinx.datetime.atStartOfDayIn
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
fun LocalDate.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long =
|
||||
atStartOfDayIn(timeZone).toEpochMilliseconds()
|
||||
fun LocalDate.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long = atStartOfDayIn(timeZone).toEpochMilliseconds()
|
||||
|
||||
fun LocalDateTime.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long =
|
||||
toInstant(timeZone).toEpochMilliseconds()
|
||||
fun LocalDateTime.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long = toInstant(timeZone).toEpochMilliseconds()
|
||||
|
||||
fun Long.toLocalDateTime(timeZone: TimeZone = TimeZone.UTC): LocalDateTime =
|
||||
Instant.fromEpochMilliseconds(this).toLocalDateTime(timeZone)
|
||||
fun Long.toLocalDateTime(timeZone: TimeZone = TimeZone.UTC): LocalDateTime = Instant.fromEpochMilliseconds(this).toLocalDateTime(timeZone)
|
||||
@@ -12,8 +12,7 @@ import java.io.FileDescriptor
|
||||
import okio.Path
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
private fun sendFileTotally(from: FileDescriptor, to: FileDescriptor): Long =
|
||||
Os.sendfile(to, from, Int64Ref(0), Long.MAX_VALUE)
|
||||
private fun sendFileTotally(from: FileDescriptor, to: FileDescriptor): Long = Os.sendfile(to, from, Int64Ref(0), Long.MAX_VALUE)
|
||||
|
||||
infix fun Path.sendTo(file: Path): Long {
|
||||
if (isAtLeastP) {
|
||||
|
||||
@@ -28,8 +28,7 @@ object FileUtils {
|
||||
// ext4 through a FUSE layer, so use that limit.
|
||||
private const val MAX_FILENAME_BYTES = 255
|
||||
|
||||
fun ensureDirectory(file: File?) =
|
||||
file?.let { if (it.exists()) it.isDirectory else it.mkdirs() } ?: false
|
||||
fun ensureDirectory(file: File?) = file?.let { if (it.exists()) it.isDirectory else it.mkdirs() } ?: false
|
||||
|
||||
/**
|
||||
* Convert byte to human readable string.<br></br>
|
||||
@@ -81,22 +80,21 @@ object FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidFatFilenameChar(c: Char) =
|
||||
when (c.code) {
|
||||
in 0x00..0x1F,
|
||||
'"'.code,
|
||||
'*'.code,
|
||||
'/'.code,
|
||||
':'.code,
|
||||
'<'.code,
|
||||
'>'.code,
|
||||
'?'.code,
|
||||
'\\'.code,
|
||||
'|'.code,
|
||||
0x7F,
|
||||
-> false
|
||||
else -> true
|
||||
}
|
||||
private fun isValidFatFilenameChar(c: Char) = when (c.code) {
|
||||
in 0x00..0x1F,
|
||||
'"'.code,
|
||||
'*'.code,
|
||||
'/'.code,
|
||||
':'.code,
|
||||
'<'.code,
|
||||
'>'.code,
|
||||
'?'.code,
|
||||
'\\'.code,
|
||||
'|'.code,
|
||||
0x7F,
|
||||
-> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
// From https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/FileUtils.java;l=1142;drc=7b647e4ea0e92f33c19b315eaed364ee067ba0aa
|
||||
/**
|
||||
@@ -135,8 +133,7 @@ object FileUtils {
|
||||
* @param filename the complete filename
|
||||
* @return null for can't find extension
|
||||
*/
|
||||
fun getExtensionFromFilename(filename: String?) =
|
||||
filename?.substringAfterLast('.', "")?.ifEmpty { null }
|
||||
fun getExtensionFromFilename(filename: String?) = filename?.substringAfterLast('.', "")?.ifEmpty { null }
|
||||
|
||||
/**
|
||||
* Get name from filename
|
||||
@@ -144,8 +141,7 @@ object FileUtils {
|
||||
* @param filename the complete filename
|
||||
* @return null for start with . dot
|
||||
*/
|
||||
fun getNameFromFilename(filename: String?) =
|
||||
filename?.substringBeforeLast('.')?.ifEmpty { null }
|
||||
fun getNameFromFilename(filename: String?) = filename?.substringBeforeLast('.')?.ifEmpty { null }
|
||||
|
||||
/**
|
||||
* Create a temp file, you need to delete it by you self.
|
||||
|
||||
@@ -116,8 +116,7 @@ abstract class RangeSet<T : Comparable<T>> :
|
||||
* @param elements ranges of values to determine presence of
|
||||
* @return whether all values are all contained within the set
|
||||
*/
|
||||
override fun containsAll(elements: Collection<ClosedRange<T>>): Boolean =
|
||||
elements.all { contains(it) }
|
||||
override fun containsAll(elements: Collection<ClosedRange<T>>): Boolean = elements.all { contains(it) }
|
||||
|
||||
/**
|
||||
* @return whether the set contains any values
|
||||
|
||||
@@ -34,8 +34,7 @@ class IntRangeSet : RangeSet<Int> {
|
||||
|
||||
private constructor(rangeSet: IntRangeSet) : super(rangeSet)
|
||||
|
||||
override fun createRange(start: Int, endInclusive: Int): IntRange =
|
||||
IntRange(start, endInclusive)
|
||||
override fun createRange(start: Int, endInclusive: Int): IntRange = IntRange(start, endInclusive)
|
||||
|
||||
override fun incrementValue(value: Int): Int = value + 1
|
||||
|
||||
|
||||
@@ -156,6 +156,5 @@ object ReadableTime {
|
||||
}
|
||||
}
|
||||
|
||||
fun getFilenamableTime(time: Instant = Clock.System.now()): String =
|
||||
FILENAMABLE_DATE_FORMAT.format(time.toLocalDateTime(TimeZone.currentSystemDefault()))
|
||||
fun getFilenamableTime(time: Instant = Clock.System.now()): String = FILENAMABLE_DATE_FORMAT.format(time.toLocalDateTime(TimeZone.currentSystemDefault()))
|
||||
}
|
||||
@@ -29,11 +29,9 @@ fun Path.moveTo(target: Path) = SystemFileSystem.atomicMove(this, target)
|
||||
|
||||
fun Path.openFileDescriptor(mode: String) = SystemFileSystem.openFileDescriptor(this, mode)
|
||||
|
||||
fun Path.openInputStream(): FileInputStream =
|
||||
ParcelFileDescriptor.AutoCloseInputStream(SystemFileSystem.openFileDescriptor(this, "r"))
|
||||
fun Path.openInputStream(): FileInputStream = ParcelFileDescriptor.AutoCloseInputStream(SystemFileSystem.openFileDescriptor(this, "r"))
|
||||
|
||||
fun Path.openOutputStream(): FileOutputStream =
|
||||
ParcelFileDescriptor.AutoCloseOutputStream(SystemFileSystem.openFileDescriptor(this, "wt"))
|
||||
fun Path.openOutputStream(): FileOutputStream = ParcelFileDescriptor.AutoCloseOutputStream(SystemFileSystem.openFileDescriptor(this, "wt"))
|
||||
|
||||
fun Path.toUri(): Uri {
|
||||
val str = toString()
|
||||
|
||||
@@ -4,11 +4,9 @@ import androidx.compose.runtime.Composable
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
fun Any?.ifNotNullThen(content: @Composable () -> Unit): (@Composable () -> Unit)? =
|
||||
if (this != null) content else null
|
||||
fun Any?.ifNotNullThen(content: @Composable () -> Unit): (@Composable () -> Unit)? = if (this != null) content else null
|
||||
|
||||
fun Boolean.ifTrueThen(content: @Composable () -> Unit): (@Composable () -> Unit)? =
|
||||
if (this) content else null
|
||||
fun Boolean.ifTrueThen(content: @Composable () -> Unit): (@Composable () -> Unit)? = if (this) content else null
|
||||
|
||||
fun roundToDP(value: Double, places: Int): Double = BigDecimal(value).setScale(places, RoundingMode.HALF_EVEN).toDouble()
|
||||
|
||||
|
||||
@@ -7,18 +7,14 @@ import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.Main, block = block)
|
||||
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job = launch(Dispatchers.Main, block = block)
|
||||
|
||||
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.IO, block = block)
|
||||
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job = launch(Dispatchers.IO, block = block)
|
||||
|
||||
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launchIO { withContext(NonCancellable, block) }
|
||||
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job = launchIO { withContext(NonCancellable, block) }
|
||||
|
||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
||||
|
||||
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
|
||||
withContext(NonCancellable, block)
|
||||
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) = withContext(NonCancellable, block)
|
||||
Reference in new issue
Block a user