1
0
mirror of https://github.com/tuskyapp/Tusky synced 2025-02-08 07:18:39 +01:00

Convert util/{HttpHeaderLink,PairedList,TimestampUtils,ThemeUtils} to Kotlin (#3046)

* Fix off-by-one error in HttpHeaderLink

Link headers with multiple URLs with multiple parameters were being parsed
incorrectly.

Detected by adding unit tests ahead of converting to Kotlin.

* Convert util/HttpHeaderLink from Java to Kotlin

* Convert util/ThemeUtils from Java to Kotlin

* Convert util/TimestampUtils from Java to Kotlin

* Add tests for PairedList

* Convert util/PairedList from Java to Kotlin

* Implement feedback from PR

* Relicense as GPL
This commit is contained in:
Nik Clayton 2022-12-31 13:01:35 +01:00 committed by GitHub
parent 0def7e7230
commit 22834431ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 624 additions and 510 deletions

View File

@ -21,6 +21,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.MenuItem; import android.view.MenuItem;
@ -35,6 +36,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter; import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
import com.keylesspalace.tusky.components.login.LoginActivity; import com.keylesspalace.tusky.components.login.LoginActivity;
@ -77,7 +79,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
/* set the taskdescription programmatically, the theme would turn it blue */ /* set the taskdescription programmatically, the theme would turn it blue */
String appName = getString(R.string.app_name); String appName = getString(R.string.app_name);
Bitmap appIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); Bitmap appIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
int recentsBackgroundColor = ThemeUtils.getColor(this, R.attr.colorSurface); int recentsBackgroundColor = MaterialColors.getColor(this, R.attr.colorSurface, Color.BLACK);
setTaskDescription(new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor)); setTaskDescription(new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor));

View File

@ -38,12 +38,12 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
@ -244,8 +244,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false) return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
.let(this::ListViewHolder) .let(this::ListViewHolder)
.apply { .apply {
val iconColor = MaterialColors.getColor(nameTextView, android.R.attr.textColorTertiary)
val context = nameTextView.context val context = nameTextView.context
val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary)
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor } val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null) nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)

View File

@ -52,6 +52,7 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.FixedSizeDrawable import com.bumptech.glide.request.target.FixedSizeDrawable
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.color.MaterialColors
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@ -84,9 +85,9 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.MainPagerAdapter import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.usecase.LogoutUsecase import com.keylesspalace.tusky.usecase.LogoutUsecase
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deleteStaleCachedMedia import com.keylesspalace.tusky.util.deleteStaleCachedMedia
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDimension
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.updateShortcut import com.keylesspalace.tusky.util.updateShortcut
@ -241,7 +242,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply { icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20 sizeDp = 20
colorInt = ThemeUtils.getColor(this@MainActivity, android.R.attr.textColorPrimary) colorInt = MaterialColors.getColor(binding.mainToolbar, android.R.attr.textColorPrimary)
} }
setOnMenuItemClickListener { setOnMenuItemClickListener {
startActivity(SearchActivity.getIntent(this@MainActivity)) startActivity(SearchActivity.getIntent(this@MainActivity))
@ -409,7 +410,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter)) header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
header.accountHeaderBackground.setBackgroundColor(ThemeUtils.getColor(this, R.attr.colorBackgroundAccent)) header.accountHeaderBackground.setBackgroundColor(MaterialColors.getColor(header, R.attr.colorBackgroundAccent))
val animateAvatars = preferences.getBoolean("animateGifAvatars", false) val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
DrawerImageLoader.init(object : AbstractDrawerImageLoader() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
@ -505,8 +506,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context)) startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
} }
badgeStyle = BadgeStyle().apply { badgeStyle = BadgeStyle().apply {
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary)) textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, R.attr.colorOnPrimary))
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary)) color = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, R.attr.colorPrimary))
} }
}, },
DividerDrawerItem(), DividerDrawerItem(),
@ -573,9 +574,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
private fun setupTabs(selectNotificationTab: Boolean) { private fun setupTabs(selectNotificationTab: Boolean) {
val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") { val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") {
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val actionBarSize = getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
binding.topNav.hide() binding.topNav.hide()

View File

@ -22,8 +22,9 @@ import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.LocaleManager import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.setAppNightMode
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
@ -72,8 +73,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false) EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
// init night mode // init night mode
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme) setAppNightMode(theme)
localeManager.setLocale() localeManager.setLocale()

View File

@ -21,7 +21,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.util.ThemeUtils import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.util.getTuskyDisplayName import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.modernLanguageCode import com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale import java.util.Locale
@ -29,7 +29,7 @@ import java.util.Locale
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) { class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getView(position, convertView, parent) as TextView).apply { return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary)) setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))
typeface = Typeface.DEFAULT_BOLD typeface = Typeface.DEFAULT_BOLD
text = super.getItem(position)?.modernLanguageCode?.uppercase() text = super.getItem(position)?.modernLanguageCode?.uppercase()
} }
@ -37,7 +37,7 @@ class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : Ar
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getDropDownView(position, convertView, parent) as TextView).apply { return (super.getDropDownView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary)) setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))
text = super.getItem(position)?.getTuskyDisplayName(context) text = super.getItem(position)?.getTuskyDisplayName(context)
} }
} }

