Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2020-01-17 14:14:15 +09:00
commit cc31f7af70
60 changed files with 626 additions and 376 deletions

View File

@ -89,6 +89,13 @@
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/*" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"

View File

@ -186,8 +186,14 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
for (listener in toolbarVisibilityListeners) {
listener(isToolbarVisible)
}
val visibility = if (isToolbarVisible) View.VISIBLE else View.INVISIBLE
val alpha = if (isToolbarVisible) 1.0f else 0.0f
if (isToolbarVisible) {
// If to be visible, need to make visible immediately and animate alpha
toolbar.alpha = 0.0f
toolbar.visibility = visibility
}
toolbar.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
@ -248,8 +254,9 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
val attachment = attachments!![viewPager.currentItem].attachment
when (attachment.type) {
Attachment.Type.IMAGE -> shareImage(directory, attachment.url)
Attachment.Type.AUDIO,
Attachment.Type.VIDEO,
Attachment.Type.GIFV -> shareVideo(directory, attachment.url)
Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url)
else -> Log.e(TAG, "Unknown media format for sharing.")
}
}
@ -313,7 +320,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
private fun shareVideo(directory: File, url: String) {
private fun shareMediaFile(directory: File, url: String) {
val uri = Uri.parse(url)
val mimeTypeMap = MimeTypeMap.getSingleton()
val extension = MimeTypeMap.getFileExtensionFromUrl(url)

View File

@ -203,7 +203,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
} else {
if (payloadForHolder instanceof List)
for (Object item : (List) payloadForHolder) {
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item)) {
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) {
holder.setCreatedAt(statusViewData.getCreatedAt());
}
}
@ -486,6 +486,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText());
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
if (statusViewData.isExpanded()) {
contentWarningButton.setText(R.string.status_content_warning_show_less);
} else {
contentWarningButton.setText(R.string.status_content_warning_show_more);
}
contentWarningButton.setOnClickListener(view -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {

View File

@ -181,8 +181,7 @@ class ComposeActivity : BaseActivity(),
setupContentWarningField(composeOptions?.contentWarning)
setupPollView()
applyShareIntent(intent, savedInstanceState)
composeEditField.requestFocus()
viewModel.setupComplete.value = true
if (composeOptions?.tootRightNow == true && calculateTextLength() > 0) {
onSendClicked()
@ -222,7 +221,7 @@ class ComposeActivity : BaseActivity(),
* instance state will be re-queued. */
val type = intent.type
if (type != null) {
if (type.startsWith("image/") || type.startsWith("video/")) {
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
val uriList = ArrayList<Uri>()
if (intent.action != null) {
when (intent.action) {
@ -376,13 +375,17 @@ class ComposeActivity : BaseActivity(),
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
val active = poll == null
&& media!!.size != 4
&& media.firstOrNull()?.type != QueuedMedia.Type.VIDEO
&& (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
enableButton(composeAddMediaButton, active, active)
enablePollButton(media.isNullOrEmpty())
}.subscribe()
viewModel.uploadError.observe {
displayTransientError(R.string.error_media_upload_sending)
}
viewModel.setupComplete.observe {
// Focus may have changed during view model setup, ensure initial focus is on the edit field
composeEditField.requestFocus()
}
}
}
@ -478,18 +481,63 @@ class ComposeActivity : BaseActivity(),
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd)
val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd)
composeEditField.text.replace(start, end, text)
val textToInsert = if (
composeEditField.text.isNotEmpty()
&& !composeEditField.text[start - 1].isWhitespace()
) " $text" else text
composeEditField.text.replace(start, end, textToInsert)
// Set the cursor after the inserted text
composeEditField.setSelection(start + text.length)
}
fun prependSelectedWordsWith(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd)
val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd)
val editorText = composeEditField.text
if (start == end) {
// No selection, just insert text at caret
editorText.insert(start, text)
// Set the cursor after the inserted text
composeEditField.setSelection(start + text.length)
} else {
var wasWord: Boolean
var isWord = end < editorText.length && !Character.isWhitespace(editorText[end])
var newEnd = end
// Iterate the selection backward so we don't have to juggle indices on insertion
var index = end - 1
while (index >= start - 1 && index >= 0) {
wasWord = isWord
isWord = !Character.isWhitespace(editorText[index])
if (wasWord && !isWord) {
// We've reached the beginning of a word, perform insert
editorText.insert(index + 1, text)
newEnd += text.length
}
--index
}
if (start == 0 && isWord) {
// Special case when the selection includes the start of the text
editorText.insert(0, text)
newEnd += text.length
}
// Keep the same text (including insertions) selected
composeEditField.setSelection(start, newEnd)
}
}
private fun atButtonClicked() {
replaceTextAtCaret("@")
prependSelectedWordsWith("@")
}
private fun hashButtonClicked() {
replaceTextAtCaret("#")
prependSelectedWordsWith("#")
}
override fun onSaveInstanceState(outState: Bundle) {
@ -843,7 +891,7 @@ class ComposeActivity : BaseActivity(),
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
val mimeTypes = arrayOf("image/*", "video/*")
val mimeTypes = arrayOf("image/*", "video/*", "audio/*")
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
startActivityForResult(intent, MEDIA_PICK_RESULT)
@ -886,6 +934,9 @@ class ComposeActivity : BaseActivity(),
is VideoSizeException -> {
R.string.error_video_upload_size
}
is AudioSizeException -> {
R.string.error_audio_upload_size
}
is VideoOrImageException -> {
R.string.error_media_upload_image_or_video
}
@ -1010,7 +1061,7 @@ class ComposeActivity : BaseActivity(),
val description: String? = null
) {
enum class Type {
IMAGE, VIDEO;
IMAGE, VIDEO, AUDIO;
}
}
@ -1075,7 +1126,7 @@ class ComposeActivity : BaseActivity(),
@JvmStatic
fun canHandleMimeType(mimeType: String?): Boolean {
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType == "text/plain")
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
}
}
}

View File

