package com.github.libretube.util import android.content.Context import android.icu.text.RelativeDateTimeFormatter import android.net.Uri import android.text.format.DateUtils import androidx.core.text.isDigitsOnly import com.github.libretube.BuildConfig import com.github.libretube.R import com.github.libretube.extensions.formatShort import com.github.libretube.extensions.toLocalDate import com.google.common.math.IntMath.pow import kotlinx.datetime.toJavaLocalDate import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.time.temporal.ChronoUnit import kotlin.time.Duration import kotlinx.datetime.LocalDate as KotlinLocalDate object TextUtils { /** * Separator used for descriptions */ const val SEPARATOR = " • " /** * Date time formatter which uses the [FormatStyle.MEDIUM] format style. */ private val MEDIUM_DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) /** * Date time formatter which doesn't use any forbidden characters for file names like ':' */ private val SAFE_FILENAME_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH_mm_ss") /** * Localize the date from a date string, using the medium format. * @param date The date to parse * @return localized date string */ fun localizeDate(date: KotlinLocalDate): String { return date.toJavaLocalDate().format(MEDIUM_DATE_FORMATTER) } fun localizeInstant(instant: kotlinx.datetime.Instant): String { return localizeDate(instant.toLocalDate()) } /** * Get time in seconds from a YouTube video link. * @return Time in seconds */ fun String.toTimeInSeconds(): Long? = parseTimeString(this)?.toLong() fun String.parseDurationString(): Float? = parseTimeString(this) private fun parseTimeString(timeString: String): Float? { if (timeString.all { it.isDigit() }) return timeString.toLongOrNull()?.toFloat() if (timeString.all { it.isDigit() || ",.:".contains(it) }) { var secondsTotal = 0 var secondsScoped = 0 var milliseconds = 0 var inMillis = false var millisLength = 0 for (char in timeString) { if (inMillis) { if (!char.isDigit()) break milliseconds *= 10 milliseconds += char.digitToInt() millisLength++ } else if (char.isDigit()) { secondsScoped *= 10 secondsScoped += char.digitToInt() } else if (char == ':') { secondsTotal += secondsScoped secondsTotal *= 60 secondsScoped = 0 } else if (",.".contains(char)) { secondsTotal += secondsScoped secondsScoped = 0 inMillis = true } } secondsTotal += secondsScoped val millisDecimal = milliseconds.toFloat() / pow(10, millisLength) return secondsTotal.toFloat() + millisDecimal } return Duration.parseOrNull(timeString)?.inWholeMilliseconds?.toFloat()?.div(1000) } /** * Get video id if the link is a valid youtube video link */ fun getVideoIdFromUri(uri: Uri) = when (uri.host) { "www.youtube.com", "m.youtube.com", "piped.video" -> uri.getQueryParameter("v") "youtu.be" -> uri.lastPathSegment else -> null } fun formatRelativeDate(unixTime: Long): CharSequence { val date = LocalDateTime.ofInstant(Instant.ofEpochMilli(unixTime), ZoneId.systemDefault()) val now = LocalDateTime.now() val months = date.until(now, ChronoUnit.MONTHS) return if (months > 0) { val years = months / 12 val (timeFormat, time) = if (years > 0) { RelativeDateTimeFormatter.RelativeUnit.YEARS to years } else { RelativeDateTimeFormatter.RelativeUnit.MONTHS to months } RelativeDateTimeFormatter.getInstance() .format(time.toDouble(), RelativeDateTimeFormatter.Direction.LAST, timeFormat) } else { val weeks = date.until(now, ChronoUnit.WEEKS) val minResolution = if (weeks > 0) DateUtils.WEEK_IN_MILLIS else 0L DateUtils.getRelativeTimeSpanString(unixTime, System.currentTimeMillis(), minResolution) } } fun formatBitrate(bitrate: Int?): String { bitrate ?: return "" return "${bitrate / 1024}kbps" } fun limitTextToLength(text: String, maxLength: Int): String { if (text.length <= maxLength) return text return text.take(maxLength) + "…" } fun getUserAgent(context: Context): String { return "${context.packageName}/${BuildConfig.VERSION_NAME}" } fun formatViewsString(context: Context, views: Long, uploaded: Long, uploader: String? = null): String { val viewsString = views.takeIf { it != -1L }?.formatShort()?.let { context.getString(R.string.view_count, it) } val uploadDate = uploaded.takeIf { it > 0 }?.let { formatRelativeDate(it) } return listOfNotNull(uploader, viewsString, uploadDate).joinToString(SEPARATOR) } /** * Timestamp of the current time which doesn't use any forbidden characters for file names like ':' */ fun getFileSafeTimeStampNow(): String { return SAFE_FILENAME_DATETIME_FORMATTER.format(LocalDateTime.now()) } }