View File

@ -24,8 +24,8 @@ import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionLi
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
import com.keylesspalace.tusky.entity.Report import com.keylesspalace.tusky.entity.Report
import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap import com.keylesspalace.tusky.util.unicodeWrap
import java.util.Date import java.util.Date
@ -41,7 +41,7 @@ class ReportNotificationViewHolder(
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName) binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, TimestampUtils.getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0) binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0)
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category) binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
// Fancy avatar inset // Fancy avatar inset

View File

@ -33,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.imageview.ShapeableImageView; import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.shape.CornerFamily; import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.ShapeAppearanceModel;
@ -53,7 +54,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.util.TouchDelegateHelper; import com.keylesspalace.tusky.util.TouchDelegateHelper;
import com.keylesspalace.tusky.view.MediaPreviewImageView; import com.keylesspalace.tusky.view.MediaPreviewImageView;
@ -170,7 +170,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent)); mediaPreviewUnloaded = new ColorDrawable(MaterialColors.getColor(itemView, R.attr.colorBackgroundAccent));
TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton)); TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton));
} }

View File

@ -23,6 +23,7 @@ import androidx.core.view.size
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.HASHTAG import com.keylesspalace.tusky.HASHTAG
import com.keylesspalace.tusky.LIST import com.keylesspalace.tusky.LIST
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
@ -30,8 +31,8 @@ import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding
import com.keylesspalace.tusky.databinding.ItemTabPreferenceSmallBinding import com.keylesspalace.tusky.databinding.ItemTabPreferenceSmallBinding
import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.setDrawableTint
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
interface ItemInteractionListener { interface ItemInteractionListener {
@ -101,7 +102,7 @@ class TabAdapter(
listener.onTabRemoved(holder.bindingAdapterPosition) listener.onTabRemoved(holder.bindingAdapterPosition)
} }
binding.removeButton.isEnabled = removeButtonEnabled binding.removeButton.isEnabled = removeButtonEnabled
ThemeUtils.setDrawableTint( setDrawableTint(
holder.itemView.context, holder.itemView.context,
binding.removeButton.drawable, binding.removeButton.drawable,
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled)
@ -120,7 +121,7 @@ class TabAdapter(
val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply { ?: Chip(context).apply {
binding.chipGroup.addView(this, binding.chipGroup.size - 1) binding.chipGroup.addView(this, binding.chipGroup.size - 1)
chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary)) chipIconTint = ColorStateList.valueOf(MaterialColors.getColor(this, android.R.attr.textColorPrimary))
} }
chip.text = arg chip.text = arg

View File

@ -41,6 +41,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.MarginPageTransformer
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.shape.ShapeAppearanceModel
@ -70,7 +71,6 @@ import com.keylesspalace.tusky.util.DefaultTextWatcher
import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.getDomain
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
@ -172,9 +172,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
* Load colors and dimensions from resources * Load colors and dimensions from resources
*/ */
private fun loadResources() { private fun loadResources() {
toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface) toolbarColor = MaterialColors.getColor(this, R.attr.colorSurface, Color.BLACK)
statusBarColorTransparent = getColor(R.color.transparent_statusbar_background) statusBarColorTransparent = getColor(R.color.transparent_statusbar_background)
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark) statusBarColorOpaque = MaterialColors.getColor(this, R.attr.colorPrimaryDark, Color.BLACK)
avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size) avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size)
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height) titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
} }

View File

@ -11,11 +11,11 @@ import androidx.core.view.setPadding
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.decodeBlurHash import com.keylesspalace.tusky.util.decodeBlurHash
import com.keylesspalace.tusky.util.getFormattedDescription import com.keylesspalace.tusky.util.getFormattedDescription
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
@ -40,7 +40,7 @@ class AccountMediaGridAdapter(
} }
) { ) {
private val baseItemBackgroundColor = ThemeUtils.getColor(context, R.attr.colorSurface) private val baseItemBackgroundColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator) private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator)
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp) private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)

View File