@ -84,6 +84,7 @@ class ComposeViewModel
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
val showContentWarning = mutableLiveData(false)
val setupComplete = mutableLiveData(false)
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
@ -142,7 +143,7 @@ class ComposeViewModel
mediaUploader.prepareMedia(uri)
.map { (type, uri, size) ->
val mediaItems = media.value!!
if (type == QueuedMedia.Type.VIDEO
if (type != QueuedMedia.Type.IMAGE
&& mediaItems.isNotEmpty()
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
throw VideoOrImageException()
@ -392,6 +393,7 @@ class ComposeViewModel
if (contentWarning != null) {
startingContentWarning = contentWarning
}
showContentWarning.value = !contentWarning.isNullOrBlank()
// recreate media list
// when coming from SavedTootActivity
@ -411,6 +413,7 @@ class ComposeViewModel
val mediaType = when (a.type) {
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
else -> QueuedMedia.Type.IMAGE
}
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)

View File

@ -69,11 +69,16 @@ class MediaPreviewAdapter(
val item = differ.currentList[position]
holder.progressImageView.setChecked(!item.description.isNullOrEmpty())
holder.progressImageView.setProgress(item.uploadPercent)
Glide.with(holder.itemView.context)
.load(item.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.progressImageView)
if (item.type == ComposeActivity.QueuedMedia.Type.AUDIO) {
// TODO: Fancy waveform display?
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
} else {
Glide.with(holder.itemView.context)
.load(item.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.progressImageView)
}
}
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {

View File

@ -63,6 +63,7 @@ interface MediaUploader {
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent>
}
class AudioSizeException : Exception()
class VideoSizeException : Exception()
class MediaTypeException : Exception()
class CouldNotOpenFileException : Exception()
@ -128,6 +129,12 @@ class MediaUploaderImpl(
"image" -> {
PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
}
"audio" -> {
if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) {
throw AudioSizeException()
}
PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
}
else -> {
throw MediaTypeException()
}
@ -196,6 +203,7 @@ class MediaUploaderImpl(
private companion object {
private const val TAG = "MediaUploaderImpl"
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels

View File

@ -78,7 +78,8 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
}
private fun initViewPager() {
wizard.adapter = ReportPagerAdapter(supportFragmentManager)
wizard.isUserInputEnabled = false
wizard.adapter = ReportPagerAdapter(this)
}
private fun subscribeObservables() {

View File

@ -16,14 +16,14 @@
package com.keylesspalace.tusky.components.report.adapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
class ReportPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manager) {
override fun getItem(position: Int): Fragment {
class ReportPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> ReportStatusesFragment.newInstance()
1 -> ReportNoteFragment.newInstance()
@ -32,5 +32,5 @@ class ReportPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manage
}
}
override fun getCount(): Int = 3
override fun getItemCount() = 3
}

View File

@ -28,13 +28,12 @@ import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter
import com.keylesspalace.tusky.di.ViewModelFactory
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.activity_search.*
import javax.inject.Inject
class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, HasAndroidInjector {
class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -95,14 +94,6 @@ class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, Ha
return super.onOptionsItemSelected(item)
}
override fun onQueryTextChange(newText: String): Boolean {
return false
}
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
private fun getPageTitle(position: Int): CharSequence? {
return when (position) {
0 -> getString(R.string.title_statuses)
@ -125,15 +116,12 @@ class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, Ha
searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName))
searchView.setOnQueryTextListener(this)
searchView.requestFocus()
searchView.maxWidth = Integer.MAX_VALUE
}
override fun androidInjector(): AndroidInjector<Any>? {
return androidInjector
}
override fun androidInjector() = androidInjector
companion object {
@JvmStatic

View File

@ -3,20 +3,17 @@ package com.keylesspalace.tusky.components.search
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.paging.PagedList
import com.keylesspalace.tusky.components.search.adapter.SearchNotestockRepository
import com.keylesspalace.tusky.components.search.adapter.SearchRepository
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.NotestockApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.util.Listing
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.ViewDataUtils
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.viewdata.StatusViewData
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
@ -26,7 +23,8 @@ class SearchViewModel @Inject constructor(
mastodonApi: MastodonApi,
notestockApi: NotestockApi,
private val timelineCases: TimelineCases,
private val accountManager: AccountManager) : RxAwareViewModel() {
private val accountManager: AccountManager
) : RxAwareViewModel() {
var currentQuery: String = ""
@ -36,56 +34,53 @@ class SearchViewModel @Inject constructor(
accountManager.activeAccount = value
}
val mediaPreviewEnabled: Boolean
get() = activeAccount?.mediaPreviewEnabled ?: false
val mediaPreviewEnabled = activeAccount?.mediaPreviewEnabled ?: false
val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
private val statusesRepository = SearchRepository<Pair<Status, StatusViewData.Concrete>>(mastodonApi)
private val accountsRepository = SearchRepository<Account>(mastodonApi)
private val hashtagsRepository = SearchRepository<HashTag>(mastodonApi)
private val notestockRepository = SearchNotestockRepository(notestockApi)
val alwaysShowSensitiveMedia: Boolean = activeAccount?.alwaysShowSensitiveMedia
?: false
val alwaysOpenSpoiler: Boolean = activeAccount?.alwaysOpenSpoiler
?: false
private val repoResultStatus = MutableLiveData<Listing<Pair<Status, StatusViewData.Concrete>>>()
val statuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> = Transformations.switchMap(repoResultStatus) { it.pagedList }
val networkStateStatus: LiveData<NetworkState> = Transformations.switchMap(repoResultStatus) { it.networkState }
val networkStateStatusRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResultStatus) { it.refreshState }
val statuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> = repoResultStatus.switchMap { it.pagedList }
val networkStateStatus: LiveData<NetworkState> = repoResultStatus.switchMap { it.networkState }
val networkStateStatusRefresh: LiveData<NetworkState> = repoResultStatus.switchMap { it.refreshState }
private val repoResultAccount = MutableLiveData<Listing<Account>>()
val accounts: LiveData<PagedList<Account>> = Transformations.switchMap(repoResultAccount) { it.pagedList }
val networkStateAccount: LiveData<NetworkState> = Transformations.switchMap(repoResultAccount) { it.networkState }
val networkStateAccountRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResultAccount) { it.refreshState }
val accounts: LiveData<PagedList<Account>> = repoResultAccount.switchMap { it.pagedList }
val networkStateAccount: LiveData<NetworkState> = repoResultAccount.switchMap { it.networkState }
val networkStateAccountRefresh: LiveData<NetworkState> = repoResultAccount.switchMap { it.refreshState }
private val repoResultHashTag = MutableLiveData<Listing<HashTag>>()
val hashtags: LiveData<PagedList<HashTag>> = Transformations.switchMap(repoResultHashTag) { it.pagedList }
val networkStateHashTag: LiveData<NetworkState> = Transformations.switchMap(repoResultHashTag) { it.networkState }
val networkStateHashTagRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResultHashTag) { it.refreshState }
val hashtags: LiveData<PagedList<HashTag>> = repoResultHashTag.switchMap { it.pagedList }
val networkStateHashTag: LiveData<NetworkState> = repoResultHashTag.switchMap { it.networkState }
val networkStateHashTagRefresh: LiveData<NetworkState> = repoResultHashTag.switchMap { it.refreshState }
private val repoResultNotestock = MutableLiveData<Listing<Pair<Status, StatusViewData.Concrete>>>()
val notestockStatuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> = Transformations.switchMap(repoResultNotestock) { it.pagedList }
val networkStateNotestock: LiveData<NetworkState> = Transformations.switchMap(repoResultNotestock) { it.networkState }
val networkStateNotestockRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResultNotestock) { it.networkState }
val notestockStatuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> = repoResultNotestock.switchMap { it.pagedList }
val networkStateNotestock: LiveData<NetworkState> = repoResultNotestock.switchMap { it.networkState }
val networkStateNotestockRefresh: LiveData<NetworkState> = repoResultNotestock.switchMap { it.networkState }
private val loadedStatuses = ArrayList<Pair<Status, StatusViewData.Concrete>>()
private val loadedNotestockStatuses = ArrayList<Pair<Status, StatusViewData.Concrete>>()
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)!!) }
?: emptyList())
it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) }
.orEmpty()
.apply {
loadedStatuses.addAll(this)
}
}
repoResultAccount.value = accountsRepository.getSearchData(SearchType.Account, query, disposables) {
it?.accounts ?: emptyList()
it?.accounts.orEmpty()
}
val hashtagQuery = if (query.startsWith("#")) query else "#$query"
repoResultHashTag.value =
hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) {
it?.hashtags ?: emptyList()
it?.hashtags.orEmpty()
}
loadedNotestockStatuses.clear()
repoResultNotestock.value = notestockRepository.getSearchData(query, disposables) {
@ -234,11 +229,11 @@ class SearchViewModel @Inject constructor(
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
val idx = loadedStatuses.indexOf(status)
if (idx >= 0) {
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setFavourited(isBookmarked).createStatusViewData())
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setBookmarked(isBookmarked).createStatusViewData())
loadedStatuses[idx] = newPair
repoResultStatus.value?.refresh?.invoke()
}
timelineCases.favourite(status.first, isBookmarked)
timelineCases.bookmark(status.first, isBookmarked)
.onErrorReturnItem(status.first)
.subscribe()
.autoDispose()

View File

@ -26,8 +26,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener)
: PagedListAdapter<Account, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
: PagedListAdapter<Account, RecyclerView.ViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context)
@ -37,21 +36,16 @@ class SearchAccountsAdapter(private val linkListener: LinkListener)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item ->
(holder as? AccountViewHolder)?.apply {
(holder as AccountViewHolder).apply {
setupWithAccount(item)
setupLinkListener(linkListener)
}
}
}
public override fun getItem(position: Int): Account? {
return super.getItem(position)
}
companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
oldItem.deepEquals(newItem)

View File

@ -15,7 +15,6 @@
package com.keylesspalace.tusky.components.search.adapter
import android.annotation.SuppressLint
import androidx.lifecycle.MutableLiveData
import androidx.paging.PositionalDataSource
import com.keylesspalace.tusky.components.search.SearchType
@ -23,16 +22,18 @@ import com.keylesspalace.tusky.entity.SearchResult
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.NetworkState
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import java.util.concurrent.Executor
class SearchDataSource<T>(
private val mastodonApi: MastodonApi,
private val searchType: SearchType,
private val searchRequest: String?,
private val searchRequest: String,
private val disposables: CompositeDisposable,
private val retryExecutor: Executor,
private val initialItems: List<T>? = null,
private val parser: (SearchResult?) -> List<T>) : PositionalDataSource<T>() {
private val parser: (SearchResult?) -> List<T>,
private val source: SearchDataSourceFactory<T>) : PositionalDataSource<T>() {
val networkState = MutableLiveData<NetworkState>()
@ -48,24 +49,20 @@ class SearchDataSource<T>(
}
}
@SuppressLint("CheckResult")
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
if (!initialItems.isNullOrEmpty()) {
callback.onResult(initialItems, 0)
callback.onResult(initialItems.toList(), 0)
} else {
networkState.postValue(NetworkState.LOADED)
retry = null
initialLoad.postValue(NetworkState.LOADING)
mastodonApi.searchObservable(
query = searchRequest ?: "",
query = searchRequest,
type = searchType.apiParameter,
resolve = true,
limit = params.requestedLoadSize,
offset = 0,
following =false)
.doOnSubscribe {
disposables.add(it)
}
.subscribe(
{ data ->
val res = parser(data)
@ -79,19 +76,18 @@ class SearchDataSource<T>(
}
initialLoad.postValue(NetworkState.error(error.message))
}
)
).addTo(disposables)
}
}
@SuppressLint("CheckResult")
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
networkState.postValue(NetworkState.LOADING)
retry = null
if(source.exhausted) {
return callback.onResult(emptyList())
}
mastodonApi.searchObservable(searchType.apiParameter, searchRequest, true, params.loadSize, params.startPosition, false)
.doOnSubscribe {
disposables.add(it)
}
.subscribe(
{ data ->
// Working around Mastodon bug where exact match is returned no matter
@ -105,9 +101,11 @@ class SearchDataSource<T>(
} else {
parser(data)
}
if(res.isEmpty()) {
source.exhausted = true
}
callback.onResult(res)
networkState.postValue(NetworkState.LOADED)
},
{ error ->
retry = {
@ -115,7 +113,7 @@ class SearchDataSource<T>(
}
networkState.postValue(NetworkState.error(error.message))
}
)
).addTo(disposables)
}

View File

@ -26,14 +26,18 @@ import java.util.concurrent.Executor
class SearchDataSourceFactory<T>(
private val mastodonApi: MastodonApi,
private val searchType: SearchType,
private val searchRequest: String?,
private val searchRequest: String,
private val disposables: CompositeDisposable,
private val retryExecutor: Executor,
private val cacheData: List<T>? = null,
private val parser: (SearchResult?) -> List<T>) : DataSource.Factory<Int, T>() {
val sourceLiveData = MutableLiveData<SearchDataSource<T>>()
var exhausted = false
override fun create(): DataSource<Int, T> {
val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser)
val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser, this)
sourceLiveData.postValue(source)
return source
}

View File

