Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2019-11-19 21:52:13 +09:00
commit 762b2225ca
173 changed files with 1801 additions and 1371 deletions

View File

@ -5,7 +5,7 @@
Yuito is fork of [Tusky](https://github.com/tuskyapp/Tusky).
Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/mastodon). Mastodon is a GNU social-compatible federated social network. That means not one entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly.
Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/mastodon). Mastodon is an ActivityPub federated social network. That means no single entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly.
## Features
@ -22,6 +22,8 @@ Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/
If you have any bug reports, feature requests or questions please open an issue or send us a toot at [@ars42525@odakyu.app](https://odakyu.app/@ars42525)!
For translating Tusky into your language, visit https://weblate.tusky.app/
### Head of development
This app was developed by [@ars42525@odakyu.app](https://odakyu.app/@ars42525).

View File

@ -3,7 +3,9 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
def getGitSha = { ->
apply from: "../instance-build.gradle"
def getGitSha = {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
@ -13,19 +15,26 @@ def getGitSha = { ->
}
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId 'net.accelf.yuito'
minSdkVersion 21
targetSdkVersion 28
targetSdkVersion 29
versionCode 4
versionName '1.1.2'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
resValue "string", "app_name", APP_NAME
buildConfigField("String", "CUSTOM_LOGO_URL", "\"$CUSTOM_LOGO_URL\"")
buildConfigField("String", "CUSTOM_INSTANCE", "\"$CUSTOM_INSTANCE\"")
buildConfigField("String", "SUPPORT_ACCOUNT_URL", "\"$SUPPORT_ACCOUNT_URL\"")
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
}
}
}
@ -61,9 +70,6 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
androidExtensions {
experimental = true
}
testOptions {
unitTests {
returnDefaultValues = true
@ -74,7 +80,6 @@ android {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
packagingOptions {
// Exclude unneeded files added by libraries
exclude 'LICENSE_OFL'
@ -94,8 +99,11 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
}
}
ext.daggerVersion = '2.24'
ext.roomVersion = '2.2.1'
ext.retrofitVersion = '2.6.0'
ext.okhttpVersion = '4.2.2'
ext.glideVersion = '4.10.0'
ext.daggerVersion = '2.25.2'
repositories {
maven {
@ -105,72 +113,84 @@ repositories {
// if libraries are changed here, they should also be changed in LicenseActivity
dependencies {
implementation('com.mikepenz:materialdrawer:6.1.2@aar') {
transitive = true
}
implementation 'androidx.core:core:1.0.2'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'com.google.android.material:material:1.1.0-alpha10'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.1.0-alpha04'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0-beta01"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.browser:browser:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.0.0"
implementation "androidx.exifinterface:exifinterface:1.0.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.sharetarget:sharetarget:1.0.0-beta01"
implementation "androidx.emoji:emoji:1.0.0"
implementation "androidx.emoji:emoji-appcompat:1.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
implementation "androidx.viewpager2:viewpager2:1.0.0-rc01"
implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.room:room-rxjava2:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "com.google.android.material:material:1.1.0-beta01"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
implementation 'com.squareup.okhttp3:okhttp:4.2.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
implementation 'org.conscrypt:conscrypt-android:2.2.1'
implementation 'com.github.connyduck:sparkbutton:2.0.1'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.mikepenz:google-material-typeface:3.0.1.3.original@aar'
implementation('com.theartofdev.edmodo:android-image-cropper:2.8.0') {
exclude group: 'com.android.support'
}
implementation 'com.evernote:android-job:1.2.6'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// EmojiCompat
implementation 'androidx.emoji:emoji:1.0.0'
implementation 'androidx.emoji:emoji-appcompat:1.0.0'
implementation 'de.c1710:filemojicompat:1.0.17'
// architecture components
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
//room
implementation 'androidx.room:room-runtime:2.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
kapt 'androidx.room:room-compiler:2.1.0'
implementation 'androidx.room:room-rxjava2:2.1.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
implementation "org.conscrypt:conscrypt-android:2.2.1"
implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
implementation "io.reactivex.rxjava2:rxjava:2.2.13"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
implementation "com.uber.autodispose:autodispose-android-archcomponents:1.4.0"
implementation "com.uber.autodispose:autodispose:1.4.0"
implementation "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
implementation "com.google.dagger:dagger-android:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'org.mockito:mockito-inline:3.0.0'
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.1', {
exclude group: 'com.android.support', module: 'support-annotations'
implementation "com.github.connyduck:sparkbutton:2.0.1"
implementation "com.github.chrisbanes:PhotoView:2.3.0"
implementation("com.mikepenz:materialdrawer:6.1.2@aar") {
transitive = true
}
implementation "com.mikepenz:google-material-typeface:3.0.1.3.original@aar"
implementation("com.theartofdev.edmodo:android-image-cropper:2.8.0") {
exclude group: "com.android.support"
}
implementation "com.evernote:android-job:1.4.2"
implementation "de.c1710:filemojicompat:1.0.17"
testImplementation "androidx.test.ext:junit:1.1.1"
testImplementation "org.robolectric:robolectric:4.3.1"
testImplementation "org.mockito:mockito-inline:3.1.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
androidTestImplementation("androidx.test.espresso:espresso-core:3.1.1", {
exclude group: "com.android.support", module: "support-annotations"
})
androidTestImplementation 'android.arch.persistence.room:testing:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
testImplementation 'androidx.test.ext:junit:1.1.1'
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
implementation 'io.reactivex.rxjava2:rxjava:2.2.12'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.4.0'
implementation 'com.uber.autodispose:autodispose:1.4.0'
implementation 'androidx.paging:paging-runtime-ktx:2.1.0'
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
androidTestImplementation "androidx.test.ext:junit:1.1.1"
//Glide
implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.10.0'
//Add some useful extensions
implementation 'androidx.core:core-ktx:1.2.0-alpha01'
debugImplementation "im.dino:dbinspector:4.0.0@aar"
implementation 'net.accelf:easter:1.0.1'
}

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:icon="@mipmap/ic_shortcut_compose"
android:shortcutId="com.keylesspalace.tusky.Compose"
android:shortcutLongLabel="@string/compose_shortcut_long_label"
android:shortcutShortLabel="@string/compose_shortcut_short_label">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.keylesspalace.tusky.SplashActivity"
android:targetPackage="com.keylesspalace.tusky"/>
<intent
android:action="com.keylesspalace.tusky.Compose"
android:targetClass="com.keylesspalace.tusky.ComposeActivity"
android:targetPackage="com.keylesspalace.tusky"/>
</shortcut>
</shortcuts>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:icon="@mipmap/ic_shortcut_compose"
android:shortcutId="com.keylesspalace.tusky.Compose"
android:shortcutLongLabel="@string/compose_shortcut_long_label"
android:shortcutShortLabel="@string/compose_shortcut_short_label">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.keylesspalace.tusky.SplashActivity"
android:targetPackage="com.keylesspalace.tusky.test"/>
<intent
android:action="com.keylesspalace.tusky.Compose"
android:targetClass="com.keylesspalace.tusky.ComposeActivity"
android:targetPackage="com.keylesspalace.tusky.test"/>
</shortcut>
</shortcuts>

View File

@ -31,7 +31,8 @@
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
android:resource="@xml/share_shortcuts" />
</activity>
<activity
android:name=".SavedTootActivity"
@ -52,7 +53,7 @@
</activity>
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden">
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
@ -91,7 +92,8 @@
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="com.keylesspalace.tusky.service.AccountChooserService" />
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity>
<activity
android:name=".ComposeActivity"
@ -106,7 +108,7 @@
android:theme="@style/TuskyBaseTheme" />
<activity
android:name=".AccountActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
<activity android:name=".EditProfileActivity" />
<activity android:name=".PreferencesActivity" />
<activity android:name=".FavouritesActivity" />
@ -153,16 +155,8 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service android:name=".service.SendTootService" />
<service
android:name=".service.AccountChooserService"
android:label="@string/app_name"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE"
tools:targetApi="23">
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"

View File

@ -12,6 +12,7 @@ import android.widget.TextView
import androidx.annotation.StringRes
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.CustomURLSpan
import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.activity_about.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import net.accelf.yuito.AccessTokenLoginActivity
@ -34,7 +35,11 @@ class AboutActivity : BottomSheetActivity(), Injectable {
onEasterEggExecute()
}
versionTextView.text = getString(R.string.about_tusky_version, BuildConfig.VERSION_NAME)
versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME)
if(BuildConfig.CUSTOM_INSTANCE.isBlank()) {
aboutPoweredByTusky.hide()
}
aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license)
aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
@ -42,7 +47,7 @@ class AboutActivity : BottomSheetActivity(), Injectable {
aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
tuskyProfileButton.setOnClickListener {
onAccountButtonClick()
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL, BuildConfig.SUPPORT_ACCOUNT_URL)
}
aboutLicensesButton.setOnClickListener {
@ -55,10 +60,6 @@ class AboutActivity : BottomSheetActivity(), Injectable {
startActivityWithSlideInAnimation(Intent(this, AccessTokenLoginActivity::class.java))
}
private fun onAccountButtonClick() {
viewUrl("https://odakyu.app/@ars42525", getString(R.string.about_tusky_account))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {

View File

@ -18,10 +18,10 @@ package com.keylesspalace.tusky
import android.animation.ArgbEvaluator
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.Menu
import android.view.MenuItem
import android.view.View
@ -35,13 +35,18 @@ import androidx.core.content.ContextCompat
import androidx.emoji.text.EmojiCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager
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.appbar.CollapsingToolbarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.di.ViewModelFactory
@ -86,8 +91,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
@ColorInt
private var toolbarColor: Int = 0
@ColorInt
private var backgroundColor: Int = 0
@ColorInt
private var statusBarColorTransparent: Int = 0
@ColorInt
private var statusBarColorOpaque: Int = 0
@ -107,6 +110,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadResources()
makeNotificationBarTransparent()
setContentView(R.layout.activity_account)
@ -119,7 +123,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
hideFab = sharedPrefs.getBoolean("fabHide", false)
loadResources()
setupToolbar()
setupTabs()
setupAccountViews()
@ -135,8 +138,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
* Load colors and dimensions from resources
*/
private fun loadResources() {
toolbarColor = ThemeUtils.getColor(this, R.attr.toolbar_background_color)
backgroundColor = ThemeUtils.getColor(this, android.R.attr.colorBackground)
toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface)
statusBarColorTransparent = ContextCompat.getColor(this, R.color.header_background_filter)
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size)
@ -187,16 +189,21 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
*/
private fun setupTabs() {
// Setup the tabs and timeline pager.
adapter = AccountPagerAdapter(supportFragmentManager, viewModel.accountId)
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media))
adapter.setPageTitles(pageTitles)
accountFragmentViewPager.pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
val pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
R.drawable.tab_page_margin_dark)
accountFragmentViewPager.setPageMarginDrawable(pageMarginDrawable)
adapter = AccountPagerAdapter(this, viewModel.accountId)
accountFragmentViewPager.adapter = adapter
accountFragmentViewPager.offscreenPageLimit = 2
accountTabLayout.setupWithViewPager(accountFragmentViewPager)
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media))
TabLayoutMediator(accountTabLayout, accountFragmentViewPager) {
tab, position ->
tab.text = pageTitles[position]
}.attach()
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin))
accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.position?.let { position ->
@ -211,9 +218,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
})
}
/**
* Setup toolbar
*/
private fun setupToolbar() {
// set toolbar top margin according to system window insets
accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets ->
@ -236,6 +240,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
ThemeUtils.setDrawableTint(this, accountToolbar.navigationIcon, R.attr.account_toolbar_icon_tint_uncollapsed)
ThemeUtils.setDrawableTint(this, accountToolbar.overflowIcon, R.attr.account_toolbar_icon_tint_uncollapsed)
val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation)
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
accountToolbar.background = toolbarBackground
accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply {
fillColor = ColorStateList.valueOf(toolbarColor)
elevation = appBarElevation
shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius))
.build()
}
accountAvatarImageView.background = avatarBackground
// Add a listener to change the toolbar icon color when it enters/exits its collapsed state.
accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
@ -281,16 +302,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountAvatarImageView.visible(scaledAvatarSize > 0)
var transparencyPercent = abs(verticalOffset) / titleVisibleHeight.toFloat()
if (transparencyPercent > 1) transparencyPercent = 1f
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f)
window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int
val evaluatedToolbarColor = argbEvaluator.evaluate(transparencyPercent, Color.TRANSPARENT, toolbarColor) as Int
val evaluatedTabBarColor = argbEvaluator.evaluate(transparencyPercent, backgroundColor, toolbarColor) as Int
accountToolbar.setBackgroundColor(evaluatedToolbarColor)
accountHeaderInfoContainer.setBackgroundColor(evaluatedTabBarColor)
accountTabLayout.setBackgroundColor(evaluatedTabBarColor)
toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor)
swipeToRefreshLayout.isEnabled = verticalOffset == 0
}
})
@ -300,7 +319,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun makeNotificationBarTransparent() {
val decorView = window.decorView
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
window.statusBarColor = Color.TRANSPARENT
window.statusBarColor = statusBarColorTransparent
}
/**

View File

@ -17,13 +17,13 @@
package com.keylesspalace.tusky
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
@ -35,7 +35,6 @@ import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDisposable
import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.extensions.LayoutContainer

View File

@ -23,7 +23,6 @@ import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
@ -34,6 +33,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.google.android.material.snackbar.Snackbar;
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
@ -52,8 +52,6 @@ import javax.inject.Inject;
public abstract class BaseActivity extends AppCompatActivity implements Injectable {
@Inject
public ThemeUtils themeUtils;
@Inject
public AccountManager accountManager;
@ -75,8 +73,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
setTheme(R.style.TuskyBlackTheme);
}
themeUtils.setAppNightMode(theme, this);
/* 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);

View File

@ -19,6 +19,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -52,7 +53,7 @@ abstract class BottomSheetActivity : BaseActivity() {
val bottomSheetLayout: LinearLayout = findViewById(R.id.item_status_bottom_sheet)
bottomSheet = BottomSheetBehavior.from(bottomSheetLayout)
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
bottomSheet.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
bottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
cancelActiveSearch()
@ -64,7 +65,7 @@ abstract class BottomSheetActivity : BaseActivity() {
}
open fun viewUrl(url: String, text: String) {
open fun viewUrl(url: String, text: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
if (forceBrowser.contains(text)) {
openLink(url)
return
@ -95,11 +96,11 @@ abstract class BottomSheetActivity : BaseActivity() {
return@subscribe
}
openLink(url)
performUrlFallbackAction(url, lookupFallbackBehavior)
}, {
if (!getCancelSearchRequested(url)) {
onEndSearch(url)
openLink(url)
performUrlFallbackAction(url, lookupFallbackBehavior)
}
})
@ -120,6 +121,13 @@ abstract class BottomSheetActivity : BaseActivity() {
startActivityWithSlideInAnimation(intent)
}
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
when (fallbackBehavior) {
PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url)
PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(this, getString(R.string.post_lookup_error_format, url), Toast.LENGTH_SHORT).show()
}
}
@VisibleForTesting
fun onBeginSearch(url: String) {
searchUrl = url
@ -201,3 +209,8 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
path.matches("^[^@]+@[^@]+$".toRegex())
}
enum class PostLookupFallbackBehavior {
OPEN_IN_BROWSER,
DISPLAY_ERROR,
}

View File

@ -26,9 +26,9 @@ import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials
@ -48,9 +48,6 @@ class LoginActivity : BaseActivity(), Injectable {
lateinit var mastodonApi: MastodonApi
private lateinit var preferences: SharedPreferences
private var domain: String = ""
private var clientId: String? = null
private var clientSecret: String? = null
private val oauthRedirectUri: String
get() {
@ -64,10 +61,16 @@ class LoginActivity : BaseActivity(), Injectable {
setContentView(R.layout.activity_login)
if (savedInstanceState != null) {
domain = savedInstanceState.getString(DOMAIN)!!
clientId = savedInstanceState.getString(CLIENT_ID)
clientSecret = savedInstanceState.getString(CLIENT_SECRET)
if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
}
if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
Glide.with(loginLogo)
.load(BuildConfig.CUSTOM_LOGO_URL)
.placeholder(null)
.into(loginLogo)
}
preferences = getSharedPreferences(
@ -113,13 +116,6 @@ class LoginActivity : BaseActivity(), Injectable {
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putString(DOMAIN, domain)
outState.putString(CLIENT_ID, clientId)
outState.putString(CLIENT_SECRET, clientSecret)
super.onSaveInstanceState(outState)
}
/**
* Obtain the oauth client credentials for this app. This is only necessary the first time the
* app is run on a given server instance. So, after the first authentication, they are
@ -129,7 +125,7 @@ class LoginActivity : BaseActivity(), Injectable {
loginButton.isEnabled = false
domain = canonicalizeDomain(domainEditText.text.toString())
val domain = canonicalizeDomain(domainEditText.text.toString())
try {
HttpUrl.Builder().host(domain).scheme("https").build()
@ -150,10 +146,16 @@ class LoginActivity : BaseActivity(), Injectable {
return
}
val credentials = response.body()
clientId = credentials!!.clientId
clientSecret = credentials.clientSecret
val clientId = credentials!!.clientId
val clientSecret = credentials.clientSecret
redirectUserToAuthorizeAndLogin(domainEditText)
preferences.edit()
.putString("domain", domain)
.putString("clientId", clientId)
.putString("clientSecret", clientSecret)
.apply()
redirectUserToAuthorizeAndLogin(domain, clientId)
}
override fun onFailure(call: Call<AppCredentials>, t: Throwable) {
@ -166,22 +168,22 @@ class LoginActivity : BaseActivity(), Injectable {
mastodonApi
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri,
OAUTH_SCOPES, getString(R.string.app_website))
OAUTH_SCOPES, getString(R.string.tusky_website))
.enqueue(callback)
setLoading(true)
}
private fun redirectUserToAuthorizeAndLogin(editText: EditText) {
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
/* To authorize this app and log in it's necessary to redirect to the domain given,
* activity_login there, and the server will redirect back to the app with its response. */
* login there, and the server will redirect back to the app with its response. */
val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
val redirectUri = oauthRedirectUri
val parameters = HashMap<String, String>()
parameters["client_id"] = clientId!!
parameters["redirect_uri"] = redirectUri
parameters["response_type"] = "code"
parameters["scope"] = OAUTH_SCOPES
val parameters = mapOf(
"client_id" to clientId,
"redirect_uri" to oauthRedirectUri,
"response_type" to "code",
"scope" to OAUTH_SCOPES
)
val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
val uri = Uri.parse(url)
if (!openInCustomTab(uri, this)) {
@ -189,21 +191,12 @@ class LoginActivity : BaseActivity(), Injectable {
if (viewIntent.resolveActivity(packageManager) != null) {
startActivity(viewIntent)
} else {
editText.error = getString(R.string.error_no_web_browser_found)
domainEditText.error = getString(R.string.error_no_web_browser_found)
setLoading(false)
}
}
}
override fun onStop() {
super.onStop()
preferences.edit()
.putString("domain", domain)
.putString("clientId", clientId)
.putString("clientSecret", clientSecret)
.apply()
}
override fun onStart() {
super.onStart()
/* Check if we are resuming during authorization by seeing if the intent contains the
@ -216,14 +209,12 @@ class LoginActivity : BaseActivity(), Injectable {
val code = uri.getQueryParameter("code")
val error = uri.getQueryParameter("error")
/* During the redirect roundtrip this Activity usually dies, which wipes out the
* instance variables, so they have to be recovered from where they were saved in
* SharedPreferences. */
domain = preferences.getNonNullString(DOMAIN, "")
clientId = preferences.getString(CLIENT_ID, null)
clientSecret = preferences.getString(CLIENT_SECRET, null)
/* restore variables from SharedPreferences */
val domain = preferences.getNonNullString(DOMAIN, "")
val clientId = preferences.getNonNullString(CLIENT_ID, "")
val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "")
if (code != null && domain.isNotEmpty() && !clientId.isNullOrEmpty() && !clientSecret.isNullOrEmpty()) {
if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) {
setLoading(true)
/* Since authorization has succeeded, the final step to log in is to exchange
@ -231,7 +222,7 @@ class LoginActivity : BaseActivity(), Injectable {
val callback = object : Callback<AccessToken> {
override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
if (response.isSuccessful) {
onLoginSuccess(response.body()!!.accessToken)
onLoginSuccess(response.body()!!.accessToken, domain)
} else {
setLoading(false)
domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
@ -250,7 +241,7 @@ class LoginActivity : BaseActivity(), Injectable {
}
}
mastodonApi.fetchOAuthToken(domain, clientId!!, clientSecret!!, redirectUri, code,
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
"authorization_code").enqueue(callback)
} else if (error != null) {
/* Authorization failed. Put the error response where the user can read it and they
@ -286,7 +277,7 @@ class LoginActivity : BaseActivity(), Injectable {
return intent.getBooleanExtra(LOGIN_MODE, false)
}
private fun onLoginSuccess(accessToken: String) {
private fun onLoginSuccess(accessToken: String, domain: String) {
setLoading(true)

View File

@ -22,12 +22,10 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
@ -35,14 +33,18 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.emoji.text.EmojiCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.viewpager.widget.ViewPager;
import androidx.preference.PreferenceManager;
import androidx.viewpager2.widget.MarginPageTransformer;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.Glide;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.keylesspalace.tusky.appstore.CacheUpdater;
import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent;
import com.keylesspalace.tusky.appstore.EventHub;
@ -58,7 +60,7 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.pager.MainPagerAdapter;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.NotificationHelper;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ShareShortcutHelper;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.materialdrawer.AccountHeader;
import com.mikepenz.materialdrawer.AccountHeaderBuilder;
@ -121,7 +123,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
private AccountHeader headerResult;
private Drawer drawer;
private TabLayout tabLayout;
private ViewPager viewPager;
private ViewPager2 viewPager;
private SharedPreferences defPrefs;
private int notificationTabPosition;
@ -149,7 +151,18 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
boolean showNotificationTab = false;
if (intent != null) {
/** there are two possibilities the accountId can be passed to MainActivity:
- from our code as long 'account_id'
- from share shortcuts as String 'android.intent.extra.shortcut.ID'
*/
long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1);
if(accountId == -1) {
String accountIdString = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
if(accountIdString != null) {
accountId = Long.parseLong(accountIdString);
}
}
boolean accountRequested = (accountId != -1);
if (accountRequested) {
@ -187,7 +200,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
setContentView(R.layout.activity_main);
composeButton = findViewById(R.id.floating_btn);
ImageButton drawerToggle = findViewById(R.id.drawer_toggle);
tabLayout = findViewById(R.id.tab_layout);
viewPager = findViewById(R.id.pager);
@ -199,10 +211,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
setupDrawer();
// Setup the navigation drawer toggle button.
ThemeUtils.setDrawableTint(this, drawerToggle.getDrawable(), R.attr.toolbar_icon_tint);
drawerToggle.setOnClickListener(v -> drawer.openDrawer());
/* Fetch user info while we're doing other things. This has to be done after setting up the
* drawer, though, because its callback touches the header in the drawer. */
fetchUserInfo();
@ -212,10 +220,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
defPrefs = PreferenceManager.getDefaultSharedPreferences(this);
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
viewPager.setPageMargin(pageMargin);
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
R.drawable.tab_page_margin_dark);
viewPager.setPageMarginDrawable(pageMarginDrawable);
viewPager.setPageTransformer(new MarginPageTransformer(pageMargin));
if (defPrefs.getBoolean("viewPagerOffScreenLimit", false)) {
viewPager.setOffscreenPageLimit(9);
}
@ -349,7 +354,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
if (intent != null) {
String statusUrl = intent.getStringExtra(STATUS_URL);
if (statusUrl != null) {
viewUrl(statusUrl, statusUrl);
viewUrl(statusUrl, statusUrl, PostLookupFallbackBehavior.DISPLAY_ERROR);
}
}
}
@ -439,6 +444,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
.withHasStableIds(true)
.withSelectedItem(-1)
.withDrawerItems(listItems)
.withToolbar(findViewById(R.id.main_toolbar))
.withOnDrawerItemClickListener((view, position, drawerItem) -> {
if (drawerItem != null) {
long drawerItemIdentifier = drawerItem.getIdentifier();
@ -534,10 +540,11 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
private void setupTabs(boolean selectNotificationTab) {
List<TabData> tabs = accountManager.getActiveAccount().getTabPreferences();
adapter = new MainPagerAdapter(tabs, getSupportFragmentManager());
adapter = new MainPagerAdapter(tabs, this);
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { }).attach();
tabLayout.removeAllTabs();
for (int i = 0; i < tabs.size(); i++) {
TabLayout.Tab tab = tabLayout.newTab()
@ -610,6 +617,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
NotificationHelper.deleteNotificationChannelsForAccount(accountManager.getActiveAccount(), MainActivity.this);
cacheUpdater.clearForUser(activeAccount.getId());
conversationRepository.deleteCacheForAccount(activeAccount.getId());
ShareShortcutHelper.removeShortcut(this, activeAccount);
AccountEntity newAccount = accountManager.logActiveAccountOut();
@ -667,6 +675,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
updateProfiles();
ShareShortcutHelper.updateShortcut(this, accountManager.getActiveAccount());
}
private void updateProfiles() {

View File

@ -19,11 +19,10 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.util.Log
import android.view.MenuItem
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.fragment.preference.*
@ -123,18 +122,11 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
"appTheme" -> {
val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
Log.d("activeTheme", theme)
themeUtils.setAppNightMode(theme, this)
ThemeUtils.setAppNightMode(theme)
restartActivitiesOnExit = true
this.restartCurrentActivity()
// MODE_NIGHT_FOLLOW_SYSTEM workaround part 2 :/
when(theme){
ThemeUtils.THEME_SYSTEM -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
//workaround end
}
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "viewPagerOffScreenLimit" -> {
restartActivitiesOnExit = true

View File

@ -17,10 +17,11 @@ package com.keylesspalace.tusky;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.preference.PreferenceManager;
import androidx.emoji.text.EmojiCompat;
import androidx.preference.PreferenceManager;
import androidx.room.Room;
import com.evernote.android.job.JobManager;
@ -30,6 +31,7 @@ import com.keylesspalace.tusky.di.AppInjector;
import com.keylesspalace.tusky.util.EmojiCompatFont;
import com.keylesspalace.tusky.util.LocaleManager;
import com.keylesspalace.tusky.util.NotificationPullJobCreator;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.uber.autodispose.AutoDisposePlugins;
import org.conscrypt.Conscrypt;
@ -91,6 +93,7 @@ public class TuskyApplication extends Application implements HasAndroidInjector
initAppInjector();
initEmojiCompat();
initNightMode();
JobManager.create(this).addJobCreator(notificationPullJobCreator);
@ -133,6 +136,12 @@ public class TuskyApplication extends Application implements HasAndroidInjector
AppInjector.INSTANCE.init(this);
}
protected void initNightMode() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
String theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT);
ThemeUtils.setAppNightMode(theme);
}
public ServiceLocator getServiceLocator() {
return serviceLocator;
}

View File

@ -37,9 +37,10 @@ import android.view.View
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
import com.bumptech.glide.request.FutureTarget
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
@ -110,23 +111,23 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
// but it cannot be expressed and if I don't specify type explicitly compilation fails
// (probably a bug in compiler)
val adapter: PagerAdapter = if (attachments != null) {
val adapter: ViewMediaAdapter = if (attachments != null) {
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
// Setup the view pager.
ImagePagerAdapter(supportFragmentManager, realAttachs, initialPosition)
ImagePagerAdapter(this, realAttachs, initialPosition)
} else {
val avatarUrl = intent.getStringExtra(EXTRA_AVATAR_URL)
?: throw IllegalArgumentException("attachment list or avatar url has to be set")
AvatarImagePagerAdapter(supportFragmentManager, avatarUrl)
AvatarImagePagerAdapter(this, avatarUrl)
}
viewPager.adapter = adapter
viewPager.currentItem = initialPosition
viewPager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
viewPager.setCurrentItem(initialPosition, false)
viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
toolbar.title = adapter.getPageTitle(position)
toolbar.title = getPageTitle(position)
}
})
@ -136,7 +137,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setDisplayShowHomeEnabled(true)
actionBar.title = adapter.getPageTitle(initialPosition)
actionBar.title = getPageTitle(initialPosition)
}
toolbar.setNavigationOnClickListener { supportFinishAfterTransition() }
toolbar.setOnMenuItemClickListener { item: MenuItem ->
@ -153,7 +154,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
window.statusBarColor = Color.BLACK
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
override fun onTransitionEnd(transition: Transition) {
(adapter as SharedElementTransitionListener).onTransitionEnd()
adapter.onTransitionEnd(viewPager.currentItem)
window.sharedElementEnterTransition.removeListener(this)
}
})
@ -198,6 +199,13 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
.start()
}
private fun getPageTitle(position: Int): CharSequence {
if(attachments == null) {
return ""
}
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size)
}
private fun downloadMedia() {
val url = attachments!![viewPager.currentItem].attachment.url
val filename = Uri.parse(url).lastPathSegment
@ -227,7 +235,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
private fun copyLink() {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText(null, attachments!![viewPager.currentItem].attachment.url)
clipboard.setPrimaryClip(ClipData.newPlainText(null, attachments!![viewPager.currentItem].attachment.url))
}
private fun shareMedia() {
@ -323,8 +331,8 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
}
interface SharedElementTransitionListener {
fun onTransitionEnd()
abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) {
abstract fun onTransitionEnd(position: Int)
}
interface NoopTransitionListener : Transition.TransitionListener {

View File

@ -16,16 +16,15 @@
package com.keylesspalace.tusky.adapter
import android.content.Context
import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.loadAvatar
import kotlinx.android.synthetic.main.item_autocomplete_account.view.*
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {

View File

@ -1,13 +1,13 @@
package com.keylesspalace.tusky.adapter;
import androidx.recyclerview.widget.RecyclerView;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;

View File

@ -15,10 +15,6 @@
package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -26,6 +22,10 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;

View File

@ -15,10 +15,6 @@
package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -26,6 +22,10 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;

View File

@ -1,9 +1,5 @@
package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -11,6 +7,10 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;

View File

@ -29,6 +29,7 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.HtmlUtils
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.PollOptionViewData
import com.keylesspalace.tusky.viewdata.buildDescription
import com.keylesspalace.tusky.viewdata.calculatePercent
class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
@ -71,10 +72,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
when(mode) {
RESULT -> {
val percent = calculatePercent(option.votesCount, voteCount)
val pollOptionText = holder.resultTextView.context.getString(R.string.poll_option_format, percent, option.title)
val emojifiedPollOptionText = CustomEmojiHelper.emojifyText(HtmlUtils.fromHtml(pollOptionText), emojis, holder.resultTextView)
val emojifiedPollOptionText = CustomEmojiHelper.emojifyText(buildDescription(option.title, percent, holder.resultTextView.context), emojis, holder.resultTextView)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
val level = percent * 100

View File

@ -57,6 +57,8 @@ import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.SparkEventListener;
import kotlin.collections.CollectionsKt;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
public static class Key {
public static final String KEY_CREATED = "created";
@ -390,7 +392,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} else {
inactiveId = ThemeUtils.getDrawableId(reblogButton.getContext(),
R.attr.status_reblog_inactive_drawable, R.drawable.reblog_inactive_dark);
activeId = R.drawable.reblog_active;
activeId = R.drawable.ic_reblog_active_24dp;
}
reblogButton.setInactiveImage(inactiveId);
reblogButton.setActiveImage(activeId);
@ -905,10 +907,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
for (int i = 0; i < args.length; i++) {
if (i < options.size()) {
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotesCount());
args[i] = HtmlUtils.fromHtml(context.getString(
R.string.poll_option_format,
percent,
options.get(i).getTitle()));
args[i] = buildDescription(options.get(i).getTitle(), percent, context);
} else {
args[i] = "";
}

View File

@ -16,7 +16,6 @@
package com.keylesspalace.tusky.components.conversation;
import android.content.Context;
import android.preference.PreferenceManager;
import android.text.InputFilter;
import android.text.TextUtils;
import android.view.View;
@ -24,6 +23,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;

View File

@ -17,13 +17,13 @@ package com.keylesspalace.tusky.components.conversation
import android.content.Intent
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator

View File

@ -59,6 +59,7 @@ class ConversationsViewModel @Inject constructor(
}
.subscribeOn(Schedulers.io())
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
.onErrorReturnItem(0)
.subscribe()
.addTo(disposables)
}
@ -77,6 +78,7 @@ class ConversationsViewModel @Inject constructor(
}
.subscribeOn(Schedulers.io())
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
.onErrorReturnItem(0)
.subscribe()
.addTo(disposables)
}

View File

@ -16,7 +16,6 @@
package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -26,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator

View File

@ -23,6 +23,7 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.ViewModelProviders
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter
@ -57,9 +58,13 @@ class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, Ha
}
private fun setupPages() {
pages.adapter = SearchPagerAdapter(this, supportFragmentManager)
tabs.setupWithViewPager(pages)
pages.adapter = SearchPagerAdapter(this)
pages.offscreenPageLimit = 4
TabLayoutMediator(tabs, pages) {
tab, position ->
tab.text = getPageTitle(position)
}.attach()
}
override fun onNewIntent(intent: Intent) {
@ -75,9 +80,7 @@ class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, Ha
.actionView as SearchView
setupSearchView(searchView)
if (viewModel.currentQuery != null) {
searchView.setQuery(viewModel.currentQuery, false)
}
return true
}
@ -100,9 +103,19 @@ class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, Ha
return false
}
private fun getPageTitle(position: Int): CharSequence? {
return when (position) {
0 -> getString(R.string.title_statuses)
1 -> getString(R.string.title_accounts)
2 -> getString(R.string.title_hashtags_dialog)
3 -> getString(R.string.title_notestock)
else -> throw IllegalArgumentException("Unknown page index: $position")
}
}
private fun handleIntent(intent: Intent) {
if (Intent.ACTION_SEARCH == intent.action) {
viewModel.currentQuery = intent.getStringExtra(SearchManager.QUERY)
viewModel.currentQuery = intent.getStringExtra(SearchManager.QUERY) ?: ""
viewModel.search(viewModel.currentQuery)
}
}

View File

@ -21,6 +21,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import javax.inject.Inject
class SearchViewModel @Inject constructor(
@ -29,7 +30,7 @@ class SearchViewModel @Inject constructor(
private val timelineCases: TimelineCases,
private val accountManager: AccountManager) : ViewModel() {
var currentQuery: String? = null
var currentQuery: String = ""
var activeAccount: AccountEntity?
get() = accountManager.activeAccount
@ -72,7 +73,7 @@ class SearchViewModel @Inject constructor(
private val loadedStatuses = ArrayList<Pair<Status, StatusViewData.Concrete>>()
private val loadedNotestockStatuses = ArrayList<Pair<Status, StatusViewData.Concrete>>()
fun search(query: String?) {
fun search(query: String) {
loadedStatuses.clear()
repoResultStatus.value = statusesRepository.getSearchData(SearchType.Status, query, disposables, initialItems = loadedStatuses) {
(it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) }
@ -84,7 +85,7 @@ class SearchViewModel @Inject constructor(
repoResultAccount.value = accountsRepository.getSearchData(SearchType.Account, query, disposables) {
it?.accounts ?: emptyList()
}
val hashtagQuery = if (query != null && query.startsWith("#")) query else "#$query"
val hashtagQuery = if (query.startsWith("#")) query else "#$query"
repoResultHashTag.value =
hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) {
it?.hashtags ?: emptyList()
@ -107,9 +108,14 @@ class SearchViewModel @Inject constructor(
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
timelineCases.delete(status.first.id)
.subscribe()
.subscribe({
if (loadedStatuses.remove(status))
repoResultStatus.value?.refresh?.invoke()
}, {
err -> Log.d(TAG, "Failed to delete status", err)
})
.addTo(disposables)
}
fun removeNotestockItem(status: Pair<Status, StatusViewData.Concrete>) {

View File

@ -22,12 +22,8 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.AccountViewHolder
import com.keylesspalace.tusky.adapter.StatusViewHolder
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.viewdata.StatusViewData
class SearchAccountsAdapter(private val linkListener: LinkListener)
: PagedListAdapter<Account, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {

View File

@ -15,18 +15,17 @@
package com.keylesspalace.tusky.components.search.adapter
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import com.keylesspalace.tusky.R
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchNotestockFragment
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment
class SearchPagerAdapter(private val context: Context, manager: FragmentManager) : FragmentPagerAdapter(manager) {
override fun getItem(position: Int): Fragment {
class SearchPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> SearchStatusesFragment.newInstance()
1 -> SearchAccountsFragment.newInstance()
@ -36,15 +35,6 @@ class SearchPagerAdapter(private val context: Context, manager: FragmentManager)
}
}
override fun getPageTitle(position: Int): CharSequence? {
return when (position) {
0 -> context.getString(R.string.title_statuses)
1 -> context.getString(R.string.title_accounts)
2 -> context.getString(R.string.title_hashtags_dialog)
3 -> context.getString(R.string.title_notestock)
else -> throw IllegalArgumentException("Unknown page index: $position")
}
}
override fun getItemCount() = 4
override fun getCount(): Int = 4
}

View File

@ -28,7 +28,7 @@ import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(),
LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener {
private var isSwipeToRefreshEnabled: Boolean = true
private var snackbarErrorRetry: Snackbar? = null
@Inject
lateinit var viewModelFactory: ViewModelFactory

View File

@ -24,7 +24,6 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Environment
import android.preference.PreferenceManager
import android.util.Log
import android.view.View
import android.widget.Toast
@ -36,6 +35,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import androidx.paging.PagedListAdapter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.*
@ -305,8 +305,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
}
R.id.status_copy_link -> {
val clipboard = activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(null, statusUrl)
clipboard.primaryClip = clip
clipboard.setPrimaryClip(ClipData.newPlainText(null, statusUrl))
return@setOnMenuItemClickListener true
}
R.id.status_open_as -> {

View File

@ -19,8 +19,8 @@ package com.keylesspalace.tusky.di
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.EventHubImpl

View File

@ -62,8 +62,7 @@ data class Account(
if (other !is Account) {
return false
}
val account = other as Account?
return account?.id == this.id
return other.id == this.id
}
fun deepEquals(other: Account): Boolean {

View File

@ -81,7 +81,6 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
private var currentCall: Call<List<Status>>? = null
private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING
private var isVisibleToUser: Boolean = false
private lateinit var accountId: String
@ -216,7 +215,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
}
})
if (isVisibleToUser) doInitialLoadingIfNeeded()
doInitialLoadingIfNeeded()
}
private fun refresh() {
@ -235,14 +234,6 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
topProgressBar?.show()
}
// That's sort of an optimization to only load media once user has opened the tab
// Attention: can be called before *any* lifecycle method!
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
this.isVisibleToUser = isVisibleToUser
if (isVisibleToUser && isAdded) doInitialLoadingIfNeeded()
}
private fun doInitialLoadingIfNeeded() {
if (isAdded) {
statusView.hide()

View File

@ -20,7 +20,6 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
@ -39,6 +38,7 @@ import androidx.arch.core.util.Function;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.util.Pair;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;

View File

@ -45,6 +45,7 @@ import com.keylesspalace.tusky.BottomSheetActivity;
import com.keylesspalace.tusky.ComposeActivity;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
import com.keylesspalace.tusky.ViewMediaActivity;
import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.components.report.ReportActivity;
@ -134,7 +135,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
}
public void onViewUrl(String url, String text) {
bottomSheetActivity.viewUrl(url, text);
bottomSheetActivity.viewUrl(url, text, PostLookupFallbackBehavior.OPEN_IN_BROWSER);
}
protected void reply(Status status) {

View File

@ -22,13 +22,29 @@ import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.core.util.Pair;
import androidx.core.widget.ContentLoadingProgressBar;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.keylesspalace.tusky.AccountListActivity;
import com.keylesspalace.tusky.BaseActivity;
@ -86,22 +102,6 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.core.util.Pair;
import androidx.core.widget.ContentLoadingProgressBar;
import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;

View File

@ -18,13 +18,12 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle
import android.text.TextUtils
import android.widget.TextView
import com.keylesspalace.tusky.SharedElementTransitionListener
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.visible
abstract class ViewMediaFragment : BaseFragment(), SharedElementTransitionListener {
abstract class ViewMediaFragment : BaseFragment() {
private var toolbarVisibiltyDisposable: Function0<Boolean>? = null
abstract fun setupMediaView(url: String, previewUrl: String?)
@ -71,6 +70,8 @@ abstract class ViewMediaFragment : BaseFragment(), SharedElementTransitionListen
}
}
abstract fun onTransitionEnd()
protected fun finalizeViewSetup(url: String, previewUrl: String?, description: String?) {
val mediaActivity = activity as ViewMediaActivity
setupMediaView(url, previewUrl)

View File

@ -19,7 +19,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@ -31,6 +30,7 @@ import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.core.util.Pair;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

View File

@ -92,9 +92,9 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
publicFiltersPreference = requirePreference("publicFilters")
threadFiltersPreference = requirePreference("threadFilters")
notificationPreference.icon = IconicsDrawable(notificationPreference.context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(notificationPreference.context, R.attr.toolbar_icon_tint))
notificationPreference.icon = IconicsDrawable(notificationPreference.context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(notificationPreference.context, R.attr.preference_icon_tint))
mutedUsersPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
blockedUsersPreference.icon = IconicsDrawable(blockedUsersPreference.context, GoogleMaterial.Icon.gmd_block).sizePx(iconSize).color(ThemeUtils.getColor(blockedUsersPreference.context, R.attr.toolbar_icon_tint))
blockedUsersPreference.icon = IconicsDrawable(blockedUsersPreference.context, GoogleMaterial.Icon.gmd_block).sizePx(iconSize).color(ThemeUtils.getColor(blockedUsersPreference.context, R.attr.preference_icon_tint))
mutedDomainsPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
notificationPreference.onPreferenceClickListener = this
@ -289,7 +289,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
private fun getTintedIcon(iconId: Int): Drawable? {
val drawable = context?.getDrawable(iconId)
ThemeUtils.setDrawableTint(context, drawable, R.attr.toolbar_icon_tint)
ThemeUtils.setDrawableTint(context, drawable, R.attr.preference_icon_tint)
return drawable
}

View File

@ -39,13 +39,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
addPreferencesFromResource(R.xml.preferences)
val themePreference: Preference = requirePreference("appTheme")
themePreference.icon = IconicsDrawable(themePreference.context, GoogleMaterial.Icon.gmd_palette).sizePx(iconSize).color(ThemeUtils.getColor(themePreference.context, R.attr.toolbar_icon_tint))
themePreference.icon = IconicsDrawable(themePreference.context, GoogleMaterial.Icon.gmd_palette).sizePx(iconSize).color(ThemeUtils.getColor(themePreference.context, R.attr.preference_icon_tint))
val emojiPreference: Preference = requirePreference("emojiCompat")
emojiPreference.icon = IconicsDrawable(emojiPreference.context, GoogleMaterial.Icon.gmd_sentiment_satisfied).sizePx(iconSize).color(ThemeUtils.getColor(emojiPreference.context, R.attr.toolbar_icon_tint))
emojiPreference.icon = IconicsDrawable(emojiPreference.context, GoogleMaterial.Icon.gmd_sentiment_satisfied).sizePx(iconSize).color(ThemeUtils.getColor(emojiPreference.context, R.attr.preference_icon_tint))
val textSizePreference: Preference = requirePreference("statusTextSize")
textSizePreference.icon = IconicsDrawable(textSizePreference.context, GoogleMaterial.Icon.gmd_format_size).sizePx(iconSize).color(ThemeUtils.getColor(textSizePreference.context, R.attr.toolbar_icon_tint))
textSizePreference.icon = IconicsDrawable(textSizePreference.context, GoogleMaterial.Icon.gmd_format_size).sizePx(iconSize).color(ThemeUtils.getColor(textSizePreference.context, R.attr.preference_icon_tint))
val timelineFilterPreferences: Preference = requirePreference("timelineFilterPreferences")
timelineFilterPreferences.setOnPreferenceClickListener {
@ -68,11 +68,11 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
val languagePreference: Preference = requirePreference("language")
languagePreference.icon = IconicsDrawable(languagePreference.context, GoogleMaterial.Icon.gmd_translate).sizePx(iconSize).color(ThemeUtils.getColor(languagePreference.context, R.attr.toolbar_icon_tint))
languagePreference.icon = IconicsDrawable(languagePreference.context, GoogleMaterial.Icon.gmd_translate).sizePx(iconSize).color(ThemeUtils.getColor(languagePreference.context, R.attr.preference_icon_tint))
val botIndicatorPreference = requirePreference("showBotOverlay")
val botDrawable = botIndicatorPreference.context.getDrawable(R.drawable.ic_bot_24dp)
ThemeUtils.setDrawableTint(context, botDrawable, R.attr.toolbar_icon_tint)
ThemeUtils.setDrawableTint(context, botDrawable, R.attr.preference_icon_tint)
botIndicatorPreference.icon = botDrawable
updateStackTracePreference()

View File

@ -1,120 +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.pager;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.keylesspalace.tusky.fragment.AccountMediaFragment;
import com.keylesspalace.tusky.fragment.TimelineFragment;
import com.keylesspalace.tusky.interfaces.RefreshableFragment;
import java.util.HashSet;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
public class AccountPagerAdapter extends FragmentPagerAdapter {
private static final int TAB_COUNT = 4;
private String accountId;
private String[] pageTitles;
private SparseArray<Fragment> fragments = new SparseArray<>(TAB_COUNT);
private final Set<Integer> pagesToRefresh = new HashSet<>();
public AccountPagerAdapter(FragmentManager manager, String accountId) {
super(manager);
this.accountId = accountId;
}
public void setPageTitles(String[] titles) {
pageTitles = titles;
}
@NonNull
@Override
public Fragment getItem(int position) {
switch (position) {
case 0: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId,false);
}
case 1: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId,false);
}
case 2: {
return TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId,false);
}
case 3: {
return AccountMediaFragment.newInstance(accountId,false);
}
default: {
throw new AssertionError("Page " + position + " is out of AccountPagerAdapter bounds");
}
}
}
@Override
public int getCount() {
return TAB_COUNT;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
Object fragment = super.instantiateItem(container, position);
if (fragment instanceof Fragment)
fragments.put(position, (Fragment) fragment);
if (pagesToRefresh.contains(position)) {
if (fragment instanceof RefreshableFragment)
((RefreshableFragment) fragment).refreshContent();
pagesToRefresh.remove(position);
}
return fragment;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
super.destroyItem(container, position, object);
fragments.remove(position);
}
@Override
public CharSequence getPageTitle(int position) {
return pageTitles[position];
}
@Nullable
public Fragment getFragment(int position) {
return fragments.get(position);
}
public void refreshContent(){
for (int i=0;i<getCount();i++){
Fragment fragment = getFragment(i);
if (fragment instanceof RefreshableFragment){
((RefreshableFragment) fragment).refreshContent();
}
else{
pagesToRefresh.add(i);
}
}
}
}

View File

@ -0,0 +1,65 @@
/* Copyright 2019 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.pager
import androidx.fragment.app.*
import com.keylesspalace.tusky.fragment.AccountMediaFragment
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.RefreshableFragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import java.lang.ref.WeakReference
class AccountPagerAdapter(
activity: FragmentActivity,
private val accountId: String
) : FragmentStateAdapter(activity) {
private val fragments = MutableList<WeakReference<Fragment>?>(TAB_COUNT) { null }
override fun getItemCount() = TAB_COUNT
override fun createFragment(position: Int): Fragment {
val fragment: Fragment = when (position) {
0 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId, false)
1 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId, false)
2 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId, false)
3 -> AccountMediaFragment.newInstance(accountId, false)
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
}
fragments[position] = WeakReference(fragment)
return fragment
}
fun getFragment(position: Int): Fragment? {
return fragments[position]?.get()
}
fun refreshContent() {
for (i in 0 until TAB_COUNT) {
val fragment = getFragment(i)
if (fragment != null && fragment is RefreshableFragment) {
(fragment as RefreshableFragment).refreshContent()
}
}
}
companion object {
private const val TAB_COUNT = 4
}
}

View File

@ -1,13 +1,16 @@
package com.keylesspalace.tusky.pager
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import com.keylesspalace.tusky.SharedElementTransitionListener
import androidx.fragment.app.FragmentActivity
import com.keylesspalace.tusky.ViewMediaAdapter
import com.keylesspalace.tusky.fragment.ViewMediaFragment
class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avatarUrl: String) : FragmentPagerAdapter(fragmentManager), SharedElementTransitionListener {
override fun getItem(position: Int): Fragment {
class AvatarImagePagerAdapter(
activity: FragmentActivity,
private val avatarUrl: String
) : ViewMediaAdapter(activity) {
override fun createFragment(position: Int): Fragment {
return if (position == 0) {
ViewMediaFragment.newAvatarInstance(avatarUrl)
} else {
@ -15,8 +18,8 @@ class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avat
}
}
override fun getCount() = 1
override fun getItemCount() = 1
override fun onTransitionEnd() {
override fun onTransitionEnd(position: Int) {
}
}

View File

@ -1,53 +1,42 @@
package com.keylesspalace.tusky.pager
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import com.keylesspalace.tusky.SharedElementTransitionListener
import androidx.fragment.app.FragmentActivity
import com.keylesspalace.tusky.ViewMediaAdapter
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewMediaFragment
import java.util.*
import java.lang.ref.WeakReference
class ImagePagerAdapter(
fragmentManager: FragmentManager,
activity: FragmentActivity,
private val attachments: List<Attachment>,
private val initialPosition: Int
) : FragmentStatePagerAdapter(fragmentManager), SharedElementTransitionListener {
) : ViewMediaAdapter(activity) {
private var primaryItem: ViewMediaFragment? = null
private var didTransition = false
private val fragments = MutableList<WeakReference<ViewMediaFragment>?>(attachments.size) { null }
override fun setPrimaryItem(container: ViewGroup, position: Int, item: Any) {
super.setPrimaryItem(container, position, item)
this.primaryItem = item as ViewMediaFragment
}
override fun getItemCount() = attachments.size
override fun getItem(position: Int): Fragment {
return if (position >= 0 && position < attachments.size) {
override fun createFragment(position: Int): Fragment {
if (position >= 0 && position < attachments.size) {
// Fragment should not wait for or start transition if it already happened but we
// instantiate the same fragment again, e.g. open the first photo, scroll to the
// forth photo and then back to the first. The first fragment will trz to start the
// forth photo and then back to the first. The first fragment will try to start the
// transition and wait until it's over and it will never take place.
ViewMediaFragment.newInstance(
val fragment = ViewMediaFragment.newInstance(
attachment = attachments[position],
shouldStartPostponedTransition = !didTransition && position == initialPosition
)
fragments[position] = WeakReference(fragment)
return fragment
} else {
throw IllegalStateException()
}
}
override fun getCount(): Int {
return attachments.size
}
override fun getPageTitle(position: Int): CharSequence {
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments.size)
}
override fun onTransitionEnd() {
override fun onTransitionEnd(position: Int) {
this.didTransition = true
primaryItem?.onTransitionEnd()
fragments[position]?.get()?.onTransitionEnd()
}
}

View File

@ -15,49 +15,23 @@
package com.keylesspalace.tusky.pager
import android.util.SparseArray
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.PagerAdapter
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.keylesspalace.tusky.TabData
import java.lang.ref.WeakReference
class MainPagerAdapter(val tabs: List<TabData>, manager: FragmentManager) : FragmentPagerAdapter(manager) {
private val fragments = SparseArray<Fragment>(tabs.size)
class MainPagerAdapter(val tabs: List<TabData>, activity: FragmentActivity) : FragmentStateAdapter(activity) {
private val fragments = MutableList<WeakReference<Fragment>?>(tabs.size) { null }
override fun getItem(position: Int): Fragment {
override fun createFragment(position: Int): Fragment {
val tab = tabs[position]
return tab.fragment(tab.arguments)
}
override fun getCount(): Int {
return tabs.size
}
override fun getPageTitle(position: Int): CharSequence? {
return null
}
override fun getItemId(position: Int): Long {
return tabs[position].hashCode() + position.toLong()
}
override fun getItemPosition(item: Any): Int {
return PagerAdapter.POSITION_NONE
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position)
if (fragment is Fragment)
fragments.put(position, fragment)
val fragment = tab.fragment(tab.arguments)
fragments[position] = WeakReference(fragment)
return fragment
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
fragments.remove(position)
}
override fun getItemCount() = tabs.size
fun getFragment(position: Int): Fragment? = fragments[position]
fun getFragment(position: Int): Fragment? = fragments[position]?.get()
}

View File

@ -1,65 +0,0 @@
/* Copyright 2019 Levi Bard
*
* 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.service
import android.annotation.TargetApi
import android.content.ComponentName
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.chooser.ChooserTarget
import android.service.chooser.ChooserTargetService
import android.text.TextUtils
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NotificationHelper
@TargetApi(23)
class AccountChooserService : ChooserTargetService(), Injectable {
// cannot inject here, it crashes on APIs < 23
lateinit var accountManager: AccountManager
override fun onCreate() {
super.onCreate()
accountManager = (application as TuskyApplication).serviceLocator.get(AccountManager::class.java)
}
override fun onGetChooserTargets(targetActivityName: ComponentName?, intentFilter: IntentFilter?): MutableList<ChooserTarget> {
val targets = mutableListOf<ChooserTarget>()
for (account in accountManager.getAllAccountsOrderedByActive()) {
val icon: Icon = if (TextUtils.isEmpty(account.profilePictureUrl)) {
Icon.createWithResource(applicationContext, R.drawable.avatar_default)
} else {
val bmp = Glide.with(this)
.asBitmap()
.load(account.profilePictureUrl)
.error(R.drawable.avatar_default)
.placeholder(R.drawable.avatar_default)
.submit()
Icon.createWithBitmap(bmp.get())
}
val bundle = Bundle()
bundle.putLong(NotificationHelper.ACCOUNT_ID, account.id)
targets.add(ChooserTarget(account.displayName, icon, 1.0f, targetActivityName, bundle))
}
return targets
}
}

View File

@ -1,4 +1,4 @@
/* Copyright 2017 Andrew Dawson
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
@ -13,29 +13,28 @@
* 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.service;
package com.keylesspalace.tusky.service
import android.annotation.TargetApi;
import android.content.Intent;
import android.service.quicksettings.TileService;
import android.annotation.TargetApi
import android.content.Intent
import android.service.quicksettings.TileService
import com.keylesspalace.tusky.ComposeActivity;
import com.keylesspalace.tusky.MainActivity
/**
* Small Addition that adds in a QuickSettings tile that opens the Compose activity when clicked
* Created by ztepps on 4/3/17.
* Small Addition that adds in a QuickSettings tile
* opens the Compose activity or shows an account selector when multiple accounts are present
*/
@TargetApi(24)
public class TuskyTileService extends TileService {
public TuskyTileService() {
super();
}
class TuskyTileService : TileService() {
@Override
public void onClick() {
Intent intent = new Intent(this, ComposeActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityAndCollapse(intent);
override fun onClick() {
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_SEND
type = "text/plain"
}
startActivityAndCollapse(intent)
}
}

View File

@ -5,12 +5,12 @@ package com.keylesspalace.tusky.util
import android.widget.ImageView
import androidx.annotation.Px
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.keylesspalace.tusky.R
private val fitCenterTransformation = FitCenter()
private val centerCropTransformation = CenterCrop()
fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) {
@ -23,7 +23,7 @@ fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boo
Glide.with(imageView)
.load(url)
.transform(
fitCenterTransformation,
centerCropTransformation,
RoundedCorners(radius)
)
.placeholder(R.drawable.avatar_default)
@ -34,7 +34,7 @@ fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boo
.asBitmap()
.load(url)
.transform(
fitCenterTransformation,
centerCropTransformation,
RoundedCorners(radius)
)
.placeholder(R.drawable.avatar_default)

View File

@ -19,7 +19,6 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
@ -32,6 +31,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.preference.PreferenceManager;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Status;

View File

@ -18,9 +18,8 @@ package com.keylesspalace.tusky.util
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.preference.PreferenceManager
import java.util.Locale
import androidx.preference.PreferenceManager
import java.util.*
class LocaleManager(context: Context) {

View File

@ -69,6 +69,8 @@ import java.util.concurrent.ExecutionException;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
public class NotificationHelper {
private static int notificationId = 0;
@ -293,6 +295,7 @@ public class NotificationHelper {
.setColor(BuildConfig.DEBUG ? Color.parseColor("#19A341") : ContextCompat.getColor(context, R.color.tusky_blue))
.setGroup(account.getAccountId())
.setAutoCancel(true)
.setShortcutId(Long.toString(account.getId()))
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
setupPreferences(account, builder);
@ -627,9 +630,9 @@ public class NotificationHelper {
builder.append('\n');
Poll poll = notification.getStatus().getPoll();
for(PollOption option: poll.getOptions()) {
int percent = PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotesCount());
CharSequence optionText = HtmlUtils.fromHtml(context.getString(R.string.poll_option_format, percent, option.getTitle()));
builder.append(optionText);
builder.append(buildDescription(option.getTitle(),
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotesCount()),
context));
builder.append('\n');
}
return builder.toString();

View File

@ -18,7 +18,9 @@ package com.keylesspalace.tusky.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.keylesspalace.tusky.BuildConfig;
@ -26,7 +28,6 @@ import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;

View File

@ -0,0 +1,102 @@
/* Copyright 2019 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>. */
@file:JvmName("ShareShortcutHelper")
package com.keylesspalace.tusky.util
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.text.TextUtils
import androidx.core.app.Person
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountEntity
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
fun updateShortcut(context: Context, account: AccountEntity) {
Single.fromCallable {
val innerSize = context.resources.getDimensionPixelSize(R.dimen.adaptive_bitmap_inner_size)
val outerSize = context.resources.getDimensionPixelSize(R.dimen.adaptive_bitmap_outer_size)
val bmp = if (TextUtils.isEmpty(account.profilePictureUrl)) {
Glide.with(context)
.asBitmap()
.load(R.drawable.avatar_default)
.submit(innerSize, innerSize)
.get()
} else {
Glide.with(context)
.asBitmap()
.load(account.profilePictureUrl)
.error(R.drawable.avatar_default)
.submit(innerSize, innerSize)
.get()
}
// inset the loaded bitmap inside a 108dp transparent canvas so it looks good as adaptive icon
val outBmp = Bitmap.createBitmap(outerSize, outerSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(outBmp)
canvas.drawBitmap(bmp, (outerSize - innerSize).toFloat() / 2f, (outerSize - innerSize).toFloat() / 2f, null)
val icon = IconCompat.createWithAdaptiveBitmap(outBmp)
val person = Person.Builder()
.setIcon(icon)
.setName(account.displayName)
.setKey(account.identifier)
.build()
// This intent will be sent when the user clicks on one of the launcher shortcuts. Intent from share sheet will be different
val intent = Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(NotificationHelper.ACCOUNT_ID, account.id)
}
val shortcutInfo = ShortcutInfoCompat.Builder(context, account.id.toString())
.setIntent(intent)
.setCategories(setOf("com.keylesspalace.tusky.Share"))
.setShortLabel(account.displayName)
.setPerson(person)
.setLongLived(true)
.setIcon(icon)
.build()
ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))
}
.subscribeOn(Schedulers.io())
.onErrorReturnItem(false)
.subscribe()
}
fun removeShortcut(context: Context, account: AccountEntity) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(account.id.toString()))
}

View File

@ -30,6 +30,7 @@ import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.view.MediaPreviewImageView
import com.keylesspalace.tusky.viewdata.PollViewData
import com.keylesspalace.tusky.viewdata.buildDescription
import com.keylesspalace.tusky.viewdata.calculatePercent
import java.text.NumberFormat
import java.text.SimpleDateFormat
@ -283,8 +284,8 @@ class StatusViewHelper(private val itemView: View) {
if (i < options.size) {
val percent = calculatePercent(options[i].votesCount, poll.votesCount)
val pollOptionText = pollResults[i].context.getString(R.string.poll_option_format, percent, options[i].title)
pollResults[i].text = CustomEmojiHelper.emojifyText(HtmlUtils.fromHtml(pollOptionText), emojis, pollResults[i])
val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context)
pollResults[i].text = CustomEmojiHelper.emojifyText(pollOptionText, emojis, pollResults[i])
pollResults[i].visibility = View.VISIBLE
val level = percent * 100

View File

@ -29,27 +29,21 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import javax.inject.Inject;
import javax.inject.Singleton;
import android.util.TypedValue;
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
@Singleton
public class ThemeUtils {
@Inject
public ThemeUtils(){}
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
private static final String THEME_NIGHT = "night";
public static final String THEME_DAY = "day";
private static final String THEME_BLACK = "black";
private static final String THEME_AUTO = "auto";
public static final String THEME_SYSTEM = "auto_system";
private static final String THEME_SYSTEM = "auto_system";
public static Drawable getDrawable(@NonNull Context context, @AttrRes int attribute,
@DrawableRes int fallbackDrawable) {
@ -63,7 +57,8 @@ public class ThemeUtils {
return context.getDrawable(resourceId);
}
public static @DrawableRes int getDrawableId(@NonNull Context context, @AttrRes int attribute,
@DrawableRes
public static int getDrawableId(@NonNull Context context, @AttrRes int attribute,
@DrawableRes int fallbackDrawableId) {
TypedValue value = new TypedValue();
if (context.getTheme().resolveAttribute(attribute, value, true)) {
@ -73,7 +68,8 @@ public class ThemeUtils {
}
}
public static @ColorInt int getColor(@NonNull Context context, @AttrRes int attribute) {
@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;
@ -82,14 +78,16 @@ public class ThemeUtils {
}
}
public static @ColorRes int getColorId(@NonNull Context context, @AttrRes int attribute) {
@ColorRes
public static int getColorId(@NonNull Context context, @AttrRes int attribute) {
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(attribute, value, true);
return value.resourceId;
}
/** this can be replaced with drawableTint in xml once minSdkVersion >= 23 */
public static @Nullable Drawable getTintedDrawable(@NonNull Context context, @DrawableRes int drawableId, @AttrRes int colorAttr) {
@Nullable
public static Drawable getTintedDrawable(@NonNull Context context, @DrawableRes int drawableId, @AttrRes int colorAttr) {
Drawable drawable = context.getDrawable(drawableId);
if(drawable == null) {
return null;
@ -102,30 +100,21 @@ public class ThemeUtils {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
}
public void setAppNightMode(String flavor, Context context) {
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_BLACK:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case THEME_AUTO:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_TIME);
break;
case THEME_SYSTEM:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
//stupid workaround to make MODE_NIGHT_FOLLOW_SYSTEM work :(
if((Settings.System.getInt(context.getContentResolver(), "display_night_theme", 0) == 1)) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else if ((Settings.System.getInt(context.getContentResolver(), "display_night_theme", 0) == 0)) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
break;
}
}

View File

@ -120,7 +120,8 @@ public class ComposeScheduleView extends ConstraintLayout {
private void openPickDateDialog() {
long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000;
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder()
.setValidator(new DateValidatorPointForward(yesterday))
.setValidator(
DateValidatorPointForward.from(yesterday))
.build();
if (scheduleDateTime == null) {
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());

View File

@ -1,45 +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.view;
import android.content.Context;
import androidx.viewpager.widget.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* This class is entirely to address a known issue with com.github.chrisbanes.photoview.PhotoView.
* ViewPager will throw exceptions when a PhotoView is placed within it, so this subclass eats those
* exceptions.
*/
public class ImageViewPager extends ViewPager {
public ImageViewPager(Context context) {
super(context);
}
public ImageViewPager(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
return false;
}
}
}

View File

@ -15,8 +15,13 @@
package com.keylesspalace.tusky.viewdata
import android.content.Context
import android.text.SpannableStringBuilder
import android.text.Spanned
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.PollOption
import com.keylesspalace.tusky.util.HtmlUtils
import java.util.*
import kotlin.math.roundToInt
@ -44,6 +49,12 @@ fun calculatePercent(fraction: Int, total: Int): Int {
}
}
fun buildDescription(title: String, percent: Int, context: Context): Spanned {
return SpannableStringBuilder(HtmlUtils.fromHtml(context.getString(R.string.poll_percent_format, percent)))
.append(" ")
.append(title)
}
fun Poll?.toViewData(): PollViewData? {
if (this == null) return null
return PollViewData(

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="false" android:color="?attr/toolbar_icon_tint"/>
<item android:state_selected="true" android:color="?attr/tab_icon_selected_tint"/>
</selector>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_primary_dark_dark" />
<corners android:radius="14dp"/>
<size android:height="100dp" android:width="100dp"/>
</shape>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_background_light" />
<corners android:radius="14dp"/>
<size android:height="100dp" android:width="100dp"/>
</shape>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/tusky_blue"
android:pathData="M17,2L17,5L5,5L5,11L7,11L7,7L17,7L17,10L21,6L17,2zM9.75,9.75L9.75,14.25L14.25,14.25L14.25,9.75L9.75,9.75zM17,13L17,17L7,17L7,14L3,18L7,22L7,19L19,19L19,13L17,13z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/tusky_blue"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/tab_page_margin_black" />
</shape>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/tab_page_margin_dark" />
</shape>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/tab_page_margin_light" />
</shape>

View File

@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/tab_page_margin_drawable">
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="640dp"
@ -45,16 +44,17 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/elephant_error"
tools:visibility="visible" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
app:layout_constraintTop_toTopOf="parent"
android:indeterminate="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="parent"
android:visibility="gone"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/tab_page_margin_drawable">
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="640dp"
@ -80,9 +79,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@android:color/transparent"
android:visibility="gone"
tools:src="@drawable/elephant_error"
tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</FrameLayout>

View File

@ -1,20 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/tab_page_margin_drawable">
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:background="?attr/window_background">
android:layout_gravity="center_horizontal">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/window_background"
android:scrollbars="vertical" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -16,8 +16,8 @@
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textDirection="anyRtl"
android:layout_gravity="center">
android:layout_gravity="center"
android:textDirection="anyRtl">
<LinearLayout
android:layout_width="match_parent"
@ -46,11 +46,24 @@
android:layout_width="wrap_content"
android:layout_height="60dp" />
<TextView
android:id="@+id/aboutPoweredByTusky"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:drawablePadding="16dp"
android:gravity="center_vertical"
android:textSize="18sp"
android:text="@string/about_powered_by_tusky"
android:textStyle="bold" />
<TextView
android:id="@+id/aboutLicenseInfoTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.2"
android:paddingStart="@dimen/text_content_margin"
android:paddingEnd="@dimen/text_content_margin"

View File

@ -17,16 +17,15 @@
android:id="@+id/accountAppBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:elevation="@dimen/actionbar_elevation">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentScrim="?attr/toolbar_background_color"
app:contentScrim="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="?android:attr/colorBackground"
app:statusBarScrim="?attr/colorSurface"
app:titleEnabled="false">
<ImageView
@ -64,7 +63,6 @@
android:layout_marginTop="6dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
@ -174,7 +172,7 @@
android:id="@+id/accountNoteTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:paddingTop="10dp"
android:textColor="?android:textColorTertiary"
@ -196,6 +194,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:text="@string/label_remote_account"
android:visibility="gone"
@ -307,7 +306,6 @@
android:textSize="?attr/status_text_medium" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- top margin equal to statusbar size will be set programmatically -->
@ -317,8 +315,10 @@
android:layout_height="?attr/actionBarSize"
android:layout_gravity="top"
android:background="@android:color/transparent"
app:contentInsetStartWithNavigation="0dp"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<com.google.android.material.tabs.TabLayout
@ -333,10 +333,11 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/accountFragmentViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/tab_page_margin_color"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
@ -355,11 +356,12 @@
android:layout_width="@dimen/account_activity_avatar_size"
android:layout_height="@dimen/account_activity_avatar_size"
android:layout_marginStart="16dp"
android:background="@drawable/avatar_background"
android:padding="3dp"
app:layout_anchor="@+id/accountHeaderInfoContainer"
app:layout_anchorGravity="top"
app:layout_scrollFlags="scroll"
app:srcCompat="@drawable/avatar_default" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -27,6 +27,7 @@
android:layout_height="178dp"
android:layout_marginBottom="50dp"
android:contentDescription="@null"
android:id="@+id/loginLogo"
app:srcCompat="@drawable/elephant_friend" />
<LinearLayout

View File

@ -13,51 +13,41 @@
android:layout_height="match_parent"
android:layout_weight="1">
<RelativeLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_appbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation">
<ImageButton
android:id="@+id/drawer_toggle"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:background="?android:colorBackground"
android:contentDescription="@string/action_open_drawer"
android:elevation="@dimen/actionbar_elevation"
app:srcCompat="@drawable/ic_menu_24dp" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
style="@style/TuskyTabAppearance"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_toEndOf="@id/drawer_toggle"
android:background="?android:colorBackground"
android:focusable="true"
android:focusableInTouchMode="true"
android:elevation="@dimen/actionbar_elevation"
app:tabGravity="fill"
app:tabIconTint="@color/tab_icon_color"
app:tabMaxWidth="0dp"
app:tabMode="fixed"
app:tabPaddingEnd="1dp"
app:tabPaddingStart="1dp"
app:tabPaddingTop="4dp"
app:tabUnboundedRipple="false" />
<androidx.viewpager.widget.ViewPager
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_below="@id/tab_layout"
android:layout_alignParentBottom="true" />
</RelativeLayout>
android:background="?attr/tab_page_margin_color"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floating_btn"
@ -73,8 +63,9 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/view_quick_toot"
<include
android:id="@+id/quick_toot_container"
layout="@layout/view_quick_toot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0" />

View File

@ -10,29 +10,29 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation"
android:stateListAnimator="@null"
app:layout_collapseMode="pin">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/toolbar_background_color"
app:contentInsetStartWithNavigation="0dp"
android:elevation="@dimen/actionbar_elevation"
app:layout_scrollFlags="scroll|snap|enterAlways"
app:navigationIcon="?attr/homeAsUpIndicator" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
style="@style/TuskyTabAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/toolbar_background_color"
app:tabGravity="fill"
app:tabMode="fixed"
app:tabTextAppearance="@style/TuskyTabAppearance"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pages"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -6,7 +6,7 @@
android:background="@android:color/black"
android:orientation="vertical">
<com.keylesspalace.tusky.view.ImageViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -4,7 +4,7 @@
android:id="@+id/layoutRoot"
android:layout_width="@dimen/timeline_width"
android:layout_height="match_parent"
android:background="?attr/tab_page_margin_drawable">
android:background="?attr/tab_page_margin_color">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"

View File

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="?attr/window_background">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
@ -29,23 +31,22 @@
android:id="@+id/statusView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:color/transparent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/elephant_error"
tools:visibility="visible" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
app:layout_constraintTop_toTopOf="parent"
android:indeterminate="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="parent"
android:visibility="gone"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -58,7 +58,8 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:background="?attr/window_background" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
@ -17,25 +17,26 @@
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center" />
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/mediaDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:background="#60000000"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:padding="8dp"
android:textAlignment="center"
android:textColor="#eee"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="Some media description" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,5 +9,7 @@
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/window_background"
android:scrollbars="vertical" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -2,9 +2,9 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/videoContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/videoContainer"
android:clickable="true"
android:focusable="true">
@ -14,6 +14,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:background="#60000000"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:padding="8dp"
android:textAlignment="center"

View File

@ -36,8 +36,9 @@
android:contentDescription="@string/action_view_profile"
android:scaleType="centerCrop"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/notification_username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notification_text"
app:layout_constraintTop_toTopOf="@id/notification_display_name"
tools:src="@drawable/avatar_default" />
<androidx.emoji.widget.EmojiTextView
@ -52,7 +53,8 @@
android:textStyle="normal|bold"
app:layout_constraintBottom_toTopOf="@id/notification_username"
app:layout_constraintStart_toEndOf="@id/notification_avatar"
app:layout_constraintTop_toTopOf="@id/notification_avatar"
app:layout_constraintTop_toBottomOf="@id/notification_text"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Test User" />
<TextView
@ -64,7 +66,7 @@
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/notification_avatar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/notification_avatar"
app:layout_constraintTop_toBottomOf="@id/notification_display_name"
tools:text="\@testuser" />

View File

@ -106,6 +106,7 @@
android:id="@+id/status_content_warning_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hyphenationFrequency="full"
android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorPrimary"
@ -146,6 +147,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:focusable="true"
android:hyphenationFrequency="full"
android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorPrimary"
@ -448,7 +450,7 @@
app:layout_constraintEnd_toStartOf="@id/status_favourite"
app:layout_constraintStart_toEndOf="@id/status_reply"
app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="@drawable/reblog_active"
sparkbutton:activeImage="@drawable/ic_reblog_active_24dp"
sparkbutton:iconSize="28dp"
sparkbutton:inactiveImage="?attr/status_reblog_inactive_drawable"
sparkbutton:primaryColor="@color/tusky_blue"

View File

@ -84,6 +84,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hyphenationFrequency="full"
android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorPrimary"
@ -120,6 +121,7 @@
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:focusable="true"
android:hyphenationFrequency="full"
android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorPrimary"
@ -536,7 +538,7 @@
app:layout_constraintEnd_toStartOf="@id/status_favourite"
app:layout_constraintStart_toEndOf="@id/status_reply"
app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="@drawable/reblog_active"
sparkbutton:activeImage="@drawable/ic_reblog_active_24dp"
sparkbutton:iconSize="28dp"
sparkbutton:inactiveImage="?attr/status_reblog_inactive_drawable"
sparkbutton:primaryColor="@color/tusky_blue"

View File

@ -13,8 +13,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginBottom="6dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="6dp"
android:drawablePadding="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
@ -39,8 +39,8 @@
android:layout_alignParentStart="true"
android:ellipsize="end"
android:maxLines="1"
android:paddingEnd="@dimen/status_display_name_padding_end"
android:paddingStart="0dp"
android:paddingEnd="@dimen/status_display_name_padding_end"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
@ -51,8 +51,8 @@
android:id="@+id/status_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/status_display_name"
android:layout_toStartOf="@+id/status_timestamp_info"
android:layout_toEndOf="@id/status_display_name"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorTertiary"
@ -77,27 +77,27 @@
android:layout_height="wrap_content"
android:layout_below="@id/status_name_bar"
android:layout_toEndOf="@id/notification_status_avatar"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
tools:text="Example CW text" />
<ToggleButton
android:id="@+id/notification_content_warning_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/notification_content_warning_description"
android:layout_toEndOf="@id/notification_status_avatar"
android:background="?attr/content_warning_button"
android:minHeight="0dp"
android:minWidth="150dp"
android:paddingBottom="4dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_toEndOf="@id/notification_status_avatar"
android:background="?attr/content_warning_button"
android:minWidth="150dp"
android:minHeight="0dp"
android:paddingLeft="16dp"
android:paddingTop="4dp"
android:paddingRight="16dp"
android:paddingBottom="4dp"
android:textAllCaps="true"
android:textOff="@string/status_content_warning_show_more"
android:textOn="@string/status_content_warning_show_less"
@ -109,6 +109,7 @@
android:layout_height="wrap_content"
android:layout_below="@id/notification_content_warning_button"
android:layout_toEndOf="@+id/notification_status_avatar"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:paddingBottom="10dp"
android:textColor="?android:textColorTertiary"
@ -120,20 +121,20 @@
android:id="@+id/button_toggle_notification_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/notification_status_avatar"
android:layout_below="@id/notification_content"
android:textOff="@string/status_content_show_less"
android:textOn="@string/status_content_show_more"
android:background="?attr/content_warning_button"
android:minHeight="0dp"
android:minWidth="150dp"
android:paddingBottom="4dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_toEndOf="@id/notification_status_avatar"
android:background="?attr/content_warning_button"
android:minWidth="150dp"
android:minHeight="0dp"
android:paddingLeft="16dp"
android:paddingTop="4dp"
android:paddingRight="16dp"
android:paddingBottom="4dp"
android:textAllCaps="true"
android:textOff="@string/status_content_show_less"
android:textOn="@string/status_content_show_more"
android:textSize="?attr/status_text_medium"
android:visibility="gone" />
@ -151,13 +152,13 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_below="@id/notification_top_text"
android:layout_marginBottom="14dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="14dp"
android:layout_marginRight="14dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="14dp"
android:contentDescription="@string/action_view_profile"
android:paddingBottom="12dp"
android:paddingRight="12dp"
android:paddingBottom="12dp"
android:scaleType="centerCrop"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:src="@drawable/avatar_default" />
@ -166,7 +167,7 @@
android:id="@+id/notification_notification_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignBottom="@+id/notification_status_avatar"
android:layout_alignEnd="@id/notification_status_avatar" />
android:layout_alignEnd="@id/notification_status_avatar"
android:layout_alignBottom="@+id/notification_status_avatar" />
</RelativeLayout>

View File

@ -12,8 +12,7 @@
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/toolbar_background_color" />
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>

View File

@ -26,7 +26,6 @@
<string name="title_direct_messages">الرسائل المباشرة</string>
<string name="title_tab_preferences">الألسنة</string>
<string name="title_view_thread">تبويق</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">المشاركات</string>
<string name="title_statuses_with_replies">يحتوي على ردود</string>
<string name="title_statuses_pinned">مدبّس</string>
@ -483,4 +482,12 @@
<string name="filter_dialog_whole_word_description">عندما تكون الكلمة أو العبارة أبجدية رقمية فقط ، فلن يتم تطبيقها إلا إذا كانت مطابقة للكلمة بأكملها</string>
<string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string>
<string name="title_scheduled_toot">التبويقات المبَرمَجة</string>
<string name="action_edit">تعديل</string>
<string name="action_access_scheduled_toot">التبويقات المبَرمَجة</string>
<string name="action_schedule_toot">برمجة تبويق</string>
<string name="action_reset_schedule">صفّر</string>
<string name="hint_configure_scheduled_toot">اضغط هنا لضبط برمجة التبويق.</string>
<string name="post_lookup_error_format">خطأ أثناء البحث عن منشور %s</string>
</resources>

View File

@ -28,7 +28,6 @@
<string name="title_direct_messages">সরাসরি বার্তা</string>
<string name="title_tab_preferences">ট্যাবগুলি</string>
<string name="title_view_thread">টুট</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">পোস্টগুলি</string>
<string name="title_statuses_with_replies">উত্তরের সাথে</string>
<string name="title_statuses_pinned">পিন করা</string>
@ -440,7 +439,6 @@
<string name="poll_info_time_relative">%s বাকি</string>
<string name="poll_info_time_absolute">%s এ শেষ হবে</string>
<string name="poll_info_closed">বন্ধ</string>
<string name="poll_option_format"> <!-- ১৫% এতে ভোট দিয়েছে! --> &lt;b&gt;%1$d%%&lt;/b&gt; %2$s</string>
<string name="poll_vote">ভোট</string>
@ -505,4 +503,13 @@
<string name="poll_new_choice_hint">পছন্দ %d</string>
<string name="edit_poll">সম্পাদন</string>
<string name="title_scheduled_toot">নির্ধারিত টুটগুলি</string>
<string name="action_edit">সম্পাদন</string>
<string name="action_access_scheduled_toot">নির্ধারিত টুটগুলি</string>
<string name="action_schedule_toot">নির্ধারিত টুট</string>
<string name="action_reset_schedule">রিসেট</string>
<string name="hint_configure_scheduled_toot">নির্ধারিত টুট কনফিগার করতে এখানে আলতো চাপুন।</string>
<string name="about_powered_by_tusky">টাস্কি দ্বারা চালিত</string>
<string name="post_lookup_error_format">%s পোস্ট অনুসন্ধানে ত্রুটি</string>
</resources>

View File

@ -23,7 +23,6 @@
<string name="title_public_local">Local</string>
<string name="title_public_federated">Federació</string>
<string name="title_view_thread">Toot</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Posts</string>
<string name="title_follows">Seguits</string>
<string name="title_followers">Seguidors</string>
@ -465,7 +464,6 @@
</plurals>
<string name="description_status_cw">Advertència: %s</string>
<string name="poll_option_format"> <!-- 15% vota aquesta acció! --> &lt;b&gt;%1$d%%&lt;/b&gt; %2$s</string>
<string name="title_statuses_pinned">Toot fixat</string>
<string name="unpin_action">Toot no fixat</string>

View File

@ -26,7 +26,6 @@
<string name="title_direct_messages">Přímé zprávy</string>
<string name="title_tab_preferences">Panely</string>
<string name="title_view_thread">Toot</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Tooty</string>
<string name="title_statuses_with_replies">S odpověďmi</string>
<string name="title_statuses_pinned">Připnuté</string>
@ -392,7 +391,6 @@
<string name="poll_info_time_relative">zbývá %s</string>
<string name="poll_info_time_absolute">končí v %s</string>
<string name="poll_info_closed">uzavřena</string>
<string name="poll_option_format"> <!-- 15% vote for this! --> &lt;b&gt;%1$d%%&lt;/b&gt; %2$s</string>
<string name="poll_vote">Hlasovat</string>
@ -466,4 +464,19 @@
<string name="poll_new_choice_hint">Možnost %d</string>
<string name="edit_poll">Upravit</string>
<string name="title_scheduled_toot">Plánované tooty</string>
<string name="action_edit">Upravit</string>
<string name="action_add_poll">Přidat anketu</string>
<string name="action_access_scheduled_toot">Plánované tooty</string>
<string name="action_schedule_toot">Naplánovat toot</string>
<string name="action_reset_schedule">Obnovit</string>
<string name="hint_configure_scheduled_toot">Klepnutím sem nastavíte plánovaný toot.</string>
<string name="pref_title_alway_open_spoiler">Vždy rozbalovat tooty označené varováními o obsahu</string>
<string name="filter_dialog_whole_word">Celé slovo</string>
<string name="filter_dialog_whole_word_description">Je-li klíčové slovo nebo fráze pouze alfanumerická, bude použita pouze, pokud odpovídá celému slovu</string>
<string name="title_accounts">Účty</string>
<string name="failed_search">Hledání selhalo</string>
<string name="post_lookup_error_format">Chyba při hledání příspěvku %s</string>
</resources>

View File

@ -278,7 +278,6 @@
<string name="error_network">Digwyddodd gwall rhwydwaith! Gwiriwch eich cysylltiad a cheisiwch eto!</string>
<string name="title_direct_messages">Negeseuon Uniongyrchol</string>
<string name="title_tab_preferences">Tabiau</string>
<string name="title_tag">#%s</string>
<string name="title_statuses_pinned">Wedi\'i binio</string>
<string name="title_domain_mutes">Parthau cudd</string>
<string name="message_empty">Dim byd yma.</string>

View File

@ -26,7 +26,6 @@
<string name="title_direct_messages">Direktnachrichten</string>
<string name="title_tab_preferences">Tabs</string>
<string name="title_view_thread">Unterhaltung</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Beiträge</string>
<string name="title_statuses_with_replies">mit Antworten</string>
<string name="title_statuses_pinned">Angeheftet</string>
@ -376,7 +375,6 @@
<string name="poll_info_time_relative">%s verbleibend</string>
<string name="poll_info_time_absolute">endet um %s</string>
<string name="poll_info_closed">Geschlossen</string>
<string name="poll_option_format"> &lt;b&gt;%1$d%%&lt;/b&gt; %2$s</string>
<string name="poll_vote">Abstimmen</string>
@ -448,4 +446,11 @@
<string name="add_poll_choice">Auswahlmöglichkeit hinzufügen</string>
<string name="poll_allow_multiple_choices">Mehrere Möglichkeiten</string>
<string name="poll_new_choice_hint">Möglichkeit %d</string>
<string name="title_scheduled_toot">Geplante Beiträge</string>
<string name="action_edit">Editieren</string>
<string name="action_access_scheduled_toot">Geplante Beiträge</string>
<string name="action_schedule_toot">Plane Beitrag</string>
<string name="action_reset_schedule">Zurücksetzen</string>
<string name="hint_configure_scheduled_toot">Drücke hier, um den geplanten Beitrag zu konfigurieren.</string>
<string name="abbreviated_in_years">Dies sind Zeitstempel für Status. Beispiele: \"16s\" oder \"2t\".</string>
</resources>

View File

@ -26,7 +26,6 @@
<string name="title_direct_messages">Rektaj mesaĝoj</string>
<string name="title_tab_preferences">Langetoj</string>
<string name="title_view_thread">Mesaĝo</string>
<string name="title_tag">#%s</string>
<string name="title_statuses">Mesaĝoj</string>
<string name="title_statuses_with_replies">Kun respondoj</string>
<string name="title_statuses_pinned">Alpinglitaj</string>
@ -391,7 +390,6 @@
<string name="poll_info_time_relative">%s restas</string>
<string name="poll_info_time_absolute">finiĝos je %s</string>
<string name="poll_info_closed">finiĝita</string>
<string name="poll_option_format"> <!-- 15% vote for this! --> &lt;b&gt;%1$d%%&lt;/b&gt; %2$s</string>
<string name="poll_vote">Voĉdoni</string>

View File

@ -18,15 +18,14 @@
<string name="error_media_download_permission">Se requiere permiso para descargar al almacenamiento.</string>
<string name="error_media_upload_image_or_video">No se pueden adjuntar imágenes y vídeos en el mismo estado.</string>
<string name="error_media_upload_sending">La subida falló.</string>
<string name="error_sender_account_gone">Error al publicar.</string>
<string name="error_sender_account_gone">Error al enviar estado.</string>
<string name="title_home">Inicio</string>
<string name="title_notifications">Notificaciones</string>
<string name="title_public_local">Local</string>
<string name="title_public_federated">Federada</string>
<string name="title_direct_messages">Mensajes Directos</string>
<string name="title_tab_preferences">Pestañas</string>
<string name="title_view_thread">Publicación</string>
<string name="title_tag">#%s</string>
<string name="title_view_thread">Estado</string>
<string name="title_statuses">Estados</string>
<string name="title_statuses_with_replies">Con respuestas</string>
<string name="title_statuses_pinned">Fijado</string>
@ -50,8 +49,8 @@
<string name="status_content_show_less">Ocultar</string>
<string name="message_empty">Nada aquí.</string>
<string name="footer_empty">Nada por aquí. ¡Arrastra hacia abajo para recargar!</string>
<string name="notification_reblog_format">%s impulsó tu toot</string>
<string name="notification_favourite_format">%s marcó como favorito tu post</string>
<string name="notification_reblog_format">%s impulsó tu estado</string>
<string name="notification_favourite_format">%s marcó como favorito tu estado</string>
<string name="notification_follow_format">%s te siguió</string>
<string name="report_username_format">Reportar @%s</string>
<string name="report_comment_hint">¿Información adicional?</string>
@ -61,7 +60,7 @@
<string name="action_favourite">Favorito</string>
<string name="action_more">Más</string>
<string name="action_compose">Redactar</string>
<string name="action_login">Iniciar sesión</string>
<string name="action_login">Iniciar sesión con Mastodon</string>
<string name="action_logout">Cerrar sesión</string>
<string name="action_logout_confirm">¿Seguro que quiere cerrar la sesión de %1$s?</string>
<string name="action_follow">Seguir</string>
@ -110,7 +109,7 @@
<string name="action_open_as">Abrir como %s</string>
<string name="action_share_as">Compartir como…</string>
<string name="send_status_link_to">Compartir URL…</string>
<string name="send_status_content_to">Compartir contenido…</string>
<string name="send_status_content_to">Compartir estado…</string>
<string name="send_media_to">Compartir medios a…</string>
<string name="confirmation_reported">¡Enviado!</string>
<string name="confirmation_unblocked">El usuario ya no está bloqueado</string>
@ -143,7 +142,7 @@
<string name="dialog_download_image">Descargar</string>
<string name="dialog_message_cancel_follow_request">¿Cancelar petición de amistad?</string>
<string name="dialog_unfollow_warning">¿Dejar de seguir esta cuenta?</string>
<string name="dialog_delete_toot_warning">¿Eliminar este toot?</string>
<string name="dialog_delete_toot_warning">¿Eliminar este estado\?</string>
<string name="visibility_public">Público: Mostrar en historias públicas</string>
<string name="visibility_unlisted">Oculto: No mostrar en historias públicas</string>
<string name="visibility_private">Privado: Sólo visible para seguidores</string>
@ -260,8 +259,8 @@
<string name="lock_account_label_description">Tendrá que admitir los seguidores manualmente</string>
<string name="compose_save_draft">¿Guardar borrador?</string>
<string name="send_toot_notification_title">Enviando estado…</string>
<string name="send_toot_notification_error_title">Error enviando estado</string>
<string name="send_toot_notification_channel_name">Enviando estado</string>
<string name="send_toot_notification_error_title">Error al enviar el estado</string>
<string name="send_toot_notification_channel_name">Enviando estados</string>
<string name="send_toot_notification_cancel_title">Envío cancelado</string>
<string name="send_toot_notification_saved_content">Una copia del estado se ha guardado en borradores</string>
<string name="action_compose_shortcut">Redactar</string>
@ -370,7 +369,7 @@
<string name="downloading_media">Descargando contenido</string>
<string name="dialog_redraft_toot_warning">¿Borrar y devolver a borradores este toot\?</string>
<string name="dialog_redraft_toot_warning">¿Borrar y devolver a borradores este estado\?</string>
<string name="pref_title_notification_filter_poll">encuestas que han terminado</string>
<string name="notification_poll_description">Notificaciones sobre encuestas que han terminado</string>
@ -416,8 +415,6 @@
<string name="action_open_reblogger">Abrir autor del impulso</string>
<string name="action_open_reblogged_by">Mostrar impulsos</string>
<string name="poll_option_format"> <!-- 15% vote for this! --> &lt;b&gt;%1$d%%&lt;/b&gt; %2$s</string>
<string name="title_domain_mutes">Dominios ocultos</string>
<string name="action_view_domain_mutes">Dominios ocultos</string>
<string name="action_mute_domain">Silenciar %s</string>
@ -445,7 +442,7 @@
<string name="report_description_remote_instance">La cuenta es de otro servidor. ¿Enviar una copia anónima del reporte\?</string>
<string name="pref_title_show_notifications_filter">Mostrar filtro de notificaciones</string>
<string name="pref_title_alway_open_spoiler">Mostrar siempre toots marcados con avisos de contenido</string>
<string name="pref_title_alway_open_spoiler">Mostrar siempre estados marcados con avisos de contenido</string>
<string name="title_accounts">Cuentas</string>
<string name="failed_search">Error al buscar</string>
@ -463,4 +460,13 @@
<string name="poll_new_choice_hint">Opción %d</string>
<string name="edit_poll">Editar</string>
<string name="title_scheduled_toot">Estados programados</string>
<string name="action_edit">Editar</string>
<string name="action_access_scheduled_toot">Estados programados</string>
<string name="action_schedule_toot">Programar estado</string>
<string name="action_reset_schedule">Reiniciar</string>
<string name="hint_configure_scheduled_toot">Pulsa aquí para configurar un estado programado.</string>
<string name="post_lookup_error_format">Error al buscar el post %s</string>
<string name="about_powered_by_tusky">Potenciado por Tusky</string>
</resources>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="error_generic">Errorea gertatu da.</string>
<string name="error_empty">Eremu hau ezin da hutsik egon.</string>
<string name="error_invalid_domain">Domeinu izen okerra.</string>
<string name="error_invalid_domain">Domeinu baliogabea sartu da</string>
<string name="error_failed_app_registration">Akatsa saioa hasterakoan.</string>
<string name="error_no_web_browser_found">Ez da web nabigatzailerik aurkitu.</string>
<string name="error_authorization_unknown">Identifikatu gabeko baimentza akatsa gertatu da.</string>
@ -27,7 +27,7 @@
<string name="title_statuses_with_replies">Erantzunekin</string>
<string name="title_follows">Jarraitzen</string>
<string name="title_followers">Jarraitzaileak</string>
<string name="title_favourites">Gogokoak</string>
<string name="title_favourites">Gogokoenak</string>
<string name="title_mutes">Isilduak</string>
<string name="title_blocks">Blokeatuak</string>
<string name="title_follow_requests">Eskakizunak</string>
@ -99,22 +99,22 @@
<string name="action_emoji_keyboard">Emoji teklatua</string>
<string name="download_image">%1$s jaisten</string>
<string name="action_copy_link">Lotura kopiatu</string>
<string name="send_status_link_to">Tutaren URL-a partekatu…</string>
<string name="send_status_link_to">Tutaren URLa partekatu…</string>
<string name="send_status_content_to">Tuta partekatu…</string>
<string name="send_media_to">Partekatu multimedia hona…</string>
<string name="send_media_to">Partekatu media hona…</string>
<string name="confirmation_reported">Bidalia!</string>
<string name="confirmation_unblocked">Erabiltzailea ez dago blokeatuta iada.</string>
<string name="confirmation_unmuted">Iada erabiltzailea ez dago mutututa.</string>
<string name="confirmation_unblocked">Erabiltzailea desblokeatuta</string>
<string name="confirmation_unmuted">Erabiltzailea isil gabetua</string>
<string name="status_sent">Bidalia!</string>
<string name="status_sent_long">Erantzuna ongi bidali da.</string>
<string name="hint_domain">Instantzia hautatu</string>
<string name="hint_domain">Zein instantzia\?</string>
<string name="hint_compose">Zer duzu buruan?</string>
<string name="hint_content_warning">Eduki abisua</string>
<string name="hint_display_name">Izena</string>
<string name="hint_content_warning">Eduki-abisua</string>
<string name="hint_display_name">Agertuko den izena</string>
<string name="hint_note">Biografia</string>
<string name="hint_search">Bilatu…</string>
<string name="search_no_results">Emaitzarik ez</string>
<string name="label_quick_reply">Erantzuna</string>
<string name="label_quick_reply">Erantzun…</string>
<string name="label_avatar">Irudia</string>
<string name="label_header">Goiburua</string>
<string name="link_whats_an_instance">Zer da instantzia?</string>
@ -124,12 +124,12 @@
\n\nInstantzia zure kontua dagoen gunea da, baino beste instantzietako erabiltzaileak zurean egongo balira bezala jarraitu ditzakezu.
\n\nInformazio gehiago <a href="https://joinmastodon.org">joinmastodon.org</a> helbidean topatuko duzu.
</string>
<string name="dialog_title_finishing_media_upload">Multimedia igoera bukatzen</string>
<string name="dialog_title_finishing_media_upload">Mediaren igoera bukatzen</string>
<string name="dialog_message_uploading_media">Igotzen…</string>
<string name="dialog_download_image">Jaitsi</string>
<string name="dialog_message_cancel_follow_request">Lagun eskaera ezeztatu?</string>
<string name="dialog_unfollow_warning">Kontu hau jarraitzeari utzi?</string>
<string name="dialog_delete_toot_warning">Tuta ezabatu?</string>
<string name="dialog_message_cancel_follow_request">Jarraipen-eskaerari uko egin\?</string>
<string name="dialog_unfollow_warning">Kontu honi jarraitzeari utzi\?</string>
<string name="dialog_delete_toot_warning">Tut hau ezabatu\?</string>
<string name="visibility_public">Publikoa: Istorio publikoetan erakutsi</string>
<string name="visibility_unlisted">Ezkutukoa: Ez erakutsi istorio publikoetan</string>
<string name="visibility_private">Pribatua: Jarraitzaileentzat soilik ikusgai</string>
@ -140,53 +140,53 @@
<string name="pref_title_notification_alert_sound">Soinuarekin jakinarazi</string>
<string name="pref_title_notification_alert_vibrate">Bibrazioarekin jakinarazi</string>
<string name="pref_title_notification_alert_light">Led-arekin jakinarazi</string>
<string name="pref_title_notification_filters">Noiz jakinarazi:</string>
<string name="pref_title_notification_filters">Noiz jakinarazi</string>
<string name="pref_title_notification_filter_mentions">Aipatzen naute</string>
<string name="pref_title_notification_filter_follows">Jarraitzen didate</string>
<string name="pref_title_notification_filter_reblogs">Bultzatzen naute</string>
<string name="pref_title_notification_filter_favourites">Nire mezuak gogoko ditu</string>
<string name="pref_title_notification_filter_favourites">Nire argitarapenak gustokoak izan dira</string>
<string name="pref_title_appearance_settings">Interfazea</string>
<string name="pref_title_app_theme">Gaia</string>
<string name="pref_title_timelines">Denbora lerroak</string>
<string name="pref_title_app_theme">Aplikazioaren gaia</string>
<string name="pref_title_timelines">Denbora-lerroak</string>
<string name="app_them_dark">Iluna</string>
<string name="app_theme_light">Argia</string>
<string name="app_theme_black">Beltza</string>
<string name="app_theme_auto">Automatikoa</string>
<string name="app_theme_auto">Automatikoa iluntzean</string>
<string name="pref_title_browser_settings">Nabigatzailea</string>
<string name="pref_title_custom_tabs">Chromeko fitxak erabili</string>
<string name="pref_title_hide_follow_button">Tut egiteko botoia ezkutatu jaisterakoan.</string>
<string name="pref_title_status_filter">Denbora-lerro filtroak</string>
<string name="pref_title_custom_tabs">Chromeko fitxa pertsonalizatuak erabili</string>
<string name="pref_title_hide_follow_button">Ezkutatu idazteko botoia mugitzean</string>
<string name="pref_title_status_filter">Denbora-lerroko iragazkiak</string>
<string name="pref_title_status_tabs">Fitxak</string>
<string name="pref_title_show_boosts">Erakutsi bultzadak</string>
<string name="pref_title_show_replies">Erakutsi erantzunak</string>
<string name="pref_title_show_media_preview">Multimedia aurreikusi</string>
<string name="pref_title_proxy_settings">Proxy-a</string>
<string name="pref_title_http_proxy_settings">HTTP Proxy-a</string>
<string name="pref_title_http_proxy_enable">HTTP Proxy-a gaitu</string>
<string name="pref_title_http_proxy_server">HTTP Proxy-aren zerbitzaria</string>
<string name="pref_title_http_proxy_port">HTTP Proxy-aren portua</string>
<string name="pref_default_post_privacy">Aurrezarritako ikusgarritasuna</string>
<string name="pref_default_media_sensitivity">Beti markatu multimedia eduki mingarri gisa</string>
<string name="pref_publishing">Bidalketak</string>
<string name="pref_title_show_media_preview">Jaitsi mediaren aurreikuspenak</string>
<string name="pref_title_proxy_settings">Proxya</string>
<string name="pref_title_http_proxy_settings">HTTP proxya</string>
<string name="pref_title_http_proxy_enable">HTTP proxya gaitu</string>
<string name="pref_title_http_proxy_server">HTTP proxyaren zerbitzaria</string>
<string name="pref_title_http_proxy_port">HTTP proxyaren portua</string>
<string name="pref_default_post_privacy">Argitarapenen aurrezarritako pribatutasuna</string>
<string name="pref_default_media_sensitivity">Media eduki-mingarri gisa beti markatu</string>
<string name="pref_publishing">Argitaratzeak (zerbitzariarekin sinkronizatua)</string>
<string name="pref_failed_to_sync">Aukerak sinkronizatzean akatsa</string>
<string name="post_privacy_public">Publikoa</string>
<string name="post_privacy_unlisted">Ezkutukoa</string>
<string name="post_privacy_followers_only">Pribatua</string>
<string name="pref_status_text_size">Testuaren tamaina</string>
<string name="status_text_size_smallest">Oso txikia</string>
<string name="post_privacy_unlisted">Zerrendatu gabea</string>
<string name="post_privacy_followers_only">Jarraitzaileak soilik</string>
<string name="pref_status_text_size">Status testuaren tamaina</string>
<string name="status_text_size_smallest">Txikiena</string>
<string name="status_text_size_small">Txikia</string>
<string name="status_text_size_medium">Erdikoa</string>
<string name="status_text_size_large">Handia</string>
<string name="status_text_size_largest">Oso handia</string>
<string name="status_text_size_largest">Handiena</string>
<string name="notification_mention_name">Aipamen berriak</string>
<string name="notification_mention_descriptions">Aipamen berrien jakinarazpenak</string>
<string name="notification_follow_name">Jarritzaile berriak</string>
<string name="notification_follow_name">Jarraitzaile berriak</string>
<string name="notification_follow_description">Jarraitzaile berrien jakinarazpenak</string>
<string name="notification_boost_name">Bultzadak</string>
<string name="notification_boost_description">Bultzatutako tuten jakinarazpenak</string>
<string name="notification_boost_description">Bultzatutako zure tuten jakinarazpenak</string>
<string name="notification_favourite_name">Gogokoak</string>
<string name="notification_favourite_description">Gogokoen jakinarazpenak</string>
<string name="notification_mention_format">%s-k aipatu zaitu</string>
<string name="notification_favourite_description">Zure tutak gogoko bezala ezartzerakoan jakinarazpenak</string>
<string name="notification_mention_format">%s-(e)k aipatu zaitu</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s eta beste %4$d</string>
<string name="notification_summary_medium">%1$s, %2$s eta %3$s</string>
<string name="notification_summary_small">%1$s eta %2$s</string>
@ -205,12 +205,11 @@
<string name="about_project_site"> Proiektuaren gunea:\n
https://accelf.net/yuito
</string>
<string name="about_bug_feature_request_site"> Akats jakinarazpenak eta hobekuntza eskariak:\n
https://github.com/accelforce/Yuito/issues
</string>
<string name="about_tusky_account">Yuito-ren profila</string>
<string name="status_share_content">Partekatu edukia</string>
<string name="status_share_link">Partekatu lotura</string>
<string name="about_bug_feature_request_site">Akatsen berri-emateak eta hobekuntza-eskariak:
\n https://github.com/accelforce/Yuito/issues</string>
<string name="about_tusky_account">Yuitoren profila</string>
<string name="status_share_content">Partekatu tutaren edukia</string>
<string name="status_share_link">Partekatu tutaren lotura</string>
<string name="status_media_images">Irudiak</string>
<string name="status_media_video">Bideoak</string>
<string name="state_follow_requested">Eskaera bidalita</string>
@ -277,4 +276,196 @@
<string name="label_remote_account">Ondorengo edukiak erabiltzailearen informazioa erdizka erakutsi dezake. Profil osoa ikusteko nabigatzailean sakatu.</string>
<string name="unpin_action">Desainguratu</string>
<string name="pin_action">Ainguratu</string>
<string name="error_network">Sareko errore bat sortu da! Zure konexioa ziurta ezazu berriro, mesedez!</string>
<string name="title_direct_messages">Mezu Zuzenak</string>
<string name="title_tab_preferences">Kategoriak</string>
<string name="title_statuses_pinned">Lotuta</string>
<string name="title_domain_mutes">Ezkutuko domeinuak</string>
<string name="title_scheduled_toot">Programatutako tutak</string>
<string name="status_username_format">\@%s</string>
<string name="message_empty">Kilkerrak besterik ez hemen.</string>
<string name="action_unreblog">Bultzada kendu</string>
<string name="action_unfavourite">Gogokoa kendu</string>
<string name="action_edit">Editatu</string>
<string name="action_delete_and_redraft">Ezabatu eta zirriborroa berriro egin</string>
<string name="action_view_domain_mutes">Ezkutuko domeinuak</string>
<string name="action_add_poll">Galdeketa gehitu</string>
<string name="action_mute_domain">%s isilarazi</string>
<string name="action_access_scheduled_toot">Programatutako tutak</string>
<string name="action_schedule_toot">Tuta programatu</string>
<string name="action_reset_schedule">Berrezarri</string>
<string name="action_add_tab">Kategoria gehitu</string>
<string name="action_links">Estekak</string>
<string name="action_mentions">Aipamenak</string>
<string name="action_open_reblogged_by">Bultzadak erakutsi</string>
<string name="action_open_faved_by">Gogokoak erakutsi</string>
<string name="title_mentions_dialog">Aipamenak</string>
<string name="title_links_dialog">Estekak</string>
<string name="action_open_media_n">Ireki media #%d</string>
<string name="action_open_as">%s bezala ireki</string>
<string name="action_share_as">... bezala partekatu</string>
<string name="download_media">Media jaisten</string>
<string name="downloading_media">Media jaisten</string>
<string name="confirmation_domain_unmuted">%s ez dago ezkutatua</string>
<string name="hint_configure_scheduled_toot">Sakatu hemen programatutako tuta konfiguratzeko.</string>
<string name="dialog_redraft_toot_warning">Tut hau ezabatu eta zirriborro berria egin\?</string>
<string name="mute_domain_warning">Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenentan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira.</string>
<string name="mute_domain_warning_dialog_ok">Domeinu osoa ezkutatu</string>
<string name="pref_title_notification_filter_poll">Galdeketak bukatu dira</string>
<string name="pref_title_timeline_filters">Iragazkiak</string>
<string name="app_theme_system">Erabili sistemaren diseinua</string>
<string name="pref_title_language">Hizkuntza</string>
<string name="pref_title_bot_overlay">Botentzako erakuslea erakutsi</string>
<string name="pref_title_animate_gif_avatars">GIF abatarrak animatu</string>
<string name="notification_poll_name">Galdeketak</string>
<string name="notification_poll_description">Bukatutako galdeketen jakinarazpenak</string>
<string name="about_tusky_version">Tusky %s</string>
<string name="action_hashtags">Traolak</string>
<string name="title_hashtags_dialog">Traolak</string>
<string name="about_powered_by_tusky">Tusky-k sustatuta</string>
<string name="abbreviated_hours_ago">%dh</string>
<string name="abbreviated_minutes_ago">%dm</string>
<string name="abbreviated_seconds_ago">%ds</string>
<string name="pref_title_alway_open_spoiler">Beti zabaldu edukien abisuekin markatutako tootak</string>
<string name="pref_title_thread_filter_keywords">Elkarrizketak</string>
<string name="filter_addition_dialog_title">Gehitu iragazkia</string>
<string name="filter_edit_dialog_title">Editatu iragazkia</string>
<string name="filter_dialog_remove_button">Kendu</string>
<string name="filter_dialog_update_button">Eguneratu</string>
<string name="filter_dialog_whole_word">Hitz osoa</string>
<string name="filter_dialog_whole_word_description">Gako-hitza edo esaldia alfanumerikoa denean bakarrik, hitz osoarekin bat datorrenean bakarrik aplikatuko da</string>
<string name="filter_add_description">Iragazteko esaldia</string>
<string name="error_create_list">Ezin izan da zerrenda sortu</string>
<string name="error_rename_list">Ezin izan da zerrendaren izena aldatu</string>
<string name="error_delete_list">Ezin izan da zerrenda ezabatu</string>
<string name="action_create_list">Zerrenda sortu</string>
<string name="action_rename_list">Zerrenda berrizendatu</string>
<string name="action_delete_list">Ezabatu zerrenda</string>
<string name="action_edit_list">Editatu zerrenda</string>
<string name="hint_search_people_list">Bilatu jarraitzen dituzun pertsonak</string>
<string name="action_add_to_list">Gehitu kontua zerrendan</string>
<string name="action_remove_from_list">Kendu kontua zerrendatik</string>
<string name="caption_notoemoji">Google-ren egungo emoji multzoa</string>
<string name="license_cc_by_4">CC-BY 4.0</string>
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
<plurals name="favs">
<item quantity="one"><b>%1$s</b> Gogoko</item>
<item quantity="other"><b>%1$s</b> Gogoko</item>
</plurals>
<plurals name="reblogs">
<item quantity="one"><b>%s</b> Bultzada</item>
<item quantity="other"><b>%s</b> Bultzada</item>
</plurals>
<string name="title_reblogged_by">Bultzatuta</string>
<string name="title_favourited_by">Gogokoa</string>
<string name="conversation_1_recipients">%1$s</string>
<string name="conversation_2_recipients">%1$s eta %2$s</string>
<string name="conversation_more_recipients">%1$s, %2$s eta %3$d gehiago</string>
<string name="max_tab_number_reached">geienezko %1$d fitxa iritsita</string>
<string name="description_status_media">Media: %s</string>
<string name="description_status_cw">Edukiaren abisua: %s</string>
<string name="description_status_media_no_description_placeholder">Deskribapenik ez</string>
<string name="description_status_reblogged">Birblogeatuta</string>
<string name="description_status_favourited">Gogotuta</string>
<string name="description_visiblity_public">Publiko</string>
<string name="description_visiblity_unlisted">Zerrendagabetuta</string>
<string name="description_visiblity_private">Jarraitzaileak</string>
<string name="description_visiblity_direct">Zuzena</string>
<string name="description_poll">Inkestatu aukerekin: %1$s, %2$s, %3$s, %4$s; %5$s</string>
<string name="hint_list_name">Zerrendaren izena</string>
<string name="edit_hashtag_title">Editatu traola</string>
<string name="edit_hashtag_hint">Traola # gabe</string>
<string name="hashtag">Traola</string>
<string name="notifications_clear">Garbitu</string>
<string name="notifications_apply_filter">Iragazi</string>
<string name="filter_apply">Aplikatu</string>
<string name="compose_shortcut_long_label">Idatzi Toot-a</string>
<string name="compose_shortcut_short_label">Idatzi</string>
<string name="notification_clear_text">Ziur zaude jakinarazpen guztiak betirako garbitu nahi dituzula\?</string>
<string name="compose_preview_image_description">%s irudiarentzako ekintzak</string>
<string name="poll_info_format"> <!-- 15 boto • Ordu 1 geratzen da --> %1$s • %2$s</string>
<plurals name="poll_info_votes">
<item quantity="one">Boto %s</item>
<item quantity="other">%s Boto</item>
</plurals>
<string name="poll_info_time_absolute">%s amaitzen da</string>
<string name="poll_info_closed">itxita</string>
<string name="poll_vote">Botatu</string>
<string name="poll_ended_voted">Botoa eman duzun galdeketa amaitu da</string>
<string name="poll_ended_created">Sortu duzun galdeketa amaitu da</string>
<plurals name="poll_timespan_days">
<item quantity="one">Egun %d</item>
<item quantity="other">%d egun</item>
</plurals>
<plurals name="poll_timespan_hours">
<item quantity="one">Ordu %d</item>
<item quantity="other">%d ordu</item>
</plurals>
<plurals name="poll_timespan_minutes">
<item quantity="one">Minutu %d</item>
<item quantity="other">%d minutu</item>
</plurals>
<plurals name="poll_timespan_seconds">
<item quantity="one">Segundu %d</item>
<item quantity="other">%d segundu</item>
</plurals>
<string name="button_continue">Jarraitu</string>
<string name="button_back">Itzuli</string>
<string name="button_done">Eginda</string>
<string name="report_sent_success">\@%s jakinarazi duzu arrakastaz</string>
<string name="hint_additional_info">Iruzkin gehigarriak</string>
<string name="report_remote_instance">%s-(r)i birbidali</string>
<string name="failed_report">Txostena huts egin du</string>
<string name="failed_fetch_statuses">Egoeren eskuratzea huts egin du</string>
<string name="report_description_1">Txostena zure zerbitzariaren moderatzaileari bidaliko zaio. Jarraian, kontu honen zergatia salatzen duzun azalpena eman dezakezu:</string>
<string name="report_description_remote_instance">Kontua beste zerbitzari batekoa da. Bidali txostenaren kopia anonimatua hara ere\?</string>
<string name="title_accounts">Kontuak</string>
<string name="failed_search">Bilaketa huts egin du</string>
<string name="pref_title_show_notifications_filter">Erakutsi jakinarazpenen iragazkia</string>
<string name="create_poll_title">Inkesta</string>
<string name="poll_duration_5_min">5 minutu</string>
<string name="poll_duration_30_min">30 minutu</string>
<string name="poll_duration_1_hour">Ordu 1</string>
<string name="poll_duration_6_hours">6 ordu</string>
<string name="poll_duration_1_day">Egun 1</string>
<string name="poll_duration_3_days">3 egun</string>
<string name="poll_duration_7_days">7 egun</string>
<string name="add_poll_choice">Gehitu aukera</string>
<string name="poll_allow_multiple_choices">Aukera anitzak</string>
<string name="poll_new_choice_hint">%d. aukera</string>
<string name="edit_poll">Editatu</string>
<string name="post_lookup_error_format">Errorea agertu da %s mezua bilatzean</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More