@ -63,6 +63,7 @@ import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract import com.canhub.cropper.CropImageContract
import com.canhub.cropper.options import com.canhub.cropper.options
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.BuildConfig
@ -86,8 +87,8 @@ import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.PickMediaFiles import com.keylesspalace.tusky.util.PickMediaFiles
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.afterTextChanged import com.keylesspalace.tusky.util.afterTextChanged
import com.keylesspalace.tusky.util.getInitialLanguage import com.keylesspalace.tusky.util.getInitialLanguage
import com.keylesspalace.tusky.util.getLocaleList import com.keylesspalace.tusky.util.getLocaleList
@ -97,6 +98,7 @@ import com.keylesspalace.tusky.util.highlightSpans
import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.modernLanguageCode import com.keylesspalace.tusky.util.modernLanguageCode
import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.setDrawableTint
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
@ -206,7 +208,7 @@ class ComposeActivity :
accountManager.setActiveAccount(accountId) accountManager.setActiveAccount(accountId)
} }
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
if (theme == "black") { if (theme == "black") {
setTheme(R.style.TuskyDialogActivityBlackTheme) setTheme(R.style.TuskyDialogActivityBlackTheme)
} }
@ -341,7 +343,7 @@ class ComposeActivity :
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 } val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary) setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
binding.composeReplyView.setOnClickListener { binding.composeReplyView.setOnClickListener {
@ -354,7 +356,7 @@ class ComposeActivity :
binding.composeReplyContentView.show() binding.composeReplyContentView.show()
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 } val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
ThemeUtils.setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary) setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null) binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
} }
} }
@ -501,7 +503,7 @@ class ComposeActivity :
displayTransientMessage(R.string.hint_media_description_missing) displayTransientMessage(R.string.hint_media_description_missing)
} }
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 } val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 }
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null) binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
@ -688,7 +690,7 @@ class ComposeActivity :
getColor(R.color.tusky_blue) getColor(R.color.tusky_blue)
} else { } else {
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
ThemeUtils.getColor(this, android.R.attr.textColorTertiary) MaterialColors.getColor(binding.composeHideMediaButton, android.R.attr.textColorTertiary)
} }
} }
binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
@ -710,7 +712,7 @@ class ComposeActivity :
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false) enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
} else { } else {
@ColorInt val color = if (binding.composeScheduleView.time == null) { @ColorInt val color = if (binding.composeScheduleView.time == null) {
ThemeUtils.getColor(this, android.R.attr.textColorTertiary) MaterialColors.getColor(binding.composeScheduleButton, android.R.attr.textColorTertiary)
} else { } else {
getColor(R.color.tusky_blue) getColor(R.color.tusky_blue)
} }
@ -906,7 +908,7 @@ class ComposeActivity :
val textColor = if (remainingLength < 0) { val textColor = if (remainingLength < 0) {
getColor(R.color.tusky_red) getColor(R.color.tusky_red)
} else { } else {
ThemeUtils.getColor(this, android.R.attr.textColorTertiary) MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
} }
binding.composeCharactersLeftView.setTextColor(textColor) binding.composeCharactersLeftView.setTextColor(textColor)
} }
@ -1007,7 +1009,7 @@ class ComposeActivity :
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
button.isEnabled = clickable button.isEnabled = clickable
ThemeUtils.setDrawableTint( setDrawableTint(
this, button.drawable, this, button.drawable,
if (colorActive) android.R.attr.textColorTertiary if (colorActive) android.R.attr.textColorTertiary
else R.attr.textColorDisabled else R.attr.textColorDisabled
@ -1016,8 +1018,8 @@ class ComposeActivity :
private fun enablePollButton(enable: Boolean) { private fun enablePollButton(enable: Boolean) {
binding.addPollTextActionTextView.isEnabled = enable binding.addPollTextActionTextView.isEnabled = enable
val textColor = ThemeUtils.getColor( val textColor = MaterialColors.getColor(
this, binding.addPollTextActionTextView,
if (enable) android.R.attr.textColorTertiary if (enable) android.R.attr.textColorTertiary
else R.attr.textColorDisabled else R.attr.textColorDisabled
) )
@ -1077,7 +1079,7 @@ class ComposeActivity :
} else { } else {
binding.composeContentWarningBar.hide() binding.composeContentWarningBar.hide()
binding.composeEditField.requestFocus() binding.composeEditField.requestFocus()
ThemeUtils.getColor(this, android.R.attr.textColorTertiary) MaterialColors.getColor(binding.composeContentWarningButton, android.R.attr.textColorTertiary)
} }
binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
} }

View File

