diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 9fec5b4ed..ad173170a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -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)); diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 5850e3210..436bef7a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 4a3d2da1b..c7530282d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -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() diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 5401b5931..4c7aeca9d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -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() diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt index ef5edd1d4..daf8381e9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt @@ -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) : ArrayAdapter(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) : 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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt index 0155f4a44..db2f79a99 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index d6cb93a92..2aa1316e8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -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)); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index 994630a14..1b8ebf729 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index f4bc8ce64..0d3e05cb3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -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) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt index e5a0b592d..fcc3bcf9d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index edbbfd227..5f708b829 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -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) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt index d60210ba5..d0e137196 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index 54bb4a4d5..44d35b669 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -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() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 82dbf163d..4212046ad 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -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" diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 99543b803..7d381c4bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -915,11 +915,11 @@ public class NotificationsFragment extends SFragment implements private void onFetchNotificationsSuccess(List notifications, String linkHeader, FetchEnd fetchEnd, int pos) { - List links = HttpHeaderLink.parse(linkHeader); - HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next"); + List 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) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java b/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java deleted file mode 100644 index 27f3dffa6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java +++ /dev/null @@ -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 . */ - -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 RFC5988 - */ -public class HttpHeaderLink { - private static class Parameter { - public String name; - public String value; - } - - private List 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 parse(@Nullable String line) { - List 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 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; - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt b/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt new file mode 100644 index 000000000..4f5e9920b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt @@ -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 . + */ +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 = 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 { + val links: MutableList = 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, + relationType: String + ): HttpHeaderLink? { + return links.find { link -> + link.parameters.any { parameter -> + parameter.name == "rel" && parameter.value == relationType + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt index 59b0b15d7..3cb34fe46 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt @@ -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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt index 69f84593c..8a2e8f491 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java b/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java deleted file mode 100644 index a0880a519..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java +++ /dev/null @@ -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 type of elements in the main list - * @param type of elements in supplementary list - */ -public final class PairedList extends AbstractList { - private final List main = new ArrayList<>(); - private final List synced = new ArrayList<>(); - private final Function 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 mapper) { - this.mapper = mapper; - } - - public List 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(); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PairedList.kt b/app/src/main/java/com/keylesspalace/tusky/util/PairedList.kt new file mode 100644 index 000000000..39a47cc70 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/PairedList.kt @@ -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 (private val mapper: Function) : AbstractMutableList() { + private val main: MutableList = ArrayList() + private val synced: MutableList = ArrayList() + + val pairedCopy: List + 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 +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index 253ea7a0a..7594e8ed6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java deleted file mode 100644 index 8c04a7d20..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java +++ /dev/null @@ -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 . */ - -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; - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt new file mode 100644 index 000000000..8c7c0b23e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt @@ -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 . */ +@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) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java deleted file mode 100644 index 5b911fb13..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java +++ /dev/null @@ -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 . */ - -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); - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt new file mode 100644 index 000000000..a6717ed4f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt @@ -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 . */ +@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()) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt index 59b359246..631b6e4ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt @@ -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) diff --git a/app/src/test/java/com/keylesspalace/tusky/util/HttpHeaderLinkTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/HttpHeaderLinkTest.kt new file mode 100644 index 000000000..ac253db03 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/HttpHeaderLinkTest.kt @@ -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) + + @Test + fun shouldParseValidLinks() { + val testData = arrayOf( + // Examples from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + TestData( + "Single URL", + "", + listOf(HttpHeaderLink("https://example.com")) + ), + TestData( + "Single URL with parameters", + "; rel=\"preconnect\"", + listOf(HttpHeaderLink("https://example.com")) + ), + TestData( + "Single encoded URL with parameters", + "; rel=\"preconnect\"", + listOf(HttpHeaderLink("https://example.com/%E8%8B%97%E6%9D%A1")) + ), + TestData( + "Multiple URLs, separated by commas", + "; rel=\"preconnect\", ; rel=\"preconnect\", ; 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", + "; 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", + "; rel=\"copyright\"; anchor=\"#foo\"", + listOf(HttpHeaderLink("/terms")) + ), + TestData( + "Multiple URLs with parameter encoding", + "; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, ; 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) + } + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/PairedListTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/PairedListTest.kt new file mode 100644 index 000000000..d5df1550b --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/PairedListTest.kt @@ -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 + + @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) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt index ded3b219f..1375b203e 100644 --- a/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt @@ -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)) } }