@ -26,8 +26,7 @@ import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.interfaces.LinkListener
class SearchHashtagsAdapter(private val linkListener: LinkListener)
: PagedListAdapter<HashTag, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
: PagedListAdapter<HashTag, RecyclerView.ViewHolder>(HASHTAG_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context)
@ -36,17 +35,14 @@ class SearchHashtagsAdapter(private val linkListener: LinkListener)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item ->
(holder as? HashtagViewHolder)?.apply {
setup(item.name, linkListener)
}
getItem(position)?.let { (name) ->
(holder as HashtagViewHolder).setup(name, linkListener)
}
}
companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() {
val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() {
override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean =
oldItem.name == newItem.name

View File

@ -29,7 +29,7 @@ class SearchRepository<T>(private val mastodonApi: MastodonApi) {
private val executor = Executors.newSingleThreadExecutor()
fun getSearchData(searchType: SearchType, searchRequest: String?, disposables: CompositeDisposable, pageSize: Int = 20,
fun getSearchData(searchType: SearchType, searchRequest: String, disposables: CompositeDisposable, pageSize: Int = 20,
initialItems: List<T>? = null, parser: (SearchResult?) -> List<T>): Listing<T> {
val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser)
val livePagedList = sourceFactory.toLiveData(

View File

@ -32,7 +32,6 @@ class SearchStatusesAdapter(
private val statusListener: StatusActionListener
) : PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status, parent, false)
@ -41,8 +40,7 @@ class SearchStatusesAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item ->
(holder as? StatusViewHolder)?.setupWithStatus(item.second, statusListener,
statusDisplayOptions)
(holder as StatusViewHolder).setupWithStatus(item.second, statusListener, statusDisplayOptions)
}
}

View File

@ -12,6 +12,7 @@ import androidx.paging.PagedList
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.AccountActivity
@ -62,8 +63,8 @@ abstract class SearchFragment<T> : Fragment(),
swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground))
ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground)
)
}
private fun subscribeObservables() {
@ -75,8 +76,9 @@ abstract class SearchFragment<T> : Fragment(),
searchProgressBar.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED)
showError(it.msg)
if (it.status == Status.FAILED) {
showError()
}
checkNoData()
})
@ -85,8 +87,9 @@ abstract class SearchFragment<T> : Fragment(),
progressBarBottom.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED)
showError(it.msg)
if (it.status == Status.FAILED) {
showError()
}
})
}
@ -99,7 +102,8 @@ abstract class SearchFragment<T> : Fragment(),
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context)
adapter = createAdapter()
searchRecyclerView.adapter = adapter
searchRecyclerView.setHasFixedSize(true)
(searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
private fun showNoData(isEmpty: Boolean) {
@ -109,7 +113,7 @@ abstract class SearchFragment<T> : Fragment(),
searchNoResultsText.hide()
}
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
private fun showError() {
if (snackbarErrorRetry?.isShown != true) {
snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry?.setAction(R.string.action_retry) {
@ -129,13 +133,12 @@ abstract class SearchFragment<T> : Fragment(),
}
protected val bottomSheetActivity
get() = (activity as? BottomSheetActivity)
get() = (activity as? BottomSheetActivity)
override fun onRefresh() {
// Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins.
swipeRefreshLayout.post {
swipeRefreshLayout.isRefreshing = false
}
viewModel.retryAllSearches()

View File

@ -59,7 +59,6 @@ import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_search.*
import java.util.*
open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
@ -70,6 +69,9 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
override val data: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>>
get() = viewModel.statuses
private val searchAdapter
get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
val statusDisplayOptions = StatusDisplayOptions(
@ -87,43 +89,43 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let {
searchAdapter.getItem(position)?.let {
viewModel.contentHiddenChange(it, isShowing)
}
}
override fun onReply(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status ->
searchAdapter.getItem(position)?.first?.let { status ->
reply(status)
}
}
override fun onFavourite(favourite: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { status ->
searchAdapter.getItem(position)?.let { status ->
viewModel.favorite(status, favourite)
}
}
override fun onQuote(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status ->
searchAdapter.getItem(position)?.first?.let { status ->
quote(status)
}
}
override fun onBookmark(bookmark: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { status ->
searchAdapter.getItem(position)?.let { status ->
viewModel.bookmark(status, bookmark)
}
}
override fun onMore(view: View, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let {
searchAdapter.getItem(position)?.first?.let {
more(it, view, position)
}
}
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.actionableStatus?.let { actionable ->
searchAdapter.getItem(position)?.first?.actionableStatus?.let { actionable ->
when (actionable.attachments[attachmentIndex].type) {
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
val attachments = AttachmentViewData.list(actionable)
@ -148,48 +150,48 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
}
override fun onViewThread(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status ->
searchAdapter.getItem(position)?.first?.let { status ->
val actionableStatus = status.actionableStatus
bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url)
}
}
override fun onOpenReblog(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status ->
searchAdapter.getItem(position)?.first?.let { status ->
bottomSheetActivity?.viewAccount(status.account.id)
}
}
override fun onExpandedChange(expanded: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let {
searchAdapter.getItem(position)?.let {
viewModel.expandedChange(it, expanded)
}
}
override fun onLoadMore(position: Int) {
//Ignore
// Not possible here
}
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let {
searchAdapter.getItem(position)?.let {
viewModel.collapsedChange(it, isCollapsed)
}
}
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let {
searchAdapter.getItem(position)?.let {
viewModel.voteInPoll(it, choices)
}
}
open fun removeItem(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let {
searchAdapter.getItem(position)?.let {
viewModel.removeItem(it)
}
}
override fun onReblog(reblog: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { status ->
searchAdapter.getItem(position)?.let { status ->
viewModel.reblog(status, reblog)
}
}
@ -199,46 +201,39 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
}
private fun reply(status: Status) {
val inReplyToId = status.actionableId
val actionableStatus = status.actionableStatus
val replyVisibility = actionableStatus.visibility
val contentWarning = actionableStatus.spoilerText
val mentions = actionableStatus.mentions
val mentionedUsernames = LinkedHashSet<String>()
mentionedUsernames.add(actionableStatus.account.username)
val loggedInUsername = viewModel.activeAccount?.username
for ((_, _, username) in mentions) {
mentionedUsernames.add(username)
}
mentionedUsernames.remove(loggedInUsername)
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
inReplyToId = inReplyToId,
replyVisibility = replyVisibility,
contentWarning = contentWarning,
val mentionedUsernames = actionableStatus.mentions.map { it.username }
.toMutableSet()
.apply {
add(actionableStatus.account.username)
remove(viewModel.activeAccount?.username)
}
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
inReplyToId = status.actionableId,
replyVisibility = actionableStatus.visibility,
contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames,
replyingStatusAuthor = actionableStatus.account.localUsername,
replyingStatusContent = actionableStatus.content.toString()
))
requireActivity().startActivity(intent)
startActivity(intent)
}
private fun quote(status: Status) {
val id = status.actionableId
val actionableStatus = status.actionableStatus
val visibility = actionableStatus.visibility
val url = actionableStatus.url
val mentions = actionableStatus.mentions
val mentionedUsernames = LinkedHashSet<String>()
mentionedUsernames.add(actionableStatus.account.username)
val loggedInUsername = viewModel.activeAccount?.username
for ((_, _, username) in mentions) {
mentionedUsernames.add(username)
}
mentionedUsernames.remove(loggedInUsername)
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
quoteId = id,
quoteUrl = url,
replyVisibility = visibility,
val mentionedUsernames = actionableStatus.mentions.map { it.username }
.toMutableSet()
.apply {
add(actionableStatus.account.name)
remove(viewModel.activeAccount?.username)
}
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
quoteId = status.actionableId,
quoteUrl = actionableStatus.url,
replyVisibility = actionableStatus.visibility,
contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames
))
startActivity(intent)
@ -280,8 +275,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
}
}
val menu = popup.menu
val openAsItem = menu.findItem(R.id.status_open_as)
val openAsItem = popup.menu.findItem(R.id.status_open_as)
when (accounts.size) {
0, 1 -> openAsItem.isVisible = false
2 -> for (account in accounts) {
@ -297,13 +291,12 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.status_share_content -> {
var statusToShare: Status? = status
if (statusToShare!!.reblog != null) statusToShare = statusToShare.reblog
val statusToShare: Status = status.actionableStatus
val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
val stringToShare = statusToShare!!.account.username +
val stringToShare = statusToShare.account.username +
" - " +
statusToShare.content.toString()
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
@ -320,7 +313,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
return@setOnMenuItemClickListener true
}
R.id.status_copy_link -> {
val clipboard = activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(null, statusUrl))
return@setOnMenuItemClickListener true
}
@ -393,7 +386,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
val uri = Uri.parse(url)
val filename = uri.lastPathSegment
val downloadManager = activity!!.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadManager = requireActivity().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(uri)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
downloadManager.enqueue(request)
@ -445,7 +438,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
deletedStatus
}
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
tootText = redraftStatus.text ?: "",
inReplyToId = redraftStatus.inReplyToId,
visibility = redraftStatus.visibility,

View File

@ -1029,6 +1029,14 @@ public class NotificationsFragment extends SFragment implements
}
}
Log.e(TAG, "Fetch failure: " + exception.getMessage());
if (fetchEnd == FetchEnd.TOP) {
topLoading = false;
}
if (fetchEnd == FetchEnd.BOTTOM) {
bottomLoading = false;
}
progressBar.setVisibility(View.GONE);
}

View File

@ -21,6 +21,7 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -31,6 +32,7 @@ import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_video.*
@ -41,11 +43,13 @@ class ViewVideoFragment : ViewMediaFragment() {
// Hoist toolbar hiding to activity so it can track state across different fragments
// This is explicitly stored as runnable so that we pass it to the handler later for cancellation
mediaActivity.onPhotoTap()
mediaController.hide()
}
private lateinit var mediaActivity: ViewMediaActivity
private val TOOLBAR_HIDE_DELAY_MS = 3000L
override lateinit var descriptionView : TextView
private lateinit var mediaController : MediaController
private var isAudio = false
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden
@ -72,14 +76,43 @@ class ViewVideoFragment : ViewMediaFragment() {
videoView.transitionName = url
videoView.setVideoPath(url)
mediaController = MediaController(mediaActivity)
mediaController = object : MediaController(mediaActivity) {
override fun show(timeout: Int) {
// We're doing manual auto-close management.
// Also, take focus back from the pause button so we can use the back button.
super.show(0)
mediaController.requestFocus()
}
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
if (event?.keyCode == KeyEvent.KEYCODE_BACK) {
if (event.action == KeyEvent.ACTION_UP) {
hide()
activity?.supportFinishAfterTransition()
}
return true
}
return super.dispatchKeyEvent(event)
}
}
mediaController.setMediaPlayer(videoView)
videoView.setMediaController(mediaController)
videoView.requestFocus()
videoView.setOnTouchListener { _, _ ->
mediaActivity.onPhotoTap()
false
}
videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener {
override fun onPause() {
handler.removeCallbacks(hideToolbar)
}
override fun onPlay() {
// Audio doesn't cause the controller to show automatically,
// and we only want to hide the toolbar if it's a video.
if (isAudio) {
mediaController.show()
} else {
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
}
}
})
videoView.setOnPreparedListener { mp ->
val containerWidth = videoContainer.measuredWidth.toFloat()
val containerHeight = videoContainer.measuredHeight.toFloat()
@ -94,10 +127,16 @@ class ViewVideoFragment : ViewMediaFragment() {
videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
}
// Wait until the media is loaded before accepting taps as we don't want toolbar to
// be hidden until then.
videoView.setOnTouchListener { _, _ ->
mediaActivity.onPhotoTap()
false
}
progressBar.hide()
mp.isLooping = true
if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) {
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
videoView.start()
}
}
@ -126,6 +165,7 @@ class ViewVideoFragment : ViewMediaFragment() {
throw IllegalArgumentException("attachment has to be set")
}
url = attachment.url
isAudio = attachment.type == Attachment.Type.AUDIO
finalizeViewSetup(url, attachment.previewUrl, attachment.description)
}
@ -136,6 +176,12 @@ class ViewVideoFragment : ViewMediaFragment() {
isDescriptionVisible = showingDescription && visible
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
if (isDescriptionVisible) {
// If to be visible, need to make visible immediately and animate alpha
descriptionView.alpha = 0.0f
descriptionView.visible(isDescriptionVisible)
}
descriptionView.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
@ -145,7 +191,7 @@ class ViewVideoFragment : ViewMediaFragment() {
})
.start()
if (visible) {
if (visible && videoView.isPlaying && !isAudio) {
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
} else {
handler.removeCallbacks(hideToolbar)

View File

@ -0,0 +1,33 @@
package com.keylesspalace.tusky.view
import android.content.Context
import android.util.AttributeSet
import android.widget.VideoView
class ExposedPlayPauseVideoView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: VideoView(context, attrs, defStyleAttr) {
private var listener: PlayPauseListener? = null
fun setPlayPauseListener(listener: PlayPauseListener) {
this.listener = listener
}
override fun start() {
super.start()
listener?.onPlay()
}
override fun pause() {
super.pause()
listener?.onPause()
}
interface PlayPauseListener {
fun onPlay()
fun onPause()
}
}

View File

@ -1,33 +0,0 @@
/* Copyright 2019 Joel Pyska
*
* 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.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
class NoSwipeViewPager @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) {
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return false
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
return false
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="?android:textColorTertiary" android:pathData="M16,9H13V14.5A2.5,2.5 0 0,1 10.5,17A2.5,2.5 0 0,1 8,14.5A2.5,2.5 0 0,1 10.5,12C11.07,12 11.58,12.19 12,12.5V7H16M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" />
</vector>

View File

@ -9,7 +9,7 @@
<include layout="@layout/toolbar_basic" />
<com.keylesspalace.tusky.view.NoSwipeViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/wizard"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -23,7 +23,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="Some media description" />
<VideoView
<com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
android:id="@+id/videoView"
android:layout_width="wrap_content"
android:layout_height="match_parent"

View File

@ -235,7 +235,6 @@
<string name="status_media_images">صور</string>
<string name="status_media_video">فيديو</string>
<string name="state_follow_requested">طلب متابعة</string>
<string name="no_content">ليس هناك محتوى</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">في %dy</string>
<string name="abbreviated_in_days">في %dd</string>
@ -498,4 +497,7 @@
<string name="list">القائمة</string>
<string name="gradient_for_media">اظهر ألوانا متدرّجة للوسائط المخفية</string>
</resources>
<string name="no_saved_status">ليس لديك أية مسودات.</string>
<string name="no_scheduled_status">ليس لديك أية منشورات مُبرمَجة للنشر.</string>
</resources>

View File

@ -282,8 +282,6 @@
<string name="state_follow_requested">অনুরোধ অনুসরণ করুন</string>
<string name="no_content">কোন উপাদান নেই</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%dy এ</string>
<string name="abbreviated_in_days">%dd এ</string>

View File

@ -203,8 +203,6 @@
<string name="status_media_images">Imatges</string>
<string name="status_media_video">Vídeo</string>
<string name="no_content">no hi ha cap contingut</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">en %d anys</string>
<string name="abbreviated_in_days">en %dd</string>

View File

@ -245,7 +245,6 @@
<string name="status_media_images">Obrázky</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">Vyžádáno sledování</string>
<string name="no_content">žádný obsah</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">za %d let</string>
<string name="abbreviated_in_days">za %d d</string>

View File

@ -214,7 +214,6 @@
<string name="status_media_images">Delweddau</string>
<string name="status_media_video">Fideo</string>
<string name="state_follow_requested">Gofyn i ddilyn</string>
<string name="no_content">dim cynnwys</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%dy </string>
<string name="abbreviated_in_days"> %dd </string>

View File

@ -241,7 +241,6 @@
<string name="status_media_images">Bilder</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">Folgeanfrage gesendet</string>
<string name="no_content">leer</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="follows_you">Folgt dir</string>
<string name="pref_title_alway_show_sensitive_media">Heikle Inhalte immer anzeigen</string>

View File

@ -241,7 +241,6 @@
<string name="status_media_images">Bildoj</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">Sekvado petita</string>
<string name="no_content">neniu enhavo</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">en %dj</string>
<string name="abbreviated_in_days">en %dt</string>

View File

@ -228,7 +228,6 @@
<string name="status_media_images">Imágenes</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">Solicitud enviada</string>
<string name="no_content">No hay nada aquí</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">en %dy</string>
<string name="abbreviated_in_days">en %dd</string>

View File

@ -213,7 +213,6 @@
<string name="status_media_images">Irudiak</string>
<string name="status_media_video">Bideoak</string>
<string name="state_follow_requested">Eskaera bidalita</string>
<string name="no_content">Hutsik dago hau</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%du-an</string>
<string name="abbreviated_in_days">%de-an</string>

View File

@ -213,7 +213,6 @@
<string name="status_media_images">تصویرها</string>
<string name="status_media_video">فیلم</string>
<string name="state_follow_requested">تقاضای پیگیری شد</string>
<string name="no_content">بدون هیچ محتوا</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">در %dسال</string>
<string name="abbreviated_in_days">در %dر</string>

View File

@ -245,7 +245,6 @@
<string name="status_media_images">Images</string>
<string name="status_media_video">Vidéo</string>
<string name="state_follow_requested">Demande de suivi effectuée</string>
<string name="no_content">aucun contenu</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">en %da</string>
<string name="abbreviated_in_days">en %dj</string>

View File

@ -226,7 +226,6 @@
<string name="status_media_images">Képek</string>
<string name="status_media_video">Videók</string>
<string name="state_follow_requested">Követés kérelmezve</string>
<string name="no_content">nincs tartalom</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="follows_you">Követ téged</string>
<string name="pref_title_alway_show_sensitive_media">Mindig mutassa a szenzitív tartalmat</string>

View File

@ -239,7 +239,6 @@
<string name="status_media_images">Immagini</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">In attesa di approvazione</string>
<string name="no_content">nessun contenuto</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">in %da</string>
<string name="abbreviated_in_days">in %dg</string>

View File

@ -243,7 +243,6 @@
<string name="status_media_images">画像</string>
<string name="status_media_video">動画</string>
<string name="state_follow_requested">フォローリクエスト中</string>
<string name="no_content">下書きはありません</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d年後</string>
<string name="abbreviated_in_days">%d日後</string>

View File

@ -293,8 +293,6 @@
<string name="state_follow_requested">팔로우 요청함</string>
<string name="no_content">임시 저장된 게시물이 없습니다</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d년 후</string>
<string name="abbreviated_in_days">%d일 후</string>

View File

@ -236,7 +236,6 @@
<string name="status_media_images">Afbeeldingen</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">Volgverzoek verzonden</string>
<string name="no_content">geen inhoud</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">over %dj</string>
<string name="abbreviated_in_days">over %dd</string>

View File

@ -247,8 +247,6 @@
<string name="state_follow_requested">Forespørsel sendt</string>
<string name="no_content">ikke noe innhold</string>
<string name="abbreviated_in_days">om %dd</string>
<string name="abbreviated_in_hours">om %dh</string>
<string name="abbreviated_in_minutes">om %dm</string>
@ -520,4 +518,5 @@
<string name="no_scheduled_status">Du har ingen planlagte statuser.</string>
</resources>
<string name="no_saved_status">Du har ikke lagret noen kladder.</string>
</resources>

View File

@ -209,7 +209,6 @@
<string name="status_media_images">Imatges</string>
<string name="status_media_video">Vidèo</string>
<string name="state_follow_requested">Demanda dabonament</string>
<string name="no_content">I a pas cap de contengut</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">en %d ans</string>
<string name="abbreviated_in_days">en %dd</string>

View File

@ -206,7 +206,6 @@
<string name="status_media_images">Obrazy</string>
<string name="status_media_video">Wideo</string>
<string name="state_follow_requested">Wysłano prośbę o możliwość śledzenia</string>
<string name="no_content">brak zawartości</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">w %d lata</string>
<string name="abbreviated_in_days">w %d dni</string>

View File

@ -226,7 +226,6 @@
<string name="status_media_images">Imagens</string>
<string name="status_media_video">Vídeo</string>
<string name="state_follow_requested">Solicitação enviada</string>
<string name="no_content">sem conteúdo</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">em %dy</string>
<string name="abbreviated_in_days">em %dd</string>
@ -463,7 +462,7 @@
<string name="poll_duration_3_days">3 dias</string>
<string name="poll_duration_7_days">7 dias</string>
<string name="add_poll_choice">Adicionar opção</string>
<string name="poll_allow_multiple_choices">Múltiplas opções</string>
<string name="poll_allow_multiple_choices">Múltiplos votos</string>
<string name="poll_new_choice_hint">Opção %d</string>
<string name="edit_poll">Editar</string>
@ -481,4 +480,8 @@
<string name="description_status_bookmarked">Salvo</string>
<string name="select_list_title">Selecionar lista</string>
<string name="list">Lista</string>
</resources>
<string name="gradient_for_media">Mostrar blur em mídias ocultas</string>
<string name="no_scheduled_status">Sem toots agendados.</string>
</resources>

View File

@ -275,8 +275,6 @@
<string name="state_follow_requested">Запрошенные подписки</string>
<string name="no_content">ничего нет</string>
<!--Отметки времени у постов: "16s" or "2d"-->
<string name="abbreviated_in_years">через %dг</string>
<string name="abbreviated_in_days">через %dд</string>

View File

@ -253,8 +253,6 @@
<string name="state_follow_requested">Prošnja za sledenje</string>
<string name="no_content">brez vsebine</string>
<string name="abbreviated_in_years">v %dy</string>
<string name="abbreviated_in_days">v %dd</string>
<string name="abbreviated_in_hours">v %dh</string>

View File

@ -14,11 +14,11 @@
<string name="error_video_upload_size">Videofiler måste vara mindre än 40MB.</string>
<string name="error_media_upload_type">Den typen av fil kan inte laddas upp.</string>
<string name="error_media_upload_opening">Den filen kunde inte öppnas.</string>
<string name="error_media_upload_permission">Tillstånd att läsa media krävs.</string>
<string name="error_media_download_permission">Tillstånd att lagra media krävs.</string>
<string name="error_media_upload_permission">Behörighet att läsa media krävs.</string>
<string name="error_media_download_permission">Behörighet att spara media krävs.</string>
<string name="error_media_upload_image_or_video">Bilder och videoklipp kan inte båda bifogas i samma status.</string>
<string name="error_media_upload_sending">Uppladdningen misslyckades.</string>
<string name="error_sender_account_gone">Misslyckades med att få ett inloggningsnamn.</string>
<string name="error_sender_account_gone">Kunde inte skicka toot.</string>
<string name="title_home">Hem</string>
<string name="title_notifications">Notifikationer</string>
<string name="title_public_local">Lokalt</string>
@ -109,7 +109,7 @@
<string name="action_links">Länkar</string>
<string name="action_mentions">Omnämnanden</string>
<string name="action_hashtags">Hashtaggar</string>
<string name="action_open_reblogger">Öppna knuffa författare</string>
<string name="action_open_reblogger">Öppna knuff författare</string>
<string name="action_open_reblogged_by">Visa knuffar</string>
<string name="action_open_faved_by">Visa favoriter</string>
<string name="title_hashtags_dialog">Hashtaggar</string>
@ -117,7 +117,7 @@
<string name="title_links_dialog">Länkar</string>
<string name="action_open_media_n">Öppna media #%d</string>
<string name="download_image">Laddar ned %1$s</string>
<string name="action_copy_link">Kopiera länken</string>
<string name="action_copy_link">Kopiera länk</string>
<string name="action_open_as">Öppen som %s</string>
<string name="action_share_as">Dela som …</string>
<string name="send_status_link_to">Dela toot-URL till…</string>
@ -158,8 +158,8 @@
<string name="visibility_unlisted">Olistad: Visa inte i offentliga tidslinjer</string>
<string name="visibility_private">Enbart-följare: Ses enbart av följare</string>
<string name="visibility_direct">Direkt: Skicka endast till nämnda användare</string>
<string name="pref_title_edit_notification_settings">Redigera notifieringar</string>
<string name="pref_title_notifications_enabled">Notifieringar</string>
<string name="pref_title_edit_notification_settings">Notifikationer</string>
<string name="pref_title_notifications_enabled">Notifikationer</string>
<string name="pref_title_notification_alerts">Alarm</string>
<string name="pref_title_notification_alert_sound">Meddela med ljud</string>
<string name="pref_title_notification_alert_vibrate">Meddela med vibration</string>
@ -173,14 +173,14 @@
<string name="pref_title_app_theme">Applikationstema</string>
<string name="pref_title_timelines">Tidslinjer</string>
<string name="pref_title_timeline_filters">Filter</string>
<string name="app_them_dark">Mörk</string>
<string name="app_theme_light">Ljus</string>
<string name="app_them_dark">Mörkt</string>
<string name="app_theme_light">Ljust</string>
<string name="app_theme_black">Svart</string>
<string name="app_theme_auto">Automatiskt vid solnedgång</string>
<string name="app_theme_system">Använd systemdesign</string>
<string name="pref_title_browser_settings">Webbläsare</string>
<string name="pref_title_custom_tabs">Använd Chrome-anpassade flikar</string>
<string name="pref_title_hide_follow_button">Dölj skriv-knappen medan du scrollar</string>
<string name="pref_title_hide_follow_button">Dölj skriv-knappen vid skrollning</string>
<string name="pref_title_language">Språk</string>
<string name="pref_title_status_filter">Filtrering av tidslinje</string>
<string name="pref_title_status_tabs">Flikar</string>
@ -208,11 +208,11 @@
<string name="notification_mention_name">Nya omnämnanden</string>
<string name="notification_mention_descriptions">Notifieringar om nya omnämnanden</string>
<string name="notification_follow_name">Nya följare</string>
<string name="notification_follow_description">Notifieringar angående nya följare</string>
<string name="notification_follow_description">Notifieringar om nya följare</string>
<string name="notification_boost_name">Knuffar</string>
<string name="notification_boost_description">Notifieringar när dina toot blir knuffade</string>
<string name="notification_boost_description">Notifieringar när dina toots blir knuffade</string>
<string name="notification_favourite_name">Favoriter</string>
<string name="notification_favourite_description">Notifieringar när dina toot blir markerade som favoriter</string>
<string name="notification_favourite_description">Notifieringar när dina toots blir markerade som favoriter</string>
<string name="notification_mention_format">%s omnämnde dig</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s och %4$d andra</string>
<string name="notification_summary_medium">%1$s, %2$s, och %3$s</string>
@ -239,8 +239,7 @@
<string name="status_share_link">Dela länk till toot</string>
<string name="status_media_images">Bilder</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">Följarförfrågning</string>
<string name="no_content">inget innehåll</string>
<string name="state_follow_requested">Följarförfrågad</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">om %dy</string>
<string name="abbreviated_in_days">om %dd</string>
@ -273,11 +272,11 @@
<string name="error_rename_list">Kunde inte byta namn på lista</string>
<string name="error_delete_list">Kunde inte radera lista</string>
<string name="action_create_list">Skapa en lista</string>
<string name="action_rename_list">Byt namn på lista</string>
<string name="action_delete_list">Ta bort denna lista</string>
<string name="action_edit_list">Redigera listan</string>
<string name="action_rename_list">Byt namn</string>
<string name="action_delete_list">Ta bort</string>
<string name="action_edit_list">Ändra</string>
<string name="hint_search_people_list">Sök efter personer du följer</string>
<string name="action_add_to_list">Lägga till kontot i listan</string>
<string name="action_add_to_list">Lägg till konto i listan</string>
<string name="action_remove_from_list">Ta bort kontot från listan</string>
<string name="compose_active_account_description">Inlägg med kontot %1$s</string>
<string name="error_failed_set_caption">Misslyckades med att ange bildtext</string>
@ -302,7 +301,7 @@
<string name="expand_collapse_all_statuses">Expandera/Dölj alla status</string>
<string name="action_open_toot">Öppna toot</string>
<string name="restart_required">Omstart av appen krävs</string>
<string name="restart_emoji">Du måste starta om Yuito för att kunna tillämpa ändringarna</string>
<string name="restart_emoji">Du måste starta om Yuito för att tillämpa ändringarna</string>
<string name="later">Senare</string>
<string name="restart">Starta om</string>
<string name="caption_systememoji">Standard-emojis för din enhet</string>
@ -330,9 +329,9 @@
<item quantity="other">&lt;b&gt;%1$s&lt;/b&gt; Favoriter</item>
</plurals>
<plurals name="reblogs">
<item quantity="one">&lt;b&gt;%s&lt;/b&gt; Knuffa</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; Knuffar</item>
</plurals>
<item quantity="one"><b>%s</b> Knuff</item>
<item quantity="other"><b>%s</b> Knuffar</item>
</plurals>
<string name="title_reblogged_by">Knuffad av</string>
<string name="title_favourited_by">Favoriserad av</string>
<string name="conversation_1_recipients">%1$s</string>
@ -377,7 +376,7 @@
<string name="notification_clear_text">Är du säker på att du vill rensa dina notifieringar permanent\?</string>
<string name="action_delete_and_redraft">Radera och skriv nytt</string>
<string name="dialog_redraft_toot_warning">Radera och skriva ny toot\?</string>
<string name="dialog_redraft_toot_warning">Radera och skriv ny toot\?</string>
<string name="poll_info_format"> <!-- 15 röster • 1 timme kvar --> %1$s • %2$s</string>
<plurals name="poll_info_votes">
@ -391,7 +390,7 @@
<string name="poll_vote">Rösta</string>
<string name="pref_title_notification_filter_poll">omröstningen är avslutad</string>
<string name="pref_title_notification_filter_poll">omröstning är avslutad</string>
<string name="notification_poll_name">Omröstningar</string>
<string name="notification_poll_description">Notifieringar när omröstningar har avslutats</string>
@ -428,7 +427,7 @@
<string name="confirmation_domain_unmuted">%s inte tystnad längre</string>
<string name="mute_domain_warning">Är du säker på att du vill blockera allt från %s\? Du kommer inte kunna se något innehåll från denna domän i publika tidslinje eller i dina notifieringar. Dina följare på domänen kommer inte att bli borttagna.</string>
<string name="mute_domain_warning_dialog_ok">Dölj hela domän</string>
<string name="mute_domain_warning_dialog_ok">Dölj hela domänen</string>
<string name="caption_notoemoji">Google\'s nuvarande emojis</string>
<string name="button_continue">Fortsätt</string>
@ -440,7 +439,7 @@
<string name="failed_report">Misslyckades att anmäla</string>
<string name="failed_fetch_statuses">Misslyckades att hämta status</string>
<string name="report_description_1">Anmälan kommer att skickas till din serveradminstratör. Du kan beskriva varför du anmäler kontot nedan:</string>
<string name="report_description_remote_instance">Kontot är från en annan server. Skicka en avidentifierad kopia av anmälan dit också\?</string>
<string name="report_description_remote_instance">Kontot är från en annan server. Skicka en anonym kopia av anmälan dit också\?</string>
<string name="pref_title_show_notifications_filter">Visa notifikationsfilter</string>
<string name="filter_dialog_whole_word">Helt ord</string>
@ -477,4 +476,8 @@
<string name="description_status_bookmarked">Bokmärkt</string>
<string name="select_list_title">Välj lista</string>
<string name="list">Lista</string>
</resources>
<string name="gradient_for_media">Visa färgglada gradienter för gömd media</string>
<string name="no_scheduled_status">Du har inga schemalagda statusar.</string>
</resources>

View File

@ -194,7 +194,6 @@
<string name="status_media_images">படங்கள்</string>
<string name="status_media_video">காணொளி</string>
<string name="state_follow_requested">கோரிக்கையைப் பின்பற்றவும்</string>
<string name="no_content">எந்த உள்ளடக்கமும் இல்லை</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%dஆ-முன்</string>
<string name="abbreviated_in_days">%dநா-முன்</string>

View File

@ -4,17 +4,17 @@
<string name="error_network">Bir ağ hatası oluştu! Lütfen bağlantınızı kontrol edin ve tekrar deneyin!</string>
<string name="error_empty">Bu alan boş bırakılamaz.</string>
<string name="error_invalid_domain">Girilen alan adı geçersiz</string>
<string name="error_failed_app_registration">Bu sunucuda kimlik doğrulama başarısız oldu.</string>
<string name="error_no_web_browser_found">Kullanılabilir tarayıcı bulunmadı.</string>
<string name="error_failed_app_registration">Kimlik doğrulama başarısız oldu.</string>
<string name="error_no_web_browser_found">Kullanılabilir bir web tarayıcı bulunmadı.</string>
<string name="error_authorization_unknown">Tanımlanamayan bir yetkilendirme hatası oluştu.</string>
<string name="error_authorization_denied">Kimlik doğrulama reddedildi.</string>
<string name="error_retrieving_oauth_token">Giriş jetonu alınamadı.</string>
<string name="error_retrieving_oauth_token">Giriş tokenı alınamadı.</string>
<string name="error_compose_character_limit">İleti çok uzun!</string>
<string name="error_image_upload_size">Dosya 8MB\'ten küçük olmalı.</string>
<string name="error_video_upload_size">Video dosyaları 40 MBden az olmalıdır.</string>
<string name="error_image_upload_size">Dosya 8MB\'den küçük olmalı.</string>
<string name="error_video_upload_size">Video dosyaları 40MBden küçük olmalıdır.</string>
<string name="error_media_upload_type">Bu tür bir dosya yüklenemiyor.</string>
<string name="error_media_upload_opening">Dosya açılamadı.</string>
<string name="error_media_upload_permission">Medya okuma izni gerekiyor.</string>
<string name="error_media_upload_permission">Medya erişim izni gerekiyor.</string>
<string name="error_media_download_permission">Medya yazma izni gerekiyor.</string>
<string name="error_media_upload_image_or_video">Aynı iletiye hem video hem resim eklenemez.</string>
<string name="error_media_upload_sending">Yükleme başarsız.</string>
@ -34,7 +34,7 @@
<string name="title_favourites">Favoriler</string>
<string name="title_mutes">Sesize alınmış kullanıcılar</string>
<string name="title_blocks">Engellenmiş kullanıcılar</string>
<string name="title_follow_requests">Takip Etme İstekleri</string>
<string name="title_follow_requests">Takip İstekleri</string>
<string name="title_edit_profile">Profil düzenle</string>
<string name="title_saved_toot">Taslaklar</string>
<string name="title_licenses">Lisanslar</string>
@ -42,27 +42,27 @@
<string name="status_boosted_format">%s yineledi</string>
<string name="status_sensitive_media_title">Hasas içerik</string>
<string name="status_media_hidden_title">Gizlenmiş medya</string>
<string name="status_sensitive_media_directions">Görüntülemek için tıklayın</string>
<string name="status_sensitive_media_directions">Görüntülemek için dokunun</string>
<string name="status_content_warning_show_more">Daha Fazla</string>
<string name="status_content_warning_show_less">Daha Az</string>
<string name="status_content_warning_show_less">Daha az</string>
<string name="status_content_show_more">Genişlet</string>
<string name="status_content_show_less">Daralt</string>
<string name="message_empty">Burada hiçbir şey yok.</string>
<string name="footer_empty">Burada henüz hiçbir şey yok. Yenilemek için aşağıya çekin!</string>
<string name="notification_reblog_format">%s iletini yineledi</string>
<string name="notification_favourite_format">%s ileti favorilerine ekledi</string>
<string name="notification_favourite_format">%s iletini favorilerine ekledi</string>
<string name="notification_follow_format">%s seni takip etti</string>
<string name="report_username_format">\@%s bildir</string>
<string name="report_comment_hint">Daha fazla yorum?</string>
<string name="action_quick_reply">Hızlı Cevapla</string>
<string name="action_reply">Yanıtla</string>
<string name="action_reblog">Yinele</string>
<string name="action_favourite">Favori</string>
<string name="action_favourite">Favorile</string>
<string name="action_more">Daha fazla</string>
<string name="action_compose">Oluştur</string>
<string name="action_login">Mastodon ile giriş yap</string>
<string name="action_logout">Çıkış Yap</string>
<string name="action_logout_confirm">%1$s hesabından çıkmak istediğinize emin misiniz\?</string>
<string name="action_logout_confirm">%1$s hesabından çıkmak istediğine emin misin\?</string>
<string name="action_follow">Takip et</string>
<string name="action_unfollow">Takibi bırak</string>
<string name="action_block">Engelle</string>
@ -107,7 +107,7 @@
<string name="download_image">%1$s indiriliyor</string>
<string name="action_copy_link">Bağlantıyı kopyala</string>
<string name="action_open_as">Farklı aç %s</string>
<string name="action_share_as">... olarak paylaş</string>
<string name="action_share_as">Farklı paylaş…</string>
<string name="send_status_link_to">İletinin adresini paylaş…</string>
<string name="send_status_content_to">İletiyi paylaş…</string>
<string name="send_media_to">Medyayı paylaş…</string>
@ -119,7 +119,7 @@
<string name="hint_domain">Hangi sunucu?</string>
<string name="hint_compose">Neler oluyor?</string>
<string name="hint_content_warning">İçerik uyarısı</string>
<string name="hint_display_name">Görünen ad</string>
<string name="hint_display_name">Görüntülenecek isim</string>
<string name="hint_note">Biyo</string>
<string name="hint_search">Ara…</string>
<string name="search_no_results">Sonuç bulunamadı</string>
@ -128,33 +128,33 @@
<string name="label_header">Başlık</string>
<string name="link_whats_an_instance">Sunucu nedir?</string>
<string name="login_connection">Bağlantı kuruluyor…</string>
<string name="dialog_whats_an_instance">Burada her hangi bir Mastodon sunucusunun adresi (mastodon.social, icosahedron.website, social.tchncs.de, ve <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">daha fazla!</a>) girilebiliri.
<string name="dialog_whats_an_instance">Burada her hangi bir Mastodon sunucusunun adresi (mastodon.social, icosahedron.website, social.tchncs.de, ve <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">daha fazlasını</a>) girebilirsin!
\n
\nEğer hesabınız henüz yok ise katılmak istediğiniz sunucunun adresini girerek hesap oluşturabilirsiniz
\nHenüz hesabın yok ise katılmak istediğin sunucunun adresini girerek hesap oluşturabilirsin
\n
\nHer bir sunucu kendi hesap kayıtlarını tutar ancak diğer sunucularda bulunan insanlarla aynı sitedeymişçesine iletişime geçip takip edebilirsiniz.
\nHer bir sunucu kendi hesap kayıtlarını tutar ancak diğer sunucularda bulunan insanlarla aynı sitedeymişçesine iletişime geçip takip edebilirsin.
\n
\nDaha fazla bilgi için <a href="https://mastodon.social/about">mastodon.social</a>. </string>
<string name="dialog_title_finishing_media_upload">Medya Yükleme Bitiriliyor</string>
<string name="dialog_title_finishing_media_upload">Medya Yükleme Tamamlanıyor</string>
<string name="dialog_message_uploading_media">Yükleniyor…</string>
<string name="dialog_download_image">İndir</string>
<string name="dialog_message_cancel_follow_request">Takip isteğini iptal et?</string>
<string name="dialog_unfollow_warning">Takibi bırak?</string>
<string name="dialog_delete_toot_warning">Bu iletiyi silmek istiyor musunuz\?</string>
<string name="visibility_public">Genel: Genel zaman çizelgelerine gönder</string>
<string name="visibility_unlisted">Listelenmemiş: Genel zaman çizelgelerinde gösterme</string>
<string name="visibility_private">Özel: Sadece takipçiler gönder</string>
<string name="visibility_direct">Doğrudan: Sadece bahsedilen kullanıcılara gönder</string>
<string name="dialog_message_cancel_follow_request">Takip isteği iptal edilsin mi\?</string>
<string name="dialog_unfollow_warning">Takibi bırakmak istiyor musun\?</string>
<string name="dialog_delete_toot_warning">Bu iletiyi silmek istiyor musun\?</string>
<string name="visibility_public">Genel: Herkese açık zaman çizelgesinde göster</string>
<string name="visibility_unlisted">Liste dışı: Herkese açık zaman çizelgelerinde gösterme</string>
<string name="visibility_private">Takipçiler: Sadece takipçilere göster</string>
<string name="visibility_direct">Doğrudan: Sadece bahsedilen kullanıcılara göster</string>
<string name="pref_title_edit_notification_settings">Bildirimler</string>
<string name="pref_title_notifications_enabled">Bildirimler</string>
<string name="pref_title_notification_alerts">Uyarılar</string>
<string name="pref_title_notification_alert_sound">Sesli uyarı</string>
<string name="pref_title_notification_alert_vibrate">Titreşim ile uyarı</string>
<string name="pref_title_notification_alert_sound">Sesle bildir</string>
<string name="pref_title_notification_alert_vibrate">Titreşimle bildir</string>
<string name="pref_title_notification_alert_light">Bildirim ışığıyla bildir</string>
<string name="pref_title_notification_filters">Beni bildir</string>
<string name="pref_title_notification_filter_mentions">Bahsedilince</string>
<string name="pref_title_notification_filter_follows">Takip edilince</string>
<string name="pref_title_notification_filter_reblogs">İletilerim yinelenince</string>
<string name="pref_title_notification_filters">Bildirim ayarları</string>
<string name="pref_title_notification_filter_mentions">bahsedilince</string>
<string name="pref_title_notification_filter_follows">takip edilince</string>
<string name="pref_title_notification_filter_reblogs">iletilerim yinelenince</string>
<string name="pref_title_notification_filter_favourites">iletilerim favorilenince</string>
<string name="pref_title_appearance_settings">Görünüm</string>
<string name="pref_title_app_theme">Uygulama Teması</string>
@ -164,7 +164,7 @@
<string name="app_theme_black">Siyah</string>
<string name="app_theme_auto">Gün batımında otomatik</string>
<string name="pref_title_browser_settings">Tarayıcı</string>
<string name="pref_title_custom_tabs">Chrome Özel Sekmelerini Kullan</string>
<string name="pref_title_custom_tabs">Tarayıcı Özel Sekmelerini Kullan</string>
<string name="pref_title_hide_follow_button">Kaydırırken yeni ileti düğmesi gizlensin</string>
<string name="pref_title_status_filter">Zaman çizelgesi filtreleme</string>
<string name="pref_title_status_tabs">Sekmeler</string>
@ -177,26 +177,26 @@
<string name="pref_title_http_proxy_server">HTTP ağ vekili sunucusu</string>
<string name="pref_title_http_proxy_port">HTTP ağ vekili bağlantı noktası</string>
<string name="pref_default_post_privacy">Varsayılan ileti gizliliği</string>
<string name="pref_default_media_sensitivity">Her zaman hassas olarak işaretle</string>
<string name="pref_default_media_sensitivity">Medyaları her zaman hassas olarak işaretle</string>
<string name="pref_publishing">Yayınlama (sunucuyla eşitlenir)</string>
<string name="pref_failed_to_sync">Ayarlar senkronize edilemedi</string>
<string name="post_privacy_public">Herkese açık</string>
<string name="post_privacy_unlisted">Liste dışı</string>
<string name="post_privacy_followers_only">Sadece takip edenler</string>
<string name="post_privacy_followers_only">Sadece takipçiler</string>
<string name="pref_status_text_size">İleti metin boyutu</string>
<string name="status_text_size_smallest">En küçük</string>
<string name="status_text_size_smallest">Çok küçük</string>
<string name="status_text_size_small">Küçük</string>
<string name="status_text_size_medium">Orta</string>
<string name="status_text_size_large">Büyük</string>
<string name="status_text_size_largest">En büyük</string>
<string name="notification_mention_name">Yeni Bahsedilenler</string>
<string name="notification_mention_descriptions">Yeni bahsedilenler hakkında bildirim</string>
<string name="notification_follow_name">Yeni Takipçiler</string>
<string name="notification_follow_description">Yeni takipçiler hakkında bildirim</string>
<string name="status_text_size_largest">Çok büyük</string>
<string name="notification_mention_name">Senden bahsedildi</string>
<string name="notification_mention_descriptions">Senden bahsedenler hakkında bildirim</string>
<string name="notification_follow_name">Yeni Takipçi</string>
<string name="notification_follow_description">Yeni takipçi hakkında bildirim</string>
<string name="notification_boost_name">Yinelemeler</string>
<string name="notification_boost_description">İletilerin yinelendiğinde</string>
<string name="notification_favourite_name">Favoriler</string>
<string name="notification_favourite_description">İletilerin favori olarak işaretlendiğinde</string>
<string name="notification_favourite_description">İletilerim favori olarak işaretlendiğinde</string>
<string name="notification_mention_format">%s senden bahsetti</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s ve %4$d diğerleri</string>
<string name="notification_summary_medium">%1$s, %2$s ve %3$s</string>
@ -212,9 +212,8 @@
to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html
* the url can be changed to link to the localized version of the license.
-->
<string name="about_project_site"> Proje Web sitesi:\n
https://accelf.net/yuito
</string>
<string name="about_project_site">Projenin internet sitesi:
\n https://accelf.net/yuito</string>
<string name="about_bug_feature_request_site">Hata raporları &amp; özellik istekleri:
\n https://github.com/accelforce/Yuito/issues</string>
<string name="about_tusky_account">Yuito\'in Profili</string>
@ -223,19 +222,18 @@
<string name="status_media_images">Görseller</string>
<string name="status_media_video">Video</string>
<string name="state_follow_requested">Takip istekleri</string>
<string name="no_content">içerik yok</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%dy</string>
<string name="abbreviated_in_days">%dd</string>
<string name="abbreviated_in_hours">%dh</string>
<string name="abbreviated_in_minutes">%dm</string>
<string name="abbreviated_in_seconds">%ds</string>
<string name="abbreviated_years_ago">%dy yıl önce</string>
<string name="abbreviated_days_ago">%dd gün önce</string>
<string name="abbreviated_hours_ago">%dh saat önce</string>
<string name="abbreviated_minutes_ago">%dm dk önce</string>
<string name="abbreviated_seconds_ago">%ds sn önce</string>
<string name="follows_you">Sizi takip ediyor</string>
<string name="abbreviated_years_ago">%dy</string>
<string name="abbreviated_days_ago">%dd</string>
<string name="abbreviated_hours_ago">%dh</string>
<string name="abbreviated_minutes_ago">%dm</string>
<string name="abbreviated_seconds_ago">%ds</string>
<string name="follows_you">Seni takip ediyor</string>
<string name="pref_title_alway_show_sensitive_media">Her zaman hassas içerikleri göster</string>
<string name="title_media">Medya</string>
@ -247,10 +245,10 @@
<string name="title_list_timeline">Zaman çizelgesini listele</string>
<string name="compose_active_account_description">%1$s hesabıyla gönderiliyor</string>
<string name="hint_describe_for_visually_impaired">Görme engelliler için açıklama
\n(%d karakter limiti)</string>
\n(%d karakter sınırı)</string>
<string name="action_set_caption">Başlık belirle</string>
<string name="action_remove">Kaldır</string>
<string name="lock_account_label">Hesabı Gizle</string>
<string name="lock_account_label">Hesabı Kilitle</string>
<string name="lock_account_label_description">Aktif edilirse takipçileri elle onaylamanız gerekir</string>
<string name="compose_save_draft">Taslaklara kaydedilsin mi\?</string>
<string name="send_toot_notification_title">İleti gönderiliyor…</string>
@ -261,17 +259,17 @@
<string name="action_compose_shortcut">Oluştur</string>
<string name="error_no_custom_emojis">%s sunucunuzun özel emoji seti yok</string>
<string name="copy_to_clipboard_success">Panoya kopyalandı</string>
<string name="emoji_style">Emoji Stili</string>
<string name="emoji_style">Emoji tipi</string>
<string name="system_default">Sistem varsayılanı</string>
<string name="download_fonts">Emoji setini kullanabilmek için indirmeniz gerekli</string>
<string name="performing_lookup_title">Araştırılıyor…</string>
<string name="expand_collapse_all_statuses">Tüm durumları Genişlet/Daralt</string>
<string name="action_open_toot">İleti aç</string>
<string name="restart_required">Uygulamayı yeniden başlatmanız gerekiyor</string>
<string name="expand_collapse_all_statuses">Tüm iletileri genişlet/daralt</string>
<string name="action_open_toot">İletiyi</string>
<string name="restart_required">Uygulamayı yeniden başlatman gerekiyor</string>
<string name="restart_emoji">Değişikliklerin uygulanabilmesi için uygulama yeniden başlatılmalı</string>
<string name="later">Sonra</string>
<string name="restart">Yeniden başlat</string>
<string name="caption_systememoji">Cihazınızın varsayılan emoji seti</string>
<string name="caption_systememoji">Cihaz varsayılan emoji seti</string>
<string name="caption_blobmoji">Android 4.4 — 7.1\'den bilinen baloncuk emojisi</string>
<string name="caption_twemoji">Mastodon\'un standart emoji seti</string>
<string name="download_failed">İndirme başarısız</string>
@ -281,7 +279,7 @@
<string name="license_apache_2">Apache Lisansı altında lisanslanmıştır (kopya aşağıda)</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>
<string name="profile_metadata_label">Profil Meta verisi</string>
<string name="profile_metadata_label">Profil meta verisi</string>
<string name="profile_metadata_add">veri ekle</string>
<string name="profile_metadata_label_label">Etiket</string>
<string name="profile_metadata_content_label">İçerik</string>
@ -290,23 +288,23 @@
<string name="unpin_action">Sabitlemeyi kaldır</string>
<string name="pin_action">Sabitle</string>
<plurals name="favs">
<item quantity="one">&lt;b&gt;%1$s&lt;/b&gt; Favori</item>
<item quantity="other">&lt;b&gt;%1$s&lt;/b&gt; Favoriler</item>
</plurals>
<item quantity="one"><b>%1$s</b> Favori</item>
<item quantity="other"><b>%1$s</b> Favori</item>
</plurals>
<plurals name="reblogs">
<item quantity="one">&lt;b&gt;%s&lt;/b&gt; Yinelenen</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; Yinelenenler</item>
<item quantity="one"><b>%s</b> Yineleme</item>
<item quantity="other"><b>%s</b> Yineleme</item>
</plurals>
<string name="title_reblogged_by">tarafından yinelendi</string>
<string name="title_favourited_by">Tarafından favorilendi</string>
<string name="title_favourited_by">favoriledi</string>
<string name="conversation_1_recipients">%1$s</string>
<string name="conversation_2_recipients">%1$s ve %2$s</string>
<string name="conversation_more_recipients">%1$s, %2$s ve %3$d daha fazlası</string>
<string name="max_tab_number_reached">%1$d maksimum sekme sayısına ulaşıldı</string>
<string name="title_domain_mutes">Gizli alanadları</string>
<string name="title_domain_mutes">Gizlenmiş alan adları</string>
<string name="action_unreblog">Yinelemeyi kaldır</string>
<string name="action_unfavourite">Favoriyi kaldır</string>
<string name="action_view_domain_mutes">Gizli alan adları</string>
<string name="action_view_domain_mutes">Gizlenmiş alan adları</string>
<string name="action_mute_domain">%s alan adını sessize al</string>
<string name="action_links">Bağlantılar</string>
<string name="action_hashtags">Hashtag\'ler</string>
@ -318,7 +316,7 @@
<string name="downloading_media">Medya indiriliyor</string>
<string name="mute_domain_warning">%s alan adınından gelen her şeyi engellemek istediğinizden emin misiniz\? Bu alan adından gelen içeriği herhangi bir genel zaman çizelgesinde veya bildirimlerinizde göremezsiniz. Bu alan adındaki takipçileriniz de kaldırılacak.</string>
<string name="pref_title_notification_filter_poll">Anket sona erince</string>
<string name="pref_title_notification_filter_poll">anket sona erince</string>
<string name="pref_title_timeline_filters">Filtreler</string>
<string name="app_theme_system">Sistem tasarımını kullan</string>
@ -336,7 +334,7 @@
<string name="filter_edit_dialog_title">Filtreyi düzenle</string>
<string name="filter_dialog_remove_button">Kaldır</string>
<string name="filter_dialog_update_button">Güncelle</string>
<string name="filter_dialog_whole_word">Tüm dünya</string>
<string name="filter_dialog_whole_word">Tüm kelime</string>
<string name="filter_dialog_whole_word_description">Anahtar kelime veya kelime öbeği yalnızca alfasayısal olduğunda, yalnızca tüm sözcükle eşleşirse uygulanır</string>
<string name="filter_add_description">Filtrelenecek ifade</string>
@ -347,7 +345,7 @@
<string name="action_rename_list">Listeyi yeniden adlandır</string>
<string name="action_delete_list">Listeyi sil</string>
<string name="action_edit_list">Listeyi düzenle</string>
<string name="hint_search_people_list">Takip ettiğiniz kişileri ara</string>
<string name="hint_search_people_list">Takip ettiğim kişilerde ara</string>
<string name="action_add_to_list">Listeye hesap ekle</string>
<string name="action_remove_from_list">Hesabı listeden kaldır</string>
@ -376,7 +374,7 @@
<string name="compose_shortcut_long_label">İleti Oluştur</string>
<string name="compose_shortcut_short_label">Oluştur</string>
<string name="notification_clear_text">Tüm bildirimlerinizi kalıcı olarak silmek istediğinizden emin misiniz\?</string>
<string name="notification_clear_text">Tüm bildirimleri kalıcı olarak silmek istediğinden emin misin\?</string>
<string name="compose_preview_image_description">%s görüntüsü için eylemler</string>
<string name="poll_info_format"> <!-- 15 oy • 1 saat kaldı --> %1$s • %2$s</string>
@ -388,8 +386,8 @@
<string name="poll_info_closed">kapandı</string>
<string name="poll_vote">Oy</string>
<string name="poll_ended_voted">Oy verdiğiniz bir anket sona erdi</string>
<string name="poll_ended_created">Oluşturduğunuz bir anket sona erdi</string>
<string name="poll_ended_voted">Oy verdiğin bir anket sona erdi</string>
<string name="poll_ended_created">Oluşturduğun bir anket sona erdi</string>
<plurals name="poll_timespan_days">
<item quantity="one">%d gün</item>
@ -414,7 +412,7 @@
<string name="report_sent_success">\@%s bildirildi</string>
<string name="hint_additional_info">Ek yorumlar</string>
<string name="report_remote_instance">%s adresine ilet</string>
<string name="failed_fetch_statuses">Durumlar getirilemedi</string>
<string name="failed_fetch_statuses">İletiler alınamadı</string>
<string name="report_description_1">"Bildirim sunucu yöneticinize gönderilecektir. Bu hesabı neden bildirdiğinizle ilgili açıklama yapabilirsiniz:"</string>
<string name="report_description_remote_instance">Hesap başka bir sunucuda. Raporun anonim bir kopyasını da oraya gönderilsin mi\?</string>
@ -430,7 +428,7 @@
<string name="action_bookmark">Yerimi</string>
<string name="action_edit">Düzenle</string>
<string name="action_delete_and_redraft">Sil ve düzenle</string>
<string name="action_view_bookmarks">Yer imleri</string>
<string name="action_view_bookmarks">Yerimleri</string>
<string name="action_add_poll">Anket ekle</string>
<string name="action_access_scheduled_toot">Zamanlanmış iletiler</string>
<string name="action_schedule_toot">İleti zamanla</string>
@ -455,6 +453,21 @@
<string name="add_poll_choice">Seçenek ekle</string>
<string name="poll_allow_multiple_choices">Çoklu seçim</string>
<string name="edit_poll">Düzenle</string>
<string name="replying_to">\@%s olarak yanıtla</string>
<string name="replying_to">Yanıtla @%s</string>
<string name="profile_badge_bot_text">Bot</string>
</resources>
<string name="confirmation_domain_unmuted">%s gizleme</string>
<string name="mute_domain_warning_dialog_ok">Alan adından herşeyi gizle</string>
<string name="gradient_for_media">Gizli medya için renkli sansür uygula</string>
<string name="pref_title_alway_open_spoiler">Hassas içerikleri göster</string>
<string name="poll_info_time_absolute">bitiş %s</string>
<string name="failed_report">Bildirilemedi</string>
<string name="poll_new_choice_hint">Seçenek %d</string>
<string name="post_lookup_error_format">%s ileti aranırken hata oluştu</string>
<string name="no_saved_status">Hiç taslağınız yok.</string>
<string name="no_scheduled_status">Hiç planlanmış durumun yok.</string>
</resources>

View File

@ -286,8 +286,6 @@
<string name="state_follow_requested">已发送关注请求</string>
<string name="no_content">没有内容</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d 年内</string>
<string name="abbreviated_in_days">%d 天内</string>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="error_generic">應用程式出現異常</string>
<string name="error_network">網絡請求出錯,請檢查互聯網連接並重試</string>
@ -281,8 +281,6 @@
<string name="state_follow_requested">已請求關注</string>
<string name="no_content">沒有內容</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d 年內</string>
<string name="abbreviated_in_days">%d 天內</string>
@ -385,11 +383,11 @@
<string name="pin_action">置頂</string>
<plurals name="favs">
<item quantity="other">&lt;b>%1$s&lt;/b> 次收藏</item>
<item quantity="other">&lt;b&gt;%1$s&lt;/b&gt; 次收藏</item>
</plurals>
<plurals name="reblogs">
<item quantity="other">&lt;b>%s&lt;/b> 次轉嘟</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; 次轉嘟</item>
</plurals>
<string name="title_reblogged_by">轉嘟</string>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="error_generic">應用程式出現異常</string>
<string name="error_network">網絡請求出錯,請檢查互聯網連接並重試</string>
@ -281,8 +281,6 @@
<string name="state_follow_requested">已請求關注</string>
<string name="no_content">沒有內容</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d 年內</string>
<string name="abbreviated_in_days">%d 天內</string>
@ -385,11 +383,11 @@
<string name="pin_action">置頂</string>
<plurals name="favs">
<item quantity="other">&lt;b>%1$s&lt;/b> 次收藏</item>
<item quantity="other">&lt;b&gt;%1$s&lt;/b&gt; 次收藏</item>
</plurals>
<plurals name="reblogs">
<item quantity="other">&lt;b>%s&lt;/b> 次轉嘟</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; 次轉嘟</item>
</plurals>
<string name="title_reblogged_by">轉嘟</string>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="error_generic">应用程序出现异常</string>
<string name="error_network">网络请求出错,请检查互联网连接并重试</string>
@ -286,8 +286,6 @@
<string name="state_follow_requested">已发送关注请求</string>
<string name="no_content">没有内容</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d 年内</string>
<string name="abbreviated_in_days">%d 天内</string>

View File

@ -280,8 +280,6 @@
<string name="state_follow_requested">已請求關注</string>
<string name="no_content">沒有內容</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d 年內</string>
<string name="abbreviated_in_days">%d 天內</string>

View File

@ -12,6 +12,7 @@
<string name="error_compose_character_limit">The status is too long!</string>
<string name="error_image_upload_size">The file must be less than 8MB.</string>
<string name="error_video_upload_size">Video files must be less than 40MB.</string>
<string name="error_audio_upload_size">Audio files must be less than 40MB.</string>
<string name="error_media_upload_type">That type of file cannot be uploaded.</string>
<string name="error_media_upload_opening">That file could not be opened.</string>
<string name="error_media_upload_permission">Permission to read media is required.</string>

View File

@ -198,6 +198,152 @@ class ComposeActivityTest {
assertEquals(activity.calculateTextLength(), additionalContent.length + (ComposeActivity.MAXIMUM_URL_LENGTH * 2))
}
@Test
fun whenSelectionIsEmpty_specialTextIsInsertedAtCaret() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
editor.setText("Some text")
for (caretIndex in listOf(9, 1, 0)) {
editor.setSelection(caretIndex)
activity.prependSelectedWordsWith(insertText)
// Text should be inserted at caret
assertEquals("Unexpected value at ${caretIndex}", insertText, editor.text.substring(caretIndex, caretIndex + insertText.length))
// Caret should be placed after inserted text
assertEquals(caretIndex + insertText.length, editor.selectionStart)
assertEquals(caretIndex + insertText.length, editor.selectionEnd)
}
}
@Test
fun whenSelectionDoesNotIncludeWordBreak_noSpecialTextIsInserted() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 1
val selectionEnd = 4
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "ome"
activity.prependSelectedWordsWith(insertText)
// Text and selection should be unmodified
assertEquals(originalText, editor.text.toString())
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesWordBreaks_startsOfAllWordsArePrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "one two three four"
val selectionStart = 2
val originalSelectionEnd = 15
val modifiedSelectionEnd = 18
editor.setText(originalText)
editor.setSelection(selectionStart, originalSelectionEnd) // "e two three f"
activity.prependSelectedWordsWith(insertText)
// text should be inserted at word starts inside selection
assertEquals("one #two #three #four", editor.text.toString())
// selection should be expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(modifiedSelectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesEnd_textIsNotAppended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 7
val selectionEnd = 9
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "xt"
activity.prependSelectedWordsWith(insertText)
// Text and selection should be unmodified
assertEquals(originalText, editor.text.toString())
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesStartAndStartIsAWord_textIsPrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 0
val selectionEnd = 3
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "Som"
activity.prependSelectedWordsWith(insertText)
// Text should be inserted at beginning
assert(editor.text.startsWith(insertText))
// selection should be expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesStartAndStartIsNotAWord_textIsNotPrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = " Some text"
val selectionStart = 0
val selectionEnd = 1
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // " "
activity.prependSelectedWordsWith(insertText)
// Text and selection should be unmodified
assertEquals(originalText, editor.text.toString())
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionBeginsAtWordStart_textIsPrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 5
val selectionEnd = 9
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "text"
activity.prependSelectedWordsWith(insertText)
// Text is prepended
assertEquals("Some #text", editor.text.toString())
// Selection is expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
}
@Test
fun whenSelectionEndsAtWordStart_textIsAppended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 1
val selectionEnd = 5
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "ome "
activity.prependSelectedWordsWith(insertText)
// Text is prepended
assertEquals("Some #text", editor.text.toString())
// Selection is expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
}
private fun clickUp() {
val menuItem = RoboMenuItem(android.R.id.home)
activity.onOptionsItemSelected(menuItem)

View File

@ -1,9 +1,9 @@
Tusky v9.0
- Vous pouvez désormais créer des sondages depuis Tusky
- Vous pouvez créer des sondages depuis Tusky
- Recherche améliorée
- Nouvelle option dans les préférences du compte pour toujours étendre les avertissements de contenu
- Les avatars dans le menu de navigation ont désormais une forme de
- Il est désormais possible de signaler des utilisateurs même si ils n'ont jamais posté de status
- Tusky refuse désormais de se connecté via les connections cleartext sur Android 6+
- Plein de petite corrections de bugs et d'améliorations
- Les avatars dans le menu de navigation ont désormais une forme arrondie
- Il est désormais possible de signaler des utilisateurs même si ils nont jamais publié de status
- Tusky refusera désormais de se connecter en texte clair sur Android 6+
- Plein de petite corrections de bugs et daméliorations

View File

@ -1 +1 @@
En klient med stöd för flera konton det sociala nätverket Mastodon
En klient med stöd för flera konton för det sociala nätverket Mastodon