@ -16,11 +16,13 @@
package com.keylesspalace.tusky.components.preference package com.keylesspalace.tusky.components.preference
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.AccountListActivity import com.keylesspalace.tusky.AccountListActivity
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
@ -47,7 +49,6 @@ import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getInitialLanguage import com.keylesspalace.tusky.util.getInitialLanguage
import com.keylesspalace.tusky.util.getLocaleList import com.keylesspalace.tusky.util.getLocaleList
import com.keylesspalace.tusky.util.getTuskyDisplayName import com.keylesspalace.tusky.util.getTuskyDisplayName
@ -80,7 +81,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_edit_notification_settings) setTitle(R.string.pref_title_edit_notification_settings)
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).apply { icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).apply {
sizeRes = R.dimen.preference_icon_size sizeRes = R.dimen.preference_icon_size
colorInt = ThemeUtils.getColor(context, R.attr.iconColor) colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
} }
setOnPreferenceClickListener { setOnPreferenceClickListener {
openNotificationPrefs() openNotificationPrefs()
@ -135,7 +136,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.action_view_blocks) setTitle(R.string.action_view_blocks)
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).apply { icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).apply {
sizeRes = R.dimen.preference_icon_size sizeRes = R.dimen.preference_icon_size
colorInt = ThemeUtils.getColor(context, R.attr.iconColor) colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
} }
setOnPreferenceClickListener { setOnPreferenceClickListener {
val intent = Intent(context, AccountListActivity::class.java) val intent = Intent(context, AccountListActivity::class.java)

View File

@ -31,8 +31,9 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.setAppNightMode
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import javax.inject.Inject import javax.inject.Inject
@ -124,9 +125,9 @@ class PreferencesActivity :
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) { when (key) {
"appTheme" -> { "appTheme" -> {
val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT) val theme = sharedPreferences.getNonNullString("appTheme", APP_THEME_DEFAULT)
Log.d("activeTheme", theme) Log.d("activeTheme", theme)
ThemeUtils.setAppNightMode(theme) setAppNightMode(theme)
restartActivitiesOnBackPressedCallback.isEnabled = true restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity() this.restartCurrentActivity()

View File

@ -31,8 +31,8 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.StatusViewHelper import com.keylesspalace.tusky.util.StatusViewHelper
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.setClickableMentions import com.keylesspalace.tusky.util.setClickableMentions
import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.setClickableText
@ -161,7 +161,7 @@ class StatusViewHolder(
binding.timestampInfo.text = if (createdAt != null) { binding.timestampInfo.text = if (createdAt != null) {
val then = createdAt.time val then = createdAt.time
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
TimestampUtils.getRelativeTimeSpanString(binding.timestampInfo.context, then, now) getRelativeTimeSpanString(binding.timestampInfo.context, then, now)
} else { } else {
// unknown minutes~ // unknown minutes~
"?m" "?m"

View File

@ -915,11 +915,11 @@ public class NotificationsFragment extends SFragment implements
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader, private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
FetchEnd fetchEnd, int pos) { FetchEnd fetchEnd, int pos) {
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader); List<HttpHeaderLink> links = HttpHeaderLink.Companion.parse(linkHeader);
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next"); HttpHeaderLink next = HttpHeaderLink.Companion.findByRelationType(links, "next");
String fromId = null; String fromId = null;
if (next != null) { if (next != null) {
fromId = next.uri.getQueryParameter("max_id"); fromId = next.getUri().getQueryParameter("max_id");
} }
switch (fetchEnd) { switch (fetchEnd) {

View File

@ -1,162 +0,0 @@
/* Written in 2017 by Andrew Dawson
*
* To the extent possible under law, the author(s) have dedicated all copyright and related and
* neighboring rights to this software to the public domain worldwide. This software is distributed
* without any warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication along with this software.
* If not, see <http://creativecommons.org/publicdomain/zero/1.0/>. */
package com.keylesspalace.tusky.util;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Represents one link and its parameters from the link header of an HTTP message.
*
* @see <a href="https://tools.ietf.org/html/rfc5988">RFC5988</a>
*/
public class HttpHeaderLink {
private static class Parameter {
public String name;
public String value;
}
private List<Parameter> parameters;
public Uri uri;
private HttpHeaderLink(String uri) {
this.uri = Uri.parse(uri);
this.parameters = new ArrayList<>();
}
private static int findAny(String s, int fromIndex, char[] set) {
for (int i = fromIndex; i < s.length(); i++) {
char c = s.charAt(i);
for (char member : set) {
if (c == member) {
return i;
}
}
}
return -1;
}
private static int findEndOfQuotedString(String line, int start) {
for (int i = start; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '\\') {
i += 1;
} else if (c == '"') {
return i;
}
}
return -1;
}
private static class ValueResult {
String value;
int end;
ValueResult() {
end = -1;
}
void setValue(String value) {
value = value.trim();
if (!value.isEmpty()) {
this.value = value;
}
}
}
private static ValueResult parseValue(String line, int start) {
ValueResult result = new ValueResult();
int foundIndex = findAny(line, start, new char[] {';', ',', '"'});
if (foundIndex == -1) {
result.setValue(line.substring(start));
return result;
}
char c = line.charAt(foundIndex);
if (c == ';' || c == ',') {
result.end = foundIndex;
result.setValue(line.substring(start, foundIndex));
return result;
} else {
int quoteEnd = findEndOfQuotedString(line, foundIndex + 1);
if (quoteEnd == -1) {
quoteEnd = line.length();
}
result.end = quoteEnd;
result.setValue(line.substring(foundIndex + 1, quoteEnd));
return result;
}
}
private static int parseParameters(String line, int start, HttpHeaderLink link) {
for (int i = start; i < line.length(); i++) {
int foundIndex = findAny(line, i, new char[] {'=', ','});
if (foundIndex == -1) {
return -1;
} else if (line.charAt(foundIndex) == ',') {
return foundIndex;
}
Parameter parameter = new Parameter();
parameter.name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim();
link.parameters.add(parameter);
ValueResult result = parseValue(line, foundIndex);
parameter.value = result.value;
if (result.end == -1) {
return -1;
} else {
i = result.end;
}
}
return -1;
}
/**
* @param line the entire link header, not including the initial "Link:"
* @return all links found in the header
*/
public static List<HttpHeaderLink> parse(@Nullable String line) {
List<HttpHeaderLink> linkList = new ArrayList<>();
if (line != null) {
for (int i = 0; i < line.length(); i++) {
int uriEnd = line.indexOf('>', i);
String uri = line.substring(line.indexOf('<', i) + 1, uriEnd);
HttpHeaderLink link = new HttpHeaderLink(uri);
linkList.add(link);
int parseEnd = parseParameters(line, uriEnd, link);
if (parseEnd == -1) {
break;
} else {
i = parseEnd;
}
}
}
return linkList;
}
/**
* @param links intended to be those returned by parse()
* @param relationType of the parameter "rel", commonly "next" or "prev"
* @return the link matching the given relation type
*/
@Nullable
public static HttpHeaderLink findByRelationType(List<HttpHeaderLink> links,
String relationType) {
for (HttpHeaderLink link : links) {
for (Parameter parameter : link.parameters) {
if (parameter.name.equals("rel") && parameter.value.equals(relationType)) {
return link;
}
}
}
return null;
}
}

View File

@ -0,0 +1,135 @@
/* Copyright 2022 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.util
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
/**
* Represents one link and its parameters from the link header of an HTTP message.
*
* @see [RFC5988](https://tools.ietf.org/html/rfc5988)
*/
class HttpHeaderLink @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) constructor(
uri: String
) {
data class Parameter(val name: String, val value: String?)
private val parameters: MutableList<Parameter> = ArrayList()
val uri: Uri = uri.toUri()
private data class ValueResult(val value: String, val end: Int = -1)
companion object {
private fun findEndOfQuotedString(line: String, start: Int): Int {
var i = start
while (i < line.length) {
val c = line[i]
if (c == '\\') {
i += 1
} else if (c == '"') {
return i
}
i++
}
return -1
}
private fun parseValue(line: String, start: Int): ValueResult {
val foundIndex = line.indexOfAny(charArrayOf(';', ',', '"'), start, false)
if (foundIndex == -1) {
return ValueResult(line.substring(start).trim())
}
val c = line[foundIndex]
return if (c == ';' || c == ',') {
ValueResult(line.substring(start, foundIndex).trim(), foundIndex)
} else {
var quoteEnd = findEndOfQuotedString(line, foundIndex + 1)
if (quoteEnd == -1) {
quoteEnd = line.length
}
ValueResult(line.substring(foundIndex + 1, quoteEnd).trim(), quoteEnd)
}
}
private fun parseParameters(line: String, start: Int, link: HttpHeaderLink): Int {
var i = start
while (i < line.length) {
val foundIndex = line.indexOfAny(charArrayOf('=', ','), i, false)
if (foundIndex == -1) {
return -1
} else if (line[foundIndex] == ',') {
return foundIndex
}
val name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim()
val result = parseValue(line, foundIndex)
val value = result.value
val parameter = Parameter(name, value)
link.parameters.add(parameter)
i = if (result.end == -1) {
return -1
} else {
result.end
}
}
return -1
}
/**
* @param line the entire link header, not including the initial "Link:"
* @return all links found in the header
*/
fun parse(line: String?): List<HttpHeaderLink> {
val links: MutableList<HttpHeaderLink> = mutableListOf()
line ?: return links
var i = 0
while (i < line.length) {
val uriEnd = line.indexOf('>', i)
val uri = line.substring(line.indexOf('<', i) + 1, uriEnd)
val link = HttpHeaderLink(uri)
links.add(link)
val parseEnd = parseParameters(line, uriEnd, link)
i = if (parseEnd == -1) {
break
} else {
parseEnd
}
i++
}
return links
}
/**
* @param links intended to be those returned by parse()
* @param relationType of the parameter "rel", commonly "next" or "prev"
* @return the link matching the given relation type
*/
fun findByRelationType(
links: List<HttpHeaderLink>,
relationType: String
): HttpHeaderLink? {
return links.find { link ->
link.parameters.any { parameter ->
parameter.name == "rel" && parameter.value == relationType
}
}
}
}
}

View File

@ -16,7 +16,9 @@
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.content.Context import android.content.Context
import android.graphics.Color
import androidx.annotation.Px import androidx.annotation.Px
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -26,6 +28,6 @@ import com.mikepenz.iconics.utils.sizePx
fun makeIcon(context: Context, icon: GoogleMaterial.Icon, @Px iconSize: Int): IconicsDrawable { fun makeIcon(context: Context, icon: GoogleMaterial.Icon, @Px iconSize: Int): IconicsDrawable {
return IconicsDrawable(context, icon).apply { return IconicsDrawable(context, icon).apply {
sizePx = iconSize sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor) colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
} }
} }

View File

@ -19,6 +19,7 @@ package com.keylesspalace.tusky.util
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
@ -33,6 +34,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.entity.Status.Mention
@ -251,9 +253,9 @@ private fun openLinkInBrowser(uri: Uri?, context: Context) {
* @param context context * @param context context
*/ */
private fun openLinkInCustomTab(uri: Uri, context: Context) { private fun openLinkInCustomTab(uri: Uri, context: Context) {
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) val toolbarColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor) val navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
val colorSchemeParams = CustomTabColorSchemeParams.Builder() val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor) .setToolbarColor(toolbarColor)
.setNavigationBarColor(navigationbarColor) .setNavigationBarColor(navigationbarColor)

View File

@ -1,94 +0,0 @@
package com.keylesspalace.tusky.util;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
/**
* This list implementation can help to keep two lists in sync - like real models and view models.
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
* This makes sure that the main list is always the source of truth.
* Main list is projected to the supplementary list by the passed mapper function.
* Paired list is newer actually exposed and clients are provided with {@code getPairedCopy()},
* {@code getPairedItem()} and {@code setPairedItem()}. This prevents modifications of the
* supplementary list size so lists are always have the same length.
* This implementation will not try to recover from exceptional cases so lists may be out of sync
* after the exception.
*
* It is most useful with immutable data because we cannot track changes inside stored objects.
* @param <T> type of elements in the main list
* @param <V> type of elements in supplementary list
*/
public final class PairedList<T, V> extends AbstractList<T> {
private final List<T> main = new ArrayList<>();
private final List<V> synced = new ArrayList<>();
private final Function<T, ? extends V> mapper;
/**
* Construct new paired list. Main and supplementary lists will be empty.
* @param mapper Function, which will be used to translate items from the main list to the
* supplementary one.
*/
public PairedList(Function<T, ? extends V> mapper) {
this.mapper = mapper;
}
public List<V> getPairedCopy() {
return new ArrayList<>(synced);
}
public V getPairedItem(int index) {
return synced.get(index);
}
@Nullable
public V getPairedItemOrNull(int index) {
if (index >= 0 && index < synced.size()) {
return synced.get(index);
} else {
return null;
}
}
public void setPairedItem(int index, V element) {
synced.set(index, element);
}
@Override
public T get(int index) {
return main.get(index);
}
@Override
public T set(int index, T element) {
synced.set(index, mapper.apply(element));
return main.set(index, element);
}
@Override
public boolean add(T t) {
synced.add(mapper.apply(t));
return main.add(t);
}
@Override
public void add(int index, T element) {
synced.add(index, mapper.apply(element));
main.add(index, element);
}
@Override
public T remove(int index) {
synced.remove(index);
return main.remove(index);
}
@Override
public int size() {
return main.size();
}
}

View File

@ -0,0 +1,74 @@
package com.keylesspalace.tusky.util
import androidx.arch.core.util.Function
/**
* This list implementation can help to keep two lists in sync - like real models and view models.
*
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
*
* This makes sure that the main list is always the source of truth.
*
* Main list is projected to the supplementary list by the passed mapper function.
*
* Paired list is newer actually exposed and clients are provided with `getPairedCopy()`,
* `getPairedItem()` and `setPairedItem()`. This prevents modifications of the
* supplementary list size so lists are always have the same length.
*
* This implementation will not try to recover from exceptional cases so lists may be out of sync
* after the exception.
*
* It is most useful with immutable data because we cannot track changes inside stored objects.
*
* @param T type of elements in the main list
* @param V type of elements in supplementary list
* @param mapper Function, which will be used to translate items from the main list to the
* supplementary one.
* @constructor
*/
class PairedList<T, V> (private val mapper: Function<T, out V>) : AbstractMutableList<T>() {
private val main: MutableList<T> = ArrayList()
private val synced: MutableList<V> = ArrayList()
val pairedCopy: List<V>
get() = ArrayList(synced)
fun getPairedItem(index: Int): V {
return synced[index]
}
fun getPairedItemOrNull(index: Int): V? {
return synced.getOrNull(index)
}
fun setPairedItem(index: Int, element: V) {
synced[index] = element
}
override fun get(index: Int): T {
return main[index]
}
override fun set(index: Int, element: T): T {
synced[index] = mapper.apply(element)
return main.set(index, element)
}
override fun add(element: T): Boolean {
synced.add(mapper.apply(element))
return main.add(element)
}
override fun add(index: Int, element: T) {
synced.add(index, mapper.apply(element))
main.add(index, element)
}
override fun removeAt(index: Int): T {
synced.removeAt(index)
return main.removeAt(index)
}
override val size: Int
get() = main.size
}

View File

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.text.InputFilter import android.text.InputFilter
import android.text.TextUtils import android.text.TextUtils
@ -24,6 +25,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
@ -85,7 +87,7 @@ class StatusViewHelper(private val itemView: View) {
return return
} }
val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(context, R.attr.colorBackgroundAccent)) val mediaPreviewUnloaded = ColorDrawable(MaterialColors.getColor(context, R.attr.colorBackgroundAccent, Color.BLACK))
val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS) val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
@ -292,7 +294,7 @@ class StatusViewHelper(private val itemView: View) {
if (useAbsoluteTime) { if (useAbsoluteTime) {
context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false)) context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false))
} else { } else {
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp) formatPollDuration(context, poll.expiresAt!!.time, timestamp)
} }
} }

