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:
parent
0def7e7230
commit
22834431ca
30 changed files with 624 additions and 510 deletions
|
@ -21,6 +21,7 @@ import android.content.SharedPreferences;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
|
@ -35,6 +36,7 @@ import androidx.core.app.ActivityCompat;
|
|||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
||||
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 */
|
||||
String appName = getString(R.string.app_name);
|
||||
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));
|
||||
|
||||
|
|
|
@ -38,12 +38,12 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
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)
|
||||
.let(this::ListViewHolder)
|
||||
.apply {
|
||||
val iconColor = MaterialColors.getColor(nameTextView, android.R.attr.textColorTertiary)
|
||||
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 }
|
||||
|
||||
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
|
||||
|
|
|
@ -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.FixedSizeDrawable
|
||||
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.OnTabSelectedListener
|
||||
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.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.usecase.LogoutUsecase
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.getDimension
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.updateShortcut
|
||||
|
@ -241,7 +242,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
||||
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
|
||||
sizeDp = 20
|
||||
colorInt = ThemeUtils.getColor(this@MainActivity, android.R.attr.textColorPrimary)
|
||||
colorInt = MaterialColors.getColor(binding.mainToolbar, android.R.attr.textColorPrimary)
|
||||
}
|
||||
setOnMenuItemClickListener {
|
||||
startActivity(SearchActivity.getIntent(this@MainActivity))
|
||||
|
@ -409,7 +410,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||
|
@ -505,8 +506,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
|
||||
}
|
||||
badgeStyle = BadgeStyle().apply {
|
||||
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
|
||||
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
|
||||
textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, R.attr.colorOnPrimary))
|
||||
color = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, R.attr.colorPrimary))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
|
@ -573,9 +574,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
|
||||
private fun setupTabs(selectNotificationTab: Boolean) {
|
||||
|
||||
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)
|
||||
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
|
||||
binding.topNav.hide()
|
||||
|
|
|
@ -22,8 +22,9 @@ import androidx.work.WorkManager
|
|||
import autodispose2.AutoDisposePlugins
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
||||
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.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.setAppNightMode
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
||||
|
@ -72,8 +73,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
|
||||
|
||||
// init night mode
|
||||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||
ThemeUtils.setAppNightMode(theme)
|
||||
val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
|
||||
setAppNightMode(theme)
|
||||
|
||||
localeManager.setLocale()
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
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.modernLanguageCode
|
||||
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) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
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
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionLi
|
|||
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
|
||||
import com.keylesspalace.tusky.entity.Report
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.TimestampUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import java.util.Date
|
||||
|
@ -41,7 +41,7 @@ class ReportNotificationViewHolder(
|
|||
|
||||
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
|
||||
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)
|
||||
|
||||
// Fancy avatar inset
|
||||
|
|
|
@ -33,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
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.shape.CornerFamily;
|
||||
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.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
||||
import com.keylesspalace.tusky.util.TouchDelegateHelper;
|
||||
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.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));
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.core.view.size
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.keylesspalace.tusky.HASHTAG
|
||||
import com.keylesspalace.tusky.LIST
|
||||
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.ItemTabPreferenceSmallBinding
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.setDrawableTint
|
||||
import com.keylesspalace.tusky.util.show
|
||||
|
||||
interface ItemInteractionListener {
|
||||
|
@ -101,7 +102,7 @@ class TabAdapter(
|
|||
listener.onTabRemoved(holder.bindingAdapterPosition)
|
||||
}
|
||||
binding.removeButton.isEnabled = removeButtonEnabled
|
||||
ThemeUtils.setDrawableTint(
|
||||
setDrawableTint(
|
||||
holder.itemView.context,
|
||||
binding.removeButton.drawable,
|
||||
(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?
|
||||
?: Chip(context).apply {
|
||||
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
|
||||
|
|
|
@ -41,6 +41,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import com.bumptech.glide.Glide
|
||||
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.shape.MaterialShapeDrawable
|
||||
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.Loading
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.getDomain
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
|
@ -172,9 +172,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
* Load colors and dimensions from resources
|
||||
*/
|
||||
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)
|
||||
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)
|
||||
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ import androidx.core.view.setPadding
|
|||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.decodeBlurHash
|
||||
import com.keylesspalace.tusky.util.getFormattedDescription
|
||||
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 mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ import com.canhub.cropper.CropImage
|
|||
import com.canhub.cropper.CropImageContract
|
||||
import com.canhub.cropper.options
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
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.Status
|
||||
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.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.afterTextChanged
|
||||
import com.keylesspalace.tusky.util.getInitialLanguage
|
||||
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.modernLanguageCode
|
||||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
import com.keylesspalace.tusky.util.setDrawableTint
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
|
@ -206,7 +208,7 @@ class ComposeActivity :
|
|||
accountManager.setActiveAccount(accountId)
|
||||
}
|
||||
|
||||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||
val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
|
||||
if (theme == "black") {
|
||||
setTheme(R.style.TuskyDialogActivityBlackTheme)
|
||||
}
|
||||
|
@ -341,7 +343,7 @@ class ComposeActivity :
|
|||
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
|
||||
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.setOnClickListener {
|
||||
|
@ -354,7 +356,7 @@ class ComposeActivity :
|
|||
binding.composeReplyContentView.show()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -501,7 +503,7 @@ class ComposeActivity :
|
|||
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 }
|
||||
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
|
||||
|
@ -688,7 +690,7 @@ class ComposeActivity :
|
|||
getColor(R.color.tusky_blue)
|
||||
} else {
|
||||
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)
|
||||
|
@ -710,7 +712,7 @@ class ComposeActivity :
|
|||
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
|
||||
} else {
|
||||
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
MaterialColors.getColor(binding.composeScheduleButton, android.R.attr.textColorTertiary)
|
||||
} else {
|
||||
getColor(R.color.tusky_blue)
|
||||
}
|
||||
|
@ -906,7 +908,7 @@ class ComposeActivity :
|
|||
val textColor = if (remainingLength < 0) {
|
||||
getColor(R.color.tusky_red)
|
||||
} else {
|
||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
|
||||
}
|
||||
binding.composeCharactersLeftView.setTextColor(textColor)
|
||||
}
|
||||
|
@ -1007,7 +1009,7 @@ class ComposeActivity :
|
|||
|
||||
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
||||
button.isEnabled = clickable
|
||||
ThemeUtils.setDrawableTint(
|
||||
setDrawableTint(
|
||||
this, button.drawable,
|
||||
if (colorActive) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled
|
||||
|
@ -1016,8 +1018,8 @@ class ComposeActivity :
|
|||
|
||||
private fun enablePollButton(enable: Boolean) {
|
||||
binding.addPollTextActionTextView.isEnabled = enable
|
||||
val textColor = ThemeUtils.getColor(
|
||||
this,
|
||||
val textColor = MaterialColors.getColor(
|
||||
binding.addPollTextActionTextView,
|
||||
if (enable) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled
|
||||
)
|
||||
|
@ -1077,7 +1079,7 @@ class ComposeActivity :
|
|||
} else {
|
||||
binding.composeContentWarningBar.hide()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
package com.keylesspalace.tusky.components.preference
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountListActivity
|
||||
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.preferenceCategory
|
||||
import com.keylesspalace.tusky.settings.switchPreference
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.getInitialLanguage
|
||||
import com.keylesspalace.tusky.util.getLocaleList
|
||||
import com.keylesspalace.tusky.util.getTuskyDisplayName
|
||||
|
@ -80,7 +81,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setTitle(R.string.pref_title_edit_notification_settings)
|
||||
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).apply {
|
||||
sizeRes = R.dimen.preference_icon_size
|
||||
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
|
||||
colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
|
||||
}
|
||||
setOnPreferenceClickListener {
|
||||
openNotificationPrefs()
|
||||
|
@ -135,7 +136,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setTitle(R.string.action_view_blocks)
|
||||
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).apply {
|
||||
sizeRes = R.dimen.preference_icon_size
|
||||
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
|
||||
colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
|
||||
}
|
||||
setOnPreferenceClickListener {
|
||||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
|
|
|
@ -31,8 +31,9 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding
|
||||
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.setAppNightMode
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
@ -124,9 +125,9 @@ class PreferencesActivity :
|
|||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"appTheme" -> {
|
||||
val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||
val theme = sharedPreferences.getNonNullString("appTheme", APP_THEME_DEFAULT)
|
||||
Log.d("activeTheme", theme)
|
||||
ThemeUtils.setAppNightMode(theme)
|
||||
setAppNightMode(theme)
|
||||
|
||||
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||
this.restartCurrentActivity()
|
||||
|
|
|
@ -31,8 +31,8 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
|
|||
import com.keylesspalace.tusky.util.StatusViewHelper
|
||||
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.TimestampUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.setClickableMentions
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
|
@ -161,7 +161,7 @@ class StatusViewHolder(
|
|||
binding.timestampInfo.text = if (createdAt != null) {
|
||||
val then = createdAt.time
|
||||
val now = System.currentTimeMillis()
|
||||
TimestampUtils.getRelativeTimeSpanString(binding.timestampInfo.context, then, now)
|
||||
getRelativeTimeSpanString(binding.timestampInfo.context, then, now)
|
||||
} else {
|
||||
// unknown minutes~
|
||||
"?m"
|
||||
|
|
|
@ -915,11 +915,11 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
|
||||
FetchEnd fetchEnd, int pos) {
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.Companion.parse(linkHeader);
|
||||
HttpHeaderLink next = HttpHeaderLink.Companion.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
fromId = next.getUri().getQueryParameter("max_id");
|
||||
}
|
||||
|
||||
switch (fetchEnd) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
135
app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt
Normal file
135
app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,9 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.Px
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
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 {
|
||||
return IconicsDrawable(context, icon).apply {
|
||||
sizePx = iconSize
|
||||
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
|
||||
colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package com.keylesspalace.tusky.util
|
|||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
|
@ -33,6 +34,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
|
|||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Status.Mention
|
||||
|
@ -251,9 +253,9 @@ private fun openLinkInBrowser(uri: Uri?, context: Context) {
|
|||
* @param context context
|
||||
*/
|
||||
private fun openLinkInCustomTab(uri: Uri, context: Context) {
|
||||
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface)
|
||||
val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
|
||||
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
|
||||
val toolbarColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
|
||||
val navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
|
||||
val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
|
||||
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(toolbarColor)
|
||||
.setNavigationBarColor(navigationbarColor)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
74
app/src/main/java/com/keylesspalace/tusky/util/PairedList.kt
Normal file
74
app/src/main/java/com/keylesspalace/tusky/util/PairedList.kt
Normal 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
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextUtils
|
||||
|
@ -24,6 +25,7 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
|
@ -85,7 +87,7 @@ class StatusViewHelper(private val itemView: View) {
|
|||
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)
|
||||
|
||||
|
@ -292,7 +294,7 @@ class StatusViewHelper(private val itemView: View) {
|
|||
if (useAbsoluteTime) {
|
||||
context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false))
|
||||
} else {
|
||||
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
|
||||
formatPollDuration(context, poll.expiresAt!!.time, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
67
app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt
Normal file
67
app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
102
app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt
Normal file
102
app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt
Normal 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())
|
||||
}
|
|
@ -16,12 +16,13 @@
|
|||
package com.keylesspalace.tusky.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.CardLicenseBinding
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
|
||||
|
@ -35,7 +36,7 @@ class LicenseCard
|
|||
init {
|
||||
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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -21,9 +21,9 @@ class TimestampUtilsTest {
|
|||
|
||||
@Test
|
||||
fun shouldShowNowForSmallTimeSpans() {
|
||||
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 300))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 300, 0))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 501, 0))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 999))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 300))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 300, 0))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 501, 0))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 999))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue