diff --git a/.github/workflows/android-ci-comment.yml b/.github/workflows/android-ci-comment.yml new file mode 100644 index 000000000..64422e284 --- /dev/null +++ b/.github/workflows/android-ci-comment.yml @@ -0,0 +1,84 @@ +name: Android CI Comment + +on: [pull_request_target] + +permissions: + issues: write + +jobs: + comment: + name: Comment on PR with APK links + runs-on: ubuntu-latest + steps: + - name: Checkout base branch + uses: actions/checkout@v3 + with: + ref: ${{ github.base_ref }} + + - name: Download Run ID Artifact + uses: actions/download-artifact@v4 + with: + name: run-id + + - name: Read Run ID + id: read-run-id + run: echo "RUN_ID=$(cat run_id.txt)" >> $GITHUB_ENV + + - name: Comment on PR with APK download links + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@v6 + with: + script: | + try { + const token = process.env.GH_TOKEN; + if (!token) { + throw new Error('GITHUB_TOKEN is not set.'); + } + + const runId = "${{ env.RUN_ID }}"; + if (!runId) { + throw new Error('Run ID not found.'); + } + + const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + + if (!artifacts || artifacts.length === 0) { + console.log('No artifacts found for this workflow run.'); + return; + } + + const betaArtifact = artifacts.find(artifact => artifact.name === "betaDebugAPK"); + const prodArtifact = artifacts.find(artifact => artifact.name === "prodDebugAPK"); + + if (!betaArtifact || !prodArtifact) { + console.log('Could not find both Beta and Prod APK artifacts.'); + console.log('Available artifacts:', artifacts.map(a => a.name).join(', ')); + return; + } + + const betaDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${runId}/artifacts/${betaArtifact.id}`; + const prodDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${runId}/artifacts/${prodArtifact.id}`; + + const commentBody = ` + 📱 **APK for pull request is ready to see the changes** 📱 + - [Download Beta APK](${betaDownloadUrl}) + - [Download Prod APK](${prodDownloadUrl}) + `; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: commentBody + }); + + console.log('Successfully posted comment with APK download links'); + } catch (error) { + console.error('Error in PR comment creation:', error); + core.setFailed(`Workflow failed: ${error.message}`); + } diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index bcbef52fd..8a744bc0a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -3,7 +3,6 @@ name: Android CI on: [push, pull_request, workflow_dispatch] permissions: - pull-requests: write contents: read actions: read @@ -107,64 +106,14 @@ jobs: with: name: prodDebugAPK path: app/build/outputs/apk/prod/debug/app-*.apk - - - name: Comment on PR with APK download links + + - name: Store Workflow Run ID if: github.event_name == 'pull_request' - uses: actions/github-script@v6 + run: echo "${{ github.run_id }}" > run_id.txt + + - name: Upload Run ID as Artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - try { - const token = process.env.GITHUB_TOKEN; - if (!token) { - throw new Error('GITHUB_TOKEN is not set. Please check workflow permissions.'); - } - - - const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - - if (!artifacts || artifacts.length === 0) { - console.log('No artifacts found for this workflow run.'); - return; - } - - const betaArtifact = artifacts.find(artifact => artifact.name === "betaDebugAPK"); - const prodArtifact = artifacts.find(artifact => artifact.name === "prodDebugAPK"); - - if (!betaArtifact || !prodArtifact) { - console.log('Could not find both Beta and Prod APK artifacts.'); - console.log('Available artifacts:', artifacts.map(a => a.name).join(', ')); - return; - } - - const betaDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${context.runId}/artifacts/${betaArtifact.id}`; - const prodDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${context.runId}/artifacts/${prodArtifact.id}`; - - const commentBody = ` - 📱 **APK for pull request is ready to see the changes** 📱 - - [Download Beta APK](${betaDownloadUrl}) - - [Download Prod APK](${prodDownloadUrl}) - `; - - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: commentBody - }); - - console.log('Successfully posted comment with APK download links'); - } catch (error) { - console.error('Error in PR comment creation:', error); - if (error.message.includes('GITHUB_TOKEN')) { - core.setFailed('Missing or invalid GITHUB_TOKEN. Please check repository secrets configuration.'); - } else if (error.status === 403) { - core.setFailed('Permission denied. Please check workflow permissions in repository settings.'); - } else { - core.setFailed(`Workflow failed: ${error.message}`); - } - } + name: run-id + path: run_id.txt diff --git a/app/build.gradle b/app/build.gradle index 2bde0d4f1..dbb5458bd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,8 +175,8 @@ dependencies { testImplementation "androidx.work:work-testing:$work_version" //Glide - implementation 'com.github.bumptech.glide:glide:4.12.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' + implementation 'com.github.bumptech.glide:glide:4.16.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' kaptTest "androidx.databinding:databinding-compiler:8.0.2" kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2" diff --git a/app/src/main/java/fr/free/nrw/commons/Media.kt b/app/src/main/java/fr/free/nrw/commons/Media.kt index 025302cfd..293321c27 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.kt +++ b/app/src/main/java/fr/free/nrw/commons/Media.kt @@ -90,6 +90,41 @@ class Media constructor( captions = captions, ) + constructor( + captions: Map, + categories: List?, + filename: String?, + fallbackDescription: String?, + author: String?, + user: String?, + dateUploaded: Date? = Date(), + license: String? = null, + licenseUrl: String? = null, + imageUrl: String? = null, + thumbUrl: String? = null, + coordinates: LatLng? = null, + descriptions: Map = emptyMap(), + depictionIds: List = emptyList(), + categoriesHiddenStatus: Map = emptyMap() + ) : this( + pageId = UUID.randomUUID().toString(), + filename = filename, + fallbackDescription = fallbackDescription, + dateUploaded = dateUploaded, + author = author, + user = user, + categories = categories, + captions = captions, + license = license, + licenseUrl = licenseUrl, + imageUrl = imageUrl, + thumbUrl = thumbUrl, + coordinates = coordinates, + descriptions = descriptions, + depictionIds = depictionIds, + categoriesHiddenStatus = categoriesHiddenStatus + ) + /** * Gets media display title * @return Media title diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt index 40c9785db..77ff1df0c 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt @@ -16,6 +16,7 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.ArrayAdapter import android.widget.Button @@ -405,9 +406,14 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C * Gets the height of the frame layout as soon as the view is ready and updates aspect ratio * of the picture. */ - view.post { - frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight - updateAspectRatio(binding.mediaDetailScrollView.width) + view.post{ + val width = binding.mediaDetailScrollView.width + if (width > 0) { + frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight + updateAspectRatio(width) + } else { + view.postDelayed({ updateAspectRatio(binding.root.width) }, 1) + } } return view diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 545e96624..b4b4e9c57 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -185,10 +185,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple * or a fragment */ private void initProvider() { - if (getParentFragment() != null) { + if (getParentFragment() instanceof MediaDetailProvider) { provider = (MediaDetailProvider) getParentFragment(); - } else { + } else if (getActivity() instanceof MediaDetailProvider) { provider = (MediaDetailProvider) getActivity(); + } else { + throw new ClassCastException("Parent must implement MediaDetailProvider"); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index d2e73441d..12ab600b2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -39,11 +39,13 @@ import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.snackbar.Snackbar @@ -51,6 +53,7 @@ import com.jakewharton.rxbinding2.view.RxView import com.jakewharton.rxbinding3.appcompat.queryTextChanges import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.MapController.NearbyPlacesInfo +import fr.free.nrw.commons.Media import fr.free.nrw.commons.R import fr.free.nrw.commons.Utils import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao @@ -67,6 +70,10 @@ import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermission import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType import fr.free.nrw.commons.location.LocationUpdateListener +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider +import fr.free.nrw.commons.navtab.NavTab import fr.free.nrw.commons.nearby.BottomSheetAdapter import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener import fr.free.nrw.commons.nearby.CheckBoxTriStates @@ -118,17 +125,25 @@ import timber.log.Timber import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Named import kotlin.concurrent.Volatile -class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmentContract.View, - WikidataP18EditListener, LocationUpdateListener, LocationPermissionCallback, ItemClickListener { +class NearbyParentFragment : CommonsDaggerSupportFragment(), + NearbyParentFragmentContract.View, + WikidataP18EditListener, + LocationUpdateListener, + LocationPermissionCallback, + ItemClickListener, + MediaDetailPagerFragment.MediaDetailProvider { var binding: FragmentNearbyParentBinding? = null val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver { @@ -163,6 +178,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen @Named("default_preferences") lateinit var applicationKvStore: JsonKvStore + @Inject + lateinit var mediaClient: MediaClient + + lateinit var mediaDetails: MediaDetailPagerFragment + + lateinit var media: Media + @Inject lateinit var bookmarkLocationDao: BookmarkLocationsDao @@ -716,6 +738,10 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen presenter?.attachView(this) registerNetworkReceiver() + binding?.coordinatorLayout?.visibility = View.VISIBLE + binding?.map?.setMultiTouchControls(true) + binding?.map?.isClickable = true + if (isResumed && (activity as? MainActivity)?.activeFragment == ActiveFragment.NEARBY) { if (activity?.let { locationPermissionsHelper?.checkLocationPermission(it) } == true) { locationPermissionGranted() @@ -1853,7 +1879,31 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } fun backButtonClicked(): Boolean { - return presenter!!.backButtonClicked() + if (::mediaDetails.isInitialized && mediaDetails.isVisible) { + removeFragment(mediaDetails) + + binding?.coordinatorLayout?.visibility = View.VISIBLE + binding?.map?.setMultiTouchControls(true) + binding?.map?.isClickable = true + + val transaction = childFragmentManager.beginTransaction() + val fragmentContainer = childFragmentManager.findFragmentById(R.id.coordinator_layout) + + if (fragmentContainer != null) { + transaction.show(fragmentContainer) + } + + transaction.commit() + childFragmentManager.executePendingTransactions() + + (activity as? MainActivity)?.showTabs() + (activity as? MainActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(false) + return true + } else { + (activity as? MainActivity)?.setSelectedItemId(NavTab.NEARBY.code()) + } + + return presenter?.backButtonClicked() ?: false } override fun onLocationPermissionDenied(toastMessage: String) { @@ -2299,7 +2349,23 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen bottomSheetAdapter!!.setClickListener(this) binding!!.bottomSheetDetails.bottomSheetRecyclerView.adapter = bottomSheetAdapter updateBookmarkButtonImage(selectedPlace!!) - binding!!.bottomSheetDetails.icon.setImageResource(selectedPlace!!.label.icon) + + selectedPlace?.pic?.substringAfterLast("/")?.takeIf { it.isNotEmpty() }?.let { imageName -> + Glide.with(binding!!.bottomSheetDetails.icon.context) + .clear(binding!!.bottomSheetDetails.icon) + Glide.with(binding!!.bottomSheetDetails.icon.context) + .load("https://commons.wikimedia.org/wiki/Special:Redirect/file/$imageName?width=25") + .placeholder(fr.free.nrw.commons.R.drawable.ic_refresh_24dp_nearby) + .error(selectedPlace!!.label.icon) + .into(binding!!.bottomSheetDetails.icon) + + binding!!.bottomSheetDetails.icon.setOnClickListener { + handleMediaClick(imageName) + } + } ?: run { + binding!!.bottomSheetDetails.icon.setImageResource(selectedPlace!!.label.icon) + } + binding!!.bottomSheetDetails.title.text = selectedPlace!!.name binding!!.bottomSheetDetails.category.text = selectedPlace!!.distance // Remove label since it is double information @@ -2354,6 +2420,101 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } } + private fun handleMediaClick(imageName: String) { + val decodedImageName = URLDecoder.decode(imageName, StandardCharsets.UTF_8.toString()) + + mediaClient.getMedia("File:$decodedImageName") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ mediaResponse -> + if (mediaResponse != null) { + // Create a Media object from the response + media = Media( + pageId = mediaResponse.pageId ?: UUID.randomUUID().toString(), + thumbUrl = mediaResponse.thumbUrl, + imageUrl = mediaResponse.imageUrl, + filename = mediaResponse.filename, + fallbackDescription = mediaResponse.fallbackDescription, + dateUploaded = mediaResponse.dateUploaded, + license = mediaResponse.license, + licenseUrl = mediaResponse.licenseUrl, + author = mediaResponse.author, + user = mediaResponse.user, + categories = mediaResponse.categories, + coordinates = mediaResponse.coordinates, + captions = mediaResponse.captions ?: emptyMap(), + descriptions = mediaResponse.descriptions ?: emptyMap(), + depictionIds = mediaResponse.depictionIds ?: emptyList(), + categoriesHiddenStatus = mediaResponse.categoriesHiddenStatus ?: emptyMap() + ) + // Remove existing fragment before showing new details + if (::mediaDetails.isInitialized && mediaDetails.isAdded) { + removeFragment(mediaDetails) + } + showMediaDetails() + } else { + Timber.e("Fetched media is null for image: $decodedImageName") + } + }, { throwable -> + Timber.e(throwable, "Error fetching media for image: $decodedImageName") + }) + } + + private fun showMediaDetails() { + binding?.map?.setMultiTouchControls(false) + binding?.map?.isClickable = false + + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + + + val transaction = childFragmentManager.beginTransaction() + + val fragmentContainer = childFragmentManager.findFragmentById(R.id.coordinator_layout) + if (fragmentContainer != null) { + transaction.hide(fragmentContainer) + } + + // Replace instead of add to ensure new fragment is used + transaction.replace(R.id.coordinator_layout, mediaDetails, "MediaDetailFragmentTag") + transaction.addToBackStack("Nearby_Parent_Fragment_Tag").commit() + childFragmentManager.executePendingTransactions() + + (activity as? MainActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + + if (mediaDetails.isAdded) { + mediaDetails.showImage(0) + } else { + Timber.e("Error: MediaDetailPagerFragment is NOT added") + } + } + + override fun getMediaAtPosition(i: Int): Media? { + return media + } + + override fun getTotalMediaCount(): Int { + return 2 + } + + override fun getContributionStateAt(position: Int): Int? { + return null + } + + override fun refreshNominatedMedia(index: Int) { + if (this::mediaDetails.isInitialized && !binding?.map?.isClickable!! == true) { + removeFragment(mediaDetails) + showMediaDetails() + } + } + + private fun removeFragment(fragment: Fragment) { + childFragmentManager + .beginTransaction() + .remove(fragment) + .commit() + childFragmentManager.executePendingTransactions() + } + private fun storeSharedPrefs(selectedPlace: Place) { applicationKvStore!!.putJson(WikidataConstants.PLACE_OBJECT, selectedPlace) val place = diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index f12c235ff..a009bd39d 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -68,10 +68,10 @@ Декъашхочун цӀе Пароль Commons Beta тӀехь хьай цӀарца чугӀо - ЧугӀо + Чувала Йицйелла пароль? ДӀайаздалар - Системин чу дахар + Системи чу валар Дехар до, собарде… титраш ​​а, йийцарш а карладохуш ду.. Дехар до, собарде… diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 4ff40afbf..91f9652c4 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -32,7 +32,7 @@ निकास स्थान चयनकर्ता जमा करें एक और विवरण जोड़ें - नया योगदान + नया योगदान जोड़ें कैमरे से योगदान जोड़ें फ़ोटो से योगदान जोड़ें पिछले योगदान गैलरी से योगदान जोड़ें diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 010e440b1..7203f3e1f 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -150,7 +150,7 @@ שינויים העלאה חיפוש קטגוריות - חפשו פריטים שהמדיה שלכם מציגה (הר, טאג\' מהאל, וכו\') + נא לחפש פריטים שהמדיה שלך מציגה (הר, טאג\' מהאל וכו\') שמירה תפריט גלישה רענון @@ -851,7 +851,7 @@ חשבון העלמת חשבון אזהרת העלמת חשבון - היעלמות היא <b>מוצא אחרון</b> וצריך <b>להשתמש בו רק כאשר אתם רוצים להפסיק לערוך לנצח</b> וגם כדי להסתיר כמה שיותר מהאסוציאציות שלכם בעבר.<br/><br/>מחיקת חשבון בויקישיתוף נעשית על ידי שינוי שם החשבון שלכם כך שאחרים לא יוכלו לזהות את התרומות שלכם בתהליך שנקרא חשבון היעלמות. <b>היעלמות אינה מבטיחה אנונימיות מוחלטת או הסרה של תרומות לפרויקטים</b>. + היעלמות היא <b>מוצא אחרון</b> וצריך להשתמש בה <b>רק כשברצונך להפסיק לערוך לתמיד</b> וגם להסתיר את הקשרים הקודמים שלך במידת האפשר.<br/><br/>מחיקת חשבון בוויקישיתוף נעשית על־ידי שינוי שם החשבון שלך כדי להסוות אותו מפני אחרים כדי שלא יוכלו לזהות את התרומות שלך בתהליך שנקרא העלמת חשבון. <b>העלמה לא מבטיחה אלמוניות מלאה או הסרת התרומות למיזמים</b>. כותרת הכותרת הועתקה ללוח ברכותינו, כל התמונות באלבום הזה הועלו או שסומנו לא להעלאה. diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 0fc6a3dc0..8c9e91ca5 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -56,7 +56,7 @@ امستنې خونديځ ته راپورته کول راپورته کول جريان لري - کارن‌نوم + کارننوم پټنوم خپل خونديځ بېټا ګڼون ته ورننوځئ ننوتل diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 6b81a5b50..9417f90ec 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -27,7 +27,6 @@ {{Identical|Submit}} {{identical|All}} - יש קצת בלבול בתרגום עם להחליף למעלה, למרות שהתרגום באופן טכני נכון, המשמעות באנגלית היא החלפה בין מצבים (במקרה הזה, החלפת מצב פעיל) אז אפשר להשתמש בהפעלה כתרגום Status text about number of uploads left.\n* %1$d represents number of uploads left, including current one See the current issue [https://phabricator.wikimedia.org/T267142 T267142] tracked in Phabricator about the <code><nowiki>|zero=</nowiki></code> option currently not supported on Translatewiki.net with the custom <code><nowiki>{{PLURAL}}</nowiki></code> rules used by this project for Android, using a non-MediaWiki syntax. {{Identical|Upload}} diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1c01761e0..802a1e350 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -4,6 +4,7 @@ * Alexandr Efremov * Av6 * Butko +* Cabadeck * DDPAT * Deltaspace * Dirruw'o @@ -177,6 +178,7 @@ Выбор категорий Искать элементы, которые изображены на вашем изображении (гора, Тадж-Махал и т. д.) Сохранить + Дополнительное меню Обновить Список (Загрузок пока нет) @@ -872,7 +874,11 @@ Использование файла SingleWebViewActivity Учётная запись + Удалить учётную запись + Предупреждение об удалении учётной записи Подпись Подпись скопирована в буфер обмена Поздравляем, все фотографии в этом альбоме либо загружены, либо помечены как не предназначенные для загрузки. + Показать в Explore + Показать в Nearby diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index d99d90628..c4b3d534b 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -90,7 +90,7 @@ Oza merkošiid maid du media govahallá (várri, Taj Mahal, jná.) Vurke Ođasmahte - Liste + Listu Eai vuos bajásluđemat) Ii oktage kategoriija heiven oktii %1$s:ain Ii oktage Wikidata mearkkuš heiven oktii %1$s:ain @@ -211,7 +211,7 @@ Áiggutgo don duođaid dán ohcama sihkkut? Sihko Statistihkka - Dássi + Dássi %d Almmuhusat Listu Čuovvovaš @@ -250,7 +250,7 @@ Buot gielaide Govvádus LASSEDIEĐUT - Čájet geavaheaddjisiiddu + Čájet geavaheaddjidieđuid Rievdat kategoriijaid Lasseásahusat Geavat