View File

@ -1,83 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
public class ThemeUtils {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
private static final String THEME_NIGHT = "night";
private static final String THEME_DAY = "day";
private static final String THEME_BLACK = "black";
private static final String THEME_AUTO = "auto";
private static final String THEME_SYSTEM = "auto_system";
@ColorInt
public static int getColor(@NonNull Context context, @AttrRes int attribute) {
TypedValue value = new TypedValue();
if (context.getTheme().resolveAttribute(attribute, value, true)) {
return value.data;
} else {
return Color.BLACK;
}
}
public static int getDimension(@NonNull Context context, @AttrRes int attribute) {
TypedArray array = context.obtainStyledAttributes(new int[] { attribute });
int dimen = array.getDimensionPixelSize(0, -1);
array.recycle();
return dimen;
}
public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
}
public static void setAppNightMode(String flavor) {
switch (flavor) {
default:
case THEME_NIGHT:
case THEME_BLACK:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case THEME_DAY:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
case THEME_AUTO:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_TIME);
break;
case THEME_SYSTEM:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
break;
}
}
}

View File

@ -0,0 +1,67 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("ThemeUtils")
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import androidx.annotation.AttrRes
import androidx.appcompat.app.AppCompatDelegate
import com.google.android.material.color.MaterialColors
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
private const val THEME_NIGHT = "night"
private const val THEME_DAY = "day"
private const val THEME_BLACK = "black"
private const val THEME_AUTO = "auto"
private const val THEME_SYSTEM = "auto_system"
const val APP_THEME_DEFAULT = THEME_NIGHT
fun getDimension(context: Context, @AttrRes attribute: Int): Int {
val array = context.obtainStyledAttributes(intArrayOf(attribute))
val dimen = array.getDimensionPixelSize(0, -1)
array.recycle()
return dimen
}
fun setDrawableTint(context: Context, drawable: Drawable, @AttrRes attribute: Int) {
drawable.setColorFilter(
MaterialColors.getColor(context, attribute, Color.BLACK),
PorterDuff.Mode.SRC_IN
)
}
fun setAppNightMode(flavor: String?) {
when (flavor) {
THEME_NIGHT, THEME_BLACK -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_YES
)
THEME_DAY -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
THEME_AUTO -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_AUTO_TIME
)
THEME_SYSTEM -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}

View File

@ -1,108 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import com.keylesspalace.tusky.R;
public class TimestampUtils {
private static final long SECOND_IN_MILLIS = 1000;
private static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
private static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
private static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
private static final long YEAR_IN_MILLIS = DAY_IN_MILLIS * 365;
/**
* This is a rough duplicate of {@link android.text.format.DateUtils#getRelativeTimeSpanString},
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
*/
public static String getRelativeTimeSpanString(Context context, long then, long now) {
long span = now - then;
boolean future = false;
if (Math.abs(span) < SECOND_IN_MILLIS) {
return context.getString(R.string.status_created_at_now);
}
else if (span < 0) {
future = true;
span = -span;
}
int format;
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_seconds;
} else {
format = R.string.abbreviated_seconds_ago;
}
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_minutes;
} else {
format = R.string.abbreviated_minutes_ago;
}
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_hours;
} else {
format = R.string.abbreviated_hours_ago;
}
} else if (span < YEAR_IN_MILLIS) {
span /= DAY_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_days;
} else {
format = R.string.abbreviated_days_ago;
}
} else {
span /= YEAR_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_years;
} else {
format = R.string.abbreviated_years_ago;
}
}
return context.getString(format, span);
}
public static String formatPollDuration(Context context, long then, long now) {
long span = then - now;
if (span < 0) {
span = 0;
}
int format;
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS;
format = R.plurals.poll_timespan_seconds;
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS;
format = R.plurals.poll_timespan_minutes;
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS;
format = R.plurals.poll_timespan_hours;
} else {
span /= DAY_IN_MILLIS;
format = R.plurals.poll_timespan_days;
}
return context.getResources().getQuantityString(format, (int) span, (int) span);
}
}

View File

@ -0,0 +1,102 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("TimestampUtils")
package com.keylesspalace.tusky.util
import android.content.Context
import com.keylesspalace.tusky.R
import kotlin.math.abs
private const val SECOND_IN_MILLIS: Long = 1000
private const val MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60
private const val HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60
private const val DAY_IN_MILLIS = HOUR_IN_MILLIS * 24
private const val YEAR_IN_MILLIS = DAY_IN_MILLIS * 365
/**
* This is a rough duplicate of [android.text.format.DateUtils.getRelativeTimeSpanString],
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
*/
fun getRelativeTimeSpanString(context: Context, then: Long, now: Long): String {
var span = now - then
var future = false
if (abs(span) < SECOND_IN_MILLIS) {
return context.getString(R.string.status_created_at_now)
} else if (span < 0) {
future = true
span = -span
}
val format: Int
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_seconds
} else {
R.string.abbreviated_seconds_ago
}
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_minutes
} else {
R.string.abbreviated_minutes_ago
}
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_hours
} else {
R.string.abbreviated_hours_ago
}
} else if (span < YEAR_IN_MILLIS) {
span /= DAY_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_days
} else {
R.string.abbreviated_days_ago
}
} else {
span /= YEAR_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_years
} else {
R.string.abbreviated_years_ago
}
}
return context.getString(format, span)
}
fun formatPollDuration(context: Context, then: Long, now: Long): String {
var span = then - now
if (span < 0) {
span = 0
}
val format: Int
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS
format = R.plurals.poll_timespan_seconds
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS
format = R.plurals.poll_timespan_minutes
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS
format = R.plurals.poll_timespan_hours
} else {
span /= DAY_IN_MILLIS
format = R.plurals.poll_timespan_days
}
return context.resources.getQuantityString(format, span.toInt(), span.toInt())
}

View File

@ -16,12 +16,13 @@
package com.keylesspalace.tusky.view package com.keylesspalace.tusky.view
import android.content.Context import android.content.Context
import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.CardLicenseBinding import com.keylesspalace.tusky.databinding.CardLicenseBinding
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.openLink
@ -35,7 +36,7 @@ class LicenseCard
init { init {
val binding = CardLicenseBinding.inflate(LayoutInflater.from(context), this) val binding = CardLicenseBinding.inflate(LayoutInflater.from(context), this)
setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) setCardBackgroundColor(MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK))
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.LicenseCard, 0, 0) val a = context.theme.obtainStyledAttributes(attrs, R.styleable.LicenseCard, 0, 0)

View File

@ -0,0 +1,77 @@
package com.keylesspalace.tusky.util
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class HttpHeaderLinkTest {
data class TestData(val name: String, val input: String, val want: List<HttpHeaderLink>)
@Test
fun shouldParseValidLinks() {
val testData = arrayOf(
// Examples from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
TestData(
"Single URL",
"<https://example.com>",
listOf(HttpHeaderLink("https://example.com"))
),
TestData(
"Single URL with parameters",
"<https://example.com>; rel=\"preconnect\"",
listOf(HttpHeaderLink("https://example.com"))
),
TestData(
"Single encoded URL with parameters",
"<https://example.com/%E8%8B%97%E6%9D%A1>; rel=\"preconnect\"",
listOf(HttpHeaderLink("https://example.com/%E8%8B%97%E6%9D%A1"))
),
TestData(
"Multiple URLs, separated by commas",
"<https://one.example.com>; rel=\"preconnect\", <https://two.example.com>; rel=\"preconnect\", <https://three.example.com>; rel=\"preconnect\"",
listOf(
HttpHeaderLink("https://one.example.com"),
HttpHeaderLink("https://two.example.com"),
HttpHeaderLink("https://three.example.com")
)
),
// Examples from https://httpwg.org/specs/rfc8288.html#rfc.section.3.5
TestData(
"Single URL, multiple parameters",
"<http://example.com/TheBook/chapter2>; rel=\"previous\"; title=\"previous chapter\"",
listOf(HttpHeaderLink("http://example.com/TheBook/chapter2"))
),
TestData(
"Root resource",
"</>; rel=\"http://example.net/foo\"",
listOf(HttpHeaderLink("/"))
),
TestData(
"Terms and anchor",
"</terms>; rel=\"copyright\"; anchor=\"#foo\"",
listOf(HttpHeaderLink("/terms"))
),
TestData(
"Multiple URLs with parameter encoding",
"</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, </TheBook/chapter4>; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel",
listOf(
HttpHeaderLink("/TheBook/chapter2"),
HttpHeaderLink("/TheBook/chapter4")
)
)
)
// Verify that the URLs are parsed correctly
for (test in testData) {
val links = HttpHeaderLink.parse(test.input)
assertEquals("${test.name}: Same size", links.size, test.want.size)
for (i in links.indices) {
assertEquals(test.name, test.want[i].uri, links[i].uri)
}
}
}
}

View File

@ -0,0 +1,91 @@
package com.keylesspalace.tusky.util
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
/**
* Tests for PairedList, with a mapper that multiples everything by 2.
*/
class PairedListTest {
private lateinit var pairedList: PairedList<Int, Int>
@Before
fun beforeEachTest() {
pairedList = PairedList { it * 2 }
for (i in 0..10) {
pairedList.add(i)
}
}
@Test
fun pairedCopy() {
val copy = pairedList.pairedCopy
for (i in 0..10) {
assertEquals(i * 2, copy[i])
}
}
@Test
fun getPairedItem() {
for (i in 0..10) {
assertEquals(i * 2, pairedList.getPairedItem(i))
}
}
@Test
fun getPairedItemOrNull() {
for (i in 0..10) {
assertEquals(i * 2, pairedList.getPairedItem(i))
}
assertNull(pairedList.getPairedItemOrNull(11))
}
@Test
fun setPairedItem() {
pairedList.setPairedItem(2, 2)
assertEquals(2, pairedList.getPairedItem(2))
}
@Test
fun get() {
for (i in 0..10) {
assertEquals(i, pairedList[i])
}
}
@Test
fun set() {
assertEquals(0, pairedList[0])
pairedList[0] = 10
assertEquals(10, pairedList[0])
assertEquals(20, pairedList.getPairedItem(0))
}
@Test
fun add() {
pairedList.add(11)
assertEquals(11, pairedList[11])
assertEquals(22, pairedList.getPairedItem(11))
}
@Test
fun addAtIndex() {
pairedList.add(11, 11)
assertEquals(11, pairedList[11])
assertEquals(22, pairedList.getPairedItem(11))
}
@Test
fun removeAt() {
pairedList.removeAt(5)
assertEquals(6, pairedList[5])
assertEquals(12, pairedList.getPairedItem(5))
}
@Test
fun size() {
assertEquals(11, pairedList.size)
}
}

View File

@ -21,9 +21,9 @@ class TimestampUtilsTest {
@Test @Test
fun shouldShowNowForSmallTimeSpans() { fun shouldShowNowForSmallTimeSpans() {
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 300)) assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 300))
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 300, 0)) assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 300, 0))
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 501, 0)) assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 501, 0))
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 999)) assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 999))
} }
} }