From 88565b70c5d4981955d8f6c5fe2803d62d356276 Mon Sep 17 00:00:00 2001 From: Shravya K Suresh <133945587+shraavv@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:41:21 +0530 Subject: [PATCH 01/97] updated the strange wording (#6378) --- .../fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt | 4 ++-- app/src/main/res/values-ar/strings.xml | 3 ++- app/src/main/res/values-cs/strings.xml | 3 ++- app/src/main/res/values-da/strings.xml | 3 ++- app/src/main/res/values-de/strings.xml | 3 ++- app/src/main/res/values-el/strings.xml | 3 ++- app/src/main/res/values-fi/strings.xml | 3 ++- app/src/main/res/values-fr/strings.xml | 3 ++- app/src/main/res/values-ia/strings.xml | 3 ++- app/src/main/res/values-io/strings.xml | 3 ++- app/src/main/res/values-it/strings.xml | 3 ++- app/src/main/res/values-iw/strings.xml | 3 ++- app/src/main/res/values-ka/strings.xml | 3 ++- app/src/main/res/values-ko/strings.xml | 3 ++- app/src/main/res/values-krc/strings.xml | 3 ++- app/src/main/res/values-lb/strings.xml | 3 ++- app/src/main/res/values-lt/strings.xml | 3 ++- app/src/main/res/values-mk/strings.xml | 3 ++- app/src/main/res/values-nl/strings.xml | 3 ++- app/src/main/res/values-pl/strings.xml | 3 ++- app/src/main/res/values-pms/strings.xml | 3 ++- app/src/main/res/values-ru/strings.xml | 3 ++- app/src/main/res/values-sl/strings.xml | 3 ++- app/src/main/res/values-sv/strings.xml | 3 ++- app/src/main/res/values-zh-rTW/strings.xml | 3 ++- app/src/main/res/values-zh/strings.xml | 3 ++- app/src/main/res/values/strings.xml | 5 +++-- 27 files changed, 55 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt index 2d92855fc..a4f08f241 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt @@ -114,13 +114,13 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() { val level = store.getString("userAchievementsLevel", "0") if (level == "0"){ binding?.moreProfile?.text = getString( - R.string.profileLevel, + R.string.profile_withoutLevel, getUserName(), getString(R.string.see_your_achievements) // Second argument ) } else { binding?.moreProfile?.text = getString( - R.string.profileLevel, + R.string.profile_withLevel, getUserName(), level ) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d3af03892..c89e246fe 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -426,7 +426,8 @@ الصور المختارة صور عبر \"الأماكن المجاورة\" المستوى %d - %s (المستوى %s) + %s (المستوى %s) + %s (%s) الصور المرفوعة لم يتم إرجاع الصور الصور المستخدمة diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f3d645530..1c27e44c2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -405,7 +405,8 @@ Nejlepší obrázky Obrázky přes „Místa v okolí“ Úroveň %d - %s (úroveň %s) + %s (úroveň %s) + %s (%s) Nahrané obrázky Nerevertované obrázky Použitých obrázků diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 5561d9563..2d04187bf 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -376,7 +376,8 @@ Udvalgte billeder Billeder via \"Steder i nærheden\" Niveau %d - %s (Niveau %s) + %s (Niveau %s) + %s (%s) Uploadede billeder Billeder, som ikke er blevet trukket tilbage Billeder brugt diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 576e48be9..91ef29d5c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -411,7 +411,8 @@ Vorgestellte Bilder Bilder über „Orte in der Nähe“ Level %d - %s (Level %s) + %s (Level %s) + %s (%s) Hochgeladene Bilder Bilder nicht zurückgesetzt Verwendete Bilder diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index c89bca41e..14d9e9e8b 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -390,7 +390,8 @@ Προβεβλημμένες εικόνες Εικόνες μέσω «Κοντινά μέρη» Επίπεδο %d - %s (Επίπεδο %s) + %s (Επίπεδο %s) + %s (%s) Εικόνες που μεταφορτώθηκαν Εικόνες που δεν ανεστράφησαν Εικόνες που χρησιμοποιήθηκαν diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 7561ca533..23e2fc233 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -362,7 +362,8 @@ Suositellut kuvat Kuvia läheltä Taso %d - %s (taso %s) + %s (taso %s) + %s (%s) Kuvia tallennettu Kuvia ei palautettu Kuvia käytetty diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 457f7257c..679e8814e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -415,7 +415,8 @@ Images remarquables Images par « Lieux à proximité » Niveau %d - %s (niveau %s) + %s (niveau %s) + %s (%s) Images téléversées Images non annulées Images utilisées diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index bdea0dbd8..46b5bc3d4 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -364,7 +364,8 @@ Imagines eminente Imagines via “Locos a proximitate” Nivello %d - %s (Nivello %s) + %s (Nivello %s) + %s (%s) Imagines incargate Imagines non revertite Imagines usate diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 1a74c2cc4..66a6cbb66 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -367,7 +367,8 @@ Remarkinda imaji Imaji tra \"Loki Vicina\" Nivelo %d - %s (Nivelo %s) + %s (Nivelo %s) + %s (%s) Imaji sendita Imaji ne reversionita Imaji uzita diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 662fb3d64..db63f7ee8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -391,7 +391,8 @@ Immagini in evidenza Immagini tramite \"Luoghi nelle vicinanze\" Livello %d - %s (Livello %s) + %s (Livello %s) + %s (%s) Immagini caricate Immagini non ripristinate Immagini utilizzate diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 51abc1041..d2938062b 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -401,7 +401,8 @@ תמונות מומלצות תמונות דרך \"מקומות בסביבה\" רמה %d - %s (רמה %s) + %s (רמה %s) + %s (%s) תמונות שהועלו תמונות שלא שוחזרו תמונות בשימוש diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 542b8895a..e6142ec34 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -369,7 +369,8 @@ რჩეული სურათები სურათები „ახლომდებარე ადგილები“ -დან დონე %d - %s (დონე %s) + %s (დონე %s) + %s (%s) სურათები ატვირთულია სურათები არ დაბრუნებულა სურათები გამოიყენება diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 469b66f10..f97ab71f8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -379,7 +379,8 @@ 알찬 그림 \"주변 장소\" 경유 이미지 레벨 %d - %s (레벨 %s) + %s (레벨 %s) + %s (%s) 사진 업로드됨 사용된 이미지 친구와 성과를 공유하세요! diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index 205865bbb..b51bfb9eb 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -368,7 +368,8 @@ Сайланнган Суратла \"Джууукъдагъы Джерле\" юсю бла суратла Дараджа %d - %s (Дараджа %s) + %s (Дараджа %s) + %s (%s) Суратла Джюклендиле Суратла Кери Алынмадыла Суратла Хайырландыла diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 1cd99e223..440c8d6cb 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -307,7 +307,8 @@ Mercie kritt Bemierkenswäert Biller Niveau %d - %s (Niveau %s) + %s (Niveau %s) + %s (%s) Eropgeluede Biller Biller net zréckgesat Benotzte Biller diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index e148a0a20..e8e2fa478 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -353,7 +353,8 @@ Rinktiniai paveikslėliai Vaizdai per „Netoliese esančios vietos“ Lygis %d - %s (%s lygis) + %s (%s lygis) + %s (%s) Vaizdai įkelti Paveikslėliai negrąžinti Naudoti vaizdai diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 3d1300106..49c0f7fd0 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -370,7 +370,8 @@ Избрани слики Слики преку „Околни места“ Степен %d - %s (Степен %s) + %s (Степен %s) + %s (%s) Подигнати слики Неоткажани слики Употребени слики diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b0cbef3e3..35104313f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -392,7 +392,8 @@ Uitgelichte afbeeldingen Afbeeldingen via \"Plaatsen in de buurt\" Niveau %d - %s (Niveau %s) + %s (Niveau %s) + %s (%s) Geüploade afbeeldingen Afbeeldingen niet teruggedraaid Gebruikte afbeeldingen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 9476382a4..4507bd032 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -408,7 +408,8 @@ Wyróżnione ilustracje Obrazy za pośrednictwem \"Pobliskie miejsca\" Poziom %d - %s (Poziom %s) + %s (Poziom %s) + %s (%s) Przesłane obrazy Nie wycofane obrazy Wykorzystane obrazy diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index a184cc604..2da184812 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -368,7 +368,8 @@ Plance an evidensa Plance për \"Pòst davzin\" Livel %d - %s (Livel %s) + %s (Livel %s) + %s (%s) Plance carià Plance nen anulà Plance dovrà diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a1579d0dd..70e306bf9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -427,7 +427,8 @@ Избранные изображения Изображения мест поблизости Уровень %d - %s (Уровень %s) + %s (Уровень %s) + %s (%s) Загружено изображений Изображения, которые не откатывались Использовано изображений diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index ebcfba109..49233c334 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -386,7 +386,8 @@ Izbrane slike Slike iz »Bližnji kraji« Raven %d - %s (raven %s) + %s (raven %s) + %s (%s) Naložene slike Nevrnjene slike Uporabljene slike diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 740304f7c..14fe9825b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -377,7 +377,8 @@ Utvalda bilder Bilder via \"Platser i närheten\" Nivå %d - %s (Nivå %s) + %s (Nivå %s) + %s (%s) Uppladdade bilder Bilder som inte har återställts Bilder som används diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index aa5ac47d6..19d381387 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -393,7 +393,8 @@ 特色圖片 圖片來自“附近的地方” 等級 %d - %s(%s級) + %s(%s級) + %s(%s) 已上傳的圖片 沒有被還原回復的圖片 有被使用到的圖片 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 8244bdc30..a91d55a7c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -427,7 +427,8 @@ 特色图片 来自“附近地点”的图片 等级%d - %s(%s級) + %s(%s級) + %s(%s) 已上传图片 未还原图片 使用过的图片 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e5712105..f58a0fd3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -383,7 +383,8 @@ Featured Images Images via \"Nearby Places\" Level %d - %s (Level %s) + %s (Level %s) + %s (%s) Images Uploaded Images Not Reverted Images Used @@ -805,7 +806,7 @@ Upload your first media by tapping on the add button. Permissions are required for functionality Learn how to write a useful description Learn how to write a useful caption - See your achievements + View your achievements Edit Image Edit Location Location updated! From 29ade1e5b7426fc8db75cd359b85a4e3898a7935 Mon Sep 17 00:00:00 2001 From: VoidRaven Date: Thu, 17 Jul 2025 06:11:28 +0530 Subject: [PATCH 02/97] Fix email verification input (#6367) * Set imeOptions to actionDone and enforce singleLine for xlarge * Set imeOptions to actionDone and enforce singleLine for landscape * Set imeOptions to actionDone and enforce singleLine * Update askUserForTwoFactorAuth for IME_ACTION_DONE to trigger performLogin * Fix email verification: Set imeOptions to actionDone and replace singleLine with maxLines=1 * Fix email verification: Set imeOptions to actionDone and replace singleLine with maxLines=1 * Fix email verification: Set imeOptions to actionDone and replace singleLine with maxLines=1 * feat: Set imeOptions to actionNext for login_password to improve focus transition * feat: Enhance keyboard visibility for email verification code input --- .../fr/free/nrw/commons/auth/LoginActivity.kt | 84 ++++++++++++++++--- .../main/res/layout-land/activity_login.xml | 6 +- .../main/res/layout-xlarge/activity_login.xml | 6 +- app/src/main/res/layout/activity_login.xml | 6 +- 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt index a606d639f..7a665197b 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -92,7 +92,19 @@ class LoginActivity : AccountAuthenticatorActivity() { aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() } signUpButton.setOnClickListener { signUp() } loginButton.setOnClickListener { performLogin() } - loginPassword.setOnEditorActionListener(::onEditorAction) + loginPassword.setOnEditorActionListener { textView, actionId, keyEvent -> + if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) { + if (actionId == EditorInfo.IME_ACTION_NEXT && lastLoginResult != null) { + askUserForTwoFactorAuthWithKeyboard() + true + } else { + performLogin() + true + } + } else { + false + } + } loginPassword.onFocusChangeListener = View.OnFocusChangeListener(::onPasswordFocusChanged) @@ -113,6 +125,39 @@ class LoginActivity : AccountAuthenticatorActivity() { } } + @VisibleForTesting + fun askUserForTwoFactorAuthWithKeyboard() { + if (binding == null) { + Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuthWithKeyboard") + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding!!.root) + } + progressDialog!!.dismiss() + if (binding != null) { + with(binding!!) { + twoFactorContainer.visibility = View.VISIBLE + twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) + loginTwoFactor.visibility = View.VISIBLE + loginTwoFactor.requestFocus() + + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(loginTwoFactor, InputMethodManager.SHOW_IMPLICIT) + + loginTwoFactor.setOnEditorActionListener { _, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) { + performLogin() + true + } else { + false + } + } + } + } else { + Timber.e("Binding is null in askUserForTwoFactorAuthWithKeyboard after reinitialization attempt") + } + showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed)) + } override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) delegate.onPostCreate(savedInstanceState) @@ -236,7 +281,7 @@ class LoginActivity : AccountAuthenticatorActivity() { } else false private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) = - actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER + actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER private fun skipLogin() { AlertDialog.Builder(this) @@ -286,14 +331,14 @@ class LoginActivity : AccountAuthenticatorActivity() { Timber.d("Requesting 2FA prompt") progressDialog!!.dismiss() lastLoginResult = loginResult - askUserForTwoFactorAuth() + askUserForTwoFactorAuthWithKeyboard() } - override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) { + override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread { Timber.d("Requesting email auth prompt") progressDialog!!.dismiss() lastLoginResult = loginResult - askUserForTwoFactorAuth() + askUserForTwoFactorAuthWithKeyboard() } override fun passwordResetPrompt(token: String?) = runOnUiThread { @@ -348,12 +393,31 @@ class LoginActivity : AccountAuthenticatorActivity() { @VisibleForTesting fun askUserForTwoFactorAuth() { + if (binding == null) { + Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth") + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding!!.root) + } progressDialog!!.dismiss() - with(binding!!) { - twoFactorContainer.visibility = View.VISIBLE - twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) - loginTwoFactor.visibility = View.VISIBLE - loginTwoFactor.requestFocus() + if (binding != null) { + with(binding!!) { + twoFactorContainer.visibility = View.VISIBLE + twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) + loginTwoFactor.visibility = View.VISIBLE + loginTwoFactor.requestFocus() + + loginTwoFactor.setOnEditorActionListener { _, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) { + performLogin() + true + } else { + false + } + } + } + } else { + Timber.e("Binding is null in askUserForTwoFactorAuth after reinitialization attempt") } val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) diff --git a/app/src/main/res/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml index b9adfd033..fa36b56ff 100644 --- a/app/src/main/res/layout-land/activity_login.xml +++ b/app/src/main/res/layout-land/activity_login.xml @@ -125,7 +125,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/password" - android:imeOptions="flagNoExtractUi" + android:imeOptions="actionNext" android:inputType="textPassword" /> @@ -148,9 +148,9 @@ android:id="@+id/login_two_factor" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/_2fa_code" - android:imeOptions="flagNoExtractUi" + android:imeOptions="actionDone" android:inputType="number" + android:maxLines="1" android:visibility="gone" tools:visibility="visible" /> diff --git a/app/src/main/res/layout-xlarge/activity_login.xml b/app/src/main/res/layout-xlarge/activity_login.xml index c255aa45f..74f89228e 100644 --- a/app/src/main/res/layout-xlarge/activity_login.xml +++ b/app/src/main/res/layout-xlarge/activity_login.xml @@ -128,7 +128,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/password" - android:imeOptions="flagNoExtractUi" + android:imeOptions="actionNext" android:inputType="textPassword" /> @@ -151,9 +151,9 @@ android:id="@+id/login_two_factor" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/_2fa_code" - android:imeOptions="flagNoExtractUi" + android:imeOptions="actionDone" android:inputType="number" + android:maxLines="1" android:visibility="gone" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 0da9f5d9f..1cdfce8ae 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -131,7 +131,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/password" - android:imeOptions="flagNoExtractUi" + android:imeOptions="actionNext" android:inputType="textPassword" /> @@ -155,7 +155,9 @@ android:id="@+id/login_two_factor" android:layout_width="match_parent" android:layout_height="wrap_content" - android:imeOptions="flagNoExtractUi" + android:imeOptions="actionDone" + android:inputType="number" + android:maxLines="1" android:visibility="gone" tools:visibility="visible" /> From da694022acd8ba5e2a3e408fd68e03d228362354 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 17 Jul 2025 14:02:00 +0200 Subject: [PATCH 03/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-az/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-br/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 10 +++++-- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fi/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 4 ++- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-ia/strings.xml | 3 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-io/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 3 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-krc/strings.xml | 5 +++- app/src/main/res/values-lt/strings.xml | 2 +- app/src/main/res/values-mk/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pms/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 32 +++++++++++----------- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-se/strings.xml | 2 +- app/src/main/res/values-sh/strings.xml | 6 ++-- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-sl/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 4 +-- app/src/main/res/values-uk/strings.xml | 4 +-- app/src/main/res/values-yue/error.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 2 +- 36 files changed, 69 insertions(+), 51 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c89e246fe..6530ab5d5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -823,7 +823,7 @@ الإذن مطلوب لهذه الوظيفة تعلم كيفية كتابة وصف مفيد تعلم كيفية كتابة تعليق مفيد - شاهد إنجازاتك + شاهد إنجازاتك تعديل الصورة تعديل الموقع تم تحديث الموقع! diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index b5f736415..c516c53d4 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -370,7 +370,7 @@ Seçilmiş şəkillər \"Yaxınlıqdakı yerlər\" vasitəsilə şəkillər Səviyyə %d - %s (Səviyyə %s) + %s (Səviyyə %s) Yüklənən şəkillər Geri qaytarılan şəkillər İstifadə olunan şəkillər diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 4dd1e508d..041ce3722 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -372,7 +372,7 @@ নির্বাচিত ছবি \"কাছাকাছি স্থান\" এর মাধ্যমে ছবি স্তর %d - %s (স্তর %s ) + %s (স্তর %s ) আপলোডকৃত চিত্র ছবিগুলো প্রত্যাবর্তন করা হয়নি ব্যবহৃত ছবি diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 85dd880ad..4d7d98cf8 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -446,7 +446,7 @@ Stumm Android Seurt rouedad Disklêriañ an implijer/ez-mañ - Gwelet ho taolioù-kaer + Gwelet ho taolioù-kaer Kemmañ ar skeudenn Kemmañ al lec\'hiadur Lec\'hiadur hizivaet! diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1c27e44c2..ba99354da 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -793,7 +793,7 @@ Pro funkčnost jsou vyžadována oprávnění Naučte se, jak psát užitečný popis Naučte se, jak psát užitečný popis - Podívejte se na své úspěchy + Podívejte se na své úspěchy Upravit obrázek Upravit polohu Poloha aktualizována! diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2d04187bf..053c28934 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -218,6 +218,7 @@ Beskrivelse Diskussion Forfatter + Uploader Upload-dato Licens Koordinater diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 91ef29d5c..409763b63 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -794,7 +794,7 @@ Für die Funktionalität sind Berechtigungen erforderlich Erfahre, wie man eine nützliche Beschreibung schreibt Erfahre, wie man eine nützliche Überschrift schreibt - Deine Erfolge ansehen + Deine Erfolge ansehen Bild bearbeiten Standort bearbeiten Standort aktualisiert! diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 14d9e9e8b..b571ed003 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -774,7 +774,7 @@ Απαιτούνται δικαιώματα για τη λειτουργικότητα Μάθετε πώς να γράψετε μια χρήσιμη περιγραφή Μάθετε πώς να γράψετε μια χρήσιμη λεζάντα - Δείτε τα επιτεύγματά σας + Δείτε τα επιτεύγματά σας Επεξεργασία εικόνας Επεξεργασία τοποθεσίας Η τοποθεσία ενημερώθηκε! diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index eb8caa3ba..e911d280c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -263,6 +263,7 @@ Descripción Discusión Autor + Subidor Fecha de subida Licencia Coordenadas @@ -421,7 +422,7 @@ Imágenes destacadas Imágenes vía \"Sitios Cercanos\" Nivel %d - %s (Nivel %s) + %s (Nivel %s) Imágenes subidas Imágenes no revertidas Imágenes utilizadas @@ -464,7 +465,7 @@ Me di cuenta que es malo para mi privacidad Cambié de opinión, no quiero que siga siendo visible públicamente Lo sentimos, esta imagen no es interesante para una enciclopedia - Subida por mí mismo el %1$s, usado en %2$d artículo(s). + Subido por mí en %1$s, usado en %2$d artículo(s) al menos. Te damos la bienvenida a Commons.\n\nCarga tu primer archivo mediante el botón Añadir. No hay categorías seleccionadas Las imágenes sin categorías raramente se pueden usar. ¿Seguro que quieres continuar sin seleccionar ninguna categoría? @@ -629,6 +630,8 @@ MULTIMEDIA CLASES HIJAS CLASES PADRES + SUBCATEGORÍAS + CATEGORÍAS DE PADRES Lugar cercano encontrado ¿Son estas imágenes de %1$s? ¿Es esta una imagen de %1$s? @@ -804,7 +807,7 @@ Se requieren permisos para la funcionalidad Aprenda a escribir una descripción útil Aprenda a escribir una leyenda útil - Ver sus logros + Ver tus logros Editar Imagen Editar Ubicación ¡Ubicación actualizada! @@ -873,4 +876,5 @@ Mostrar en las cercanías Creado y cargado por: %1$s Creado por %1$s y cargado por %2$s + Nominar para borrado diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 2611abf0d..213d02259 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -506,7 +506,7 @@ با تشکر از ارائه بازخورد بازخورد شما چیست؟ بازخورد شما - دستاوردهای خود را ببینید + دستاوردهای خود را ببینید ناتوان از بارگیری داده‌های مکانی نامزدشده برای حذف diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 23e2fc233..64b0bb401 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -667,7 +667,7 @@ Tervetuloa koko näytön valintatilaan Käytä kahta sormea lähentääksesi ja loitontaaksesi. Pyyhkäise nopeasti ja pitkään suorittaaksesi nämä toiminnot: \n- Vasen/Oikea: Siirry edelliseen/seuraavaan \n- Ylös: Valitse\n- Alas: Merkitse ei-tallennettavaksi. - Näytä omat saavutukset + Näytä omat saavutukset Muokkaa kuvaa Muokkaa sijaintia Poista sijainti diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 679e8814e..9d9697f7c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -16,6 +16,7 @@ * Fitoschido * Friday83260 * Gomoko +* Goombiis * GrandEscogriffe * Happy13241 * Hecatonchire @@ -257,6 +258,7 @@ Description Discussion Auteur + Téléverseur Date de téléversement Licence Coordonnées @@ -803,7 +805,7 @@ Des autorisations sont nécessaires pour la fonctionnalité Apprendre à écrire une description utile Apprendre à écrire une légende utile - Voir vos réalisations + Consultez vos réalisations Modifier l’image Modifier l’emplacement Emplacement mis à jour ! diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index dcb769ca1..70e767a51 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -322,7 +322,7 @@ धन्यवाद प्राप्त किया निर्वाचित चित्र स्तर %d - %s (स्तर %s ) + %s (स्तर %s ) चित्र अपलोड हुआ चित्रों को वापस नहीं किया गया उपयोग हुए चित्र diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index 46b5bc3d4..9c631ec28 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -206,6 +206,7 @@ Description Discussion Autor + Incargator Data de incargamento Licentia Coordinatas @@ -750,7 +751,7 @@ Permissiones es necessari pro functionalitate Apprende como scriber un description utile Appende como scriber un legenda utile - Vider tu realisationes + Examinar tu realisationes Modificar imagine Modificar position Position actualisate! diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 8a44ef84d..420197fa1 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -732,7 +732,7 @@ Izin diperlukan untuk fungsionalitas ini Pelajari cara menulis deskripsi yang berguna Pelajari cara menulis takarir yang berguna - Lihat pencapaian Anda + Lihat pencapaian Anda Edit Gambar Edit Lokasi Lokasi diperbarui! diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 66a6cbb66..b9f0c8137 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -724,7 +724,7 @@ Ne povis partigar ca arkivo Savez quale skribar utila deskripto Savez quale skribar utila etiketo - Videz vua sucesi + Videz vua sucesi Modifikar imajo Aktualigar lokizo Lokizo aktualigita! diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d2938062b..6f3345f4f 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -243,6 +243,7 @@ תיאור דיון יוצר + מעלה תאריך העלאה רישיון נקודות ציון @@ -793,7 +794,7 @@ צריך הרשאות כדי להשתמש בזה הסבר על כתיבת תיאור מועיל הסבר על כתיבת כיתוב מועיל - הצגת ההישגים שלך + הצגת ההישגים שלך עריכת תמונה עריכת מיקום המיקום עודכן! diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f97ab71f8..4fa6329b2 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -672,7 +672,7 @@ 기능에 대한 권한이 필요합니다 유용한 설명을 추가하는 법 알아보기 유용한 캡션을 추가하는 법 알아보기 - 업적 보기 + 업적 보기 그림 편집 위치 편집 위치가 갱신되었습니다! diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index b51bfb9eb..5dd7af15f 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -210,6 +210,7 @@ Ачыкълау Сюзюу Автор + Джюклеген Джюклениу дата Лицензия Координатла @@ -577,6 +578,8 @@ МЕДИА БАЛА КЛАССЛА АНА КЛАССЛА + ТЮБ КАТЕГОРИЯЛА + ТАМЫР КАТЕГОРИЯЛА Джууукъдагъы Джер Табылды Была %1$s суратламыдыла? Бу %1$s суратымыды? @@ -752,7 +755,7 @@ Ишлерча болууу ючюн эркинликле керекдиле Хайырланырча ачыкълау къалай джазаргъа керек болгъанын юренигиз Хайырланырча тюб джазыуну къалай джазаргъа керек болгъанын юренигиз - Джетишимлеригизге къарагъыз + Джетишимлеригизни кёргюзюгюз Суратны Тюзет Локацияны Тюзет Локация джангыртылды! diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index e8e2fa478..c13874a74 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -722,7 +722,7 @@ Funkcionalumui reikalingi leidimai Sužinokite, kaip parašyti naudingą aprašymą Sužinokite, kaip parašyti naudingą antraštę - Pamatykite savo pasiekimus + Pamatykite savo pasiekimus Redaguoti paveikslėlį Redaguoti vietą Vieta atnaujinta! diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 49c0f7fd0..8895247f7 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -758,7 +758,7 @@ Се бараат дозволи за функцијата Дознајте како да напишете корисен опис Дознајте како да напишете корисно толкување - Видете ги вашите достигнувања + Видете ги вашите достигнувања Уреди слика Уреди местоположба Местоположбата е подновена! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 35104313f..e2c3b5b9e 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -778,7 +778,7 @@ Voor deze functionaliteit zijn toestemmingen vereist Leer hoe u een nuttige beschrijving schrijft Leer hoe u een nuttig bijschrift schrijft - Bekijk uw prestaties + Bekijk uw prestaties Afbeelding Bewerken Locatie bewerken Locatie bijgewerkt! diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4507bd032..1861070bb 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,6 +1,7 @@ - Página da wiki Commons no Facebook - Código-fonte da wiki Commons no Github - Logótipo da wiki Commons - Sítio da wiki Commons + Página do Facebook da Commons + Código Fonte do Github da Commons + Logótipo da Commons + Site da Web da Commons Sair do selecionador de localização - Enviar + Submeter Adicionar outra descrição Adicionar nova contribuição Adicionar contribuição da câmara Adicionar contribuição de fotos Adicionar contribuição da galeria de contribuições anteriores Legendas - Descrição da língua + Descrição do Idioma Legenda Descrição Imagem Todas Alternar para cima - Vista de pesquisa - Estado do local + Visualização da Pesquisa + Estado do Local Imagem do Dia a carregar %1$d ficheiro @@ -78,9 +78,9 @@ Comentários Privacidade Commons - Configurações - Carregar na wiki Commons - Carregamento em progresso + Definições + Enviar para a Commons + Envio em progresso Nome de utilizador Palavra-passe Entrar na sua conta da wiki Commons Beta @@ -88,9 +88,9 @@ Esqueceu-se da palavra-passe? Registar-se A iniciar sessão - Aguarde, por favor… - A atualizar legendas e descrições - Aguarde, por favor… + Por favor, aguarde… + A atualizar as legendas e descrições + Por favor, aguarde… Sessão iniciada! O início de sessão falhou! O ficheiro não foi encontrado. Tente outro, por favor. @@ -381,7 +381,7 @@ Imagens destacadas Imagens via \"Locais próximos\" Nível %d - %s (Nível %s) + %s (Nível %s) Imagens carregadas Imagens não revertidas Imagens usadas @@ -764,7 +764,7 @@ São necessárias permissões para a funcionalidade Aprenda a escrever uma descrição útil Aprenda a escrever uma legenda útil - Ver as suas realizações + Ver as suas realizações Editar imagem Editar localização Localização actualizada! diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 70e306bf9..8302e8954 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -9,6 +9,7 @@ * Deltaspace * Dirruw'o * Eleferen +* Elzav * Envlh * Facenapalm * Fenixs-ru diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index c4b3d534b..386c5e6f4 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -256,7 +256,7 @@ Geavat Lassedieđut Android-veršuvdna - Geahča iežat olahusaid + Geahča iežat olahusaid Rievdat gova Giite almmuheaddji diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 97271b945..e14f87f28 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -154,8 +154,8 @@ Nema razgovora Nepoznata licenca Preučitaj - Potrebna dozvola: Treba da se pročita iz spoljašnje memorije. Privitak bez ovoga nema pristupa Vašoj galeriji. - Potrebna dozvola: Treba da se zapiše na spoljašnju memoriju. Privitak bez ovoga nema pristupa Vašoj kameri/galeriji. + Potrebno ovlaštenje: Čitanje vanjske memorije. Aplikacija ne može pristupiti galeriji bez toga. + Potrebno ovlaštenje: Pisanje u vanjskoj memoriji. Aplikacija ne može pristupiti kameri/galeriji bez toga. U redu Upozorenje Postavi @@ -215,7 +215,7 @@ U budućnosti ćete se morati prijaviti kako biste postavili slike. Prijavite se da biste koristili ovu funkciju „U blizini“ možda ne radi kako treba. Lokacija nije dostupna. - Potrebna je dozvola za prikaz liste lokacija u blizini + Potrebno je ovlaštenje za prikaz liste lokacija u blizini Upute Wikidata Wikipedia diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index cbaca17c5..c777c5e56 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -757,7 +757,7 @@ Povolenia sú potrebné pre zabezpečenie funkčnosti Zistite, ako napísať užitočný popis Zistite, ako napísať užitočný titulok - Pozrite si svoje úspechy + Pozrite si svoje úspechy Upraviť obrázok Upraviť polohu Poloha aktualizovaná! diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 49233c334..414d85e47 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -776,7 +776,7 @@ Za delovanje so potrebna dovoljenja Naučite se napisati koristen opis Naučite se napisati koristen napis - Oglejte si svoje dosežke + Oglejte si svoje dosežke Uredi sliko Uredi lokacijo Lokacija posodobljena! diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 14fe9825b..c5f361ee1 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -761,7 +761,7 @@ Behörigheter krävs för funktionalitet Lär dig hur du skriver en användbar beskrivning Lär dig hur du skriver en användbar bildtext - Se dina prestationer + Se dina prestationer Redigera bild Redigera plats Plats uppdaterades! diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c741e89c4..6499caac1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -399,7 +399,7 @@ Seçkin Resimler \"Yakındaki Yerler\"den Resimler Seviye %d - %s (Seviye %s) + %s (Seviye %s) Resimler Yüklendi Resimler Geri Alınmadı Resimler Kullanıldı @@ -782,7 +782,7 @@ İşlevsellik için izinler gereklidir Yararlı bir açıklamanın nasıl yazılacağını öğrenin Nasıl faydalı bir alt yazı yazılacağını öğrenin - Başarılarınızı görün + Başarılarınızı görün Görseli düzenle Konumu Düzenle Konum güncellendi! diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 3ea969dab..ce7963b60 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -401,7 +401,7 @@ Вибрані зображення Зображення місць поблизу Рівень %d - %s (Рівень %s) + %s (Рівень %s) Завантажені зображення Не відхилені зображення Використані зображення @@ -792,7 +792,7 @@ Для роботи потрібні дозволи Дізнайтеся, як написати корисний опис Дізнайтеся, як написати корисний підпис - Перегляньте свої досягнення + Перегляньте свої досягнення Редагувати зображення Редагувати розташування Розташування оновлено diff --git a/app/src/main/res/values-yue/error.xml b/app/src/main/res/values-yue/error.xml index 68579e4a0..b8834d48c 100644 --- a/app/src/main/res/values-yue/error.xml +++ b/app/src/main/res/values-yue/error.xml @@ -1,9 +1,11 @@ 同享壞咗 哎呀。出咗錯! + 話畀我哋知你做緊啲咩,然後透過電郵分享畀我哋,我哋會幫我哋解決問題! 多謝你! diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 19d381387..0bcc201e4 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -235,6 +235,7 @@ 描述 討論 作者 + 上傳者 上傳日期 授權協議 座標 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index a91d55a7c..8d2a5d6b9 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -812,7 +812,7 @@ 功能需要权限 了解如何写出有用的描述 了解如何写出有用的注释 - 查看您的成果 + 查看您的成果 编辑图片 编辑位置 位置已更新! From 11e3e37263864faf1b88d462c6344f937b281890 Mon Sep 17 00:00:00 2001 From: Ritika Pahwa Date: Sat, 19 Jul 2025 14:46:01 +0530 Subject: [PATCH 04/97] Bump up version code to 1054 for v5.6.0 release --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2e391a24f..69bb328ff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "fr.free.nrw.commons" minSdk = 21 targetSdk = 34 - versionCode = 1053 - versionName = "5.5.0" + versionCode = 1054 + versionName = "5.6.0" setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From f1e6f1ad310c5a1ef560c3762346d9fc987d9427 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 21 Jul 2025 14:02:00 +0200 Subject: [PATCH 05/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-cs/strings.xml | 10 +++- app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-lb/strings.xml | 6 +-- app/src/main/res/values-mk/strings.xml | 3 +- app/src/main/res/values-ne/strings.xml | 75 ++++++++++++++++++++------ app/src/main/res/values-nl/strings.xml | 5 +- app/src/main/res/values-ps/strings.xml | 12 +++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sh/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 9 +++- app/src/main/res/values-zh/strings.xml | 5 +- 14 files changed, 106 insertions(+), 28 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index ba99354da..f5e936c8c 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -19,6 +19,7 @@ * Patriccck * Patrik L. * Robins7 +* Segoulas * Spotter * The astrea * Vlad5250 @@ -247,6 +248,7 @@ Popis Diskuse Autor + Nahrávač Datum nahrání souboru Licence Souřadnice @@ -449,7 +451,7 @@ Uvědomil/a jsem si, že je to špatné pro mé soukromí Změnil/a jsem názor, nechci, aby to bylo veřejně viditelné Omlouváme se, že tento obrázek není zajímavý pro encyklopedii - Náhráno mnou %1$s, použito v(e) %2$d článku/článcích. + Nahráno mnou %1$s, použito v alespoň %2$d článku/článcích. Vítejte na Commons!\n\nNahrajte svá první média klepnutím na tlačítko přidat. Nebyly vybrány žádné kategorie Obrázky bez kategorií jsou používány jen zřídka. Opravdu chcete nahrát obrázek bez výběru kategorií? @@ -618,6 +620,8 @@ MÉDIA PODŘAZENÉ TŘÍDY NADŘAZENÉ TŘÍDY + PODKATEGORIE + NADŘAZENÉ KATEGORIE Místo v okolí nalezeno Je na těchto obrázcích %1$s? Je toto obrázek místa %1$s? @@ -793,7 +797,7 @@ Pro funkčnost jsou vyžadována oprávnění Naučte se, jak psát užitečný popis Naučte se, jak psát užitečný popis - Podívejte se na své úspěchy + Zhlédněte své úspěchy Upravit obrázek Upravit polohu Poloha aktualizována! @@ -825,6 +829,7 @@ Diskuze Napište komentář o položce „%1$s“. Bude veřejně viditelný. „%1$s“ již neexistuje, nelze z něj již tedy pořídit obrázek. + \'%1$s\' je na odlišném místě. „%1$s“ je na jiném místě. Zadejte prosím správné místo, a pokud je to možné, napište správnou zeměpisnou šířku a délku. Jiný problém nebo informace (vysvětlete prosím níže). Vaše zpětná vazba bude zveřejněna na následující stránce wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> @@ -863,4 +868,5 @@ Zobrazit v kartě Poblíž Vytvořil/a a nahrál/a: %1$s Vytvořeno uživatelem %1$s a nahráno uživatelem %2$s + Nominováno na smazání diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e911d280c..2507bbecc 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -423,6 +423,7 @@ Imágenes vía \"Sitios Cercanos\" Nivel %d %s (Nivel %s) + %s (%s) Imágenes subidas Imágenes no revertidas Imágenes utilizadas diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 213d02259..2611abf0d 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -506,7 +506,7 @@ با تشکر از ارائه بازخورد بازخورد شما چیست؟ بازخورد شما - دستاوردهای خود را ببینید + دستاوردهای خود را ببینید ناتوان از بارگیری داده‌های مکانی نامزدشده برای حذف diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index f2a606ab9..75cb51542 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -273,7 +273,7 @@ Precísase permiso para amosar unha lista de lugares preto de aquí COMO CHEGAR WIKIDATA - WIKIPEDIA + Wikipedia COMMONS Avalíenos FAQ diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 6f3345f4f..05f309500 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -794,7 +794,7 @@ צריך הרשאות כדי להשתמש בזה הסבר על כתיבת תיאור מועיל הסבר על כתיבת כיתוב מועיל - הצגת ההישגים שלך + הצגת ההישגים שלך עריכת תמונה עריכת מיקום המיקום עודכן! diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 440c8d6cb..6ad1f2971 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -179,7 +179,7 @@ Warnung Eroplueden Jo - Neen + Nee Beschrëftung Titel Motiven @@ -349,8 +349,8 @@ Merci fir %1$s schécken Entsprécht dat de Copyright-Reegelen? Ass dëst richteg kategoriséiert? - Klickt op NEEN fir dëst Bild fir d\'Läsche virzeschloen, falls et guer net nëtzlech ass. - Logoen, Screenshotten oder Filmplakate sinn oft Urheberrechtsverletzungen. Klickt op NEEN fir virzeschloen, datt dëst Bild geläscht gëtt + Klickt op NEE fir virzeschloen, datt dëst Bild geläscht gi soll, falls et guer net nëtzlech ass. + Logoen, Screenshotten oder Filmplakate sinn oft Urheberrechtsverletzungen.\nKlickt op NEE fir virzeschloen, datt dëst Bild geläscht gi soll Oh, dat ass mol net kategoriséiert! Et ass eng Urheberrechtsverletzung, well et Nächst Bild diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 8895247f7..ed525193b 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -212,6 +212,7 @@ Опис Разговор Автор + Подигач Датум на подигање Лиценца Координати @@ -758,7 +759,7 @@ Се бараат дозволи за функцијата Дознајте како да напишете корисен опис Дознајте како да напишете корисно толкување - Видете ги вашите достигнувања + Погледајте ги вашите достигнувања Уреди слика Уреди местоположба Местоположбата е подновена! diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index ef94ba081..c33f2c8b1 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -15,12 +15,47 @@ कमन्सकाे Github स्रोत कोड कमन्सकाे लाेगाे कमन्सकाे वेबसाइट + स्थान चयनकर्ताबाट बाहिर निस्कनुहोस् बुझाउनुहोस् + अर्को विवरण थप्नुहोस् + नयाँ योगदान थप्नुहोस् + क्यामराबाट योगदान थप्नुहोस् + फोटोहरूबाट योगदान थप्नुहोस् + अघिल्लो योगदान ग्यालरीबाट योगदान थप्नुहोस् + शीर्षकहरू + भाषा विवरण + शीर्षक विवरण चित्र सबै + दृश्य खोज्नुहोस् + स्थानको अवस्था दिनकाे उत्कृष्ट तस्वीर - यी तस्वीरहरू %1$s अनुमतिपत्र अनुसार प्राप्त हुनेछ + + %1$d फाइल अपलोड हुँदैछ + %1$d फाइलहरू अपलोड हुँदैछ + + + (%1$d) + (%1$d) + + अपलोडहरू सुरु गर्दै + + %d अपलोड प्रशोधन गर्दै + %d अपलोडहरू प्रशोधन गर्दै + + + %d अपलोड + %d अपलोडहरू + + + यो तस्वीर %1$s अन्तर्गत इजाजतपत्र प्राप्त हुनेछ + यी तस्वीरहरू %1$s अन्तर्गत इजाजतपत्र प्राप्त हुनेछन्। + + + %1$d अपलोड + %1$d अपलोडहरू + अन्वेषण स्वरूप सामान्य @@ -29,6 +64,7 @@ कमन्स मेरो अभिरुचिहरू कमन्समा उर्ध्वभरण गर्नुहाेस् + अर्ध्वभरण कार्य हुँदैछ प्रयोगकर्ता नाम पासवर्ड तपाईको कमन्स बिटा खातामा प्रवेश गर्नुहोस् @@ -37,18 +73,21 @@ खाता खाेल्नुहाेस् प्रवेश गर्दै कृपया प्रतीक्षा गर्नुहोस् … + शीर्षक र विवरणहरू अद्यावधिक गर्दै कृपया प्रतीक्षा गर्नुहोस् … - प्रवेश सफल! - प्रवेश सफल हुन सकेन! + प्रवेश सफल! + प्रवेश सफल हुन सकेन! चित्र भेटिएन। कृपया अर्को चित्र प्रयास गर्नुहोस्। - प्रमाणिकरण असफल भयो, कृपया पुनः प्रवेश गर्नुहोस्! + पुन: प्रयास गर्ने अधिकतम सीमा पुग्यो! कृपया अपलोड रद्द गर्नुहोस् र फेरि प्रयास गर्नुहोस्। + ब्याट्री अप्टिमाइजेसन बन्द गर्ने? + प्रमाणिकरण असफल भयो। कृपया पुनः प्रवेश गर्नुहोस्! उर्ध्वभरण सुरू भयो! %1$s उर्ध्वभरण गरियो ! तपाईको उर्ध्वभरण हेर्नको लागि ट्याप गर्नुहोस् - %1$s उर्ध्वभरण सुरू गर्दैै + %s: उर्ध्वभरण सुरू गर्दैै %1$s उर्ध्वभरण गरिँदै %1$s उर्ध्वभरण सकाउँदै - %1$s उर्ध्वभरण असफल भयो + %1$s उर्ध्वभरण असफल भयो हेर्नको लागि ट्याप गर्नुहोस् हेर्नको लागि ट्याप गर्नुहोस् मेरा वर्तमानका उर्ध्वभरणहरू @@ -62,13 +101,13 @@ मेरा उर्ध्वभरणहरू बाड्ने चित्र पृष्ठ हेर्नुहोस् - शीर्षक (आवश्यक) + शीर्षक (आवश्यक) वर्णन - प्रवेश गर्न असमर्थ - जडान खराबी + प्रवेश गर्न असमर्थ - जडान खराबी धेरै असफल प्रयासहरू भए। कृपया केही मिनेट पछि पुन: प्रयास गर्नुहोस माफ गर्नुहोस, यो प्रयोगकर्तालाई कमन्समा प्रतिबन्ध गरिएको छ तपाईंले आफ्नो दुई कारक प्रमाणीकरण अङ्क प्रदान गर्नुपर्नेछ। - प्रवेश सफल हुन सकेन + प्रवेश सफल हुन सकेन उर्ध्वभरण गर्ने यो सेटलाई नाम दिनुहोस् परिवर्तनहरू @@ -87,7 +126,7 @@ श्रेणी बारेमा विकिमिडिया कमन्स अनुप्रयाेग एक स्वतन्त्र स्रोत अनुप्रयाेग हो। यो अनुप्रयाेग विकिमिडिया समुदायका अनुदानप्राप्तकर्ताहरू र स्वयंसेवकहरूद्वारा निर्मित एवम् प्रबन्धित छ। विकिमिडिया फाउण्डेसन यस अनुप्रयाेगकाे निर्माण, विकास र प्रबन्धनमा कुनै पनि प्रकारले संलग्न छैन। - <a href=\"https://github.com/commons-app/apps-android-commons\">गिटहब</a> मा स्रोत। <a href=\" https://github.com/commons-app/apps-android-commons/issues\">बगजिल्ला</a> मा बग छ। + बग प्रतिवेदन र सुझावहरूको लागि नयाँ <a href=\"<span class=\"notranslate\" translate=\"no\">%1$s \">गिटहब समस्या</a> सिर्जना गर्नुहोस्। गोपनीयता नीति श्रेयहरू बारेमा @@ -181,7 +220,10 @@ कमन्स मूल्याङ्कन गर्नुहाेस् प्राय सोधिएका प्रश्नहरू + प्रयोगकर्ता निर्देशिका इन्टरनेट उपलब्ध छैन + सूचनाहरू प्राप्त गर्दा त्रुटि भयो + समीक्षाको लागि छवि प्राप्त गर्दा त्रुटि भयो। फेरि प्रयास गर्न रिफ्रेस थिच्नुहोस्। कुनै सूचनाहरू फेला परेन अनुवाद भाषाहरू @@ -189,6 +231,9 @@ अगाडि बढ्नुहोस् रद्द गर्नुहोस् पुनः प्रयास गर्नुहोस् + यो ठाउँको फोटो चाहिन्छ। + यो ठाउँको फोटो पहिले नै छ। + यो ठाउँ अब अवस्थित छैन। कुनैपनि तस्वीर फेला परेन निषेधित तपाईलाई कमन्स सम्पादन गर्नबाट प्रतिबन्ध गरिएको छ @@ -217,13 +262,13 @@ तथ्याङ्कहरू धन्यवाद प्राप्त भयाे विशेष तस्वीरहरू - स्तर + स्तर %d प्रयाेग भएका तस्वीरहरू कमन्स सूचना योगदानहरू नजिकैको सूचनाहरू - सूचनाहरू (अभिलेख) + सूचनाहरू (अभिलेखित) सूची भण्डारण अनुमति अर्को @@ -254,7 +299,7 @@ लोगो सफल सफल - तपाईले हालसम्म कुनैपनि सम्पादन गर्नु भएकाे छैन + तपाईले हालसम्म कुनैपनि सम्पादन गर्नु भएकाे छैन %s खाता सृजना गरियो! नजिकैको स्थान भेटियो पुस्तक चिनोहरू @@ -262,7 +307,7 @@ पुस्तक चिनाेमा थपियाे भित्तेपत्रकाे रूपमा चयन गर्नुहोस् वालपेपर सेट गर्दै। कृपया प्रतीक्षा गर्नुहोस् … - पुनर्निर्धारित + प्रणाली पछ्याउनुहोस् गाढा हल्का पुस्तक चिनोहरू @@ -279,7 +324,7 @@ थप जान्नुहोस् विकी लभ्स मोनुमेन्टस् अनुमति आवश्यक - प्रयोगकर्ता पृष्ठ हेर्नुहोस् + प्रयोगकर्ता पृष्ठ हेर्नुहोस् सम्पादन श्रेणीहरू उन्नत विकल्प तपाईँले निकटता क्वेरी अनुकूलन गर्न सक्नुहुन्छ। त्रुटी भएमा पूर्ववत् गर्नुभएर लागू गर्नुहोस्। diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e2c3b5b9e..f2a732dce 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -234,6 +234,7 @@ Beschrijving Overleg Auteur + Uploader Uploaddatum Licentie Coördinaten @@ -601,6 +602,8 @@ MEDIA SUBKLASSEN HOOFDKLASSEN + SUBCATEGORIEËN + BOVENLIGGENDE CATEGORIEËN Plaats in de buurt gevonden Zijn dit afbeeldingen van %1$s? Is dit een afbeelding van %1$s? @@ -778,7 +781,7 @@ Voor deze functionaliteit zijn toestemmingen vereist Leer hoe u een nuttige beschrijving schrijft Leer hoe u een nuttig bijschrift schrijft - Bekijk uw prestaties + Bekijk uw prestaties Afbeelding Bewerken Locatie bewerken Locatie bijgewerkt! diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 6083dc833..3762b46d1 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -195,20 +195,24 @@ اړينه پرېښولی: بهرنۍ زېرمه ولولئ. کاريال ستاسو انځورتونه ته پرته له دې لاسرسی نشي موندلی. ښه گواښنه + د دوتنې غبرگونی نوم وموندل شو راپورته‌کول هو نه نيونگ سرليک + ويي‌انځوريز استازيتوب څرگندونه شننه ليکوال + راپورته‌کوونکی راپورته‌کېدلو نېټه منښتليک کورډيناټونه هېڅ نه دي چمتو شوي ازمېښتي ازمايښتگر شئ ويکيپېډياښه راغلئ + لمېسل‌رښتې هرکلی ناگارل پرانېستل تړل @@ -218,9 +222,17 @@ په اړه اوڼنې غبرگون + گيټ‌هاب له لارې غبرگون وتل + لارښوونيزې + خبرتياوې بياکتنه هيڅ څرگنداوی ونهٔ موندل شو + ټولگړې دوتنې مخ + ويکي‌اومتوک توکی + ويکيپېډيا ليکنه + انځور ډېر تياره دی. + انځور ډېر جړ دی. تگلوري ويکي‌اومتوک ويکيپېډيا diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8302e8954..b3a5102d8 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -270,6 +270,7 @@ Описание Обсуждение Автор + Загружающий Дата загрузки Лицензия Координаты diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index e14f87f28..9fbec8e17 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -2,6 +2,7 @@ Ostava na Facebooku diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 6499caac1..0809c24e0 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -241,6 +241,7 @@ Açıklama Tartışma Yazar + Yükleyici Yükleme tarihi Lisans Koordinatlar @@ -400,6 +401,7 @@ \"Yakındaki Yerler\"den Resimler Seviye %d %s (Seviye %s) + %s (%s) Resimler Yüklendi Resimler Geri Alınmadı Resimler Kullanıldı @@ -442,7 +444,7 @@ Gizliliğimin kötü olduğunu fark ettim Fikrimi değiştirdim, artık herkese görünür olmasını istemiyorum Üzgünüz, bu resim bir ansiklopedi için ilginç değil - Kendi başıma %1$s üzerine yüklendi, %2$d makalede kullanıldı. + %1$s tarafımdan yüklendi, en az %2$d madde de kullanıldı. Commons\'a Hoş Geldiniz!\n\nEkle düğmesine dokunarak ilk medyanızı yükleyin. Kategori Seçilmedi Kategorisiz görüntüler nadiren kullanılabilir. Kategori seçmeden devam etmek istediğinizden emin misiniz? @@ -607,6 +609,8 @@ MEDYA ALT SINIFLAR ÜST SINIFLAR + ALT KATEGORİLER + ÜST KATEGORİLER Yakındaki Yer Bulundu Bunlar %1$s resimleri mi? Bu bir %1$s resmi mi? @@ -782,7 +786,7 @@ İşlevsellik için izinler gereklidir Yararlı bir açıklamanın nasıl yazılacağını öğrenin Nasıl faydalı bir alt yazı yazılacağını öğrenin - Başarılarınızı görün + Başarılarınızı görüntüleyin Görseli düzenle Konumu Düzenle Konum güncellendi! @@ -851,4 +855,5 @@ Yakınlarda Göster Oluşturan ve yükleyen: %1$s %1$s tarafından oluşturuldu ve %2$s tarafından yüklendi + Silinmeye aday gösterildi diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 8d2a5d6b9..3fca731d4 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -269,6 +269,7 @@ 描述 讨论 作者 + 上传者 上传日期 许可协议 坐标 @@ -636,6 +637,8 @@ 媒体 子类别 父类别 + 子分类 + 父级分类 找到附近地点 这些是%1$s的图片吗? 这是%1$s的图片吗? @@ -812,7 +815,7 @@ 功能需要权限 了解如何写出有用的描述 了解如何写出有用的注释 - 查看您的成果 + 查看您的成就 编辑图片 编辑位置 位置已更新! From ee33a9350fb71d13a86e229e5083684d561ff8a9 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 24 Jul 2025 14:02:07 +0200 Subject: [PATCH 06/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ban/strings.xml | 4 ++-- app/src/main/res/values-in/strings.xml | 31 ++++++++++++++----------- app/src/main/res/values-io/strings.xml | 27 +++++++++++++++++++-- app/src/main/res/values-ne/strings.xml | 2 +- app/src/main/res/values-ps/error.xml | 4 ++++ app/src/main/res/values-ps/strings.xml | 4 ++-- app/src/main/res/values-sl/strings.xml | 20 ++++++++-------- 7 files changed, 61 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index fa34d7f68..b4bdbe59a 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -23,7 +23,7 @@ Makejang Alih Duur Cingakan Panyelehan - Genah Negara + Genah Pernyataan Gambar rahina mangkin %1$d berkas kaunggah @@ -270,7 +270,7 @@ Statistik Haturan Suksma Katampi Gambar Pilihan - Tingkat + Tingkat %d Gambar Kaupload Gambar Kaanggén Pituut diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 420197fa1..33ee73b09 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -110,17 +110,20 @@ Ambil foto Sekitar Unggahan saya + Salin pranala + Pranala telah disalin ke papan klip Bagikan Lihat halaman berkas Takarir (Wajib) Berikan takarir untuk berkas ini Deskripsi Takarir - Tidak dapat login - kesalahan pada jaringan + Tidak dapat login - kesalahan pada jaringan Terlalu banyak percobaan masuk yang gagal. Harap coba lagi dalam beberapa menit Maaf, pengguna ini telah diblokir di Commons Anda harus memberikan kode otentikasi dua faktor milik Anda - Gagal masuk log + Kode verifikasi masuk log telah dikirim ke alamat surel Anda. Mohon berikan kode untuk masuk log. + Gagal masuk log Unggah Beri nama set ini Modifikasi @@ -179,7 +182,7 @@ Mohon JANGAN diunggah: Foto narsis atau foto teman Anda Gambar yang Anda unduh dari Internet - Cuplikan layar suatu aplikasi + Cuplikan layar suatu aplikasi tidak bebas Contoh unggahan: Judul: Gedung Opera Sydney Deskripsi: Gedung Opera Sydney yang dilihat dari seberang teluk @@ -200,7 +203,7 @@ Lisensi tidak diketahui Segarkan Meminta Izin Akses Penyimpanan - Perlu Izin: Membaca penyimpanan eksternal. Tanpa hal ini aplikasi tidak dapat mengakses galeri Anda. + Perlu Izin: Membaca penyimpanan eksternal. Tanpa izin ini aplikasi tidak dapat mengakses galeri Anda. Perlu Izin: Menulis penyimpanan eksternal. Tanpa hal ini aplikasi tidak dapat mengakses kamera/galeri. Meminta Izin Lokasi OKE @@ -351,7 +354,7 @@ Apakah tangkapan layar ini OKE untuk diunggah? Bagikan Aplikasi Putar - Galat saat mengambil tempat terdekat. + Tidak dapat memuat tempat-tempat terdekat. Tidak ada gambar di area ini Tidak ditemukan tempat yang dekat Galat saat mengambil monumen terdekat. @@ -368,7 +371,7 @@ Ucapan terima kasih diterima Gambar Pilihan Gambar via \"Tempat di Sekitar\" - Tingkat + Tingkat %d Gambar Diunggah Gambar Tidak Dikembalikan Gambar Digunakan @@ -410,7 +413,7 @@ Saya menyadari itu buruk untuk privasi saya Saya berubah pikiran, saya tidak ingin itu terlihat publik lagi Maaf gambar ini tidak menarik untuk ensiklopedia - Diunggah saya sendiri pada %1$s, digunakan dalam %2$d artikel. + Diunggah saya sendiri pada %1$s, digunakan paling tidak dalam %2$d artikel. Unggah media pertama Anda dengan mengetuk tombol. Tidak ada Kategori yang Dipilih Gambar tanpa kategori jarang dapat digunakan. Apakah Anda yakin ingin mengirim tanpa memilih kategori? @@ -427,7 +430,7 @@ Jangan pernah menanyakan ini lagi Tanya untuk izin lokasi Minta izin lokasi ketika diperlukan untuk fitur tampilan kartu pemberitahuan sekitar. - Terjadi kesalahan. Kami tidak dapat mengambil pencapaian Anda + Terjadi kesalahan. Kami tidak dapat mengambil pencapaian Anda Anda telah membuat begitu banyak kontribusi sehingga sistem perhitungan pencapaian kami tidak dapat menanggulanginya. Ini adalah pencapaian yang tertinggi. Berakhir pada: Sinahang penyobyahan @@ -435,7 +438,7 @@ Izinkan Tutup Anda tidak akan melihat kampanye ini lagi. Namun, Anda bisa mengaktifkan kembali pemberitahuan ini di Pengaturan jika diinginkan. - Fungsi ini membutuhkan hubungan jaringan, tolong periksa pengaturan jaringan Anda. + Fungsi ini memerlukan hubungan jaringan. Tolong periksa pengaturan jaringan Anda. Terjadi masalah saat memproses gambar. Harap mengulang lagi. Dapatkan token untuk menyunting Menambahkan templat untuk pemeriksaan kategori @@ -543,7 +546,7 @@ Tidak bisa menambahkan koordinat. Tidak bisa menambahkan deskripsi. Tidak bisa menambahkan takarir. - Tidak bisa mendapatkan koordinat. + Koordinat gambar tidak diperbarui Tidak bisa memperoleh deskripsi. Sunting deskripsi dan takarir Bagikan gambar melalui @@ -558,7 +561,7 @@ Perlu foto Jenis tempat: Jembatas, museum, hotel, dll. - Terjadi kesalahan ketika masuk log, Anda perlu mengubah kata sandi Anda !! + Terjadi kesalahan ketika masuk log. Anda perlu mengatur ulang kata sandi Anda !! MEDIA KELAS ANAK KELAS INDUK @@ -578,7 +581,7 @@ Untuk hasil terbaik, pilih mode Akurasi Tinggi. Nyalakan lokasi? Tempat sekitar perlu lokasi yang diaktifkan agar bekerja dengan benar - Anda perlu memberikan akses ke lokasi Anda saat ini untuk mengatur lokasi secara otomatis. + Anda perlu memberikan izin lokasi Anda saat ini untuk mengatur lokasi secara otomatis. Apakah Anda menangkap kedua gambar ini di tempat yang sama? Apakah Anda ingin menggunakan lintang/bujur dari gambar yang di kanan? Muat Lebih Banyak Tempat tidak ditemukan, coba ubah kriteria pencarian Anda. @@ -681,7 +684,7 @@ Peta Sekitar harus membaca STATUS TELEPON untuk berfungsi dengan benar Kontribusi Pengguna: %s Pencapaian Pengguna: %s - Lihat halaman pengguna + Lihat profil pengguna Sunting penggambaran Sunting kategori Opsi Lanjutan @@ -732,7 +735,7 @@ Izin diperlukan untuk fungsionalitas ini Pelajari cara menulis deskripsi yang berguna Pelajari cara menulis takarir yang berguna - Lihat pencapaian Anda + Lihat pencapaian Anda Edit Gambar Edit Lokasi Lokasi diperbarui! diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index b9f0c8137..7c93e4f85 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -209,6 +209,7 @@ Deskripto Diskuto Autoro + Adkarganto Dato sendita Licenco Koordinati @@ -411,7 +412,7 @@ Me konstatis ke ol esas mala por mea privateso Me chanjis mea ideo: me ne pluse deziras ke ol esos publike videbla Pardonez! Ca imajo ne esas interesanta por ula enciklopedio - Adjuntita da me, che %1$s, uzita en %2$d artiklo/artikli. + Adjuntita da me che %1$s; uzita en adminime %2$d artiklo/artikli. Bonveno a Commons!\n\nSendez vua unesma arkivo kliktanta sur butono \"adjuntez\" (\'\'add\'\'). Nula kategorio selektita Imaji sen kategorii rare esas uzebla. Ka vu fakte deziras sendar ol sen selektar irga kategorio? @@ -437,11 +438,13 @@ Permisar Eskartar Voluntez kapabligar registrago di lokizo en \'\'Settings\'\', e probez itere.\n\nNoto: l\'arkivo sendanta povas ne havar informo pri lokizo, se l\'\'\'app\'\' ne povas rekuperar l\'informo pri lokizo en kurta intervalo. + La kamero en l\'utensilo bezonas permiso por adjuntar ca informo en imaji, se l\'informo ne esas disponebla che EXIF. Voluntez permisar ke l\'\'\'app\'\' acesez vua lokizo, e probez itere.\n\nAtencez: Esas posibla ke l\'imajo sendonta ne havos informo rekuperebla pri lokizo, se l\'\'\'app\'\' ne povos rekuperor ol pos kurta intervalo. La programo \'\'app\'\' ne enrejistros informo pri lokizo en la fotografuri pro manko di permisi Sen kapabligar GPS, l\'enrejistro di la lokizo en la fotografuri ne facesas. Uzez selektilo di fotografuri segun dokumenti La nova funciono \'\'Android photo picker\'\' povas perdar informo pri lokizo. Kapabligez ol, se vu semblas uzar ol. Deskapabliganta ol povos deskuplar la nova funciono \'\'Android photo picker\'\'. Posible perdos informo pri lokizo. + Vu ne pluse vidos ta kampanii. Tamen, vu povos itere kapabligar ca avizo en Ajusti (\'\'Settings\'\'), se vu deziros. Ca funciono bezonas ligilo ad interreto. Verifikez vua ajusti pri konekti. Eventis eroro dum procesado dil imajo. Voluntez probar ol itere! Kaptanta \'\'token\'\' por redaktar. @@ -574,6 +577,8 @@ \'\'MEDIA\'\' SUBKLASI KLASI PLU ABSTRAKTA + SUB-KATEGORII + PRECIPUA KATEGORII Loko proxima trovesis Ka ca imaji apartenas a %1$s? Ka to esas imajo di %1$s? @@ -637,8 +642,10 @@ Uzita Mea rango Kapabligesis por uzar kun limitizita konekti! + Posibleso pri uzo kun limitizita konekto deskapabligita. La sendo di arkivi rikomencos nun. Modo por limitizita retoligilo Imaji di qualeso + Imaji kun qualeso esas diagrami o fotografuri qui havas qualesi (maxim-multa-kaze teknikala) ed esas valoroza por projeti de Wikimedia Duriganta sendajo... Pauzanta sendajo... Nuliganta sendajo... @@ -671,6 +678,8 @@ Retroirar Bonveno a personalizita selektilo di imaji Ecelanta + Ca imajo ja sendesis a Commons. + Por teknikala motivi, l\'utensilo \'\'app\'\' ne povas fidinde sendar plua kam %1$d pikturi samatempe. La limito %1$d superesis per %2$d. Eskartar Maximo: %1$d Eroro: Limito pri sendajo transpasita @@ -687,12 +696,17 @@ Redaktar deskripturi Redaktar kategorii Progresiva selektaji (advanced options) + Vu povas ajustar la demando \"Vicini\" (\'\'Nearby\'\'). Se erori aparos, riadjustez ed aplikez. Aplikar Restaurar Nula lokizo trovita Ka vu deziras informar la loko de ube vu obtenis ca imajo?\nInformo pri la lokizo helpos editeri trovar vua imajo, do ol divenos plu utila.\nDanko! Adjuntez lokizo + Voluntez removar de ca e-postala mesajo irga informo quan vu ne deziras divenar publika. Anke konciez ke vua e-postal-adreso quan vu esas uzanta, vua nomo e l\'imajo asociita a vu divenos publike videbla. Detali + Sucesi nur esas disponebla en la definitiva versiono. Voluntez verifikar la dokumentigo pri developo. + La klasifiko-tabelo nur esas disponebla en la definitiva versiono. Voluntez verifikar la dokumentigo pri developo. + Voluntez sendar nur pikturi facita da vu. Senderi di imaji kun autoroyuro ne libera blokusesos. To aplikesas anke por probi \'\'beta\'\'. Danko por probar l\'utensilo \'\'app\'\'! nivelo di API versiono di Android Fabrikanto dil aparato @@ -706,6 +720,9 @@ Indikez por ne sendar ol Itere indikez por sendar ol Indikanta ke ol ne sendesos + Indikita kom por ne sendar + Montrar imaji ja traktita + Celanta imaji ja traktita Ne trovesis plusa imaji Ca imajo ja sendesis Ne povis selektar ca imajo por sendar (\'\'upload\'\') @@ -720,11 +737,15 @@ Demandar blokuso di ca uzero Bonveno a selekto di Modo \"tota-skreno\" Uzez du fingri por augmentar o diminutar \'\'zoom\'\'. + Glitez rapide e longe por facar lo sequanta: \n- Sinistre/Dextre: Irar al antea/nexta\n- Adsupre: Selektar\n- Adinfre: Indikez kom ne por sendar. + Por establisar l\'avataro di vua klasifiko-tabelo, kliktez \"Uzar kom avataro\" en la 3-punti menuo de irga imajo. Koordinati ne esas l\'exakta, tamen l\'individuo qua sendis ca imajo kredas ke la koordinati quin lu informis esas suficante proxima. + Permiso pri enmagazinigado neaceptata Ne povis partigar ca arkivo + Uzar la funcionado bezonas permisi Savez quale skribar utila deskripto Savez quale skribar utila etiketo - Videz vua sucesi + Vidar vua sucesi Modifikar imajo Aktualigar lokizo Lokizo aktualigita! @@ -735,6 +756,7 @@ Dankar l\'autoro Eroro sendanta danki al autoro. La tempo-quanto por vua \'\'log in\'\' finis. Voluntez itere enirar. + Nula utensilo \'\'app\'\' disponebla por apertar arkivi GPX Konservo sucesoza di arkivo Ka vu deziras apertar arkivo GPX? Ka vu deziras apartar l\'arkivo KML? @@ -748,6 +770,7 @@ Diskuto Dicez irgu pri l\'arkivo \'%1$s\'. Ol esos videbla publike. + \'%1$s\' esas en diferanta loko. Extinganta la tota sendaji... Arkivi sendita Vartanta diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index c33f2c8b1..295211314 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -351,7 +351,7 @@ यस प्रयोगकर्तालाई रोक लगाउन अनुरोध गर्नुहोस् तस्विर सम्पादन गर्नुहोस् स्थान सम्पादन गर्नुहोस् - स्थान अपडेट गरियो! + स्थान अद्यावधिक गरियो! यो स्थान हटाउनुहोस् स्थान चेतावनी हटाउनुहोस् स्थान हटाइयो! diff --git a/app/src/main/res/values-ps/error.xml b/app/src/main/res/values-ps/error.xml index 94bb26441..2fbd3f195 100644 --- a/app/src/main/res/values-ps/error.xml +++ b/app/src/main/res/values-ps/error.xml @@ -1,7 +1,11 @@ + ويکي‌خونديځ خراب‌شوی + اوو. يو څه ناسم پېښ شول! + موږ ته ووایاست چې تاسو څه کول غواړئ، بيايې له موږ سره د برېښليک له لارې شريک کړئ. دا به موږ سره د هغې سمولو کې مرسته وکړي! مننه! diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 3762b46d1..7afe73e95 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -111,7 +111,7 @@ د دوتنې مخ کتل نيونگ (اړين دی) مهرباني وکړئ، د دې دوتنې لپاره نيونگ ورکړئ - څرگندونه + څرگنداوی نيونگ غونډال ته ننوتنه ناشونې ده - د جال پاتې راتلنه ډیری ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ. @@ -202,7 +202,7 @@ نيونگ سرليک ويي‌انځوريز استازيتوب - څرگندونه + څرگنداوی شننه ليکوال راپورته‌کوونکی diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 414d85e47..38dd9be1c 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -294,7 +294,7 @@ V prihodnosti se boste morali za nalaganje slik prijaviti. Za uporabo te možnosti se prijavite Kopiraj vikibesedilo v odložišče - Vikibesedilo je skopirano v odložišče + Vikibesedilo je kopirano v odložišče Bližnje mogoče ne bo pravilno delovalo. Kraj ni na voljo. Internet ni na voljo. Prikazani so samo predpomnjeni kraji. Dostop do lokacije je bil zavrnjen. Za uporabo te možnosti ročno nastavite svojo lokacijo. @@ -312,7 +312,7 @@ Napaka pri pridobivanju obvestil Napaka pri pridobivanju slike za pregled. Pritisnite Osveži za ponovni poskus. Ni najdenih obvestil - Prevedi + Prevodi Jeziki Izberite jezike, za katere želite pošiljati prevode Nadaljuj @@ -393,7 +393,7 @@ Uporabljene slike Delite svoje dosežke s prijatelji! Ko boste izpolnili te zahteve, se bo vaša raven zvišala. Predmeti v razdelku »Statistika« ne štejejo. - Potrebno najmanj: + Potrebno najmanj:&nbsp; Število slik, ki ste jih naložili v Zbirko, ne glede na program za nalaganje Delež slik v Zbirki, ki niso bile izbrisane Število slik, ki ste jih naložili v Zbirko in se uporabljajo v člankih Wikipedije @@ -425,10 +425,10 @@ Dodali niste nobenega zaznamka Zaznamki Dnevniško beleženje se je začelo. Prosimo, ZNOVA ZAŽENITE aplikacijo, opravite dejanje, ki ga želite zabeležiti, in nato znova tapnite »Pošlji dnevniški zapis«. - Naložil sem jo pomotoma + Naložil_a sem jo pomotoma Nisem vedel_a, da bo javno vidna Spoznal sem, da je to slabo za mojo zasebnost - Premislil_a sem si, ne želim več biti javno viden_na + Premislil_a sem si, ne želim več, da je javno vidna Ta slika žal ni zanimiva za enciklopedijo. Sliko sem naložil/a sam/a dne %1$s. Uporablja se v %2$d članku(ih). Pozdravljeni v Wikimedijini zbirki!\n\nTapnite gumb dodaj in naložite svojo prvo predstavnostno datoteko. @@ -479,7 +479,7 @@ Zahvala uporabniku_ci %1$s je bila uspešno poslana Pošiljanje zahvale uporabniku_ci %1$s ni uspelo Pošiljanje zahvale: neuspešno - Pošiljam zahvalo uporabniku/ici %1$s + Pošiljam zahvalo za %1$s Ali so tu upoštevane avtorske pravice? Ali je to pravilno kategorizirano? Ali to spada v okvir projekta? @@ -510,7 +510,7 @@ Slike, naložene z Bližnjimi kraji, so slike, ki so naložene z odkrivanjem krajev na zemljevidu. Ta možnost vam omogoča, da urejevalcem, ki so opravili koristno urejanje, pošljete zahvalo – z uporabo kratke povezave na strani zgodovine ali strani primerjave. Kopiraj na naslednjo predstavnostno datoteko - Skopirano + Kopirano Zgledi dobrih slik za nalaganje v Zbirko Zgledi slik, ki niso primerne za nalaganje Preskoči to sliko @@ -665,7 +665,7 @@ Način omejene povezanosti izklopljen. Nalaganje se bo zdaj nadaljevalo. Način omejene povezanosti Kakovostne slike - Kakovostne slike so ponazoritve ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie + Kakovostne slike so ilustracije ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie Nalaganje se nadaljuje ... Zaustavljam nalaganje ... Preklicujem nalaganje ... @@ -762,7 +762,7 @@ Prijava Nastavitev belega ozadja Nastavitev črnega ozadja - Prijavi kršitev + Prijava kršitve Prijavi uporabnika Prijavi to vsebino Zahtevaj blokiranje uporabnika @@ -834,7 +834,7 @@ Zbirka Drugi vikiji - Uporabe datotek + Uporabe datoteke SingleWebViewActivity Račun Izgini račun From 90ab7a27662196181fbbca84566beb84de90e6ab Mon Sep 17 00:00:00 2001 From: Sonal Yadav Date: Fri, 25 Jul 2025 20:05:14 +0530 Subject: [PATCH 07/97] Correct NearbyResultItem label mapping for place name display (#6382) * Fix Nearby place name missing in Nearby Place Found popup * minor change --------- Co-authored-by: Sonal Yadav --- .../java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt index 6fc2d0795..504a045fd 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt @@ -7,7 +7,7 @@ class NearbyResultItem( private val wikipediaArticle: ResultTuple?, private val commonsArticle: ResultTuple?, private val location: ResultTuple?, - @field:SerializedName("itemLabel") + @field:SerializedName("label") private val label: ResultTuple?, @field:SerializedName("streetAddress") private val address: ResultTuple?, private val icon: ResultTuple?, From 532bd8baa6332b34e8cedf754eee7dc839212426 Mon Sep 17 00:00:00 2001 From: Ritika Pahwa Date: Sat, 26 Jul 2025 12:27:03 +0530 Subject: [PATCH 08/97] Revert "Optimise SPARQL query for single entity metadata using wikibase:label (#6376)" This reverts commit e5dbcfc2a10f17b0f44bd61f62c40dc43444ec6c. --- .../commons/nearby/model/NearbyResultItem.kt | 3 +- .../main/resources/queries/query_for_item.rq | 57 ++++++++++++------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt index 504a045fd..c39d8901d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt @@ -7,7 +7,6 @@ class NearbyResultItem( private val wikipediaArticle: ResultTuple?, private val commonsArticle: ResultTuple?, private val location: ResultTuple?, - @field:SerializedName("label") private val label: ResultTuple?, @field:SerializedName("streetAddress") private val address: ResultTuple?, private val icon: ResultTuple?, @@ -16,7 +15,7 @@ class NearbyResultItem( @field:SerializedName("commonsCategory") private val commonsCategory: ResultTuple?, @field:SerializedName("pic") private val pic: ResultTuple?, @field:SerializedName("destroyed") private val destroyed: ResultTuple?, - @field:SerializedName("itemDescription") private val description: ResultTuple?, + @field:SerializedName("description") private val description: ResultTuple?, @field:SerializedName("endTime") private val endTime: ResultTuple?, @field:SerializedName("monument") private val monument: ResultTuple?, @field:SerializedName("dateOfOfficialClosure") private val dateOfOfficialClosure: ResultTuple?, diff --git a/app/src/main/resources/queries/query_for_item.rq b/app/src/main/resources/queries/query_for_item.rq index e4421da81..2957b9b5b 100644 --- a/app/src/main/resources/queries/query_for_item.rq +++ b/app/src/main/resources/queries/query_for_item.rq @@ -1,32 +1,50 @@ SELECT ?item - ?itemLabel - ?itemDescription - ?class - ?classLabel - ?pic - ?destroyed - ?endTime - ?wikipediaArticle - ?commonsArticle - ?commonsCategory - ?dateOfOfficialClosure - ?pointInTime + (SAMPLE(?label) AS ?label) + (SAMPLE(?class) AS ?class) + (SAMPLE(?description) AS ?description) + (SAMPLE(?classLabel) AS ?classLabel) + (SAMPLE(?pic) AS ?pic) + (SAMPLE(?destroyed) AS ?destroyed) + (SAMPLE(?endTime) AS ?endTime) + (SAMPLE(?wikipediaArticle) AS ?wikipediaArticle) + (SAMPLE(?commonsArticle) AS ?commonsArticle) + (SAMPLE(?commonsCategory) AS ?commonsCategory) + (SAMPLE(?dateOfOfficialClosure) AS ?dateOfOfficialClosure) + (SAMPLE(?pointInTime) AS ?pointInTime) WHERE { SERVICE { - VALUES ?item {${ENTITY}} + values ?item { + ${ENTITY} + } } - # Get item label/class label/description in the preferred language of the user, or fallback. - SERVICE wikibase:label { bd:serviceParam wikibase:language "${LANG},en,aa,ab,ae,af,ak,am,an,ar,as,av,ay,az,ba,be,bg,bh,bi,bm,bn,bo,br,bs,ca,ce,ch,co,cr,cs,cu,cv,cy,da,de,dv,dz,ee,el,eo,es,et,eu,fa,ff,fi,fj,fo,fr,fy,ga,gd,gl,gn,gu,gv,ha,he,hi,ho,hr,ht,hu,hy,hz,ia,id,ie,ig,ii,ik,io,is,it,iu,ja,jv,ka,kg,ki,kj,kk,kl,km,kn,ko,kr,ks,ku,kv,kw,ky,la,lb,lg,li,ln,lo,lt,lu,lv,mg,mh,mi,mk,ml,mn,mo,mr,ms,mt,my,na,nb,nd,ne,ng,nl,nn,no,ny,oc,oj,om,or,os,pa,pi,pl,ps,pt,qu,rm,rn,ro,ru,rw,sa,sc,sd,se,sg,sh,si,sk,sl,sm,sn,so,sq,sr,ss,st,su,sv,sw,ta,te,tg,th,ti,tk,tl,tn,to,tr,ts,tt,tw,ty,ug,uk,ur,uz,ve,vi,vo,wa,wo,xh,yi,yo,za,zh,zu". } + # Get the label in the preferred language of the user, or any other language if no label is available in that language. + OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage. FILTER (lang(?itemLabelPreferredLanguage) = "${LANG}")} + OPTIONAL {?item rdfs:label ?itemLabelAnyLanguage} + BIND(COALESCE(?itemLabelPreferredLanguage, ?itemLabelAnyLanguage, "?") as ?label) - # Get class (such as forest or bridge) - OPTIONAL {?item p:P31/ps:P31 ?class} + # Get the description in the preferred language of the user, or any other language if no description is available in that language. + OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage. FILTER (lang(?itemDescriptionPreferredLanguage) = "${LANG}")} + OPTIONAL {?item schema:description ?itemDescriptionAnyLanguage} + BIND(COALESCE(?itemDescriptionPreferredLanguage, ?itemDescriptionAnyLanguage, "?") as ?description) - # Get picture (items without a picture will be shown in red on the Nearby map) + # Get the class label in the preferred language of the user, or any other language if no label is available in that language. + OPTIONAL { + ?item p:P31/ps:P31 ?class. + OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage. FILTER (lang(?classLabelPreferredLanguage) = "${LANG}")} + OPTIONAL {?class rdfs:label ?classLabelAnyLanguage} + BIND(COALESCE(?classLabelPreferredLanguage, ?classLabelAnyLanguage, "?") as ?classLabel) + } + + OPTIONAL { + ?item p:P31/ps:P31 ?class. + } + + # Get picture OPTIONAL {?item wdt:P18 ?pic} - # Get existence (whether an item still exists or not) + # Get existence OPTIONAL {?item wdt:P576 ?destroyed} OPTIONAL {?item wdt:P582 ?endTime} OPTIONAL {?item wdt:P3999 ?dateOfOfficialClosure} @@ -47,3 +65,4 @@ WHERE { ?commonsArticle schema:isPartOf . } } +GROUP BY ?item \ No newline at end of file From b2816e1459c69b7466f7516587960eae2f2bf591 Mon Sep 17 00:00:00 2001 From: Ritika Pahwa Date: Sat, 26 Jul 2025 12:29:08 +0530 Subject: [PATCH 09/97] Bump up version code to 1055 for v5.6.1 release Revert SPARQL optimisation --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 69bb328ff..674a6473f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "fr.free.nrw.commons" minSdk = 21 targetSdk = 34 - versionCode = 1054 - versionName = "5.6.0" + versionCode = 1055 + versionName = "5.6.1" setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 929711da98be7630e0a9c2eab92d9d34c08d46e2 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 28 Jul 2025 14:01:44 +0200 Subject: [PATCH 10/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-b+tt+Cyrl/strings.xml | 232 ++++++++++++++++++ app/src/main/res/values-ce/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-da/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-ia/strings.xml | 9 +- app/src/main/res/values-io/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-iw/strings.xml | 1 - app/src/main/res/values-ka/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 6 +- app/src/main/res/values-krc/strings.xml | 1 - app/src/main/res/values-lb/strings.xml | 1 - app/src/main/res/values-lt/strings.xml | 1 - app/src/main/res/values-mk/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pms/strings.xml | 1 - app/src/main/res/values-ps/strings.xml | 80 ++++-- app/src/main/res/values-qq/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sl/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - 29 files changed, 301 insertions(+), 52 deletions(-) create mode 100644 app/src/main/res/values-b+tt+Cyrl/strings.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6530ab5d5..c9c0323c8 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -427,7 +427,6 @@ صور عبر \"الأماكن المجاورة\" المستوى %d %s (المستوى %s) - %s (%s) الصور المرفوعة لم يتم إرجاع الصور الصور المستخدمة diff --git a/app/src/main/res/values-b+tt+Cyrl/strings.xml b/app/src/main/res/values-b+tt+Cyrl/strings.xml new file mode 100644 index 000000000..c4d30ef11 --- /dev/null +++ b/app/src/main/res/values-b+tt+Cyrl/strings.xml @@ -0,0 +1,232 @@ + + + + Викиҗыентыкның Facebook бите + Викиҗыентыкның гитхабтагы башлангыч кодлары + Викиҗыентык логотипы + Викиҗыентыкның веб-сайты + Урынны сайлау тәрәзәсеннән чыгарга + Сакларга + Башка тасвирлама өстәргә + Яңа кертем өстәргә + Камерадан яңа кертем өстәргә + Камерадан яңа кертем өстәргә + Алдагы кертемнәр галереясыннан фото өстәргә + Язмалар + Тел тасвирламасы + Язма + Тасвирлама + Сурәт + Барысы + Күчертергә + Күренешне эзлә + Урын халәте + Көн сурәте + + %1$d файл йөкләнә + %1$d файл йөкләнә + %1$d файл йөкләнә + %1$d файл йөкләнә + + + (%1$d) + (%1$d) + (%1$d) + (%1$d) + + Йөкләү башлана + + %d йөкләүне эшкәртү + %d йөкләүне эшкәртү + %d йөкләүне эшкәртү + %d йөкләүне эшкәртү + + + %d йөкләү + %d йөкләү + %d йөкләү + %d йөкләү + + Тикшерергә + Күренеш + Гомуми + Кире элемтә + Шәхсилек + Викиҗыентык + Көйләнмәләр + Викиҗыентыкка йөкләргә + Йөкләү бара... + Кулланучы исеме + Серсүз + Commons Beta хисапъязмагызга керегез + Керү + Серсүзне оныттыгызмы? + Теркәлү + Керү бара… + Бераз көтегезче... + Язмалар һәм тасвирламалар яңартыла + Бераз көтегезче... + Керү уңышлы башкарылды! + Системага кереп булмады! + Файл табылмады. Башка файлны кулланып карагызчы. + Кабатлаулар саны нык артып китте! Йөкләүне кире кагыгыз яки яңадан кабатлагыз. + Батәринең оптималь кулланылышын сүндерергәме? + Йөкләү башланды! + %1$s төялде! + Төялгән файлыгызны карау өчен басыгыз + Файлны төяү: %s + %1$s йөкләнә + %1$s йөкләве тәмамлана + %1$s йөкләнә алмады + %1$s йөкләнүе туктатылып калды + Карау өчен басыгыз + Карау өчен басыгыз + Минем соңгы төяүләрем + Төяү хатасы + Төяү бара + Галереядан + Фото ясарга + Якында + Минем төяүләрем + Сылтаманы күчереп ал + Сылтама алмашу буферына күчереп алынды + Уртаклашырга + Файл битен күрсәтергә + Язма (Зарур) + Бу файлның исемен билгеләгезче + Тасвирлама + Язма + Кереп булмый - челтәр хатасы + Гафу итегез, мондый исемле кулланучы Викиҗыентыкта блокланган булган. + Системага кереп булмады! + Төяү + Бу файллар төркеме өчен исемне кертегез + Төя + Төркемнәрне сайла + Сакла + Яңарту + Исемлек + Төялгән файллар юк әле! + Төркемнәр + Көйләнмәләр + Теркәл + Сакланган сурәтләр + Кулланучы селекторы + Төркем + Тикшер + Кушымта турында + Яшеренлек сәясәте + Төзүчеләр + Кушымта турында + Почта клиенты урнаштырылмаган + Күптән түгел кулланылган төркемнәр + Беренче синхронлаштыруны көтү... + Сез әле бер сурәтне дә төямәдегез. + Кабатла + Кире как + Иңләргә + Гадәттәге рөхсәтнамә килешүе + Алдагы исемне һәм тамвирламаны куллан + Күренеш + Attribution-ShareAlike 4.0 + Attribution 4.0 + Attribution-ShareAlike 3.0 + Attribution 3.0 + Викиҗыентыктагы сурәтләр Википедиянең күпчелек күләмендә кулланыла. + Төягән сурәтләрегез бөтен дөньядагы кешеләргә белем алырга ярдәм итә ала! + Зинһар, бары тик үзегез ясаган яки төшергән сурәтләрне генә төягез: + Табигать объектлары (мәсәлән, чәчәкләр, хайваннар, таулар) + Файдалы җисемнәр (мәсәлән, велосипедлар, вокзаллар) + Билгеле кешеләр (мәсәлән, мэрыгыз, сез очраткан олимпияче-спортсменнар) + Зинһар, боларны ТӨЯМӘГЕЗ: + Селфилар яки дусларыгызның фотолары + Интернеттан иңләгән фотолар + Ирекле булмаган программаларның скриншотлары + Төяү мисалы: + Атамасы: Сидней опера театры + Сурәтләрегезне төягез. Википедия мәкаләләрен кызыклырак ясарга булышыгыз! + Википедиядә кулланылучы сурәтләр Викиҗыентыкта саклана. + Төягән сурәтләрегез бөтен дөньядагы кешеләргә белем алырга ярдәм итә ала. + Авторлык хокуклары белән сакланган материалларны, мәсәлән, Интернетта табылган плакатлар, китап тышлары һ. б. ш. сурәтләрне кулланмаска тырышыгыз. + Бу сезгә аңлашыламы? + Әйе! + Тулырак мәгълүмат + Төркемнәр + Йөкләнә... + Бернәрсә дә сайланмаган + Язмасыз + Тасвирламасыз + Фикерләшү юк + Билгесез лицензия + Яңарту + Тышкы саклагычны куллану рөхсәтен сорау + Локациягезне табуны сорау + Кушымтада ясалган фотолар өчен локацияне яздыр + Ярар + Игътибар + Кабатлана торган файл исеме табылды + Төя + Әйе + Юк + Язма + Исем + Тасвирланган феномен + Тасвирлама + Фикер алышу + Автор + Төяү вакыты + Лицензия + Координатлар + Билгеләнмәгән + Бета-тестерга әйләнергә + Сез чыннан да чыгарга телисезме? + Астөркемнәр табылмады. + Баш тарту + Ачарга + Ябарга + Баш бит + Төяргә + Якын-тирәдә + Кушымта турында + Көйләнмәләр + Кире элемтә + GitHub аркылы кире элемтә + Чыгарга + Кулланма + Белдермәләр + Тикшерү + тасвирлама табылмады + Файлның Викиҗыентыктагы бите + Викимәгълүмат элементы + Википедия мәкаләсе + Сурәт артык караңгы. + Бу сурәт Викиҗыентыкта бар инде. + Бу сурәт башка урында ясалган булган. + Барыбер бу сурәтне төяргә телисезме? + Тоташтыру хатасы + Сурәттә читенлекләр табылды + Кушымтада ясалган фотоларны сакла + Җайланма камерасы ярдәмендә эшләнгән фотоларны җайланмада сакла + Хата! Сылтама табылмады + Бетерергә тәкъдим ит + Бу сурәтне бетерергә тәкъдим ителде. + Күбрәк мәгълүмат өчен битне карагыз + Калдырып үт + Керү + Викитекст алмашу буферына күчереп алынды + Юнәлешләр + Викимәгълүмат + Википедия + Викиҗыентык + Безне бәяләгез + Еш бирелгән сораулар (ЕБС, ЧаВо) + Бору + Саклауга рөхсәтләр кире кагылды + Бу объект белән уртаклашу мөмкин түгел + diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index 3855a80cd..b30b2bf3a 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -379,7 +379,7 @@ 3. Хьайн суьртана догӀу йаззаман дакъа лаха 4. «Хийца» иконкин тӀетаӀайе (къоламах тера йу) хӀокху декъана 5. Вики-код йогӀучу метте дӀайазйе - 7. Йаззам дӀайазбе + 7. Йаззам дӀайазбан Вики-код буфер чу копийе пауза кхидӀа diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f5e936c8c..ffc1733b5 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -408,7 +408,6 @@ Obrázky přes „Místa v okolí“ Úroveň %d %s (úroveň %s) - %s (%s) Nahrané obrázky Nerevertované obrázky Použitých obrázků diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 053c28934..141bbabc6 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -378,7 +378,6 @@ Billeder via \"Steder i nærheden\" Niveau %d %s (Niveau %s) - %s (%s) Uploadede billeder Billeder, som ikke er blevet trukket tilbage Billeder brugt diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 409763b63..369999a8e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -412,7 +412,6 @@ Bilder über „Orte in der Nähe“ Level %d %s (Level %s) - %s (%s) Hochgeladene Bilder Bilder nicht zurückgesetzt Verwendete Bilder diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index b571ed003..bb7a9ea98 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -391,7 +391,6 @@ Εικόνες μέσω «Κοντινά μέρη» Επίπεδο %d %s (Επίπεδο %s) - %s (%s) Εικόνες που μεταφορτώθηκαν Εικόνες που δεν ανεστράφησαν Εικόνες που χρησιμοποιήθηκαν diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2507bbecc..e911d280c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -423,7 +423,6 @@ Imágenes vía \"Sitios Cercanos\" Nivel %d %s (Nivel %s) - %s (%s) Imágenes subidas Imágenes no revertidas Imágenes utilizadas diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 64b0bb401..e640bf981 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -363,7 +363,6 @@ Kuvia läheltä Taso %d %s (taso %s) - %s (%s) Kuvia tallennettu Kuvia ei palautettu Kuvia käytetty diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9d9697f7c..8b8a57a28 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -418,7 +418,6 @@ Images par « Lieux à proximité » Niveau %d %s (niveau %s) - %s (%s) Images téléversées Images non annulées Images utilisées diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index 9c631ec28..18d4a68af 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -235,7 +235,7 @@ A proposito Parametros Commentario - Retroaction per GitHub + Commentarios per GitHub Clauder session Tutorial Notificationes @@ -366,7 +366,6 @@ Imagines via “Locos a proximitate” Nivello %d %s (Nivello %s) - %s (%s) Imagines incargate Imagines non revertite Imagines usate @@ -719,8 +718,8 @@ Modello del apparato Nomine del apparato Typo de rete - Gratias pro dar retroaction - Error durante le invio del retroaction + Gratias pro dar commentario + Error durante le invio del commentario Que es tu commentario? Tu commentario Marcar como non a incargar @@ -784,7 +783,7 @@ ‘%1$s’ se trova in un altere loco. ‘%1$s’ es in un altere loco. Per favor specifica le loco correcte hic infra, e si possibile, indica le latitude e longitude correcte. Altere problema o information (per favor explica hic infra). - Tu retroaction apparera sur le sequente pagina wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + Tu commentario apparera sur le sequente pagina wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Es tu secur de voler cancellar tote le incargamentos? Cancella tote le incargamentos… Incargamentos diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 7c93e4f85..0c41d5492 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -369,7 +369,6 @@ Imaji tra \"Loki Vicina\" Nivelo %d %s (Nivelo %s) - %s (%s) Imaji sendita Imaji ne reversionita Imaji uzita diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index db63f7ee8..84b16c1f5 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -392,7 +392,6 @@ Immagini tramite \"Luoghi nelle vicinanze\" Livello %d %s (Livello %s) - %s (%s) Immagini caricate Immagini non ripristinate Immagini utilizzate diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 05f309500..6021a663f 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -403,7 +403,6 @@ תמונות דרך \"מקומות בסביבה\" רמה %d %s (רמה %s) - %s (%s) תמונות שהועלו תמונות שלא שוחזרו תמונות בשימוש diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index e6142ec34..b3374b223 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -370,7 +370,6 @@ სურათები „ახლომდებარე ადგილები“ -დან დონე %d %s (დონე %s) - %s (%s) სურათები ატვირთულია სურათები არ დაბრუნებულა სურათები გამოიყენება diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4fa6329b2..d297d07ac 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -222,6 +222,7 @@ 설명 토론 저자 + 올린 사람 올린 날짜 라이선스 좌표 @@ -380,7 +381,6 @@ \"주변 장소\" 경유 이미지 레벨 %d %s (레벨 %s) - %s (%s) 사진 업로드됨 사용된 이미지 친구와 성과를 공유하세요! @@ -539,6 +539,8 @@ 미디어 자식 클래스 상위 클래스 + 하위 분류 + 상위 분류 주변 장소 발견 %1$s의 사진이 맞습니까? %1$s의 사진이 맞습니까? @@ -583,8 +585,10 @@ 아바타로 설정 매년 매주 + 항상 업로드 근처 + 사용됨 내 순위 제한된 연결 모드 고품질 사진 diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index 5dd7af15f..af4299c0b 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -370,7 +370,6 @@ \"Джууукъдагъы Джерле\" юсю бла суратла Дараджа %d %s (Дараджа %s) - %s (%s) Суратла Джюклендиле Суратла Кери Алынмадыла Суратла Хайырландыла diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 6ad1f2971..a130e860d 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -308,7 +308,6 @@ Bemierkenswäert Biller Niveau %d %s (Niveau %s) - %s (%s) Eropgeluede Biller Biller net zréckgesat Benotzte Biller diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index c13874a74..2aa65c0f4 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -354,7 +354,6 @@ Vaizdai per „Netoliese esančios vietos“ Lygis %d %s (%s lygis) - %s (%s) Vaizdai įkelti Paveikslėliai negrąžinti Naudoti vaizdai diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index ed525193b..73a522996 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -372,7 +372,6 @@ Слики преку „Околни места“ Степен %d %s (Степен %s) - %s (%s) Подигнати слики Неоткажани слики Употребени слики diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f2a732dce..0282d99ed 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -394,7 +394,6 @@ Afbeeldingen via \"Plaatsen in de buurt\" Niveau %d %s (Niveau %s) - %s (%s) Geüploade afbeeldingen Afbeeldingen niet teruggedraaid Gebruikte afbeeldingen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1861070bb..4d064ff06 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -410,7 +410,6 @@ Obrazy za pośrednictwem \"Pobliskie miejsca\" Poziom %d %s (Poziom %s) - %s (%s) Przesłane obrazy Nie wycofane obrazy Wykorzystane obrazy diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 683254b61..ac099cd7f 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -370,7 +370,6 @@ Plance për \"Pòst davzin\" Livel %d %s (Livel %s) - %s (%s) Plance carià Plance nen anulà Plance dovrà diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 7afe73e95..64a7762a7 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -10,42 +10,42 @@ --> د خونديځ فيسبوک پاڼه - خونديځ ګيټهوب سرچينه کوډ + خونديځ گيټ‌هاب سرچينه کوډ خونديځ نښان خونديځ وېبپاڼه له ځای ټاکونکي وتل سپارل - بل سپيناوی ورزياتول - نوې ونډې ورزياتول - د کامرې له لارې ونډه ورزياتول - انځورونو له لارې ونډه ورزياتول - د پخوانيو ونډو له انځورتونه د ونډې ورزياتول + بل څرگنداوی ورگډول + نوې ونډې ورگډول + د کامرې له لارې ونډه ورگډول + انځورونو له لارې ونډه ورگډول + د پخوانيو ونډو له انځورتونه د ونډې ورگډول نيونگې - ژبې سپيناوی + ژبې څرگنداوی نيونگ - سپيناوی + څرگنداوی انځور ټول - پورته کول - لټون ليد + پورته بدلول + لټون کتنه ځای حالت ورځې انځور - %1$d دوتنه پورته کول - %1$d دوتنې پورته کول + %1$d دوتنه راپورته‌کول + %1$d دوتنې راپورته‌کول (%1$d) (%1$d) - پورته کولو پيل + راپورته‌کول پيلول - جريان %d پورته کول - پورته کولو %d جريان + راپورته‌کولو %d بهير + راپورته‌کولو %d بهير %d upload - %d پورته کول + %d راپورته‌کول دا انځور به د منښتليک %1$s لاندې وي @@ -100,7 +100,7 @@ لږ نابريال شو %1$d%% بشپړ - د برسېرېدلو په حال کې… + راپورته‌کېږي له انځورتون څخه انځور اخيستل نژدې @@ -114,8 +114,8 @@ څرگنداوی نيونگ غونډال ته ننوتنه ناشونې ده - د جال پاتې راتلنه - ډیری ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ. - بخښنه غواړو، په دي کارن د کامنز لخوا بنديز ولګول شو + ډېرې ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ. + بخښنه غواړو، په دې کارن د کامنز لخوا بنديز ولگول شو تاسو بايد خپل دوه لامليز تاييد کوډ ورکړئ. ستاسو برېښليک پتې ته د ننوتلو تاييد کوډ لېږل شوی دی. مهرباني وکړئ د ننوتلو لپاره کوډ ورکړئ. غونډال کې ننوتنه نابريالۍ شوه @@ -187,9 +187,9 @@ رابرسېرېږي... هېڅ هم نه دی ټاکل شوی هيڅ نيونگ نشته - څرگندونه نشته + څرگنداوی نشته هيڅ شننه نشته - نامعلوم جواز + ناجوت منښتليک تازه کول د زېرمه کولو د پرېښولي غوښتنه کول اړينه پرېښولی: بهرنۍ زېرمه ولولئ. کاريال ستاسو انځورتونه ته پرته له دې لاسرسی نشي موندلی. @@ -211,7 +211,7 @@ کورډيناټونه هېڅ نه دي چمتو شوي ازمېښتي ازمايښتگر شئ - ويکيپېډياښه راغلئ + ويکيپېډيا ته ښه راغلئ لمېسل‌رښتې هرکلی ناگارل پرانېستل @@ -237,6 +237,7 @@ ويکي‌اومتوک ويکيپېډيا خونديځ + و مو ارزوئ ډ‌ځ‌پ کارن لارښود ښوونې پرېښودل @@ -250,6 +251,7 @@ پرمخ‌ځه ناگارل بيا هڅه‌کول + دا تاسو ته نږدې ځايونه دي چې د ويکيپېډيا ليکنې يې د ښودلو لپاره انځورونو ته اړتيا لري.\n\n\'د دې ځای پلټل\' باندې کليک کولو سره نخچه تاله کوي او د هغه ځای شاوخوا نږدې سيمې لټون پيلول. داځای انځور ته اړتيا لري. دا ځای لادمخه انځور لري. دا ځای نور شتون نه لري. @@ -265,5 +267,39 @@ وروستۍ پلټنې: وروستۍ پلټل شوې پوښتنې وروستۍ ژبې پوښتنې + رسنۍ + وېشنيزې + توکي + ټاکلې + موبايل له لارې راپورته‌شوی + نخشه + انځور په ویکي‌اومتوک کې %1$s ته ورگډ شو! + پای ته رسېږي په: + ټاکنيزې‌سيالۍ ښکاره‌کول + روانې ټاکنيزې‌سيالۍ وگورئ + پرېښول + تړل + د لاسوند پربنسټ انځور راخيستونکی کارول + د انځور پروسس‌کولو پرمهال تېروتنه رامنځته شوه. مهرباني وکړئ بيا هڅه وکړئ! + د سمون لپاره نښه ترلاسه کول + د وېشنيزې سم‌کتنې لپاره کينډۍ ورگډول + د %1$s وېشنيزې سم‌کتنې لپاره غوښتنه‌کول + وېشنيزې سم‌کتنې غوښتنه‌کول + وېشنيزې سم‌کتنې غوښتنه وشوه + د وېشنيزې سم‌کتنې غوښتنه کار نه کوي + د %1$s وېشنيزې سم‌کتنې لپاره غوښتنه وشوه + د %1$s سم‌کتنې لپاره غوښتنه نشي کېدای + د %1$s وېشنيزې سم‌کتنې لپاره غوښتنه‌کول + وشو + مننې لېږل: برياليتوب + %1$s ته په برياليتوب سره مننه ولېږل شوه + %1$s ته د مننې لېږلو کې پاتې راغی + مننې لېږل: ناکامي + %1$s لپاره د مننې لېږل + ايا دا ډ لمېسل‌رښتو سره سم دی؟ + ايا دا په سمه توگه ډلبندي شوي دي؟ + ايا دا د منلو وړ دي؟ + ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟ + که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ. گڼون diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 7b521a8ac..1f80d9946 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -173,6 +173,7 @@ {{Identical|Delete}} Èstatistik To see the correct translation for your language, please go to https://commons.wikimedia.org/wiki/Commons:Featured_pictures and select your language in \"This project page in other languages\". + {{optional}} {{Identical|Nearby}} Refers to the next \'\'\'step\'\'\' in the uploading process. Refers to the previous \'\'\'step\'\'\' in the uploading process. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b3a5102d8..552fdcf98 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -430,7 +430,6 @@ Изображения мест поблизости Уровень %d %s (Уровень %s) - %s (%s) Загружено изображений Изображения, которые не откатывались Использовано изображений diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 38dd9be1c..389ec403a 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -387,7 +387,6 @@ Slike iz »Bližnji kraji« Raven %d %s (raven %s) - %s (%s) Naložene slike Nevrnjene slike Uporabljene slike diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index c5f361ee1..2540c39bb 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -378,7 +378,6 @@ Bilder via \"Platser i närheten\" Nivå %d %s (Nivå %s) - %s (%s) Uppladdade bilder Bilder som inte har återställts Bilder som används diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0809c24e0..836098422 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -401,7 +401,6 @@ \"Yakındaki Yerler\"den Resimler Seviye %d %s (Seviye %s) - %s (%s) Resimler Yüklendi Resimler Geri Alınmadı Resimler Kullanıldı From 869371b485234eac42d1e8bbb871e14facf196c0 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 31 Jul 2025 14:02:22 +0200 Subject: [PATCH 11/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-b+tt+Cyrl/strings.xml | 1 + app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 8 ++++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-b+tt+Cyrl/strings.xml b/app/src/main/res/values-b+tt+Cyrl/strings.xml index c4d30ef11..486f16332 100644 --- a/app/src/main/res/values-b+tt+Cyrl/strings.xml +++ b/app/src/main/res/values-b+tt+Cyrl/strings.xml @@ -180,6 +180,7 @@ Тасвирлама Фикер алышу Автор + Төяп куючы Төяү вакыты Лицензия Координатлар diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 6ee931542..e4df74900 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -271,6 +271,7 @@ При качването използвайте персонализирано авторско име вместо потребителското си име Персонализирано авторско име Наблизо + Известия Известия (прочетени) Списък Следваща diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 2540c39bb..bc7bfb5c9 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -219,6 +219,7 @@ Beskrivning Diskussion Skapare + Uppladdare Uppladdningsdatum Licens Koordinater @@ -420,7 +421,7 @@ Jag insåg att den är dålig för mitt privatliv Jag ändrade mig, jag vill inte längre att den ska vara synlig offentligt Tyvärr, denna bild är inte intressant för en encyklopedi - Laddades upp av mig den %1$s och används i %2$d artiklar. + Laddades upp av mig den %1$s och används i minst %2$d artiklar. Välkommen till Commons!\n\nLadda upp din första mediafil genom att trycka på knappen för att lägga till. Inga kategorier har valts Bilder utan kategorier används sällan. Är du säker på att du vill fortsätta utan att välja kategorier? @@ -585,6 +586,8 @@ MEDIA UNDERORDNADE KLASSER ÖVERORDNADE KLASSER + UNDERKATEGORIER + ÖVERORDNADE KATEGORIER Hittade platser i närheten Föreställer de här bilderna %1$s? Är detta en bild på %1$s? @@ -760,7 +763,7 @@ Behörigheter krävs för funktionalitet Lär dig hur du skriver en användbar beskrivning Lär dig hur du skriver en användbar bildtext - Se dina prestationer + Se dina prestationer Redigera bild Redigera plats Plats uppdaterades! @@ -829,4 +832,5 @@ Visa i \"I närheten\" Skapades och laddades upp av: %1$s Skapad av %1$s och laddades upp av %2$s + Nominerad för radering From 8de57304bf9445b1a0f79b361950475d6d246901 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 31 Jul 2025 18:26:16 -0500 Subject: [PATCH 12/97] Convert bookmarks package to kotlin (#6387) * Convert BookmarkItemsController to kotlin * Split BookmarkItemsDao apart and converted to Kotlin * Convert and cleanup content providers * Convert BookmarkItemsFragment to kotlin * Convert BookmarkPicturesFragment to kotlin * Convert BookmarkPicturesDao to kotlin and share some useful DB methods * Convert BookmarkPicturesController to kotlin * Convert BookmarkFragment to kotlin * Convert BookmarksPagerAdapter to kotlin * Convert BookmarkListRootFragment to kotlin --- .../fr/free/nrw/commons/CommonsApplication.kt | 9 +- .../commons/bookmarks/BookmarkFragment.java | 105 ------ .../nrw/commons/bookmarks/BookmarkFragment.kt | 98 ++++++ .../bookmarks/BookmarkListRootFragment.java | 267 -------------- .../bookmarks/BookmarkListRootFragment.kt | 226 ++++++++++++ .../bookmarks/BookmarksPagerAdapter.java | 94 ----- .../bookmarks/BookmarksPagerAdapter.kt | 82 +++++ .../items/BookmarkItemsContentProvider.java | 129 ------- .../items/BookmarkItemsContentProvider.kt | 101 ++++++ .../items/BookmarkItemsController.java | 27 -- .../items/BookmarkItemsController.kt | 23 ++ .../bookmarks/items/BookmarkItemsDao.java | 329 ------------------ .../bookmarks/items/BookmarkItemsDao.kt | 199 +++++++++++ .../items/BookmarkItemsFragment.java | 81 ----- .../bookmarks/items/BookmarkItemsFragment.kt | 62 ++++ .../bookmarks/items/BookmarkItemsTable.kt | 90 +++++ .../BookmarkPicturesContentProvider.java | 120 ------- .../BookmarkPicturesContentProvider.kt | 100 ++++++ .../pictures/BookmarkPicturesController.java | 63 ---- .../pictures/BookmarkPicturesController.kt | 38 ++ .../pictures/BookmarkPicturesDao.java | 227 ------------ .../bookmarks/pictures/BookmarkPicturesDao.kt | 141 ++++++++ .../pictures/BookmarkPicturesFragment.java | 218 ------------ .../pictures/BookmarkPicturesFragment.kt | 201 +++++++++++ .../bookmarks/pictures/BookmarksTable.kt | 54 +++ .../category/CategoryContentProvider.kt | 55 +-- .../fr/free/nrw/commons/data/DBOpenHelper.kt | 13 +- .../di/CommonsDaggerContentProvider.kt | 11 + .../RecentSearchesContentProvider.java | 202 ----------- .../RecentSearchesContentProvider.kt | 174 +++++++++ .../commons/media/MediaDetailPagerFragment.kt | 4 +- .../RecentLanguagesContentProvider.kt | 51 +-- .../free/nrw/commons/utils/DatabaseUtils.kt | 32 ++ .../BookmarkListRootFragmentUnitTest.kt | 2 +- .../bookmarks/BookmarksPagerAdapterTests.kt | 8 + .../LoggedOutBookmarksPagerAdapterTests.kt | 8 + .../items/BookmarkItemsControllerTest.kt | 2 +- .../bookmarks/items/BookmarkItemsDaoTest.kt | 38 +- .../items/BookmarkItemsFragmentUnitTest.kt | 2 +- .../pictures/BookmarkPictureDaoTest.kt | 26 +- .../BookmarkPicturesControllerTest.kt | 2 +- .../BookmarkPicturesFragmentUnitTests.kt | 6 +- .../nrw/commons/category/CategoryDaoTest.kt | 2 +- .../recentsearches/RecentSearchesDaoTest.kt | 4 +- 44 files changed, 1738 insertions(+), 1988 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsTable.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarksTable.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt index 90ab0393a..89fdaa055 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt @@ -15,9 +15,8 @@ import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.imagepipeline.core.ImagePipelineConfig import fr.free.nrw.commons.auth.LoginActivity import fr.free.nrw.commons.auth.SessionManager -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable import fr.free.nrw.commons.category.CategoryDao import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler import fr.free.nrw.commons.concurrency.ThreadPoolService @@ -257,8 +256,8 @@ class CommonsApplication : MultiDexApplication() { } catch (e: SQLiteException) { Timber.e(e) } - BookmarkPicturesDao.Table.onDelete(db) - BookmarkItemsDao.Table.onDelete(db) + BookmarksTable.onDelete(db) + BookmarkItemsTable.onDelete(db) } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java deleted file mode 100644 index 9100fb63c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.free.nrw.commons.bookmarks; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentManager; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentBookmarksBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.theme.BaseActivity; -import javax.inject.Inject; -import fr.free.nrw.commons.contributions.ContributionController; -import javax.inject.Named; - -public class BookmarkFragment extends CommonsDaggerSupportFragment { - - private FragmentManager supportFragmentManager; - private BookmarksPagerAdapter adapter; - FragmentBookmarksBinding binding; - - @Inject - ContributionController controller; - /** - * To check if the user is loggedIn or not. - */ - @Inject - @Named("default_preferences") - public - JsonKvStore applicationKvStore; - - @NonNull - public static BookmarkFragment newInstance() { - BookmarkFragment fragment = new BookmarkFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - public void setScroll(boolean canScroll) { - if (binding!=null) { - binding.viewPagerBookmarks.setCanScroll(canScroll); - } - } - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - binding = FragmentBookmarksBinding.inflate(inflater, container, false); - - // Activity can call methods in the fragment by acquiring a - // reference to the Fragment from FragmentManager, using findFragmentById() - supportFragmentManager = getChildFragmentManager(); - - adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(), - applicationKvStore.getBoolean("login_skipped")); - binding.viewPagerBookmarks.setAdapter(adapter); - binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks); - - ((MainActivity) getActivity()).showTabs(); - ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); - - setupTabLayout(); - return binding.getRoot(); - } - - /** - * This method sets up the tab layout. If the adapter has only one element it sets the - * visibility of tabLayout to gone. - */ - public void setupTabLayout() { - binding.tabLayout.setVisibility(View.VISIBLE); - if (adapter.getCount() == 1) { - binding.tabLayout.setVisibility(View.GONE); - } - } - - - public void onBackPressed() { - if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition()))) - .backPressed()) { - // The event is handled internally by the adapter , no further action required. - return; - } - // Event is not handled by the adapter ( performed back action ) change action bar. - ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } - - @Override - public void onDestroy() { - super.onDestroy(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt new file mode 100644 index 000000000..48db40ad0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt @@ -0,0 +1,98 @@ +package fr.free.nrw.commons.bookmarks + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import fr.free.nrw.commons.contributions.ContributionController +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentBookmarksBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.theme.BaseActivity +import javax.inject.Inject +import javax.inject.Named + +class BookmarkFragment : CommonsDaggerSupportFragment() { + private var adapter: BookmarksPagerAdapter? = null + + @JvmField + var binding: FragmentBookmarksBinding? = null + + @JvmField + @Inject + var controller: ContributionController? = null + + /** + * To check if the user is loggedIn or not. + */ + @JvmField + @Inject + @Named("default_preferences") + var applicationKvStore: JsonKvStore? = null + + fun setScroll(canScroll: Boolean) { + binding?.let { + it.viewPagerBookmarks.isCanScroll = canScroll + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreateView(inflater, container, savedInstanceState) + binding = FragmentBookmarksBinding.inflate(inflater, container, false) + + // Activity can call methods in the fragment by acquiring a + // reference to the Fragment from FragmentManager, using findFragmentById() + val supportFragmentManager = childFragmentManager + + adapter = BookmarksPagerAdapter( + supportFragmentManager, requireContext(), + applicationKvStore!!.getBoolean("login_skipped") + ) + binding!!.viewPagerBookmarks.adapter = adapter + binding!!.tabLayout.setupWithViewPager(binding!!.viewPagerBookmarks) + + (requireActivity() as MainActivity).showTabs() + (requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + + setupTabLayout() + return binding!!.root + } + + /** + * This method sets up the tab layout. If the adapter has only one element it sets the + * visibility of tabLayout to gone. + */ + fun setupTabLayout() { + binding!!.tabLayout.visibility = View.VISIBLE + if (adapter!!.count == 1) { + binding!!.tabLayout.visibility = View.GONE + } + } + + + fun onBackPressed() { + if (((adapter!!.getItem(binding!!.tabLayout.selectedTabPosition)) as BookmarkListRootFragment).backPressed()) { + // The event is handled internally by the adapter , no further action required. + return + } + + // Event is not handled by the adapter ( performed back action ) change action bar. + (requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + } + + override fun onDestroy() { + super.onDestroy() + binding = null + } + + companion object { + fun newInstance(): BookmarkFragment = BookmarkFragment().apply { + retainInstance = true + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java deleted file mode 100644 index e14cbbb6f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java +++ /dev/null @@ -1,267 +0,0 @@ -package fr.free.nrw.commons.bookmarks; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.category.GridViewAdapter; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.navtab.NavTab; -import java.util.ArrayList; -import java.util.Iterator; -import timber.log.Timber; - -public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements - FragmentManager.OnBackStackChangedListener, - MediaDetailProvider, - AdapterView.OnItemClickListener, CategoryImagesCallback { - - private MediaDetailPagerFragment mediaDetails; - //private BookmarkPicturesFragment bookmarkPicturesFragment; - private BookmarkLocationsFragment bookmarkLocationsFragment; - public Fragment listFragment; - private BookmarksPagerAdapter bookmarksPagerAdapter; - - FragmentFeaturedRootBinding binding; - - public BookmarkListRootFragment() { - //empty constructor necessary otherwise crashes on recreate - } - - public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) { - String title = bundle.getString("categoryName"); - int order = bundle.getInt("order"); - final int orderItem = bundle.getInt("orderItem"); - - switch (order){ - case 0: listFragment = new BookmarkPicturesFragment(); - break; - - case 1: listFragment = new BookmarkLocationsFragment(); - break; - - case 3: listFragment = new BookmarkCategoriesFragment(); - break; - } - if(orderItem == 2) { - listFragment = new BookmarkItemsFragment(); - } - - Bundle featuredArguments = new Bundle(); - featuredArguments.putString("categoryName", title); - listFragment.setArguments(featuredArguments); - this.bookmarksPagerAdapter = bookmarksPagerAdapter; - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (savedInstanceState == null) { - setFragment(listFragment, mediaDetails); - } - } - - public void setFragment(Fragment fragment, Fragment otherFragment) { - if (fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (fragment.isAdded() && otherFragment == null) { - getChildFragmentManager() - .beginTransaction() - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .add(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded()) { - getChildFragmentManager() - .beginTransaction() - .replace(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - } - - public void removeFragment(Fragment fragment) { - getChildFragmentManager() - .beginTransaction() - .remove(fragment) - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - - @Override - public void onAttach(final Context context) { - super.onAttach(context); - } - - @Override - public void onMediaClicked(int position) { - Timber.d("on media clicked"); - /*container.setVisibility(View.VISIBLE); - ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); - mediaDetails = new MediaDetailPagerFragment(false, true, position); - setFragment(mediaDetails, bookmarkPicturesFragment);*/ - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * - * @param i It is the index of which media object is to be returned which is same as current - * index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - if (bookmarksPagerAdapter.getMediaAdapter() == null) { - // not yet ready to return data - return null; - } else { - return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i); - } - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain - * same number of media items as that of media elements in adapter. - * - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - if (bookmarksPagerAdapter.getMediaAdapter() == null) { - return 0; - } - return bookmarksPagerAdapter.getMediaAdapter().getCount(); - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (mediaDetails != null && !listFragment.isVisible()) { - removeFragment(mediaDetails); - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - ((BookmarkFragment) getParentFragment()).setScroll(false); - setFragment(mediaDetails, listFragment); - mediaDetails.showImage(index); - } - } - - /** - * This method is called on success of API call for featured images or mobile uploads. The - * viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails != null) { - mediaDetails.notifyDataSetChanged(); - } - } - - public boolean backPressed() { - //check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException - if (mediaDetails != null) { - if (mediaDetails.isVisible()) { - // todo add get list fragment - ((BookmarkFragment) getParentFragment()).setupTabLayout(); - ArrayList removed = mediaDetails.getRemovedItems(); - removeFragment(mediaDetails); - ((BookmarkFragment) getParentFragment()).setScroll(true); - setFragment(listFragment, mediaDetails); - ((MainActivity) getActivity()).showTabs(); - if (listFragment instanceof BookmarkPicturesFragment) { - GridViewAdapter adapter = ((GridViewAdapter) ((BookmarkPicturesFragment) listFragment) - .getAdapter()); - Iterator i = removed.iterator(); - while (i.hasNext()) { - adapter.remove(adapter.getItem((int) i.next())); - } - mediaDetails.clearRemoved(); - - } - } else { - moveToContributionsFragment(); - } - } else { - moveToContributionsFragment(); - } - // notify mediaDetails did not handled the backPressed further actions required. - return false; - } - - void moveToContributionsFragment() { - ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); - ((MainActivity) getActivity()).showTabs(); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Timber.d("on media clicked"); - binding.exploreContainer.setVisibility(View.VISIBLE); - ((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - ((BookmarkFragment) getParentFragment()).setScroll(false); - setFragment(mediaDetails, listFragment); - mediaDetails.showImage(position); - } - - @Override - public void onBackStackChanged() { - - } - - @Override - public void onDestroy() { - super.onDestroy(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.kt new file mode 100644 index 000000000..a9ed33abc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.kt @@ -0,0 +1,226 @@ +package fr.free.nrw.commons.bookmarks + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemClickListener +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.category.GridViewAdapter +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment.Companion.newInstance +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.navtab.NavTab +import timber.log.Timber + +class BookmarkListRootFragment : CommonsDaggerSupportFragment, + FragmentManager.OnBackStackChangedListener, MediaDetailProvider, OnItemClickListener, + CategoryImagesCallback { + private var mediaDetails: MediaDetailPagerFragment? = null + private val bookmarkLocationsFragment: BookmarkLocationsFragment? = null + var listFragment: Fragment? = null + private var bookmarksPagerAdapter: BookmarksPagerAdapter? = null + + var binding: FragmentFeaturedRootBinding? = null + + constructor() + + constructor(bundle: Bundle, bookmarksPagerAdapter: BookmarksPagerAdapter) { + val title = bundle.getString("categoryName") + val order = bundle.getInt("order") + val orderItem = bundle.getInt("orderItem") + + when (order) { + 0 -> listFragment = BookmarkPicturesFragment() + 1 -> listFragment = BookmarkLocationsFragment() + 3 -> listFragment = BookmarkCategoriesFragment() + } + if (orderItem == 2) { + listFragment = BookmarkItemsFragment() + } + + val featuredArguments = Bundle() + featuredArguments.putString("categoryName", title) + listFragment!!.setArguments(featuredArguments) + this.bookmarksPagerAdapter = bookmarksPagerAdapter + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreate(savedInstanceState) + binding = FragmentFeaturedRootBinding.inflate(inflater, container, false) + return binding!!.getRoot() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (savedInstanceState == null) { + setFragment(listFragment!!, mediaDetails) + } + } + + fun setFragment(fragment: Fragment, otherFragment: Fragment?) { + if (fragment.isAdded() && otherFragment != null) { + getChildFragmentManager() + .beginTransaction() + .hide(otherFragment) + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + getChildFragmentManager().executePendingTransactions() + } else if (fragment.isAdded() && otherFragment == null) { + getChildFragmentManager() + .beginTransaction() + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + getChildFragmentManager().executePendingTransactions() + } else if (!fragment.isAdded() && otherFragment != null) { + getChildFragmentManager() + .beginTransaction() + .hide(otherFragment) + .add(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + getChildFragmentManager().executePendingTransactions() + } else if (!fragment.isAdded()) { + getChildFragmentManager() + .beginTransaction() + .replace(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + getChildFragmentManager().executePendingTransactions() + } + } + + fun removeFragment(fragment: Fragment) { + getChildFragmentManager() + .beginTransaction() + .remove(fragment) + .commit() + getChildFragmentManager().executePendingTransactions() + } + + override fun onMediaClicked(position: Int) { + Timber.d("on media clicked") + /*container.setVisibility(View.VISIBLE); + ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); + mediaDetails = new MediaDetailPagerFragment(false, true, position); + setFragment(mediaDetails, bookmarkPicturesFragment);*/ + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * + * @param i It is the index of which media object is to be returned which is same as current + * index of viewPager. + * @return Media Object + */ + override fun getMediaAtPosition(i: Int): Media? = + bookmarksPagerAdapter!!.mediaAdapter?.getItem(i) as Media? + + /** + * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain + * same number of media items as that of media elements in adapter. + * + * @return Total Media count in the adapter + */ + override fun getTotalMediaCount(): Int = + bookmarksPagerAdapter!!.mediaAdapter?.count ?: 0 + + override fun getContributionStateAt(position: Int): Int? { + return null + } + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (mediaDetails != null && !listFragment!!.isVisible()) { + removeFragment(mediaDetails!!) + mediaDetails = newInstance(false, true) + (parentFragment as BookmarkFragment).setScroll(false) + setFragment(mediaDetails!!, listFragment) + mediaDetails!!.showImage(index) + } + } + + /** + * This method is called on success of API call for featured images or mobile uploads. The + * viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + if (mediaDetails != null) { + mediaDetails!!.notifyDataSetChanged() + } + } + + fun backPressed(): Boolean { + //check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException + if (mediaDetails != null) { + if (mediaDetails!!.isVisible()) { + // todo add get list fragment + (parentFragment as BookmarkFragment).setupTabLayout() + val removed: ArrayList = mediaDetails!!.removedItems + removeFragment(mediaDetails!!) + (parentFragment as BookmarkFragment).setScroll(true) + setFragment(listFragment!!, mediaDetails) + (requireActivity() as MainActivity).showTabs() + if (listFragment is BookmarkPicturesFragment) { + val adapter = ((listFragment as BookmarkPicturesFragment) + .getAdapter() as GridViewAdapter?) + val i: MutableIterator<*> = removed.iterator() + while (i.hasNext()) { + adapter!!.remove(adapter.getItem(i.next() as Int)) + } + mediaDetails!!.clearRemoved() + } + } else { + moveToContributionsFragment() + } + } else { + moveToContributionsFragment() + } + // notify mediaDetails did not handled the backPressed further actions required. + return false + } + + fun moveToContributionsFragment() { + (requireActivity() as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code()) + (requireActivity() as MainActivity).showTabs() + } + + override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + Timber.d("on media clicked") + binding!!.exploreContainer.visibility = View.VISIBLE + (parentFragment as BookmarkFragment).binding!!.tabLayout.setVisibility(View.GONE) + mediaDetails = newInstance(false, true) + (parentFragment as BookmarkFragment).setScroll(false) + setFragment(mediaDetails!!, listFragment) + mediaDetails!!.showImage(position) + } + + override fun onBackStackChanged() = Unit + + override fun onDestroy() { + super.onDestroy() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java deleted file mode 100644 index f0620032a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons.bookmarks; - -import android.content.Context; -import android.os.Bundle; -import android.widget.ListAdapter; - -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; - -import java.util.ArrayList; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; - -public class BookmarksPagerAdapter extends FragmentPagerAdapter { - - private ArrayList pages; - - /** - * Default Constructor - * @param fm - * @param context - * @param onlyPictures is true if the fragment requires only BookmarkPictureFragment - * (i.e. when no user is logged in). - */ - BookmarksPagerAdapter(FragmentManager fm, Context context,boolean onlyPictures) { - super(fm); - pages = new ArrayList<>(); - Bundle picturesBundle = new Bundle(); - picturesBundle.putString("categoryName", context.getString(R.string.title_page_bookmarks_pictures)); - picturesBundle.putInt("order", 0); - pages.add(new BookmarkPages( - new BookmarkListRootFragment(picturesBundle, this), - context.getString(R.string.title_page_bookmarks_pictures))); - if (!onlyPictures) { - // if onlyPictures is false we also add the location fragment. - Bundle locationBundle = new Bundle(); - locationBundle.putString("categoryName", - context.getString(R.string.title_page_bookmarks_locations)); - locationBundle.putInt("order", 1); - pages.add(new BookmarkPages( - new BookmarkListRootFragment(locationBundle, this), - context.getString(R.string.title_page_bookmarks_locations))); - - locationBundle.putInt("orderItem", 2); - pages.add(new BookmarkPages( - new BookmarkListRootFragment(locationBundle, this), - context.getString(R.string.title_page_bookmarks_items))); - } - final Bundle categoriesBundle = new Bundle(); - categoriesBundle.putString("categoryName", - context.getString(R.string.title_page_bookmarks_categories)); - categoriesBundle.putInt("order", 3); - pages.add(new BookmarkPages( - new BookmarkListRootFragment(categoriesBundle, this), - context.getString(R.string.title_page_bookmarks_categories))); - notifyDataSetChanged(); - } - - @Override - public Fragment getItem(int position) { - return pages.get(position).getPage(); - } - - @Override - public int getCount() { - return pages.size(); - } - - @Nullable - @Override - public CharSequence getPageTitle(int position) { - return pages.get(position).getTitle(); - } - - /** - * Return the Adapter used to display the picture gridview - * @return adapter - */ - public ListAdapter getMediaAdapter() { - BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment); - return fragment.getAdapter(); - } - - /** - * Update the pictures list for the bookmark fragment - */ - public void requestPictureListUpdate() { - BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment); - fragment.onResume(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.kt new file mode 100644 index 000000000..a7cbf0e68 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.kt @@ -0,0 +1,82 @@ +package fr.free.nrw.commons.bookmarks + +import android.content.Context +import android.widget.ListAdapter +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment + +class BookmarksPagerAdapter internal constructor( + fm: FragmentManager, context: Context, onlyPictures: Boolean +) : FragmentPagerAdapter(fm) { + private val pages = mutableListOf() + + /** + * Default Constructor + * @param fm + * @param context + * @param onlyPictures is true if the fragment requires only BookmarkPictureFragment + * (i.e. when no user is logged in). + */ + init { + pages.add( + BookmarkPages( + BookmarkListRootFragment( + bundleOf( + "categoryName" to context.getString(R.string.title_page_bookmarks_pictures), + "order" to 0 + ), this + ), context.getString(R.string.title_page_bookmarks_pictures) + ) + ) + if (!onlyPictures) { + // if onlyPictures is false we also add the location fragment. + val locationBundle = bundleOf( + "categoryName" to context.getString(R.string.title_page_bookmarks_locations), + "order" to 1 + ) + + pages.add( + BookmarkPages( + BookmarkListRootFragment(locationBundle, this), + context.getString(R.string.title_page_bookmarks_locations) + ) + ) + + locationBundle.putInt("orderItem", 2) + pages.add( + BookmarkPages( + BookmarkListRootFragment(locationBundle, this), + context.getString(R.string.title_page_bookmarks_items) + ) + ) + } + pages.add( + BookmarkPages( + BookmarkListRootFragment( + bundleOf( + "categoryName" to context.getString(R.string.title_page_bookmarks_categories), + "order" to 3 + ), this), + context.getString(R.string.title_page_bookmarks_categories) + ) + ) + notifyDataSetChanged() + } + + override fun getItem(position: Int): Fragment = pages[position].page!! + + override fun getCount(): Int = pages.size + + override fun getPageTitle(position: Int): CharSequence? = pages[position].title + + /** + * Return the Adapter used to display the picture gridview + * @return adapter + */ + val mediaAdapter: ListAdapter? + get() = (((pages[0].page as BookmarkListRootFragment).listFragment) as BookmarkPicturesFragment).getAdapter() +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.java deleted file mode 100644 index 3a85ec159..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.java +++ /dev/null @@ -1,129 +0,0 @@ -package fr.free.nrw.commons.bookmarks.items; - -import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_ID; -import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.TABLE_NAME; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.text.TextUtils; -import androidx.annotation.NonNull; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.CommonsDaggerContentProvider; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * Handles private storage for bookmarked items - */ -public class BookmarkItemsContentProvider extends CommonsDaggerContentProvider { - - private static final String BASE_PATH = "bookmarksItems"; - public static final Uri BASE_URI = - Uri.parse("content://" + BuildConfig.BOOKMARK_ITEMS_AUTHORITY + "/" + BASE_PATH); - - - /** - * Append bookmark items ID to the base uri - */ - public static Uri uriForName(final String id) { - return Uri.parse(BASE_URI + "/" + id); - } - - @Inject - DBOpenHelper dbOpenHelper; - - @Override - public String getType(@NonNull final Uri uri) { - return null; - } - - /** - * Queries the SQLite database for the bookmark items - * @param uri : contains the uri for bookmark items - * @param projection : contains the all fields of the table - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - * @param sortOrder : ascending or descending - */ - @SuppressWarnings("ConstantConditions") - @Override - public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection, - final String[] selectionArgs, final String sortOrder) { - final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); - queryBuilder.setTables(TABLE_NAME); - final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - final Cursor cursor = queryBuilder.query(db, projection, selection, - selectionArgs, null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - /** - * Handles the update query of local SQLite Database - * @param uri : contains the uri for bookmark items - * @param contentValues : new values to be entered to db - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - */ - @SuppressWarnings("ConstantConditions") - @Override - public int update(@NonNull final Uri uri, final ContentValues contentValues, - final String selection, final String[] selectionArgs) { - final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - final int rowsUpdated; - if (TextUtils.isEmpty(selection)) { - final int id = Integer.parseInt(uri.getLastPathSegment()); - rowsUpdated = sqlDB.update(TABLE_NAME, - contentValues, - COLUMN_ID + " = ?", - new String[]{String.valueOf(id)}); - } else { - throw new IllegalArgumentException( - "Parameter `selection` should be empty when updating an ID"); - } - - getContext().getContentResolver().notifyChange(uri, null); - return rowsUpdated; - } - - /** - * Handles the insertion of new bookmark items record to local SQLite Database - * @param uri - * @param contentValues - * @return - */ - @SuppressWarnings("ConstantConditions") - @Override - public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) { - final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - final long id = sqlDB.insert(TABLE_NAME, null, contentValues); - getContext().getContentResolver().notifyChange(uri, null); - return Uri.parse(BASE_URI + "/" + id); - } - - /** - * Handles the deletion of new bookmark items record to local SQLite Database - * @param uri - * @param s - * @param strings - * @return - */ - @SuppressWarnings("ConstantConditions") - @Override - public int delete(@NonNull final Uri uri, final String s, final String[] strings) { - final int rows; - final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - Timber.d("Deleting bookmark name %s", uri.getLastPathSegment()); - rows = db.delete( - TABLE_NAME, - "item_id = ?", - new String[]{uri.getLastPathSegment()} - ); - getContext().getContentResolver().notifyChange(uri, null); - return rows; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.kt new file mode 100644 index 000000000..8007ba208 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsContentProvider.kt @@ -0,0 +1,101 @@ +package fr.free.nrw.commons.bookmarks.items + +import android.content.ContentValues +import android.database.Cursor +import android.database.sqlite.SQLiteQueryBuilder +import android.net.Uri +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.TABLE_NAME +import fr.free.nrw.commons.di.CommonsDaggerContentProvider +import androidx.core.net.toUri +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID + +/** + * Handles private storage for bookmarked items + */ +class BookmarkItemsContentProvider : CommonsDaggerContentProvider() { + override fun getType(uri: Uri): String? = null + + /** + * Queries the SQLite database for the bookmark items + * @param uri : contains the uri for bookmark items + * @param projection : contains the all fields of the table + * @param selection : handles Where + * @param selectionArgs : the condition of Where clause + * @param sortOrder : ascending or descending + */ + override fun query( + uri: Uri, projection: Array?, selection: String?, + selectionArgs: Array?, sortOrder: String? + ): Cursor { + val queryBuilder = SQLiteQueryBuilder().apply { + tables = TABLE_NAME + } + + return queryBuilder.query( + requireDb(), projection, selection, + selectionArgs, null, null, sortOrder + ).apply { + setNotificationUri(requireContext().contentResolver, uri) + } + } + + /** + * Handles the update query of local SQLite Database + * @param uri : contains the uri for bookmark items + * @param contentValues : new values to be entered to db + * @param selection : handles Where + * @param selectionArgs : the condition of Where clause + */ + override fun update( + uri: Uri, contentValues: ContentValues?, + selection: String?, selectionArgs: Array? + ): Int { + val rowsUpdated: Int + if (selection.isNullOrEmpty()) { + val id = uri.lastPathSegment!!.toInt() + rowsUpdated = requireDb().update( + TABLE_NAME, + contentValues, + "$COLUMN_ID = ?", + arrayOf(id.toString()) + ) + } else { + throw IllegalArgumentException( + "Parameter `selection` should be empty when updating an ID" + ) + } + + requireContext().contentResolver.notifyChange(uri, null) + return rowsUpdated + } + + /** + * Handles the insertion of new bookmark items record to local SQLite Database + */ + override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { + val id = requireDb().insert(TABLE_NAME, null, contentValues) + requireContext().contentResolver.notifyChange(uri, null) + return "$BASE_URI/$id".toUri() + } + + + /** + * Handles the deletion of new bookmark items record to local SQLite Database + */ + override fun delete(uri: Uri, s: String?, strings: Array?): Int { + val rows: Int = requireDb().delete( + TABLE_NAME, + "$COLUMN_ID = ?", + arrayOf(uri.lastPathSegment) + ) + requireContext().contentResolver.notifyChange(uri, null) + return rows + } + + companion object { + private const val BASE_PATH = "bookmarksItems" + val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_ITEMS_AUTHORITY}/$BASE_PATH".toUri() + fun uriForName(id: String) = "$BASE_URI/$id".toUri() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.java deleted file mode 100644 index d059e4cc4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.java +++ /dev/null @@ -1,27 +0,0 @@ -package fr.free.nrw.commons.bookmarks.items; - -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Handles loading bookmarked items from Database - */ -@Singleton -public class BookmarkItemsController { - - @Inject - BookmarkItemsDao bookmarkItemsDao; - - @Inject - public BookmarkItemsController() {} - - /** - * Load from DB the bookmarked items - * @return a list of DepictedItem objects. - */ - public List loadFavoritesItems() { - return bookmarkItemsDao.getAllBookmarksItems(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt new file mode 100644 index 000000000..d1a9ef785 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsController.kt @@ -0,0 +1,23 @@ +package fr.free.nrw.commons.bookmarks.items + +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Handles loading bookmarked items from Database + */ +@Singleton +class BookmarkItemsController @Inject constructor() { + @JvmField + @Inject + var bookmarkItemsDao: BookmarkItemsDao? = null + + /** + * Load from DB the bookmarked items + * @return a list of DepictedItem objects. + */ + fun loadFavoritesItems(): List { + return bookmarkItemsDao?.getAllBookmarksItems() ?: emptyList() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java deleted file mode 100644 index 6788a8290..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java +++ /dev/null @@ -1,329 +0,0 @@ -package fr.free.nrw.commons.bookmarks.items; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; -import fr.free.nrw.commons.category.CategoryItem; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; -import javax.inject.Singleton; -import org.apache.commons.lang3.StringUtils; - -/** - * Handles database operations for bookmarked items - */ -@Singleton -public class BookmarkItemsDao { - - private final Provider clientProvider; - - @Inject - public BookmarkItemsDao( - @Named("bookmarksItem") final Provider clientProvider) { - this.clientProvider = clientProvider; - } - - - /** - * Find all persisted items bookmarks on database - * @return list of bookmarks - */ - public List getAllBookmarksItems() { - final List items = new ArrayList<>(); - final ContentProviderClient db = clientProvider.get(); - try (final Cursor cursor = db.query( - BookmarkItemsContentProvider.BASE_URI, - Table.ALL_FIELDS, - null, - new String[]{}, - null)) { - while (cursor != null && cursor.moveToNext()) { - items.add(fromCursor(cursor)); - } - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - return items; - } - - - /** - * Look for a bookmark in database and in order to insert or delete it - * @param depictedItem : Bookmark object - * @return boolean : is bookmark now favorite ? - */ - public boolean updateBookmarkItem(final DepictedItem depictedItem) { - final boolean bookmarkExists = findBookmarkItem(depictedItem.getId()); - if (bookmarkExists) { - deleteBookmarkItem(depictedItem); - } else { - addBookmarkItem(depictedItem); - } - return !bookmarkExists; - } - - /** - * Add a Bookmark to database - * @param depictedItem : Bookmark to add - */ - private void addBookmarkItem(final DepictedItem depictedItem) { - final ContentProviderClient db = clientProvider.get(); - try { - db.insert(BookmarkItemsContentProvider.BASE_URI, toContentValues(depictedItem)); - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Delete a bookmark from database - * @param depictedItem : Bookmark to delete - */ - private void deleteBookmarkItem(final DepictedItem depictedItem) { - final ContentProviderClient db = clientProvider.get(); - try { - db.delete(BookmarkItemsContentProvider.uriForName(depictedItem.getId()), null, null); - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Find a bookmark from database based on its name - * @param depictedItemID : Bookmark to find - * @return boolean : is bookmark in database ? - */ - public boolean findBookmarkItem(final String depictedItemID) { - if (depictedItemID == null) { //Avoiding NPE's - return false; - } - final ContentProviderClient db = clientProvider.get(); - try (final Cursor cursor = db.query( - BookmarkItemsContentProvider.BASE_URI, - Table.ALL_FIELDS, - Table.COLUMN_ID + "=?", - new String[]{depictedItemID}, - null - )) { - if (cursor != null && cursor.moveToFirst()) { - return true; - } - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - return false; - } - - /** - * Recives real data from cursor - * @param cursor : Object for storing database data - * @return DepictedItem - */ - @SuppressLint("Range") - DepictedItem fromCursor(final Cursor cursor) { - final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); - final String description - = cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)); - final String imageUrl = cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE)); - final String instanceListString - = cursor.getString(cursor.getColumnIndex(Table.COLUMN_INSTANCE_LIST)); - final List instanceList = StringToArray(instanceListString); - final String categoryNameListString = cursor.getString(cursor - .getColumnIndex(Table.COLUMN_CATEGORIES_NAME_LIST)); - final List categoryNameList = StringToArray(categoryNameListString); - final String categoryDescriptionListString = cursor.getString(cursor - .getColumnIndex(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST)); - final List categoryDescriptionList = StringToArray(categoryDescriptionListString); - final String categoryThumbnailListString = cursor.getString(cursor - .getColumnIndex(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST)); - final List categoryThumbnailList = StringToArray(categoryThumbnailListString); - final List categoryList = convertToCategoryItems(categoryNameList, - categoryDescriptionList, categoryThumbnailList); - final boolean isSelected - = Boolean.parseBoolean(cursor.getString(cursor - .getColumnIndex(Table.COLUMN_IS_SELECTED))); - final String id = cursor.getString(cursor.getColumnIndex(Table.COLUMN_ID)); - - return new DepictedItem( - fileName, - description, - imageUrl, - instanceList, - categoryList, - isSelected, - id - ); - } - - private List convertToCategoryItems(List categoryNameList, - List categoryDescriptionList, List categoryThumbnailList) { - List categoryItems = new ArrayList<>(); - for(int i=0; i StringToArray(final String listString) { - final String[] elements = listString.split(","); - return Arrays.asList(elements); - } - - /** - * Converts string to List - * @param list list of items - * @return string comma separated single string of items - */ - private String ArrayToString(final List list) { - if (list != null) { - return StringUtils.join(list, ','); - } - return null; - } - - /** - * Takes data from DepictedItem and create a content value object - * @param depictedItem depicted item - * @return ContentValues - */ - private ContentValues toContentValues(final DepictedItem depictedItem) { - - final List namesOfCommonsCategories = new ArrayList<>(); - for (final CategoryItem category : - depictedItem.getCommonsCategories()) { - namesOfCommonsCategories.add(category.getName()); - } - - final List descriptionsOfCommonsCategories = new ArrayList<>(); - for (final CategoryItem category : - depictedItem.getCommonsCategories()) { - descriptionsOfCommonsCategories.add(category.getDescription()); - } - - final List thumbnailsOfCommonsCategories = new ArrayList<>(); - for (final CategoryItem category : - depictedItem.getCommonsCategories()) { - thumbnailsOfCommonsCategories.add(category.getThumbnail()); - } - - final ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_NAME, depictedItem.getName()); - cv.put(Table.COLUMN_DESCRIPTION, depictedItem.getDescription()); - cv.put(Table.COLUMN_IMAGE, depictedItem.getImageUrl()); - cv.put(Table.COLUMN_INSTANCE_LIST, ArrayToString(depictedItem.getInstanceOfs())); - cv.put(Table.COLUMN_CATEGORIES_NAME_LIST, ArrayToString(namesOfCommonsCategories)); - cv.put(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST, - ArrayToString(descriptionsOfCommonsCategories)); - cv.put(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST, - ArrayToString(thumbnailsOfCommonsCategories)); - cv.put(Table.COLUMN_IS_SELECTED, depictedItem.isSelected()); - cv.put(Table.COLUMN_ID, depictedItem.getId()); - return cv; - } - - /** - * Table of bookmarksItems data - */ - public static final class Table { - public static final String TABLE_NAME = "bookmarksItems"; - public static final String COLUMN_NAME = "item_name"; - public static final String COLUMN_DESCRIPTION = "item_description"; - public static final String COLUMN_IMAGE = "item_image_url"; - public static final String COLUMN_INSTANCE_LIST = "item_instance_of"; - public static final String COLUMN_CATEGORIES_NAME_LIST = "item_name_categories"; - public static final String COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories"; - public static final String COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories"; - public static final String COLUMN_IS_SELECTED = "item_is_selected"; - public static final String COLUMN_ID = "item_id"; - - public static final String[] ALL_FIELDS = { - COLUMN_NAME, - COLUMN_DESCRIPTION, - COLUMN_IMAGE, - COLUMN_INSTANCE_LIST, - COLUMN_CATEGORIES_NAME_LIST, - COLUMN_CATEGORIES_DESCRIPTION_LIST, - COLUMN_CATEGORIES_THUMBNAIL_LIST, - COLUMN_IS_SELECTED, - COLUMN_ID - }; - - static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; - static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_NAME + " STRING," - + COLUMN_DESCRIPTION + " STRING," - + COLUMN_IMAGE + " STRING," - + COLUMN_INSTANCE_LIST + " STRING," - + COLUMN_CATEGORIES_NAME_LIST + " STRING," - + COLUMN_CATEGORIES_DESCRIPTION_LIST + " STRING," - + COLUMN_CATEGORIES_THUMBNAIL_LIST + " STRING," - + COLUMN_IS_SELECTED + " STRING," - + COLUMN_ID + " STRING PRIMARY KEY" - + ");"; - - /** - * Creates table - * @param db SQLiteDatabase - */ - public static void onCreate(final SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * Deletes database - * @param db SQLiteDatabase - */ - public static void onDelete(final SQLiteDatabase db) { - db.execSQL(DROP_TABLE_STATEMENT); - onCreate(db); - } - - /** - * Updates database - * @param db SQLiteDatabase - * @param from starting - * @param to end - */ - public static void onUpdate(final SQLiteDatabase db, int from, final int to) { - if (from == to) { - return; - } - if (from < 18) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - - if (from == 18) { - // table added in version 19 - onCreate(db); - from++; - onUpdate(db, from, to); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt new file mode 100644 index 000000000..d64ab16b3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt @@ -0,0 +1,199 @@ +package fr.free.nrw.commons.bookmarks.items + +import android.annotation.SuppressLint +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.os.RemoteException +import androidx.core.content.contentValuesOf +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.BASE_URI +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.uriForName +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME +import fr.free.nrw.commons.category.CategoryItem +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.utils.arrayToString +import fr.free.nrw.commons.utils.getString +import fr.free.nrw.commons.utils.getStringArray +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider +import javax.inject.Singleton + +/** + * Handles database operations for bookmarked items + */ +@Singleton +class BookmarkItemsDao @Inject constructor( + @param:Named("bookmarksItem") private val clientProvider: Provider +) { + /** + * Find all persisted items bookmarks on database + * @return list of bookmarks + */ + fun getAllBookmarksItems(): List { + val items: MutableList = mutableListOf() + val db = clientProvider.get() + try { + db.query( + BASE_URI, + BookmarkItemsTable.ALL_FIELDS, + null, + arrayOf(), + null + ).use { cursor -> + while (cursor != null && cursor.moveToNext()) { + items.add(fromCursor(cursor)) + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + return items + } + + + /** + * Look for a bookmark in database and in order to insert or delete it + * @param depictedItem : Bookmark object + * @return boolean : is bookmark now favorite ? + */ + fun updateBookmarkItem(depictedItem: DepictedItem): Boolean { + val bookmarkExists = findBookmarkItem(depictedItem.id) + if (bookmarkExists) { + deleteBookmarkItem(depictedItem) + } else { + addBookmarkItem(depictedItem) + } + return !bookmarkExists + } + + /** + * Add a Bookmark to database + * @param depictedItem : Bookmark to add + */ + private fun addBookmarkItem(depictedItem: DepictedItem) { + val db = clientProvider.get() + try { + db.insert(BASE_URI, toContentValues(depictedItem)) + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Delete a bookmark from database + * @param depictedItem : Bookmark to delete + */ + private fun deleteBookmarkItem(depictedItem: DepictedItem) { + val db = clientProvider.get() + try { + db.delete(uriForName(depictedItem.id), null, null) + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Find a bookmark from database based on its name + * @param depictedItemID : Bookmark to find + * @return boolean : is bookmark in database ? + */ + fun findBookmarkItem(depictedItemID: String?): Boolean { + if (depictedItemID == null) { //Avoiding NPE's + return false + } + val db = clientProvider.get() + try { + db.query( + BASE_URI, + BookmarkItemsTable.ALL_FIELDS, + COLUMN_ID + "=?", + arrayOf(depictedItemID), + null + ).use { cursor -> + if (cursor != null && cursor.moveToFirst()) { + return true + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + return false + } + + /** + * Recives real data from cursor + * @param cursor : Object for storing database data + * @return DepictedItem + */ + @SuppressLint("Range") + fun fromCursor(cursor: Cursor) = with(cursor) { + DepictedItem( + getString(COLUMN_NAME), + getString(COLUMN_DESCRIPTION), + getString(COLUMN_IMAGE), + getStringArray(COLUMN_INSTANCE_LIST), + convertToCategoryItems( + getStringArray(COLUMN_CATEGORIES_NAME_LIST), + getStringArray(COLUMN_CATEGORIES_DESCRIPTION_LIST), + getStringArray(COLUMN_CATEGORIES_THUMBNAIL_LIST) + ), + getString(COLUMN_IS_SELECTED).toBoolean(), + getString(COLUMN_ID) + ) + } + + private fun convertToCategoryItems( + categoryNameList: List, + categoryDescriptionList: List, + categoryThumbnailList: List + ): List { + return buildList { + for (i in categoryNameList.indices) { + add( + CategoryItem( + categoryNameList[i], + categoryDescriptionList[i], + categoryThumbnailList[i], + false + ) + ) + } + } + } + + /** + * Takes data from DepictedItem and create a content value object + * @param depictedItem depicted item + * @return ContentValues + */ + private fun toContentValues(depictedItem: DepictedItem): ContentValues { + return contentValuesOf( + COLUMN_NAME to depictedItem.name, + COLUMN_DESCRIPTION to depictedItem.description, + COLUMN_IMAGE to depictedItem.imageUrl, + COLUMN_INSTANCE_LIST to arrayToString(depictedItem.instanceOfs), + COLUMN_CATEGORIES_NAME_LIST to arrayToString(depictedItem.commonsCategories.map { it.name }), + COLUMN_CATEGORIES_DESCRIPTION_LIST to arrayToString(depictedItem.commonsCategories.map { it.description }), + COLUMN_CATEGORIES_THUMBNAIL_LIST to arrayToString(depictedItem.commonsCategories.map { it.thumbnail }), + COLUMN_IS_SELECTED to depictedItem.isSelected, + COLUMN_ID to depictedItem.id, + ) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.java deleted file mode 100644 index 75a0fa7a4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.java +++ /dev/null @@ -1,81 +0,0 @@ -package fr.free.nrw.commons.bookmarks.items; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import dagger.android.support.DaggerFragment; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import java.util.List; -import javax.inject.Inject; -import org.jetbrains.annotations.NotNull; - -/** - * Tab fragment to show list of bookmarked Wikidata Items - */ -public class BookmarkItemsFragment extends DaggerFragment { - - private FragmentBookmarksItemsBinding binding; - - @Inject - BookmarkItemsController controller; - - public static BookmarkItemsFragment newInstance() { - return new BookmarkItemsFragment(); - } - - @Override - public View onCreateView( - @NonNull final LayoutInflater inflater, - final ViewGroup container, - final Bundle savedInstanceState - ) { - binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - initList(requireContext()); - } - - @Override - public void onResume() { - super.onResume(); - initList(requireContext()); - } - - /** - * Get list of DepictedItem and sets to the adapter - * @param context context - */ - private void initList(final Context context) { - final List depictItems = controller.loadFavoritesItems(); - final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context); - binding.listView.setAdapter(adapter); - binding.loadingImagesProgressBar.setVisibility(View.GONE); - if (depictItems.isEmpty()) { - binding.statusMessage.setText(R.string.bookmark_empty); - binding.statusMessage.setVisibility(View.VISIBLE); - } else { - binding.statusMessage.setVisibility(View.GONE); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.kt new file mode 100644 index 000000000..aa9dcccc0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragment.kt @@ -0,0 +1,62 @@ +package fr.free.nrw.commons.bookmarks.items + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dagger.android.support.DaggerFragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding +import javax.inject.Inject + +/** + * Tab fragment to show list of bookmarked Wikidata Items + */ +class BookmarkItemsFragment : DaggerFragment() { + private var binding: FragmentBookmarksItemsBinding? = null + + @JvmField + @Inject + var controller: BookmarkItemsController? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false) + return binding!!.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initList(requireContext()) + } + + override fun onResume() { + super.onResume() + initList(requireContext()) + } + + /** + * Get list of DepictedItem and sets to the adapter + * @param context context + */ + private fun initList(context: Context) { + val depictItems = controller!!.loadFavoritesItems() + binding!!.listView.adapter = BookmarkItemsAdapter(depictItems, context) + binding!!.loadingImagesProgressBar.visibility = View.GONE + if (depictItems.isEmpty()) { + binding!!.statusMessage.setText(R.string.bookmark_empty) + binding!!.statusMessage.visibility = View.VISIBLE + } else { + binding!!.statusMessage.visibility = View.GONE + } + } + + override fun onDestroy() { + super.onDestroy() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsTable.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsTable.kt new file mode 100644 index 000000000..b1b03c71b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsTable.kt @@ -0,0 +1,90 @@ +package fr.free.nrw.commons.bookmarks.items + +import android.database.sqlite.SQLiteDatabase + +/** + * Table of bookmarksItems data + */ +object BookmarkItemsTable { + const val TABLE_NAME = "bookmarksItems" + const val COLUMN_NAME = "item_name" + const val COLUMN_DESCRIPTION = "item_description" + const val COLUMN_IMAGE = "item_image_url" + const val COLUMN_INSTANCE_LIST = "item_instance_of" + const val COLUMN_CATEGORIES_NAME_LIST = "item_name_categories" + const val COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories" + const val COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories" + const val COLUMN_IS_SELECTED = "item_is_selected" + const val COLUMN_ID = "item_id" + + val ALL_FIELDS = arrayOf( + COLUMN_NAME, + COLUMN_DESCRIPTION, + COLUMN_IMAGE, + COLUMN_INSTANCE_LIST, + COLUMN_CATEGORIES_NAME_LIST, + COLUMN_CATEGORIES_DESCRIPTION_LIST, + COLUMN_CATEGORIES_THUMBNAIL_LIST, + COLUMN_IS_SELECTED, + COLUMN_ID + ) + + const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" + + val CREATE_TABLE_STATEMENT = + """CREATE TABLE $TABLE_NAME ( + $COLUMN_NAME STRING, + $COLUMN_DESCRIPTION STRING, + $COLUMN_IMAGE STRING, + $COLUMN_INSTANCE_LIST STRING, + $COLUMN_CATEGORIES_NAME_LIST STRING, + $COLUMN_CATEGORIES_DESCRIPTION_LIST STRING, + $COLUMN_CATEGORIES_THUMBNAIL_LIST STRING, + $COLUMN_IS_SELECTED STRING, + $COLUMN_ID STRING PRIMARY KEY + );""".trimIndent() + + /** + * Creates table + * + * @param db SQLiteDatabase + */ + fun onCreate(db: SQLiteDatabase) { + db.execSQL(CREATE_TABLE_STATEMENT) + } + + /** + * Deletes database + * + * @param db SQLiteDatabase + */ + fun onDelete(db: SQLiteDatabase) { + db.execSQL(DROP_TABLE_STATEMENT) + onCreate(db) + } + + /** + * Updates database + * + * @param db SQLiteDatabase + * @param from starting + * @param to end + */ + fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { + if (from == to) { + return + } + + if (from < 18) { + // doesn't exist yet + onUpdate(db, from + 1, to) + return + } + + if (from == 18) { + // table added in version 19 + onCreate(db) + onUpdate(db, from + 1, to) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java deleted file mode 100644 index 2aac07902..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java +++ /dev/null @@ -1,120 +0,0 @@ -package fr.free.nrw.commons.bookmarks.pictures; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -// We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though) -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import javax.inject.Inject; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.CommonsDaggerContentProvider; -import timber.log.Timber; - -import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME; -import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.TABLE_NAME; - -/** - * Handles private storage for Bookmark pictures - */ -public class BookmarkPicturesContentProvider extends CommonsDaggerContentProvider { - - private static final String BASE_PATH = "bookmarks"; - public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_AUTHORITY + "/" + BASE_PATH); - - /** - * Append bookmark pictures name to the base uri - */ - public static Uri uriForName(String name) { - return Uri.parse(BASE_URI.toString() + "/" + name); - } - - @Inject - DBOpenHelper dbOpenHelper; - - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - /** - * Queries the SQLite database for the bookmark pictures - * @param uri : contains the uri for bookmark pictures - * @param projection - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - * @param sortOrder : ascending or descending - */ - @SuppressWarnings("ConstantConditions") - @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); - queryBuilder.setTables(TABLE_NAME); - - SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - - return cursor; - } - - /** - * Handles the update query of local SQLite Database - * @param uri : contains the uri for bookmark pictures - * @param contentValues : new values to be entered to db - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - */ - @SuppressWarnings("ConstantConditions") - @Override - public int update(@NonNull Uri uri, ContentValues contentValues, String selection, - String[] selectionArgs) { - SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - int rowsUpdated; - if (TextUtils.isEmpty(selection)) { - int id = Integer.valueOf(uri.getLastPathSegment()); - rowsUpdated = sqlDB.update(TABLE_NAME, - contentValues, - COLUMN_MEDIA_NAME + " = ?", - new String[]{String.valueOf(id)}); - } else { - throw new IllegalArgumentException( - "Parameter `selection` should be empty when updating an ID"); - } - getContext().getContentResolver().notifyChange(uri, null); - return rowsUpdated; - } - - /** - * Handles the insertion of new bookmark pictures record to local SQLite Database - */ - @SuppressWarnings("ConstantConditions") - @Override - public Uri insert(@NonNull Uri uri, ContentValues contentValues) { - SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - long id = sqlDB.insert(BookmarkPicturesDao.Table.TABLE_NAME, null, contentValues); - getContext().getContentResolver().notifyChange(uri, null); - return Uri.parse(BASE_URI + "/" + id); - } - - @SuppressWarnings("ConstantConditions") - @Override - public int delete(@NonNull Uri uri, String s, String[] strings) { - int rows; - SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - Timber.d("Deleting bookmark name %s", uri.getLastPathSegment()); - rows = db.delete(TABLE_NAME, - "media_name = ?", - new String[]{uri.getLastPathSegment()} - ); - getContext().getContentResolver().notifyChange(uri, null); - return rows; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.kt new file mode 100644 index 000000000..bf6f6039b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.kt @@ -0,0 +1,100 @@ +package fr.free.nrw.commons.bookmarks.pictures + +import android.content.ContentValues +import android.database.Cursor +import android.database.sqlite.SQLiteQueryBuilder +import android.net.Uri +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.di.CommonsDaggerContentProvider +import androidx.core.net.toUri +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.TABLE_NAME + +/** + * Handles private storage for Bookmark pictures + */ +class BookmarkPicturesContentProvider : CommonsDaggerContentProvider() { + override fun getType(uri: Uri): String? = null + + /** + * Queries the SQLite database for the bookmark pictures + * @param uri : contains the uri for bookmark pictures + * @param projection + * @param selection : handles Where + * @param selectionArgs : the condition of Where clause + * @param sortOrder : ascending or descending + */ + override fun query( + uri: Uri, projection: Array?, selection: String?, + selectionArgs: Array?, sortOrder: String? + ): Cursor { + val queryBuilder = SQLiteQueryBuilder().apply { + tables = TABLE_NAME + } + + val cursor = queryBuilder.query( + requireDb(), projection, selection, + selectionArgs, null, null, sortOrder + ) + cursor.setNotificationUri(requireContext().contentResolver, uri) + + return cursor + } + + /** + * Handles the update query of local SQLite Database + * @param uri : contains the uri for bookmark pictures + * @param contentValues : new values to be entered to db + * @param selection : handles Where + * @param selectionArgs : the condition of Where clause + */ + override fun update( + uri: Uri, contentValues: ContentValues?, selection: String?, + selectionArgs: Array? + ): Int { + val rowsUpdated: Int + if (selection.isNullOrEmpty()) { + val id = uri.lastPathSegment!!.toInt() + rowsUpdated = requireDb().update( + TABLE_NAME, + contentValues, + "$COLUMN_MEDIA_NAME = ?", + arrayOf(id.toString()) + ) + } else { + throw IllegalArgumentException( + "Parameter `selection` should be empty when updating an ID" + ) + } + requireContext().contentResolver.notifyChange(uri, null) + return rowsUpdated + } + + /** + * Handles the insertion of new bookmark pictures record to local SQLite Database + */ + override fun insert(uri: Uri, contentValues: ContentValues?): Uri { + val id = requireDb().insert(TABLE_NAME, null, contentValues) + requireContext().contentResolver.notifyChange(uri, null) + return "$BASE_URI/$id".toUri() + } + + override fun delete(uri: Uri, s: String?, strings: Array?): Int { + val rows: Int = requireDb().delete( + TABLE_NAME, + "media_name = ?", + arrayOf(uri.lastPathSegment) + ) + requireContext().contentResolver.notifyChange(uri, null) + return rows + } + + companion object { + private const val BASE_PATH = "bookmarks" + @JvmField + val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_AUTHORITY}/$BASE_PATH".toUri() + + @JvmStatic + fun uriForName(name: String): Uri = "$BASE_URI/$name".toUri() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java deleted file mode 100644 index 7b644586c..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.free.nrw.commons.bookmarks.pictures; - -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.bookmarks.models.Bookmark; -import fr.free.nrw.commons.media.MediaClient; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.Single; -import io.reactivex.functions.Function; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class BookmarkPicturesController { - - private final MediaClient mediaClient; - private final BookmarkPicturesDao bookmarkDao; - - private List currentBookmarks; - - @Inject - public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) { - this.mediaClient = mediaClient; - this.bookmarkDao = bookmarkDao; - currentBookmarks = new ArrayList<>(); - } - - /** - * Loads the Media objects from the raw data stored in DB and the API. - * @return a list of bookmarked Media object - */ - Single> loadBookmarkedPictures() { - List bookmarks = bookmarkDao.getAllBookmarks(); - currentBookmarks = bookmarks; - return Observable.fromIterable(bookmarks) - .flatMap((Function>) this::getMediaFromBookmark) - .toList(); - } - - private Observable getMediaFromBookmark(Bookmark bookmark) { - return mediaClient.getMedia(bookmark.getMediaName()) - .toObservable() - .onErrorResumeNext(Observable.empty()); - } - - /** - * Loads the Media objects from the raw data stored in DB and the API. - * @return a list of bookmarked Media object - */ - boolean needRefreshBookmarkedPictures() { - List bookmarks = bookmarkDao.getAllBookmarks(); - return bookmarks.size() != currentBookmarks.size(); - } - - /** - * Cancels the requests to the API and the DB - */ - void stop() { - //noop - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt new file mode 100644 index 000000000..5ee88d973 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.kt @@ -0,0 +1,38 @@ +package fr.free.nrw.commons.bookmarks.pictures + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.bookmarks.models.Bookmark +import fr.free.nrw.commons.media.MediaClient +import io.reactivex.Observable +import io.reactivex.Single +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BookmarkPicturesController @Inject constructor( + private val mediaClient: MediaClient, + private val bookmarkDao: BookmarkPicturesDao +) { + private var currentBookmarks: List = listOf() + + /** + * Loads the Media objects from the raw data stored in DB and the API. + * @return a list of bookmarked Media object + */ + fun loadBookmarkedPictures(): Single> { + val bookmarks = bookmarkDao.getAllBookmarks() + currentBookmarks = bookmarks + return Observable.fromIterable(bookmarks).flatMap { + mediaClient.getMedia(it.mediaName) + .toObservable() + .onErrorResumeNext(Observable.empty()) + }.toList() + } + + fun needRefreshBookmarkedPictures(): Boolean { + val bookmarks = bookmarkDao.getAllBookmarks() + return bookmarks.size != currentBookmarks.size + } + + fun stop() = Unit +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java deleted file mode 100644 index c214ae996..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java +++ /dev/null @@ -1,227 +0,0 @@ -package fr.free.nrw.commons.bookmarks.pictures; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; -import javax.inject.Singleton; - -import fr.free.nrw.commons.bookmarks.models.Bookmark; - -import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI; - -@Singleton -public class BookmarkPicturesDao { - - private final Provider clientProvider; - - @Inject - public BookmarkPicturesDao(@Named("bookmarks") Provider clientProvider) { - this.clientProvider = clientProvider; - } - - - /** - * Find all persisted pictures bookmarks on database - * - * @return list of bookmarks - */ - @NonNull - public List getAllBookmarks() { - List items = new ArrayList<>(); - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - BookmarkPicturesContentProvider.BASE_URI, - Table.ALL_FIELDS, - null, - new String[]{}, - null); - while (cursor != null && cursor.moveToNext()) { - items.add(fromCursor(cursor)); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return items; - } - - - /** - * Look for a bookmark in database and in order to insert or delete it - * - * @param bookmark : Bookmark object - * @return boolean : is bookmark now fav ? - */ - public boolean updateBookmark(Bookmark bookmark) { - boolean bookmarkExists = findBookmark(bookmark); - if (bookmarkExists) { - deleteBookmark(bookmark); - } else { - addBookmark(bookmark); - } - return !bookmarkExists; - } - - /** - * Add a Bookmark to database - * - * @param bookmark : Bookmark to add - */ - private void addBookmark(Bookmark bookmark) { - ContentProviderClient db = clientProvider.get(); - try { - db.insert(BASE_URI, toContentValues(bookmark)); - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Delete a bookmark from database - * - * @param bookmark : Bookmark to delete - */ - private void deleteBookmark(Bookmark bookmark) { - ContentProviderClient db = clientProvider.get(); - try { - if (bookmark.getContentUri() == null) { - throw new RuntimeException("tried to delete item with no content URI"); - } else { - db.delete(bookmark.getContentUri(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Find a bookmark from database based on its name - * - * @param bookmark : Bookmark to find - * @return boolean : is bookmark in database ? - */ - public boolean findBookmark(Bookmark bookmark) { - if (bookmark == null) {//Avoiding NPE's - return false; - } - - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - BookmarkPicturesContentProvider.BASE_URI, - Table.ALL_FIELDS, - Table.COLUMN_MEDIA_NAME + "=?", - new String[]{bookmark.getMediaName()}, - null); - if (cursor != null && cursor.moveToFirst()) { - return true; - } - } catch (RemoteException e) { - // This feels lazy, but to hell with checked exceptions. :) - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return false; - } - - @SuppressLint("Range") - @NonNull - Bookmark fromCursor(Cursor cursor) { - String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME)); - return new Bookmark( - fileName, - cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)), - BookmarkPicturesContentProvider.uriForName(fileName) - ); - } - - private ContentValues toContentValues(Bookmark bookmark) { - ContentValues cv = new ContentValues(); - cv.put(BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME, bookmark.getMediaName()); - cv.put(BookmarkPicturesDao.Table.COLUMN_CREATOR, bookmark.getMediaCreator()); - return cv; - } - - - public static class Table { - public static final String TABLE_NAME = "bookmarks"; - - public static final String COLUMN_MEDIA_NAME = "media_name"; - public static final String COLUMN_CREATOR = "media_creator"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_MEDIA_NAME, - COLUMN_CREATOR - }; - - public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; - - public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_MEDIA_NAME + " STRING PRIMARY KEY," - + COLUMN_CREATOR + " STRING" - + ");"; - - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - public static void onDelete(SQLiteDatabase db) { - db.execSQL(DROP_TABLE_STATEMENT); - onCreate(db); - } - - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from < 7) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - - if (from == 7) { - // table added in version 8 - onCreate(db); - from++; - onUpdate(db, from, to); - return; - } - - if (from == 8) { - from++; - onUpdate(db, from, to); - return; - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt new file mode 100644 index 000000000..e30b3160d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt @@ -0,0 +1,141 @@ +package fr.free.nrw.commons.bookmarks.pictures + +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.os.RemoteException +import androidx.core.content.contentValuesOf +import fr.free.nrw.commons.bookmarks.models.Bookmark +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.uriForName +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.ALL_FIELDS +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME +import fr.free.nrw.commons.utils.getString +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +class BookmarkPicturesDao @Inject constructor( + @param:Named("bookmarks") private val clientProvider: Provider +) { + /** + * Find all persisted pictures bookmarks on database + * + * @return list of bookmarks + */ + fun getAllBookmarks(): List { + val items: MutableList = mutableListOf() + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, ALL_FIELDS, null, arrayOf(), null + ) + while (cursor != null && cursor.moveToNext()) { + items.add(fromCursor(cursor)) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return items + } + + /** + * Look for a bookmark in database and in order to insert or delete it + * + * @param bookmark : Bookmark object + * @return boolean : is bookmark now fav ? + */ + fun updateBookmark(bookmark: Bookmark): Boolean { + val bookmarkExists = findBookmark(bookmark) + if (bookmarkExists) { + deleteBookmark(bookmark) + } else { + addBookmark(bookmark) + } + return !bookmarkExists + } + + /** + * Add a Bookmark to database + * + * @param bookmark : Bookmark to add + */ + private fun addBookmark(bookmark: Bookmark) { + val db = clientProvider.get() + try { + db.insert(BASE_URI, toContentValues(bookmark)) + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Delete a bookmark from database + * + * @param bookmark : Bookmark to delete + */ + private fun deleteBookmark(bookmark: Bookmark) { + val db = clientProvider.get() + try { + if (bookmark.contentUri == null) { + throw RuntimeException("tried to delete item with no content URI") + } else { + db.delete(bookmark.contentUri!!, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Find a bookmark from database based on its name + * + * @param bookmark : Bookmark to find + * @return boolean : is bookmark in database ? + */ + fun findBookmark(bookmark: Bookmark?): Boolean { + if (bookmark == null) { + return false + } + + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, ALL_FIELDS, "$COLUMN_MEDIA_NAME=?", arrayOf(bookmark.mediaName), null + ) + if (cursor != null && cursor.moveToFirst()) { + return true + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return false + } + + fun fromCursor(cursor: Cursor): Bookmark { + val fileName = cursor.getString(COLUMN_MEDIA_NAME) + return Bookmark( + fileName, cursor.getString(COLUMN_CREATOR), uriForName(fileName) + ) + } + + private fun toContentValues(bookmark: Bookmark): ContentValues = contentValuesOf( + COLUMN_MEDIA_NAME to bookmark.mediaName, + COLUMN_CREATOR to bookmark.mediaCreator + ) +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java deleted file mode 100644 index 9f02e4631..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java +++ /dev/null @@ -1,218 +0,0 @@ -package fr.free.nrw.commons.bookmarks.pictures; - -import static android.view.View.GONE; -import static android.view.View.VISIBLE; - -import android.annotation.SuppressLint; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListAdapter; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import dagger.android.support.DaggerFragment; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment; -import fr.free.nrw.commons.category.GridViewAdapter; -import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.List; -import javax.inject.Inject; -import timber.log.Timber; - -public class BookmarkPicturesFragment extends DaggerFragment { - - private GridViewAdapter gridAdapter; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - private FragmentBookmarksPicturesBinding binding; - @Inject - BookmarkPicturesController controller; - - /** - * Create an instance of the fragment with the right bundle parameters - * @return an instance of the fragment - */ - public static BookmarkPicturesFragment newInstance() { - return new BookmarkPicturesFragment(); - } - - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState - ) { - binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); - initList(); - } - - @Override - public void onStop() { - super.onStop(); - controller.stop(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - binding = null; - } - - @Override - public void onResume() { - super.onResume(); - if (controller.needRefreshBookmarkedPictures()) { - binding.bookmarkedPicturesList.setVisibility(GONE); - if (gridAdapter != null) { - gridAdapter.clear(); - ((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged(); - } - initList(); - } - } - - /** - * Checks for internet connection and then initializes - * the recycler view with bookmarked pictures - */ - @SuppressLint("CheckResult") - private void initList() { - if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { - handleNoInternet(); - return; - } - - binding.loadingImagesProgressBar.setVisibility(VISIBLE); - binding.statusMessage.setVisibility(GONE); - - compositeDisposable.add(controller.loadBookmarkedPictures() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handleSuccess, this::handleError)); - } - - /** - * Handles the UI updates for no internet scenario - */ - private void handleNoInternet() { - binding.loadingImagesProgressBar.setVisibility(GONE); - if (gridAdapter == null || gridAdapter.isEmpty()) { - binding.statusMessage.setVisibility(VISIBLE); - binding.statusMessage.setText(getString(R.string.no_internet)); - } else { - ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet); - } - } - - /** - * Logs and handles API error scenario - * @param throwable - */ - private void handleError(Throwable throwable) { - Timber.e(throwable, "Error occurred while loading images inside a category"); - try{ - ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images); - initErrorView(); - }catch (Exception e){ - e.printStackTrace(); - } - } - - /** - * Handles the UI updates for a error scenario - */ - private void initErrorView() { - binding.loadingImagesProgressBar.setVisibility(GONE); - if (gridAdapter == null || gridAdapter.isEmpty()) { - binding.statusMessage.setVisibility(VISIBLE); - binding.statusMessage.setText(getString(R.string.no_images_found)); - } else { - binding.statusMessage.setVisibility(GONE); - } - } - - /** - * Handles the UI updates when there is no bookmarks - */ - private void initEmptyBookmarkListView() { - binding.loadingImagesProgressBar.setVisibility(GONE); - if (gridAdapter == null || gridAdapter.isEmpty()) { - binding.statusMessage.setVisibility(VISIBLE); - binding.statusMessage.setText(getString(R.string.bookmark_empty)); - } else { - binding.statusMessage.setVisibility(GONE); - } - } - - /** - * Handles the success scenario - * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter - * @param collection List of new Media to be displayed - */ - private void handleSuccess(List collection) { - if (collection == null) { - initErrorView(); - return; - } - if (collection.isEmpty()) { - initEmptyBookmarkListView(); - return; - } - - if (gridAdapter == null) { - setAdapter(collection); - } else { - if (gridAdapter.containsAll(collection)) { - binding.loadingImagesProgressBar.setVisibility(GONE); - binding.statusMessage.setVisibility(GONE); - binding.bookmarkedPicturesList.setVisibility(VISIBLE); - binding.bookmarkedPicturesList.setAdapter(gridAdapter); - return; - } - gridAdapter.addItems(collection); - ((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged(); - } - binding.loadingImagesProgressBar.setVisibility(GONE); - binding.statusMessage.setVisibility(GONE); - binding.bookmarkedPicturesList.setVisibility(VISIBLE); - } - - /** - * Initializes the adapter with a list of Media objects - * @param mediaList List of new Media to be displayed - */ - private void setAdapter(List mediaList) { - gridAdapter = new GridViewAdapter( - this.getContext(), - R.layout.layout_category_images, - mediaList - ); - binding.bookmarkedPicturesList.setAdapter(gridAdapter); - } - - /** - * It return an instance of gridView adapter which helps in extracting media details - * used by the gridView - * @return GridView Adapter - */ - public ListAdapter getAdapter() { - return binding.bookmarkedPicturesList.getAdapter(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.kt new file mode 100644 index 000000000..e8c61371a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.kt @@ -0,0 +1,201 @@ +package fr.free.nrw.commons.bookmarks.pictures + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView.OnItemClickListener +import android.widget.ListAdapter +import dagger.android.support.DaggerFragment +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment +import fr.free.nrw.commons.category.GridViewAdapter +import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding +import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished +import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import javax.inject.Inject + +class BookmarkPicturesFragment : DaggerFragment() { + private var gridAdapter: GridViewAdapter? = null + private val compositeDisposable = CompositeDisposable() + + private var binding: FragmentBookmarksPicturesBinding? = null + + @JvmField + @Inject + var controller: BookmarkPicturesController? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false) + return binding!!.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding!!.bookmarkedPicturesList.onItemClickListener = + parentFragment as OnItemClickListener? + initList() + } + + override fun onStop() { + super.onStop() + controller!!.stop() + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + binding = null + } + + override fun onResume() { + super.onResume() + if (controller!!.needRefreshBookmarkedPictures()) { + binding!!.bookmarkedPicturesList.visibility = View.GONE + gridAdapter?.let { + it.clear() + (parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged() + } + initList() + } + } + + /** + * Checks for internet connection and then initializes + * the recycler view with bookmarked pictures + */ + @SuppressLint("CheckResult") + private fun initList() { + if (!isInternetConnectionEstablished(context)) { + handleNoInternet() + return + } + + binding!!.loadingImagesProgressBar.visibility = View.VISIBLE + binding!!.statusMessage.visibility = View.GONE + + compositeDisposable.add( + controller!!.loadBookmarkedPictures() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(::handleSuccess, ::handleError) + ) + } + + /** + * Handles the UI updates for no internet scenario + */ + private fun handleNoInternet() { + binding!!.loadingImagesProgressBar.visibility = View.GONE + if (gridAdapter == null || gridAdapter!!.isEmpty) { + binding!!.statusMessage.visibility = View.VISIBLE + binding!!.statusMessage.text = getString(R.string.no_internet) + } else { + showShortSnackbar(binding!!.parentLayout, R.string.no_internet) + } + } + + /** + * Logs and handles API error scenario + * @param throwable + */ + private fun handleError(throwable: Throwable) { + Timber.e(throwable, "Error occurred while loading images inside a category") + try { + showShortSnackbar(binding!!.root, R.string.error_loading_images) + initErrorView() + } catch (e: Exception) { + Timber.e(e) + } + } + + /** + * Handles the UI updates for a error scenario + */ + private fun initErrorView() { + binding!!.loadingImagesProgressBar.visibility = View.GONE + if (gridAdapter == null || gridAdapter!!.isEmpty) { + binding!!.statusMessage.visibility = View.VISIBLE + binding!!.statusMessage.text = getString(R.string.no_images_found) + } else { + binding!!.statusMessage.visibility = View.GONE + } + } + + /** + * Handles the UI updates when there is no bookmarks + */ + private fun initEmptyBookmarkListView() { + binding!!.loadingImagesProgressBar.visibility = View.GONE + if (gridAdapter == null || gridAdapter!!.isEmpty) { + binding!!.statusMessage.visibility = View.VISIBLE + binding!!.statusMessage.text = getString(R.string.bookmark_empty) + } else { + binding!!.statusMessage.visibility = View.GONE + } + } + + /** + * Handles the success scenario + * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter + * @param collection List of new Media to be displayed + */ + private fun handleSuccess(collection: List?) { + if (collection == null) { + initErrorView() + return + } + if (collection.isEmpty()) { + initEmptyBookmarkListView() + return + } + + if (gridAdapter == null) { + setAdapter(collection) + } else { + if (gridAdapter!!.containsAll(collection)) { + binding!!.loadingImagesProgressBar.visibility = View.GONE + binding!!.statusMessage.visibility = View.GONE + binding!!.bookmarkedPicturesList.visibility = View.VISIBLE + binding!!.bookmarkedPicturesList.adapter = gridAdapter + return + } + gridAdapter!!.addItems(collection) + (parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged() + } + binding!!.loadingImagesProgressBar.visibility = View.GONE + binding!!.statusMessage.visibility = View.GONE + binding!!.bookmarkedPicturesList.visibility = View.VISIBLE + } + + /** + * Initializes the adapter with a list of Media objects + * @param mediaList List of new Media to be displayed + */ + private fun setAdapter(mediaList: List) { + gridAdapter = GridViewAdapter( + requireContext(), + R.layout.layout_category_images, + mediaList.toMutableList() + ) + binding?.let { it.bookmarkedPicturesList.adapter = gridAdapter } + } + + /** + * It return an instance of gridView adapter which helps in extracting media details + * used by the gridView + * @return GridView Adapter + */ + fun getAdapter(): ListAdapter? = binding?.bookmarkedPicturesList?.adapter +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarksTable.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarksTable.kt new file mode 100644 index 000000000..6a8f4d541 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarksTable.kt @@ -0,0 +1,54 @@ +package fr.free.nrw.commons.bookmarks.pictures + +import android.database.sqlite.SQLiteDatabase + +object BookmarksTable { + const val TABLE_NAME: String = "bookmarks" + const val COLUMN_MEDIA_NAME: String = "media_name" + const val COLUMN_CREATOR: String = "media_creator" + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + val ALL_FIELDS = arrayOf( + COLUMN_MEDIA_NAME, + COLUMN_CREATOR + ) + + const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME" + + const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME (" + + "$COLUMN_MEDIA_NAME STRING PRIMARY KEY, " + + "$COLUMN_CREATOR STRING" + + ");") + + fun onCreate(db: SQLiteDatabase) = + db.execSQL(CREATE_TABLE_STATEMENT) + + fun onDelete(db: SQLiteDatabase) { + db.execSQL(DROP_TABLE_STATEMENT) + onCreate(db) + } + + fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { + if (from == to) { + return + } + + if (from < 7) { + // doesn't exist yet + onUpdate(db, from+1, to) + return + } + + if (from == 7) { + // table added in version 8 + onCreate(db) + onUpdate(db, from+1, to) + return + } + + if (from == 8) { + onUpdate(db, from+1, to) + return + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.kt index ddd7f5ae4..f5cec0fce 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.kt @@ -9,12 +9,9 @@ import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteQueryBuilder import android.net.Uri import android.text.TextUtils -import androidx.annotation.NonNull import fr.free.nrw.commons.BuildConfig -import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.CommonsDaggerContentProvider -import timber.log.Timber -import javax.inject.Inject +import androidx.core.net.toUri class CategoryContentProvider : CommonsDaggerContentProvider() { @@ -23,9 +20,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID) } - @Inject - lateinit var dbOpenHelper: DBOpenHelper - @SuppressWarnings("ConstantConditions") override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? { @@ -34,7 +28,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { } val uriType = uriMatcher.match(uri) - val db = dbOpenHelper.readableDatabase + val db = requireDb() val cursor: Cursor? = when (uriType) { CATEGORIES -> queryBuilder.query( @@ -58,45 +52,37 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { else -> throw IllegalArgumentException("Unknown URI $uri") } - cursor?.setNotificationUri(context?.contentResolver, uri) + cursor?.setNotificationUri(requireContext().contentResolver, uri) return cursor } - override fun getType(uri: Uri): String? { - return null - } + override fun getType(uri: Uri): String? = null @SuppressWarnings("ConstantConditions") - override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { + override fun insert(uri: Uri, contentValues: ContentValues?): Uri { val uriType = uriMatcher.match(uri) - val sqlDB = dbOpenHelper.writableDatabase val id: Long when (uriType) { CATEGORIES -> { - id = sqlDB.insert(TABLE_NAME, null, contentValues) + id = requireDb().insert(TABLE_NAME, null, contentValues) } else -> throw IllegalArgumentException("Unknown URI: $uri") } - context?.contentResolver?.notifyChange(uri, null) - return Uri.parse("${Companion.BASE_URI}/$id") + requireContext().contentResolver?.notifyChange(uri, null) + return "${BASE_URI}/$id".toUri() } @SuppressWarnings("ConstantConditions") - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - // Not implemented - return 0 - } + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 @SuppressWarnings("ConstantConditions") override fun bulkInsert(uri: Uri, values: Array): Int { - Timber.d("Hello, bulk insert! (CategoryContentProvider)") val uriType = uriMatcher.match(uri) - val sqlDB = dbOpenHelper.writableDatabase + val sqlDB = requireDb() sqlDB.beginTransaction() when (uriType) { CATEGORIES -> { for (value in values) { - Timber.d("Inserting! %s", value) sqlDB.insert(TABLE_NAME, null, value) } sqlDB.setTransactionSuccessful() @@ -104,7 +90,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { else -> throw IllegalArgumentException("Unknown URI: $uri") } sqlDB.endTransaction() - context?.contentResolver?.notifyChange(uri, null) + requireContext().contentResolver?.notifyChange(uri, null) return values.size } @@ -112,17 +98,18 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { override fun update(uri: Uri, contentValues: ContentValues?, selection: String?, selectionArgs: Array?): Int { val uriType = uriMatcher.match(uri) - val sqlDB = dbOpenHelper.writableDatabase val rowsUpdated: Int when (uriType) { CATEGORIES_ID -> { if (TextUtils.isEmpty(selection)) { val id = uri.lastPathSegment?.toInt() ?: throw IllegalArgumentException("Invalid ID") - rowsUpdated = sqlDB.update(TABLE_NAME, + rowsUpdated = requireDb().update( + TABLE_NAME, contentValues, "$COLUMN_ID = ?", - arrayOf(id.toString())) + arrayOf(id.toString()) + ) } else { throw IllegalArgumentException( "Parameter `selection` should be empty when updating an ID") @@ -130,7 +117,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { } else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType") } - context?.contentResolver?.notifyChange(uri, null) + requireContext().contentResolver?.notifyChange(uri, null) return rowsUpdated } @@ -165,13 +152,9 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { "$COLUMN_TIMES_USED INTEGER" + ");" - fun uriForId(id: Int): Uri { - return Uri.parse("${BASE_URI}/$id") - } + fun uriForId(id: Int): Uri = Uri.parse("${BASE_URI}/$id") - fun onCreate(db: SQLiteDatabase) { - db.execSQL(CREATE_TABLE_STATEMENT) - } + fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT) fun onDelete(db: SQLiteDatabase) { db.execSQL(DROP_TABLE_STATEMENT) @@ -200,6 +183,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { private const val CATEGORIES = 1 private const val CATEGORIES_ID = 2 private const val BASE_PATH = "categories" - val BASE_URI: Uri = Uri.parse("content://${BuildConfig.CATEGORY_AUTHORITY}/${Companion.BASE_PATH}") + val BASE_URI: Uri = "content://${BuildConfig.CATEGORY_AUTHORITY}/${BASE_PATH}".toUri() } } diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt index 7cb7f60f7..7bbe5de06 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt @@ -4,9 +4,8 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteException import android.database.sqlite.SQLiteOpenHelper -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable import fr.free.nrw.commons.category.CategoryDao import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao @@ -30,16 +29,16 @@ class DBOpenHelper( */ override fun onCreate(db: SQLiteDatabase) { CategoryDao.Table.onCreate(db) - BookmarkPicturesDao.Table.onCreate(db) - BookmarkItemsDao.Table.onCreate(db) + BookmarksTable.onCreate(db) + BookmarkItemsTable.onCreate(db) RecentSearchesDao.Table.onCreate(db) RecentLanguagesDao.Table.onCreate(db) } override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) { CategoryDao.Table.onUpdate(db, from, to) - BookmarkPicturesDao.Table.onUpdate(db, from, to) - BookmarkItemsDao.Table.onUpdate(db, from, to) + BookmarksTable.onUpdate(db, from, to) + BookmarkItemsTable.onUpdate(db, from, to) RecentSearchesDao.Table.onUpdate(db, from, to) RecentLanguagesDao.Table.onUpdate(db, from, to) deleteTable(db, CONTRIBUTIONS_TABLE) diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt index c1bda689c..4c77d1aad 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt @@ -1,14 +1,25 @@ package fr.free.nrw.commons.di import android.content.ContentProvider +import android.database.sqlite.SQLiteDatabase +import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance +import javax.inject.Inject abstract class CommonsDaggerContentProvider : ContentProvider() { + @JvmField + @Inject + var dbOpenHelper: DBOpenHelper? = null + override fun onCreate(): Boolean { inject() return true } + fun requireDbOpenHelper(): DBOpenHelper = dbOpenHelper!! + + fun requireDb(): SQLiteDatabase = requireDbOpenHelper().writableDatabase!! + private fun inject() { val injection = getInstance(context!!) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java deleted file mode 100644 index ab6dd7b05..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java +++ /dev/null @@ -1,202 +0,0 @@ -package fr.free.nrw.commons.explore.recentsearches; - -import android.content.ContentValues; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import javax.inject.Inject; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.CommonsDaggerContentProvider; -import timber.log.Timber; - -import static android.content.UriMatcher.NO_MATCH; -import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS; -import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID; -import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME; - - -/** - * This class contains functions for executing queries for - * inserting, searching, deleting, editing recent searches in SqLite DB - **/ -public class RecentSearchesContentProvider extends CommonsDaggerContentProvider { - - // For URI matcher - private static final int RECENT_SEARCHES = 1; - private static final int RECENT_SEARCHES_ID = 2; - private static final String BASE_PATH = "recent_searches"; - public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.RECENT_SEARCH_AUTHORITY + "/" + BASE_PATH); - private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH); - - static { - uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES); - uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH + "/#", RECENT_SEARCHES_ID); - } - - public static Uri uriForId(int id) { - return Uri.parse(BASE_URI.toString() + "/" + id); - } - - @Inject DBOpenHelper dbOpenHelper; - - /** - * This functions executes query for searching recent searches in SqLite DB - **/ - @SuppressWarnings("ConstantConditions") - @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); - queryBuilder.setTables(TABLE_NAME); - - int uriType = uriMatcher.match(uri); - - SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - Cursor cursor; - - switch (uriType) { - case RECENT_SEARCHES: - cursor = queryBuilder.query(db, projection, selection, selectionArgs, - null, null, sortOrder); - break; - case RECENT_SEARCHES_ID: - cursor = queryBuilder.query(db, - ALL_FIELDS, - "_id = ?", - new String[]{uri.getLastPathSegment()}, - null, - null, - sortOrder - ); - break; - default: - throw new IllegalArgumentException("Unknown URI" + uri); - } - - cursor.setNotificationUri(getContext().getContentResolver(), uri); - - return cursor; - } - - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - /** - * This functions executes query for inserting a recentSearch object in SqLite DB - **/ - @SuppressWarnings("ConstantConditions") - @Override - public Uri insert(@NonNull Uri uri, ContentValues contentValues) { - int uriType = uriMatcher.match(uri); - SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - long id; - switch (uriType) { - case RECENT_SEARCHES: - id = sqlDB.insert(TABLE_NAME, null, contentValues); - break; - default: - throw new IllegalArgumentException("Unknown URI: " + uri); - } - getContext().getContentResolver().notifyChange(uri, null); - return Uri.parse(BASE_URI + "/" + id); - } - - /** - * This functions executes query for deleting a recentSearch object in SqLite DB - **/ - @Override - public int delete(@NonNull Uri uri, String s, String[] strings) { - int rows; - int uriType = uriMatcher.match(uri); - SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - switch (uriType) { - case RECENT_SEARCHES_ID: - Timber.d("Deleting recent searches id %s", uri.getLastPathSegment()); - rows = db.delete(RecentSearchesDao.Table.TABLE_NAME, - "_id = ?", - new String[]{uri.getLastPathSegment()} - ); - break; - default: - throw new IllegalArgumentException("Unknown URI" + uri); - } - getContext().getContentResolver().notifyChange(uri, null); - return rows; - } - - /** - * This functions executes query for inserting multiple recentSearch objects in SqLite DB - **/ - @SuppressWarnings("ConstantConditions") - @Override - public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { - Timber.d("Hello, bulk insert! (RecentSearchesContentProvider)"); - int uriType = uriMatcher.match(uri); - SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - sqlDB.beginTransaction(); - switch (uriType) { - case RECENT_SEARCHES: - for (ContentValues value : values) { - Timber.d("Inserting! %s", value); - sqlDB.insert(TABLE_NAME, null, value); - } - break; - default: - throw new IllegalArgumentException("Unknown URI: " + uri); - } - sqlDB.setTransactionSuccessful(); - sqlDB.endTransaction(); - getContext().getContentResolver().notifyChange(uri, null); - return values.length; - } - - /** - * This functions executes query for updating a particular recentSearch object in SqLite DB - **/ - @SuppressWarnings("ConstantConditions") - @Override - public int update(@NonNull Uri uri, ContentValues contentValues, String selection, - String[] selectionArgs) { - /* - SQL Injection warnings: First, note that we're not exposing this to the - outside world (exported="false"). Even then, we should make sure to sanitize - all user input appropriately. Input that passes through ContentValues - should be fine. So only issues are those that pass in via concating. - - In here, the only concat created argument is for id. It is cast to an int, - and will error out otherwise. - */ - int uriType = uriMatcher.match(uri); - SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - int rowsUpdated; - switch (uriType) { - case RECENT_SEARCHES_ID: - if (TextUtils.isEmpty(selection)) { - int id = Integer.valueOf(uri.getLastPathSegment()); - rowsUpdated = sqlDB.update(TABLE_NAME, - contentValues, - COLUMN_ID + " = ?", - new String[]{String.valueOf(id)}); - } else { - throw new IllegalArgumentException( - "Parameter `selection` should be empty when updating an ID"); - } - break; - default: - throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType); - } - getContext().getContentResolver().notifyChange(uri, null); - return rowsUpdated; - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt new file mode 100644 index 000000000..f30636db7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt @@ -0,0 +1,174 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.content.ContentValues +import android.content.UriMatcher +import android.database.Cursor +import android.database.sqlite.SQLiteQueryBuilder +import android.net.Uri +import androidx.core.net.toUri +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.di.CommonsDaggerContentProvider +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME + +/** + * This class contains functions for executing queries for + * inserting, searching, deleting, editing recent searches in SqLite DB + */ +class RecentSearchesContentProvider : CommonsDaggerContentProvider() { + + /** + * This functions executes query for searching recent searches in SqLite DB + */ + override fun query( + uri: Uri, projection: Array?, selection: String?, + selectionArgs: Array?, sortOrder: String? + ): Cursor { + val queryBuilder = SQLiteQueryBuilder().apply { + tables = TABLE_NAME + } + + val uriType = uriMatcher.match(uri) + + val cursor = when (uriType) { + RECENT_SEARCHES -> queryBuilder.query( + requireDb(), projection, selection, selectionArgs, + null, null, sortOrder + ) + + RECENT_SEARCHES_ID -> queryBuilder.query( + requireDb(), + ALL_FIELDS, + "$COLUMN_ID = ?", + arrayOf(uri.lastPathSegment), + null, + null, + sortOrder + ) + + else -> throw IllegalArgumentException("Unknown URI$uri") + } + + cursor.setNotificationUri(requireContext().contentResolver, uri) + + return cursor + } + + override fun getType(uri: Uri): String? = null + + /** + * This functions executes query for inserting a recentSearch object in SqLite DB + */ + override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { + val uriType = uriMatcher.match(uri) + val id: Long = when (uriType) { + RECENT_SEARCHES -> requireDb().insert(TABLE_NAME, null, contentValues) + + else -> throw IllegalArgumentException("Unknown URI: $uri") + } + requireContext().contentResolver.notifyChange(uri, null) + return "$BASE_URI/$id".toUri() + } + + /** + * This functions executes query for deleting a recentSearch object in SqLite DB + */ + override fun delete(uri: Uri, s: String?, strings: Array?): Int { + val rows: Int + val uriType = uriMatcher.match(uri) + when (uriType) { + RECENT_SEARCHES_ID -> { + rows = requireDb().delete( + TABLE_NAME, + "_id = ?", + arrayOf(uri.lastPathSegment) + ) + } + + else -> throw IllegalArgumentException("Unknown URI - $uri") + } + requireContext().contentResolver.notifyChange(uri, null) + return rows + } + + /** + * This functions executes query for inserting multiple recentSearch objects in SqLite DB + */ + override fun bulkInsert(uri: Uri, values: Array): Int { + val uriType = uriMatcher.match(uri) + val sqlDB = requireDb() + sqlDB.beginTransaction() + when (uriType) { + RECENT_SEARCHES -> for (value in values) { + sqlDB.insert(TABLE_NAME, null, value) + } + + else -> throw IllegalArgumentException("Unknown URI: $uri") + } + sqlDB.setTransactionSuccessful() + sqlDB.endTransaction() + requireContext().contentResolver.notifyChange(uri, null) + return values.size + } + + /** + * This functions executes query for updating a particular recentSearch object in SqLite DB + */ + override fun update( + uri: Uri, contentValues: ContentValues?, selection: String?, + selectionArgs: Array? + ): Int { + /* + SQL Injection warnings: First, note that we're not exposing this to the + outside world (exported="false"). Even then, we should make sure to sanitize + all user input appropriately. Input that passes through ContentValues + should be fine. So only issues are those that pass in via concating. + + In here, the only concat created argument is for id. It is cast to an int, + and will error out otherwise. + */ + val uriType = uriMatcher.match(uri) + val rowsUpdated: Int + when (uriType) { + RECENT_SEARCHES_ID -> if (selection.isNullOrEmpty()) { + val id = uri.lastPathSegment!!.toInt() + rowsUpdated = requireDb().update( + TABLE_NAME, + contentValues, + "$COLUMN_ID = ?", + arrayOf(id.toString()) + ) + } else { + throw IllegalArgumentException( + "Parameter `selection` should be empty when updating an ID" + ) + } + + else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType") + } + requireContext().contentResolver.notifyChange(uri, null) + return rowsUpdated + } + + companion object { + // For URI matcher + private const val RECENT_SEARCHES = 1 + private const val RECENT_SEARCHES_ID = 2 + private const val BASE_PATH = "recent_searches" + + @JvmField + val BASE_URI: Uri = "content://${BuildConfig.RECENT_SEARCH_AUTHORITY}/$BASE_PATH".toUri() + + private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) + + init { + uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES) + uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, "$BASE_PATH/#", RECENT_SEARCHES_ID) + } + + @JvmStatic + fun uriForId(id: Int): Uri = "$BASE_URI/$id".toUri() + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt index b66c888aa..92cca611e 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt @@ -166,7 +166,7 @@ class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeLis val mediaDetailFragment = adapter!!.currentMediaDetailFragment when (item.itemId) { R.id.menu_bookmark_current_image -> { - val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark) + val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark!!) val snackbar = if (bookmarkExists) Snackbar.make( requireView(), R.string.add_bookmark, @@ -436,7 +436,7 @@ ${m.pageTitle.canonicalUri}""" bookmark = Bookmark( m.filename, m.getAuthorOrUser(), - BookmarkPicturesContentProvider.uriForName(m.filename) + BookmarkPicturesContentProvider.uriForName(m.filename!!) ) updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image)) val contributionState = provider.getContributionStateAt(position) diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt index facc4384f..04c18fbb7 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt @@ -3,17 +3,13 @@ package fr.free.nrw.commons.recentlanguages import android.content.ContentValues import android.database.Cursor -import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteQueryBuilder import android.net.Uri -import android.text.TextUtils import fr.free.nrw.commons.BuildConfig -import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.CommonsDaggerContentProvider import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME -import javax.inject.Inject -import timber.log.Timber +import androidx.core.net.toUri /** @@ -23,27 +19,17 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { companion object { private const val BASE_PATH = "recent_languages" - val BASE_URI: Uri = - Uri.parse( - "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH" - ) + val BASE_URI: Uri = "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH".toUri() /** * Append language code to the base URI * @param languageCode Code of a language */ @JvmStatic - fun uriForCode(languageCode: String): Uri { - return Uri.parse("$BASE_URI/$languageCode") - } + fun uriForCode(languageCode: String): Uri = "$BASE_URI/$languageCode".toUri() } - @Inject - lateinit var dbOpenHelper: DBOpenHelper - - override fun getType(uri: Uri): String? { - return null - } + override fun getType(uri: Uri): String? = null /** * Queries the SQLite database for the recently used languages @@ -60,11 +46,12 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { selectionArgs: Array?, sortOrder: String? ): Cursor? { - val queryBuilder = SQLiteQueryBuilder() - queryBuilder.tables = TABLE_NAME - val db = dbOpenHelper.readableDatabase + val queryBuilder = SQLiteQueryBuilder().apply { + tables = TABLE_NAME + } + val cursor = queryBuilder.query( - db, + requireDb(), projection, selection, selectionArgs, @@ -72,7 +59,7 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { null, sortOrder ) - cursor.setNotificationUri(context?.contentResolver, uri) + cursor.setNotificationUri(requireContext().contentResolver, uri) return cursor } @@ -89,12 +76,11 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { selection: String?, selectionArgs: Array? ): Int { - val sqlDB = dbOpenHelper.writableDatabase val rowsUpdated: Int if (selection.isNullOrEmpty()) { val id = uri.lastPathSegment?.toInt() ?: throw IllegalArgumentException("Invalid URI: $uri") - rowsUpdated = sqlDB.update( + rowsUpdated = requireDb().update( TABLE_NAME, contentValues, "$COLUMN_NAME = ?", @@ -104,7 +90,7 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { throw IllegalArgumentException("Parameter `selection` should be empty when updating an ID") } - context?.contentResolver?.notifyChange(uri, null) + requireContext().contentResolver?.notifyChange(uri, null) return rowsUpdated } @@ -114,14 +100,13 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { * @param contentValues : new values to be entered to the database */ override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { - val sqlDB = dbOpenHelper.writableDatabase - val id = sqlDB.insert( + val id = requireDb().insert( TABLE_NAME, null, contentValues ) - context?.contentResolver?.notifyChange(uri, null) - return Uri.parse("$BASE_URI/$id") + requireContext().contentResolver?.notifyChange(uri, null) + return "$BASE_URI/$id".toUri() } /** @@ -129,14 +114,12 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { * @param uri : contains the URI for recently used languages */ override fun delete(uri: Uri, s: String?, strings: Array?): Int { - val db = dbOpenHelper.readableDatabase - Timber.d("Deleting recently used language %s", uri.lastPathSegment) - val rows = db.delete( + val rows = requireDb().delete( TABLE_NAME, "language_code = ?", arrayOf(uri.lastPathSegment) ) - context?.contentResolver?.notifyChange(uri, null) + requireContext().contentResolver?.notifyChange(uri, null) return rows } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt new file mode 100644 index 000000000..69560279b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.utils + +import android.annotation.SuppressLint +import android.database.Cursor + +fun Cursor.getStringArray(name: String): List = + stringToArray(getString(name)) + +@SuppressLint("Range") +fun Cursor.getString(name: String): String = + getString(getColumnIndex(name)) + +/** + * Converts string to List + * @param listString comma separated single string from of list items + * @return List of string + */ +fun stringToArray(listString: String?): List { + if (listString.isNullOrEmpty()) return emptyList(); + val elements = listString.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + return listOf(*elements) +} + +/** + * Converts string to List + * @param list list of items + * @return string comma separated single string of items + */ +fun arrayToString(list: List?): String? { + return list?.joinToString(",") +} + diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt index d168fe6e4..c2c33667b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt @@ -288,7 +288,7 @@ class BookmarkListRootFragmentUnitTest { @Test @Throws(Exception::class) fun testOnItemClick() { - fragment.onItemClick(null, null, 0, 0) + fragment.onItemClick(null, view, 0, 0) verify(childFragmentManager).beginTransaction() verify(childFragmentTransaction).commit() verify(childFragmentManager).executePendingTransactions() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt index bf143ecc0..3123bc1d0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapterTests.kt @@ -2,12 +2,20 @@ package fr.free.nrw.commons.bookmarks import android.content.Context import androidx.fragment.app.FragmentManager +import fr.free.nrw.commons.TestCommonsApplication import org.junit.Assert import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class BookmarksPagerAdapterTests { @Mock private lateinit var bookmarksPagerAdapter: BookmarksPagerAdapter diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt index 7ba7d559e..19d612aba 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/LoggedOutBookmarksPagerAdapterTests.kt @@ -2,15 +2,23 @@ package fr.free.nrw.commons.bookmarks import android.content.Context import androidx.fragment.app.FragmentManager +import fr.free.nrw.commons.TestCommonsApplication import org.junit.Assert import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode /** * BookmarksPagerAdapter when user is not loggedIn. */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class LoggedOutBookmarksPagerAdapterTests { @Mock private lateinit var bookmarksPagerAdapter: BookmarksPagerAdapter diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt index 98279520d..38bf6b267 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsControllerTest.kt @@ -21,7 +21,7 @@ class BookmarkItemsControllerTest { @Before fun setup() { MockitoAnnotations.openMocks(this) - whenever(bookmarkDao!!.allBookmarksItems) + whenever(bookmarkDao!!.getAllBookmarksItems()) .thenReturn(mockBookmarkList) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt index 60ae7869d..e7ecbe074 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDaoTest.kt @@ -18,20 +18,20 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.TestCommonsApplication -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_CATEGORIES_DESCRIPTION_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_CATEGORIES_NAME_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_CATEGORIES_THUMBNAIL_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_DESCRIPTION -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_ID -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_IMAGE -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_INSTANCE_LIST -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_IS_SELECTED -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_NAME -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.onCreate -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.onDelete -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.onUpdate +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.CREATE_TABLE_STATEMENT +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.DROP_TABLE_STATEMENT +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onCreate +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onDelete +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onUpdate import fr.free.nrw.commons.category.CategoryItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import org.junit.Assert @@ -135,7 +135,7 @@ class BookmarkItemsDaoTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) .thenReturn(createCursor(14)) - val result = testObject.allBookmarksItems + val result = testObject.getAllBookmarksItems() Assert.assertEquals(14, (result.size)) } @@ -145,20 +145,20 @@ class BookmarkItemsDaoTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow( RemoteException(""), ) - testObject.allBookmarksItems + testObject.getAllBookmarksItems() } @Test fun getAllItemsBookmarksReturnsEmptyList_emptyCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) .thenReturn(createCursor(0)) - Assert.assertTrue(testObject.allBookmarksItems.isEmpty()) + Assert.assertTrue(testObject.getAllBookmarksItems().isEmpty()) } @Test fun getAllItemsBookmarksReturnsEmptyList_nullCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - Assert.assertTrue(testObject.allBookmarksItems.isEmpty()) + Assert.assertTrue(testObject.getAllBookmarksItems().isEmpty()) } @Test @@ -167,7 +167,7 @@ class BookmarkItemsDaoTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) whenever(mockCursor.moveToFirst()).thenReturn(false) - testObject.allBookmarksItems + testObject.getAllBookmarksItems() verify(mockCursor).close() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragmentUnitTest.kt index 12292af91..086fd703b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/items/BookmarkItemsFragmentUnitTest.kt @@ -88,7 +88,7 @@ class BookmarkItemsFragmentUnitTest { context = ApplicationProvider.getApplicationContext() OkHttpConnectionFactory.CLIENT = createTestClient() val activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get() - fragment = BookmarkItemsFragment.newInstance() + fragment = BookmarkItemsFragment() val fragmentManager: FragmentManager = activity.supportFragmentManager val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() fragmentTransaction.add(fragment, null) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt index 02668ff1c..5932f24bc 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt @@ -19,14 +19,14 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.bookmarks.models.Bookmark -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_CREATOR -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.onCreate -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.onDelete -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.onUpdate +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.CREATE_TABLE_STATEMENT +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.DROP_TABLE_STATEMENT +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onCreate +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onDelete +import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onUpdate import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -84,7 +84,7 @@ class BookmarkPictureDaoTest { fun getAllBookmarks() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14)) - var result = testObject.allBookmarks + var result = testObject.getAllBookmarks() assertEquals(14, (result.size)) } @@ -92,19 +92,19 @@ class BookmarkPictureDaoTest { @Test(expected = RuntimeException::class) fun getAllBookmarksTranslatesExceptions() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) - testObject.allBookmarks + testObject.getAllBookmarks() } @Test fun getAllBookmarksReturnsEmptyList_emptyCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0)) - assertTrue(testObject.allBookmarks.isEmpty()) + assertTrue(testObject.getAllBookmarks().isEmpty()) } @Test fun getAllBookmarksReturnsEmptyList_nullCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - assertTrue(testObject.allBookmarks.isEmpty()) + assertTrue(testObject.getAllBookmarks().isEmpty()) } @Test @@ -113,7 +113,7 @@ class BookmarkPictureDaoTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) whenever(mockCursor.moveToFirst()).thenReturn(false) - testObject.allBookmarks + testObject.getAllBookmarks() verify(mockCursor).close() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt index bb0718c27..154a5a9b3 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesControllerTest.kt @@ -35,7 +35,7 @@ class BookmarkPicturesControllerTest { fun setup() { MockitoAnnotations.initMocks(this) val mockMedia = mockMedia - whenever(bookmarkDao!!.allBookmarks) + whenever(bookmarkDao!!.getAllBookmarks()) .thenReturn(mockBookmarkList) whenever( mediaClient!!.getMedia( diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt index 03de2638d..b1dae0fab 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragmentUnitTests.kt @@ -88,7 +88,7 @@ class BookmarkPicturesFragmentUnitTests { context = ApplicationProvider.getApplicationContext() OkHttpConnectionFactory.CLIENT = createTestClient() val activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get() - fragment = BookmarkPicturesFragment.newInstance() + fragment = BookmarkPicturesFragment() val fragmentManager: FragmentManager = activity.supportFragmentManager val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() fragmentTransaction.add(fragment, null) @@ -156,13 +156,13 @@ class BookmarkPicturesFragmentUnitTests { val method: Method = BookmarkPicturesFragment::class.java.getDeclaredMethod("setAdapter", List::class.java) method.isAccessible = true - method.invoke(fragment, mediaList) + method.invoke(fragment, emptyList()) } @Test @Throws(Exception::class) fun testGetAdapter() { - fragment.adapter + fragment.getAdapter() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt index e93f48c55..3499ffaee 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt @@ -29,7 +29,7 @@ import fr.free.nrw.commons.category.CategoryDao.Table.DROP_TABLE_STATEMENT import fr.free.nrw.commons.category.CategoryDao.Table.onCreate import fr.free.nrw.commons.category.CategoryDao.Table.onDelete import fr.free.nrw.commons.category.CategoryDao.Table.onUpdate -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.uriForId +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt index 3e550b670..c772f796e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt @@ -18,8 +18,8 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.explore.models.RecentSearch -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.BASE_URI -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.uriForId +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_LAST_USED From 516039c91d4d53c37db50aaba97bff050756e78d Mon Sep 17 00:00:00 2001 From: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:34:34 +0530 Subject: [PATCH 13/97] Add v5.6.1 to CHANGELOG.md --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72fda2f5d..fc22a2b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Wikimedia Commons for Android +## v5.6.1 + +### What's changed +* The app no longer uploads images to Wikidata if one exists already for a given item +* File usage displays correctly now +* No more infinite circular progress bar on nominating an image for deletion +* Enhanced location updates while using GPS +* Author/uploader names are now available in Media Details for Commons licensing compliance +* Improved usage of popups in Nearby +* Bug fixes and stability improvements + ## v5.5.0 ### What's changed From 6f36cae767328ecafbd9b10eefa926ef96329c64 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Sun, 3 Aug 2025 21:44:00 -0500 Subject: [PATCH 14/97] Convert explore package to kotlin (#6389) * Convert WikidataItemDetailsActivity to kotlin * Convert RecentSearchesDao to kotlin * Convert RecentSearchesFragment to kotlin * Convert ExploreListRootFragment to kotlin * Convert the ParentViewPager to kotlin * Convert ExploreMapRootFragment to kotlin * Convert SearchActivity to kotlin * Convert ExploreFragment to kotlin * Convert ExploreMapCalls and ExploreMapContract to kotlin * Convert ExploreMapController to kotlin * Convert the map presenter to kotlin * Convert the ExploreMapFragment to kotlin * Fix import issue --- .../nrw/commons/bookmarks/BookmarkFragment.kt | 2 +- .../fr/free/nrw/commons/data/DBOpenHelper.kt | 6 +- .../nrw/commons/explore/ExploreFragment.java | 260 ---- .../nrw/commons/explore/ExploreFragment.kt | 229 ++++ .../explore/ExploreListRootFragment.java | 215 ---- .../explore/ExploreListRootFragment.kt | 182 +++ .../explore/ExploreMapRootFragment.java | 239 ---- .../commons/explore/ExploreMapRootFragment.kt | 201 +++ .../nrw/commons/explore/ParentViewPager.java | 66 - .../nrw/commons/explore/ParentViewPager.kt | 25 + .../nrw/commons/explore/SearchActivity.java | 285 ----- .../nrw/commons/explore/SearchActivity.kt | 252 ++++ .../depictions/PageableDepictionsFragment.kt | 2 +- .../WikidataItemDetailsActivity.java | 302 ----- .../depictions/WikidataItemDetailsActivity.kt | 295 +++++ .../commons/explore/map/ExploreMapCalls.java | 34 - .../commons/explore/map/ExploreMapCalls.kt | 25 + .../explore/map/ExploreMapContract.java | 45 - .../commons/explore/map/ExploreMapContract.kt | 43 + .../explore/map/ExploreMapController.java | 213 --- .../explore/map/ExploreMapController.kt | 219 ++++ .../explore/map/ExploreMapFragment.java | 1140 ----------------- .../commons/explore/map/ExploreMapFragment.kt | 1091 ++++++++++++++++ .../explore/map/ExploreMapPresenter.java | 237 ---- .../explore/map/ExploreMapPresenter.kt | 223 ++++ .../RecentSearchesContentProvider.kt | 6 +- .../recentsearches/RecentSearchesDao.java | 275 ---- .../recentsearches/RecentSearchesDao.kt | 180 +++ .../RecentSearchesFragment.java | 149 --- .../recentsearches/RecentSearchesFragment.kt | 153 +++ .../recentsearches/RecentSearchesTable.kt | 71 + .../nrw/commons/profile/ProfileActivity.kt | 2 +- .../commons/upload/UploadProgressActivity.kt | 6 +- .../free/nrw/commons/utils/DatabaseUtils.kt | 8 + .../explore/ExploreFragmentUnitTest.kt | 4 +- .../recentsearches/RecentSearchesDaoTest.kt | 18 +- .../RecentSearchesFragmentUnitTest.kt | 14 - 37 files changed, 3218 insertions(+), 3499 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt index 48db40ad0..51f15b23c 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt @@ -33,7 +33,7 @@ class BookmarkFragment : CommonsDaggerSupportFragment() { fun setScroll(canScroll: Boolean) { binding?.let { - it.viewPagerBookmarks.isCanScroll = canScroll + it.viewPagerBookmarks.canScroll = canScroll } } diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt index 7bbe5de06..55ddec5bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt @@ -7,7 +7,7 @@ import android.database.sqlite.SQLiteOpenHelper import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable import fr.free.nrw.commons.category.CategoryDao -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao @@ -31,7 +31,7 @@ class DBOpenHelper( CategoryDao.Table.onCreate(db) BookmarksTable.onCreate(db) BookmarkItemsTable.onCreate(db) - RecentSearchesDao.Table.onCreate(db) + RecentSearchesTable.onCreate(db) RecentLanguagesDao.Table.onCreate(db) } @@ -39,7 +39,7 @@ class DBOpenHelper( CategoryDao.Table.onUpdate(db, from, to) BookmarksTable.onUpdate(db, from, to) BookmarkItemsTable.onUpdate(db, from, to) - RecentSearchesDao.Table.onUpdate(db, from, to) + RecentSearchesTable.onUpdate(db, from, to) RecentLanguagesDao.Table.onUpdate(db, from, to) deleteTable(db, CONTRIBUTIONS_TABLE) deleteTable(db, BOOKMARKS_LOCATIONS) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java deleted file mode 100644 index 475d14287..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java +++ /dev/null @@ -1,260 +0,0 @@ -package fr.free.nrw.commons.explore; - -import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE; -import static fr.free.nrw.commons.ViewPagerAdapter.pairOf; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentPagerAdapter; -import androidx.viewpager.widget.ViewPager.OnPageChangeListener; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentExploreBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.ActivityUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; -import javax.inject.Named; -import kotlin.Pair; - -public class ExploreFragment extends CommonsDaggerSupportFragment { - - private static final String FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons"; - private static final String MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android"; - private static final String EXPLORE_MAP = "Map"; - private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment"; - - - public FragmentExploreBinding binding; - ViewPagerAdapter viewPagerAdapter; - private ExploreListRootFragment featuredRootFragment; - private ExploreListRootFragment mobileRootFragment; - private ExploreMapRootFragment mapRootFragment; - @Inject - @Named("default_preferences") - public JsonKvStore applicationKvStore; - - // Nearby map state (for if we came from Nearby fragment) - private double prevZoom; - private double prevLatitude; - private double prevLongitude; - - public void setScroll(boolean canScroll) { - if (binding != null) { - binding.viewPager.setCanScroll(canScroll); - } - } - - @NonNull - public static ExploreFragment newInstance() { - ExploreFragment fragment = new ExploreFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - loadNearbyMapData(); - binding = FragmentExploreBinding.inflate(inflater, container, false); - - viewPagerAdapter = new ViewPagerAdapter(requireContext(), getChildFragmentManager(), - FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setId(R.id.viewPager); - binding.tabLayout.setupWithViewPager(binding.viewPager); - binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { - - } - - @Override - public void onPageSelected(int position) { - if (position == 2) { - binding.viewPager.setCanScroll(false); - } else { - binding.viewPager.setCanScroll(true); - } - } - - @Override - public void onPageScrollStateChanged(int state) { - - } - }); - setTabs(); - setHasOptionsMenu(true); - - // if we came from 'Show in Explore' in Nearby, jump to Map tab - if (isCameFromNearbyMap()) { - binding.viewPager.setCurrentItem(2); - } - return binding.getRoot(); - } - - /** - * Sets the titles in the tabLayout and fragments in the viewPager - */ - public void setTabs() { - Bundle featuredArguments = new Bundle(); - featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY); - - Bundle mobileArguments = new Bundle(); - mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY); - - Bundle mapArguments = new Bundle(); - mapArguments.putString("categoryName", EXPLORE_MAP); - - // if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root - if (isCameFromNearbyMap()) { - mapArguments.putDouble("prev_zoom", prevZoom); - mapArguments.putDouble("prev_latitude", prevLatitude); - mapArguments.putDouble("prev_longitude", prevLongitude); - } - - featuredRootFragment = new ExploreListRootFragment(featuredArguments); - mobileRootFragment = new ExploreListRootFragment(mobileArguments); - mapRootFragment = new ExploreMapRootFragment(mapArguments); - - ((MainActivity) getActivity()).showTabs(); - ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); - - viewPagerAdapter.setTabs( - pairOf(R.string.explore_tab_title_featured, featuredRootFragment), - pairOf(R.string.explore_tab_title_mobile, mobileRootFragment), - pairOf(R.string.explore_tab_title_map, mapRootFragment) - ); - viewPagerAdapter.notifyDataSetChanged(); - } - - /** - * Fetch Nearby map camera data from fragment arguments if any. - */ - public void loadNearbyMapData() { - // get fragment arguments - if (getArguments() != null) { - prevZoom = getArguments().getDouble("prev_zoom"); - prevLatitude = getArguments().getDouble("prev_latitude"); - prevLongitude = getArguments().getDouble("prev_longitude"); - } - } - - /** - * Checks if fragment arguments contain data from Nearby map. if present, then the user - * navigated from Nearby using 'Show in Explore'. - * - * @return true if user navigated from Nearby map - **/ - public boolean isCameFromNearbyMap() { - return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0; - } - - public boolean onBackPressed() { - if (binding.tabLayout.getSelectedTabPosition() == 0) { - if (featuredRootFragment.backPressed()) { - ((BaseActivity) getActivity()).getSupportActionBar() - .setDisplayHomeAsUpEnabled(false); - return true; - } - } else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment - if (mobileRootFragment.backPressed()) { - ((BaseActivity) getActivity()).getSupportActionBar() - .setDisplayHomeAsUpEnabled(false); - return true; - } - } else { //explore map fragment - if (mapRootFragment.backPressed()) { - ((BaseActivity) getActivity()).getSupportActionBar() - .setDisplayHomeAsUpEnabled(false); - return true; - } - } - return false; - } - - /** - * This method inflates the menu in the toolbar - */ - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - // if logged in 'Show in Nearby' menu item is visible - if (applicationKvStore.getBoolean("login_skipped") == false) { - inflater.inflate(R.menu.explore_fragment_menu, menu); - - MenuItem others = menu.findItem(R.id.list_item_show_in_nearby); - - if (binding.viewPager.getCurrentItem() == 2) { - others.setVisible(true); - } - - // if on Map tab, show all menu options, else only show search - binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { - } - - @Override - public void onPageSelected(int position) { - others.setVisible((position == 2)); - } - - @Override - public void onPageScrollStateChanged(int state) { - if (state == SCROLL_STATE_IDLE && binding.viewPager.getCurrentItem() == 2) { - onPageSelected(2); - } - } - }); - } else { - inflater.inflate(R.menu.menu_search, menu); - } - super.onCreateOptionsMenu(menu, inflater); - } - - /** - * This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is - * available to open search page of the app - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - // Handle item selection - switch (item.getItemId()) { - case R.id.action_search: - ActivityUtils.startActivityWithFlags(getActivity(), SearchActivity.class); - return true; - case R.id.list_item_show_in_nearby: - mapRootFragment.loadNearbyMapFromExplore(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - binding = null; - } -} - - diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt new file mode 100644 index 000000000..ea96b50a3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt @@ -0,0 +1,229 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentPagerAdapter +import androidx.viewpager.widget.ViewPager +import androidx.viewpager.widget.ViewPager.OnPageChangeListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentExploreBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags +import javax.inject.Inject +import javax.inject.Named + +class ExploreFragment : CommonsDaggerSupportFragment() { + + @JvmField + @Inject + @Named("default_preferences") + var applicationKvStore: JsonKvStore? = null + + private var featuredRootFragment: ExploreListRootFragment? = null + private var mobileRootFragment: ExploreListRootFragment? = null + private var mapRootFragment: ExploreMapRootFragment? = null + private var prevZoom = 0.0 + private var prevLatitude = 0.0 + private var prevLongitude = 0.0 + private var viewPagerAdapter: ViewPagerAdapter? = null + var binding: FragmentExploreBinding? = null + + fun setScroll(canScroll: Boolean) { + if (binding != null) { + binding!!.viewPager.canScroll = canScroll + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreate(savedInstanceState) + loadNearbyMapData() + binding = FragmentExploreBinding.inflate(inflater, container, false) + + viewPagerAdapter = ViewPagerAdapter( + requireContext(), childFragmentManager, + FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT + ) + + binding!!.viewPager.adapter = viewPagerAdapter + binding!!.viewPager.id = R.id.viewPager + binding!!.tabLayout.setupWithViewPager(binding!!.viewPager) + binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener { + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit + override fun onPageScrollStateChanged(state: Int) = Unit + override fun onPageSelected(position: Int) { + binding!!.viewPager.canScroll = position != 2 + } + }) + setTabs() + setHasOptionsMenu(true) + + // if we came from 'Show in Explore' in Nearby, jump to Map tab + if (isCameFromNearbyMap) { + binding!!.viewPager.currentItem = 2 + } + return binding!!.root + } + + /** + * Sets the titles in the tabLayout and fragments in the viewPager + */ + fun setTabs() { + val featuredArguments = Bundle() + featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY) + + val mobileArguments = Bundle() + mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY) + + val mapArguments = Bundle() + mapArguments.putString("categoryName", EXPLORE_MAP) + + // if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root + if (isCameFromNearbyMap) { + mapArguments.putDouble("prev_zoom", prevZoom) + mapArguments.putDouble("prev_latitude", prevLatitude) + mapArguments.putDouble("prev_longitude", prevLongitude) + } + + featuredRootFragment = ExploreListRootFragment(featuredArguments) + mobileRootFragment = ExploreListRootFragment(mobileArguments) + mapRootFragment = ExploreMapRootFragment(mapArguments) + + (activity as MainActivity).showTabs() + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + + viewPagerAdapter!!.setTabs( + R.string.explore_tab_title_featured to featuredRootFragment!!, + R.string.explore_tab_title_mobile to mobileRootFragment!!, + R.string.explore_tab_title_map to mapRootFragment!! + ) + viewPagerAdapter!!.notifyDataSetChanged() + } + + /** + * Fetch Nearby map camera data from fragment arguments if any. + */ + private fun loadNearbyMapData() { + // get fragment arguments + if (arguments != null) { + with (requireArguments()) { + prevZoom = getDouble("prev_zoom") + prevLatitude = getDouble("prev_latitude") + prevLongitude = getDouble("prev_longitude") + } + } + } + + /** + * Checks if fragment arguments contain data from Nearby map. if present, then the user + * navigated from Nearby using 'Show in Explore'. + * + * @return true if user navigated from Nearby map + */ + private val isCameFromNearbyMap: Boolean + get() = prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0 + + fun onBackPressed(): Boolean { + if (binding!!.tabLayout.selectedTabPosition == 0) { + if (featuredRootFragment!!.backPressed()) { + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + return true + } + } else if (binding!!.tabLayout.selectedTabPosition == 1) { //Mobile root fragment + if (mobileRootFragment!!.backPressed()) { + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + return true + } + } else { //explore map fragment + if (mapRootFragment!!.backPressed()) { + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + return true + } + } + return false + } + + /** + * This method inflates the menu in the toolbar + */ + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + // if logged in 'Show in Nearby' menu item is visible + if (applicationKvStore!!.getBoolean("login_skipped") == false) { + inflater.inflate(R.menu.explore_fragment_menu, menu) + + val others = menu.findItem(R.id.list_item_show_in_nearby) + + if (binding!!.viewPager.currentItem == 2) { + others.setVisible(true) + } + + // if on Map tab, show all menu options, else only show search + binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener { + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit + + override fun onPageSelected(position: Int) { + others.setVisible((position == 2)) + } + + override fun onPageScrollStateChanged(state: Int) { + if (state == ViewPager.SCROLL_STATE_IDLE && binding!!.viewPager.currentItem == 2) { + onPageSelected(2) + } + } + }) + } else { + inflater.inflate(R.menu.menu_search, menu) + } + super.onCreateOptionsMenu(menu, inflater) + } + + /** + * This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is + * available to open search page of the app + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle item selection + + when (item.itemId) { + R.id.action_search -> { + startActivityWithFlags(requireActivity(), SearchActivity::class.java) + return true + } + + R.id.list_item_show_in_nearby -> { + mapRootFragment!!.loadNearbyMapFromExplore() + return true + } + + else -> return super.onOptionsItemSelected(item) + } + } + + override fun onDestroy() { + super.onDestroy() + binding = null + } + + companion object { + private const val FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons" + private const val MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android" + private const val EXPLORE_MAP = "Map" + + fun newInstance(): ExploreFragment = ExploreFragment().apply { + retainInstance = true + } + } +} + + diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java deleted file mode 100644 index e3ad90119..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java +++ /dev/null @@ -1,215 +0,0 @@ -package fr.free.nrw.commons.explore; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.navtab.NavTab; - -public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements - MediaDetailProvider, CategoryImagesCallback { - - private MediaDetailPagerFragment mediaDetails; - private CategoriesMediaFragment listFragment; - - private FragmentFeaturedRootBinding binding; - - public ExploreListRootFragment() { - //empty constructor necessary otherwise crashes on recreate - } - - public ExploreListRootFragment(Bundle bundle) { - String title = bundle.getString("categoryName"); - listFragment = new CategoriesMediaFragment(); - Bundle featuredArguments = new Bundle(); - featuredArguments.putString("categoryName", title); - listFragment.setArguments(featuredArguments); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (savedInstanceState == null) { - setFragment(listFragment, mediaDetails); - } - } - - public void setFragment(Fragment fragment, Fragment otherFragment) { - if (fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (fragment.isAdded() && otherFragment == null) { - getChildFragmentManager() - .beginTransaction() - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .add(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded()) { - getChildFragmentManager() - .beginTransaction() - .replace(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - } - - public void removeFragment(Fragment fragment) { - getChildFragmentManager() - .beginTransaction() - .remove(fragment) - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - - @Override - public void onAttach(final Context context) { - super.onAttach(context); - } - - @Override - public void onMediaClicked(int position) { - if (binding!=null) { - binding.exploreContainer.setVisibility(View.VISIBLE); - } - if (((ExploreFragment) getParentFragment()).binding!=null) { - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); - } - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - ((ExploreFragment) getParentFragment()).setScroll(false); - setFragment(mediaDetails, listFragment); - mediaDetails.showImage(position); - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * - * @param i It is the index of which media object is to be returned which is same as current - * index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - if (listFragment != null) { - return listFragment.getMediaAtPosition(i); - } else { - return null; - } - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain - * same number of media items as that of media elements in adapter. - * - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - if (listFragment != null) { - return listFragment.getTotalMediaCount(); - } else { - return 0; - } - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (mediaDetails != null && !listFragment.isVisible()) { - removeFragment(mediaDetails); - onMediaClicked(index); - } - } - - /** - * This method is called on success of API call for featured images or mobile uploads. The - * viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails != null) { - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * Performs back pressed action on the fragment. Return true if the event was handled by the - * mediaDetails otherwise returns false. - * - * @return - */ - public boolean backPressed() { - if (null != mediaDetails && mediaDetails.isVisible()) { - if (((ExploreFragment) getParentFragment()).binding != null) { - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE); - } - removeFragment(mediaDetails); - ((ExploreFragment) getParentFragment()).setScroll(true); - setFragment(listFragment, mediaDetails); - ((MainActivity) getActivity()).showTabs(); - return true; - } else { - if (((MainActivity) getActivity()) != null) { - ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); - } - } - if (((MainActivity) getActivity()) != null) { - ((MainActivity) getActivity()).showTabs(); - } - return false; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt new file mode 100644 index 000000000..32acebbb1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt @@ -0,0 +1,182 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.navtab.NavTab + +class ExploreListRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider, + CategoryImagesCallback { + private var mediaDetails: MediaDetailPagerFragment? = null + private var listFragment: CategoriesMediaFragment? = null + private var binding: FragmentFeaturedRootBinding? = null + + constructor() + + constructor(bundle: Bundle) { + listFragment = CategoriesMediaFragment().apply { + arguments = bundleOf( + "categoryName" to bundle.getString("categoryName") + ) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreate(savedInstanceState) + + binding = FragmentFeaturedRootBinding.inflate(inflater, container, false) + return binding!!.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (savedInstanceState == null) { + setFragment(listFragment!!, mediaDetails) + } + } + + fun setFragment(fragment: Fragment, otherFragment: Fragment?) { + if (fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (fragment.isAdded && otherFragment == null) { + childFragmentManager + .beginTransaction() + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .add(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded) { + childFragmentManager + .beginTransaction() + .replace(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } + } + + private fun removeFragment(fragment: Fragment) { + childFragmentManager + .beginTransaction() + .remove(fragment) + .commit() + childFragmentManager.executePendingTransactions() + } + + override fun onMediaClicked(position: Int) { + if (binding != null) { + binding!!.exploreContainer.visibility = View.VISIBLE + } + if ((parentFragment as ExploreFragment).binding != null) { + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = + View.GONE + } + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + (parentFragment as ExploreFragment).setScroll(false) + setFragment(mediaDetails!!, listFragment) + mediaDetails!!.showImage(position) + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * + * @param i It is the index of which media object is to be returned which is same as current + * index of viewPager. + * @return Media Object + */ + override fun getMediaAtPosition(i: Int): Media? = listFragment?.getMediaAtPosition(i) + + /** + * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain + * same number of media items as that of media elements in adapter. + * + * @return Total Media count in the adapter + */ + override fun getTotalMediaCount(): Int = listFragment?.getTotalMediaCount() ?: 0 + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (mediaDetails != null && !listFragment!!.isVisible) { + removeFragment(mediaDetails!!) + onMediaClicked(index) + } + } + + /** + * This method is called on success of API call for featured images or mobile uploads. The + * viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + mediaDetails?.notifyDataSetChanged() + } + + /** + * Performs back pressed action on the fragment. Return true if the event was handled by the + * mediaDetails otherwise returns false. + * + * @return + */ + fun backPressed(): Boolean { + if (null != mediaDetails && mediaDetails!!.isVisible) { + if ((parentFragment as ExploreFragment).binding != null) { + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = + View.VISIBLE + } + removeFragment(mediaDetails!!) + (parentFragment as ExploreFragment).setScroll(true) + setFragment(listFragment!!, mediaDetails) + (activity as MainActivity).showTabs() + return true + } else { + if ((activity as MainActivity?) != null) { + (activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code()) + } + } + if ((activity as MainActivity?) != null) { + (activity as MainActivity).showTabs() + } + return false + } + + override fun onDestroy() { + super.onDestroy() + + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java deleted file mode 100644 index 31a8e11ba..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java +++ /dev/null @@ -1,239 +0,0 @@ -package fr.free.nrw.commons.explore; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.map.ExploreMapFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.navtab.NavTab; - -public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements - MediaDetailProvider, CategoryImagesCallback { - - private MediaDetailPagerFragment mediaDetails; - private ExploreMapFragment mapFragment; - - private FragmentFeaturedRootBinding binding; - - public ExploreMapRootFragment() { - //empty constructor necessary otherwise crashes on recreate - } - - @NonNull - public static ExploreMapRootFragment newInstance() { - ExploreMapRootFragment fragment = new ExploreMapRootFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - public ExploreMapRootFragment(Bundle bundle) { - // get fragment arguments - String title = bundle.getString("categoryName"); - double zoom = bundle.getDouble("prev_zoom"); - double latitude = bundle.getDouble("prev_latitude"); - double longitude = bundle.getDouble("prev_longitude"); - - mapFragment = new ExploreMapFragment(); - Bundle featuredArguments = new Bundle(); - featuredArguments.putString("categoryName", title); - - // if we came from 'Show in Explore' in Nearby, pass on zoom and center - if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) { - featuredArguments.putDouble("prev_zoom", zoom); - featuredArguments.putDouble("prev_latitude", latitude); - featuredArguments.putDouble("prev_longitude", longitude); - } - mapFragment.setArguments(featuredArguments); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); - - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (savedInstanceState == null) { - setFragment(mapFragment, mediaDetails); - } - } - - public void setFragment(Fragment fragment, Fragment otherFragment) { - if (fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (fragment.isAdded() && otherFragment == null) { - getChildFragmentManager() - .beginTransaction() - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .add(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded()) { - getChildFragmentManager() - .beginTransaction() - .replace(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - } - - public void removeFragment(Fragment fragment) { - getChildFragmentManager() - .beginTransaction() - .remove(fragment) - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - - @Override - public void onAttach(final Context context) { - super.onAttach(context); - } - - @Override - public void onMediaClicked(int position) { - binding.exploreContainer.setVisibility(View.VISIBLE); - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - ((ExploreFragment) getParentFragment()).setScroll(false); - setFragment(mediaDetails, mapFragment); - mediaDetails.showImage(position); - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * - * @param i It is the index of which media object is to be returned which is same as current - * index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - if (mapFragment != null && mapFragment.mediaList != null) { - return mapFragment.mediaList.get(i); - } else { - return null; - } - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain - * same number of media items as that of media elements in adapter. - * - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - if (mapFragment != null && mapFragment.mediaList != null) { - return mapFragment.mediaList.size(); - } else { - return 0; - } - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (mediaDetails != null && !mapFragment.isVisible()) { - removeFragment(mediaDetails); - onMediaClicked(index); - } - } - - /** - * This method is called on success of API call for featured images or mobile uploads. The - * viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails != null) { - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * Performs back pressed action on the fragment. Return true if the event was handled by the - * mediaDetails otherwise returns false. - * - * @return - */ - public boolean backPressed() { - if (null != mediaDetails && mediaDetails.isVisible()) { - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE); - removeFragment(mediaDetails); - ((ExploreFragment) getParentFragment()).setScroll(true); - setFragment(mapFragment, mediaDetails); - ((MainActivity) getActivity()).showTabs(); - return true; - - } - if (mapFragment != null && mapFragment.isVisible()) { - if (mapFragment.backButtonClicked()) { - // Explore map fragment handled the event no further action required. - return true; - } else { - ((MainActivity) getActivity()).showTabs(); - return false; - } - } else { - ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); - } - ((MainActivity) getActivity()).showTabs(); - return false; - } - - public void loadNearbyMapFromExplore() { - mapFragment.loadNearbyMapFromExplore(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt new file mode 100644 index 000000000..af65834eb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt @@ -0,0 +1,201 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.map.ExploreMapFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.navtab.NavTab + +class ExploreMapRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider, + CategoryImagesCallback { + private var mediaDetails: MediaDetailPagerFragment? = null + private var mapFragment: ExploreMapFragment? = null + private var binding: FragmentFeaturedRootBinding? = null + + constructor() + + constructor(bundle: Bundle) { + // get fragment arguments + val title = bundle.getString("categoryName") + val zoom = bundle.getDouble("prev_zoom") + val latitude = bundle.getDouble("prev_latitude") + val longitude = bundle.getDouble("prev_longitude") + + mapFragment = ExploreMapFragment() + val featuredArguments = bundleOf( + "categoryName" to title + ) + + // if we came from 'Show in Explore' in Nearby, pass on zoom and center + if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) { + featuredArguments.putDouble("prev_zoom", zoom) + featuredArguments.putDouble("prev_latitude", latitude) + featuredArguments.putDouble("prev_longitude", longitude) + } + mapFragment!!.arguments = featuredArguments + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreate(savedInstanceState) + + binding = FragmentFeaturedRootBinding.inflate(inflater, container, false) + + return binding!!.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (savedInstanceState == null) { + setFragment(mapFragment!!, mediaDetails) + } + } + + fun setFragment(fragment: Fragment, otherFragment: Fragment?) { + if (fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (fragment.isAdded && otherFragment == null) { + childFragmentManager + .beginTransaction() + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .add(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded) { + childFragmentManager + .beginTransaction() + .replace(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } + } + + private fun removeFragment(fragment: Fragment) { + childFragmentManager + .beginTransaction() + .remove(fragment) + .commit() + childFragmentManager.executePendingTransactions() + } + + override fun onMediaClicked(position: Int) { + binding!!.exploreContainer.visibility = View.VISIBLE + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.GONE + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + (parentFragment as ExploreFragment).setScroll(false) + setFragment(mediaDetails!!, mapFragment) + mediaDetails!!.showImage(position) + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * + * @param i It is the index of which media object is to be returned which is same as current + * index of viewPager. + * @return Media Object + */ + override fun getMediaAtPosition(i: Int): Media? = mapFragment?.mediaList?.get(i) + + /** + * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain + * same number of media items as that of media elements in adapter. + * + * @return Total Media count in the adapter + */ + override fun getTotalMediaCount(): Int = mapFragment?.mediaList?.size ?: 0 + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (mediaDetails != null && !mapFragment!!.isVisible) { + removeFragment(mediaDetails!!) + onMediaClicked(index) + } + } + + /** + * This method is called on success of API call for featured images or mobile uploads. The + * viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + mediaDetails?.notifyDataSetChanged() + } + + /** + * Performs back pressed action on the fragment. Return true if the event was handled by the + * mediaDetails otherwise returns false. + * + * @return + */ + fun backPressed(): Boolean { + if (null != mediaDetails && mediaDetails!!.isVisible) { + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.VISIBLE + removeFragment(mediaDetails!!) + (parentFragment as ExploreFragment).setScroll(true) + setFragment(mapFragment!!, mediaDetails) + (activity as MainActivity).showTabs() + return true + } + if (mapFragment != null && mapFragment!!.isVisible) { + if (mapFragment!!.backButtonClicked()) { + // Explore map fragment handled the event no further action required. + return true + } else { + (activity as MainActivity).showTabs() + return false + } + } else { + (activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code()) + } + (activity as MainActivity).showTabs() + return false + } + + fun loadNearbyMapFromExplore() = mapFragment?.loadNearbyMapFromExplore() + + override fun onDestroy() { + super.onDestroy() + + binding = null + } + + companion object { + fun newInstance(): ExploreMapRootFragment = ExploreMapRootFragment().apply { + retainInstance = true + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java deleted file mode 100644 index 4112cda95..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java +++ /dev/null @@ -1,66 +0,0 @@ -package fr.free.nrw.commons.explore; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import androidx.viewpager.widget.ViewPager; - -/** - * ParentViewPager A custom viewPager whose scrolling can be enabled and disabled. - */ -public class ParentViewPager extends ViewPager { - - /** - * Boolean variable that stores the current state of pager scroll i.e(enabled or disabled) - */ - private boolean canScroll = true; - - - /** - * Default constructors - */ - public ParentViewPager(Context context) { - super(context); - } - - public ParentViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - } - - - /** - * Setter method for canScroll. - */ - public void setCanScroll(boolean canScroll) { - this.canScroll = canScroll; - } - - - /** - * Getter method for canScroll. - */ - public boolean isCanScroll() { - return canScroll; - } - - - /** - * Method that prevents scrolling if canScroll is set to false. - */ - @Override - public boolean onTouchEvent(MotionEvent ev) { - return canScroll && super.onTouchEvent(ev); - } - - - /** - * A facilitator method that allows parent to intercept touch events before its children. thus - * making it possible to prevent swiping parent on child end. - */ - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - return canScroll && super.onInterceptTouchEvent(ev); - } - - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt new file mode 100644 index 000000000..36e85e70a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.explore + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.viewpager.widget.ViewPager + +/** + * ParentViewPager A custom viewPager whose scrolling can be enabled and disabled. + */ +class ParentViewPager : ViewPager { + var canScroll: Boolean = true + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + override fun onTouchEvent(ev: MotionEvent): Boolean { + return canScroll && super.onTouchEvent(ev) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + return canScroll && super.onInterceptTouchEvent(ev) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java deleted file mode 100644 index b27ffc338..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ /dev/null @@ -1,285 +0,0 @@ -package fr.free.nrw.commons.explore; - -import static fr.free.nrw.commons.ViewPagerAdapter.pairOf; - -import android.os.Bundle; -import android.text.TextUtils; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import com.jakewharton.rxbinding2.view.RxView; -import com.jakewharton.rxbinding2.widget.RxSearchView; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.databinding.ActivitySearchBinding; -import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment; -import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; -import fr.free.nrw.commons.explore.media.SearchMediaFragment; -import fr.free.nrw.commons.explore.models.RecentSearch; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.FragmentUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import java.util.Date; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * Represents search screen of this app - */ - -public class SearchActivity extends BaseActivity - implements MediaDetailProvider, CategoryImagesCallback { - - @Inject - RecentSearchesDao recentSearchesDao; - - private SearchMediaFragment searchMediaFragment; - private SearchCategoryFragment searchCategoryFragment; - private SearchDepictionsFragment searchDepictionsFragment; - private RecentSearchesFragment recentSearchesFragment; - private FragmentManager supportFragmentManager; - private MediaDetailPagerFragment mediaDetails; - ViewPagerAdapter viewPagerAdapter; - - private ActivitySearchBinding binding; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivitySearchBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setTitle(getString(R.string.title_activity_search)); - setSupportActionBar(binding.toolbarSearch); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed()); - supportFragmentManager = getSupportFragmentManager(); - setSearchHistoryFragment(); - viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive - binding.tabLayout.setupWithViewPager(binding.viewPager); - setTabs(); - binding.searchBox.setQueryHint(getString(R.string.search_commons)); - binding.searchBox.onActionViewExpanded(); - binding.searchBox.clearFocus(); - - } - - /** - * This method sets the search history fragment. - * Search history fragment is displayed when query is empty. - */ - private void setSearchHistoryFragment() { - recentSearchesFragment = new RecentSearchesFragment(); - FragmentTransaction transaction = supportFragmentManager.beginTransaction(); - transaction.add(R.id.searchHistoryContainer, recentSearchesFragment).commit(); - } - - /** - * Sets the titles in the tabLayout and fragments in the viewPager - */ - public void setTabs() { - searchMediaFragment = new SearchMediaFragment(); - searchDepictionsFragment = new SearchDepictionsFragment(); - searchCategoryFragment= new SearchCategoryFragment(); - - viewPagerAdapter.setTabs( - pairOf(R.string.search_tab_title_media, searchMediaFragment), - pairOf(R.string.search_tab_title_categories, searchCategoryFragment), - pairOf(R.string.search_tab_title_depictions, searchDepictionsFragment) - ); - viewPagerAdapter.notifyDataSetChanged(); - getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox) - .takeUntil(RxView.detaches(binding.searchBox)) - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handleSearch, Timber::e - )); - } - - private void handleSearch(final CharSequence query) { - if (!TextUtils.isEmpty(query)) { - saveRecentSearch(query.toString()); - binding.viewPager.setVisibility(View.VISIBLE); - binding.tabLayout.setVisibility(View.VISIBLE); - binding.searchHistoryContainer.setVisibility(View.GONE); - - if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) { - searchDepictionsFragment.onQueryUpdated(query.toString()); - } - - if (FragmentUtils.isFragmentUIActive(searchMediaFragment)) { - searchMediaFragment.onQueryUpdated(query.toString()); - } - - if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) { - searchCategoryFragment.onQueryUpdated(query.toString()); - } - - } - else { - //Open RecentSearchesFragment - recentSearchesFragment.updateRecentSearches(); - binding.viewPager.setVisibility(View.GONE); - binding.tabLayout.setVisibility(View.GONE); - setSearchHistoryFragment(); - binding.searchHistoryContainer.setVisibility(View.VISIBLE); - } - } - - private void saveRecentSearch(@NonNull final String query) { - final RecentSearch recentSearch = recentSearchesDao.find(query); - // Newly searched query... - if (recentSearch == null) { - recentSearchesDao.save(new RecentSearch(null, query, new Date())); - } else { - recentSearch.setLastSearched(new Date()); - recentSearchesDao.save(recentSearch); - } - } - - /** - * returns Media Object at position - * @param i position of Media in the imagesRecyclerView adapter. - */ - @Override - public Media getMediaAtPosition(int i) { - return searchMediaFragment.getMediaAtPosition(i); - } - - /** - * returns total number of images present in the imagesRecyclerView adapter. - */ - @Override - public int getTotalMediaCount() { - return searchMediaFragment.getTotalMediaCount(); - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - onBackPressed(); - onMediaClicked(index); - } - } - - /** - * This method is called on success of API call for image Search. - * The viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails!=null){ - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * Open media detail pager fragment on click of image in search results - * @param index item index that should be opened - */ - @Override - public void onMediaClicked(int index) { - ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox)); - binding.tabLayout.setVisibility(View.GONE); - binding.viewPager.setVisibility(View.GONE); - binding.mediaContainer.setVisibility(View.VISIBLE); - binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open - if (mediaDetails == null || !mediaDetails.isVisible()) { - // set isFeaturedImage true for featured images, to include author field on media detail - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - supportFragmentManager - .beginTransaction() - .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) - .add(R.id.mediaContainer, mediaDetails) - .addToBackStack(null) - .commit(); - // Reason for using hide, add instead of replace is to maintain scroll position after - // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 - // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 - supportFragmentManager.executePendingTransactions(); - } - mediaDetails.showImage(index); - } - - /** - * This method is called on Screen Rotation - */ - @Override - protected void onResume() { - if (supportFragmentManager.getBackStackEntryCount()==1){ - //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. - //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 - // This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing. - // - onBackPressed(); - } - super.onResume(); - } - - /** - * This method is called on backPressed of anyFragment in the activity. - * If condition is called when mediaDetailFragment is opened. - */ - @Override - public void onBackPressed() { - //Remove the backstack entry that gets added when share button is clicked - //fixing:https://github.com/commons-app/apps-android-commons/issues/2296 - if (getSupportFragmentManager().getBackStackEntryCount() == 2) { - supportFragmentManager - .beginTransaction() - .remove(mediaDetails) - .commit(); - supportFragmentManager.popBackStack(); - supportFragmentManager.executePendingTransactions(); - } - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - // back to search so show search toolbar and hide navigation toolbar - binding.searchBox.setVisibility(View.VISIBLE);//set the searchview - binding.tabLayout.setVisibility(View.VISIBLE); - binding.viewPager.setVisibility(View.VISIBLE); - binding.mediaContainer.setVisibility(View.GONE); - } else { - binding.toolbarSearch.setVisibility(View.GONE); - } - super.onBackPressed(); - } - - /** - * This method is called on click of a recent search to update query in SearchView. - * @param query Recent Search Query - */ - public void updateText(String query) { - binding.searchBox.setQuery(query, true); - // Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details. - // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 - binding.viewPager.requestFocus(); - } - - @Override protected void onDestroy() { - super.onDestroy(); - //Dispose the disposables when the activity is destroyed - getCompositeDisposable().dispose(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt new file mode 100644 index 000000000..7b7bb2cd5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt @@ -0,0 +1,252 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import androidx.fragment.app.FragmentManager +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxSearchView +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.databinding.ActivitySearchBinding +import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment +import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment +import fr.free.nrw.commons.explore.media.SearchMediaFragment +import fr.free.nrw.commons.explore.models.RecentSearch +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.FragmentUtils.isFragmentUIActive +import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard +import io.reactivex.android.schedulers.AndroidSchedulers +import timber.log.Timber +import java.util.Date +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Represents search screen of this app + */ +class SearchActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback { + @JvmField + @Inject + var recentSearchesDao: RecentSearchesDao? = null + + private var searchMediaFragment: SearchMediaFragment? = null + private var searchCategoryFragment: SearchCategoryFragment? = null + private var searchDepictionsFragment: SearchDepictionsFragment? = null + private var recentSearchesFragment: RecentSearchesFragment? = null + private var supportFragmentManager: FragmentManager? = null + private var mediaDetails: MediaDetailPagerFragment? = null + private var viewPagerAdapter: ViewPagerAdapter? = null + private var binding: ActivitySearchBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySearchBinding.inflate(layoutInflater) + setContentView(binding!!.root) + + title = getString(R.string.title_activity_search) + setSupportActionBar(binding!!.toolbarSearch) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + binding!!.toolbarSearch.setNavigationOnClickListener { onBackPressed() } + supportFragmentManager = getSupportFragmentManager() + setSearchHistoryFragment() + viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager()) + binding!!.viewPager.adapter = viewPagerAdapter + binding!!.viewPager.offscreenPageLimit = 2 // Because we want all the fragments to be alive + binding!!.tabLayout.setupWithViewPager(binding!!.viewPager) + setTabs() + binding!!.searchBox.queryHint = getString(R.string.search_commons) + binding!!.searchBox.onActionViewExpanded() + binding!!.searchBox.clearFocus() + } + + /** + * This method sets the search history fragment. + * Search history fragment is displayed when query is empty. + */ + private fun setSearchHistoryFragment() { + recentSearchesFragment = RecentSearchesFragment() + val transaction = supportFragmentManager!!.beginTransaction() + transaction.add(R.id.searchHistoryContainer, recentSearchesFragment!!).commit() + } + + /** + * Sets the titles in the tabLayout and fragments in the viewPager + */ + fun setTabs() { + searchMediaFragment = SearchMediaFragment() + searchDepictionsFragment = SearchDepictionsFragment() + searchCategoryFragment = SearchCategoryFragment() + + viewPagerAdapter!!.setTabs( + R.string.search_tab_title_media to searchMediaFragment!!, + R.string.search_tab_title_categories to searchCategoryFragment!!, + R.string.search_tab_title_depictions to searchDepictionsFragment!! + ) + viewPagerAdapter!!.notifyDataSetChanged() + compositeDisposable.add( + RxSearchView.queryTextChanges(binding!!.searchBox) + .takeUntil(RxView.detaches(binding!!.searchBox)) + .debounce(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(::handleSearch, Timber::e) + ) + } + + private fun handleSearch(query: CharSequence) { + if (!TextUtils.isEmpty(query)) { + saveRecentSearch(query.toString()) + binding!!.viewPager.visibility = View.VISIBLE + binding!!.tabLayout.visibility = View.VISIBLE + binding!!.searchHistoryContainer.visibility = View.GONE + + if (isFragmentUIActive(searchDepictionsFragment)) { + searchDepictionsFragment!!.onQueryUpdated(query.toString()) + } + + if (isFragmentUIActive(searchMediaFragment)) { + searchMediaFragment!!.onQueryUpdated(query.toString()) + } + + if (isFragmentUIActive(searchCategoryFragment)) { + searchCategoryFragment!!.onQueryUpdated(query.toString()) + } + } else { + //Open RecentSearchesFragment + recentSearchesFragment!!.updateRecentSearches() + binding!!.viewPager.visibility = View.GONE + binding!!.tabLayout.visibility = View.GONE + setSearchHistoryFragment() + binding!!.searchHistoryContainer.visibility = View.VISIBLE + } + } + + private fun saveRecentSearch(query: String) { + val recentSearch = recentSearchesDao!!.find(query) + // Newly searched query... + if (recentSearch == null) { + recentSearchesDao!!.save(RecentSearch(null, query, Date())) + } else { + recentSearch.lastSearched = Date() + recentSearchesDao!!.save(recentSearch) + } + } + + override fun getMediaAtPosition(i: Int): Media? = searchMediaFragment!!.getMediaAtPosition(i) + + override fun getTotalMediaCount(): Int = searchMediaFragment!!.getTotalMediaCount() + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (getSupportFragmentManager().backStackEntryCount == 1) { + onBackPressed() + onMediaClicked(index) + } + } + + /** + * This method is called on success of API call for image Search. + * The viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + mediaDetails?.notifyDataSetChanged() + } + + /** + * Open media detail pager fragment on click of image in search results + * @param position item index that should be opened + */ + override fun onMediaClicked(position: Int) { + hideKeyboard(findViewById(R.id.searchBox)) + binding!!.tabLayout.visibility = View.GONE + binding!!.viewPager.visibility = View.GONE + binding!!.mediaContainer.visibility = View.VISIBLE + binding!!.searchBox.visibility = + View.GONE // to remove searchview when mediaDetails fragment open + if (mediaDetails == null || !mediaDetails!!.isVisible) { + // set isFeaturedImage true for featured images, to include author field on media detail + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + supportFragmentManager!! + .beginTransaction() + .hide(supportFragmentManager!!.fragments[supportFragmentManager!!.backStackEntryCount]) + .add(R.id.mediaContainer, mediaDetails!!) + .addToBackStack(null) + .commit() + // Reason for using hide, add instead of replace is to maintain scroll position after + // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 + // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 + supportFragmentManager!!.executePendingTransactions() + } + mediaDetails!!.showImage(position) + } + + /** + * This method is called on Screen Rotation + */ + override fun onResume() { + if (supportFragmentManager!!.backStackEntryCount == 1) { + //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. + //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 + // This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing. + onBackPressed() + } + super.onResume() + } + + /** + * This method is called on backPressed of anyFragment in the activity. + * If condition is called when mediaDetailFragment is opened. + */ + override fun onBackPressed() { + //Remove the backstack entry that gets added when share button is clicked + //fixing:https://github.com/commons-app/apps-android-commons/issues/2296 + if (getSupportFragmentManager().backStackEntryCount == 2) { + supportFragmentManager!! + .beginTransaction() + .remove(mediaDetails!!) + .commit() + supportFragmentManager!!.popBackStack() + supportFragmentManager!!.executePendingTransactions() + } + if (getSupportFragmentManager().backStackEntryCount == 1) { + // back to search so show search toolbar and hide navigation toolbar + binding!!.searchBox.visibility = View.VISIBLE //set the searchview + binding!!.tabLayout.visibility = View.VISIBLE + binding!!.viewPager.visibility = View.VISIBLE + binding!!.mediaContainer.visibility = View.GONE + } else { + binding!!.toolbarSearch.visibility = View.GONE + } + super.onBackPressed() + } + + /** + * This method is called on click of a recent search to update query in SearchView. + * @param query Recent Search Query + */ + fun updateText(query: String?) { + binding!!.searchBox.setQuery(query, true) + // Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details. + // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 + binding!!.viewPager.requestFocus() + } + + override fun onDestroy() { + super.onDestroy() + //Dispose the disposables when the activity is destroyed + compositeDisposable.dispose() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt index 9c41628a2..7c1d08bac 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt @@ -7,6 +7,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem abstract class PageableDepictionsFragment : BasePagingFragment() { override val errorTextId: Int = R.string.error_loading_depictions override val pagedListAdapter by lazy { - DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) } + DepictionAdapter { WikidataItemDetailsActivity.startYourself(requireContext(), it) } } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java deleted file mode 100644 index ec5ea42a4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java +++ /dev/null @@ -1,302 +0,0 @@ -package fr.free.nrw.commons.explore.depictions; - -import static fr.free.nrw.commons.ViewPagerAdapter.pairOf; -import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import androidx.fragment.app.FragmentManager; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding; -import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment; -import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; -import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.structure.depictions.DepictModel; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import fr.free.nrw.commons.wikidata.WikidataConstants; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import javax.inject.Inject; - -/** - * Activity to show depiction media, parent classes and child classes of depicted items in Explore - */ -public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailProvider, - CategoryImagesCallback { - private FragmentManager supportFragmentManager; - private DepictedImagesFragment depictionImagesListFragment; - private MediaDetailPagerFragment mediaDetailPagerFragment; - - /** - * Name of the depicted item - * Ex: Rabbit - */ - - @Inject BookmarkItemsDao bookmarkItemsDao; - private CompositeDisposable compositeDisposable; - @Inject - DepictModel depictModel; - private String wikidataItemName; - private ActivityWikidataItemDetailsBinding binding; - - ViewPagerAdapter viewPagerAdapter; - private DepictedItem wikidataItem; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - compositeDisposable = new CompositeDisposable(); - supportFragmentManager = getSupportFragmentManager(); - viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setOffscreenPageLimit(2); - binding.tabLayout.setupWithViewPager(binding.viewPager); - - final DepictedItem depictedItem = getIntent().getParcelableExtra( - WikidataConstants.BOOKMARKS_ITEMS); - wikidataItem = depictedItem; - setSupportActionBar(binding.toolbarBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTabs(); - setPageTitle(); - } - - /** - * Gets the passed wikidataItemName from the intents and displays it as the page title - */ - private void setPageTitle() { - if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) { - setTitle(getIntent().getStringExtra("wikidataItemName")); - } - } - - /** - * This method is called on success of API call for featured Images. - * The viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetailPagerFragment !=null){ - mediaDetailPagerFragment.notifyDataSetChanged(); - } - } - - /** - * This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab, - * Set the fragments according to the tab selected in the viewPager. - */ - private void setTabs() { - depictionImagesListFragment = new DepictedImagesFragment(); - ChildDepictionsFragment childDepictionsFragment = new ChildDepictionsFragment(); - ParentDepictionsFragment parentDepictionsFragment = new ParentDepictionsFragment(); - wikidataItemName = getIntent().getStringExtra("wikidataItemName"); - String entityId = getIntent().getStringExtra("entityId"); - if (getIntent() != null && wikidataItemName != null) { - Bundle arguments = new Bundle(); - arguments.putString("wikidataItemName", wikidataItemName); - arguments.putString("entityId", entityId); - depictionImagesListFragment.setArguments(arguments); - parentDepictionsFragment.setArguments(arguments); - childDepictionsFragment.setArguments(arguments); - } - - viewPagerAdapter.setTabs( - pairOf(R.string.title_for_media, depictionImagesListFragment), - pairOf(R.string.title_for_subcategories, childDepictionsFragment), - pairOf(R.string.title_for_parent_categories, parentDepictionsFragment) - ); - binding.viewPager.setOffscreenPageLimit(2); - viewPagerAdapter.notifyDataSetChanged(); - - } - - - /** - * Shows media detail fragment when user clicks on any image in the list - */ - @Override - public void onMediaClicked(int position) { - binding.tabLayout.setVisibility(View.GONE); - binding.viewPager.setVisibility(View.GONE); - binding.mediaContainer.setVisibility(View.VISIBLE); - if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { - // set isFeaturedImage true for featured images, to include author field on media detail - mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); - FragmentManager supportFragmentManager = getSupportFragmentManager(); - supportFragmentManager - .beginTransaction() - .replace(R.id.mediaContainer, mediaDetailPagerFragment) - .addToBackStack(null) - .commit(); - supportFragmentManager.executePendingTransactions(); - } - mediaDetailPagerFragment.showImage(position); - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * @param i It is the index of which media object is to be returned which is same as - * current index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - return depictionImagesListFragment.getMediaAtPosition(i); - } - - /** - * This method is called on backPressed of anyFragment in the activity. - * If condition is called when mediaDetailFragment is opened. - */ - @Override - public void onBackPressed() { - if (supportFragmentManager.getBackStackEntryCount() == 1){ - binding.tabLayout.setVisibility(View.VISIBLE); - binding.viewPager.setVisibility(View.VISIBLE); - binding.mediaContainer.setVisibility(View.GONE); - } - super.onBackPressed(); - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment - * The viewpager will contain same number of media items as that of media elements in adapter. - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - return depictionImagesListFragment.getTotalMediaCount(); - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - onBackPressed(); - onMediaClicked(index); - } - } - - /** - * Consumers should be simply using this method to use this activity. - * - * @param context A Context of the application package implementing this class. - * @param depictedItem Name of the depicts for displaying its details - */ - public static void startYourself(Context context, DepictedItem depictedItem) { - Intent intent = new Intent(context, WikidataItemDetailsActivity.class); - intent.putExtra("wikidataItemName", depictedItem.getName()); - intent.putExtra("entityId", depictedItem.getId()); - intent.putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem); - context.startActivity(intent); - } - - /** - * This function inflates the menu - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater menuInflater=getMenuInflater(); - menuInflater.inflate(R.menu.menu_wikidata_item,menu); - - updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item)); - - return super.onCreateOptionsMenu(menu); - } - - /** - * This method handles the logic on item select in toolbar menu - * Currently only 1 choice is available to open Wikidata item details page in browser - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - switch (item.getItemId()){ - case R.id.browser_actions_menu_items: - String entityId=getIntent().getStringExtra("entityId"); - Uri uri = Uri.parse("https://www.wikidata.org/wiki/" + entityId); - handleWebUrl(this, uri); - return true; - case R.id.menu_bookmark_current_item: - - if(getIntent().getStringExtra("fragment") != null) { - compositeDisposable.add(depictModel.getDepictions( - getIntent().getStringExtra("entityId") - ).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(depictedItems -> { - final boolean bookmarkExists = bookmarkItemsDao.updateBookmarkItem( - depictedItems.get(0)); - final Snackbar snackbar - = bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.add_bookmark, Snackbar.LENGTH_LONG) - : Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.remove_bookmark, - Snackbar.LENGTH_LONG); - - snackbar.show(); - updateBookmarkState(item); - })); - - } else { - final boolean bookmarkExists - = bookmarkItemsDao.updateBookmarkItem(wikidataItem); - final Snackbar snackbar - = bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.add_bookmark, Snackbar.LENGTH_LONG) - : Snackbar.make(findViewById(R.id.toolbar_layout), R.string.remove_bookmark, - Snackbar.LENGTH_LONG); - - snackbar.show(); - updateBookmarkState(item); - } - return true; - case android.R.id.home: - onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void updateBookmarkState(final MenuItem item) { - final boolean isBookmarked; - if(getIntent().getStringExtra("fragment") != null) { - isBookmarked - = bookmarkItemsDao.findBookmarkItem(getIntent().getStringExtra("entityId")); - } else { - isBookmarked = bookmarkItemsDao.findBookmarkItem(wikidataItem.getId()); - } - final int icon - = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px - : R.drawable.menu_ic_round_star_border_24px; - item.setIcon(icon); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt new file mode 100644 index 000000000..4696ae8d4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt @@ -0,0 +1,295 @@ +package fr.free.nrw.commons.explore.depictions + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding +import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment +import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment +import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.upload.structure.depictions.DepictModel +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.utils.handleWebUrl +import fr.free.nrw.commons.wikidata.WikidataConstants +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +/** + * Activity to show depiction media, parent classes and child classes of depicted items in Explore + */ +class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback { + @JvmField + @Inject + var bookmarkItemsDao: BookmarkItemsDao? = null + + @JvmField + @Inject + var depictModel: DepictModel? = null + + private var supportFragmentManager: FragmentManager? = null + private var depictionImagesListFragment: DepictedImagesFragment? = null + private var mediaDetailPagerFragment: MediaDetailPagerFragment? = null + private var binding: ActivityWikidataItemDetailsBinding? = null + + var viewPagerAdapter: ViewPagerAdapter? = null + private var wikidataItem: DepictedItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityWikidataItemDetailsBinding.inflate(layoutInflater) + setContentView(binding!!.root) + supportFragmentManager = getSupportFragmentManager() + viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager()) + binding!!.viewPager.adapter = viewPagerAdapter + binding!!.viewPager.offscreenPageLimit = 2 + binding!!.tabLayout.setupWithViewPager(binding!!.viewPager) + + wikidataItem = intent.getParcelableExtra(WikidataConstants.BOOKMARKS_ITEMS) + setSupportActionBar(binding!!.toolbarBinding.toolbar) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + setTabs() + setPageTitle() + } + + /** + * Gets the passed wikidataItemName from the intents and displays it as the page title + */ + private fun setPageTitle() { + if (intent != null && intent.getStringExtra("wikidataItemName") != null) { + title = intent.getStringExtra("wikidataItemName") + } + } + + /** + * This method is called on success of API call for featured Images. + * The viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + if (mediaDetailPagerFragment != null) { + mediaDetailPagerFragment!!.notifyDataSetChanged() + } + } + + /** + * This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab, + * Set the fragments according to the tab selected in the viewPager. + */ + private fun setTabs() { + depictionImagesListFragment = DepictedImagesFragment() + val childDepictionsFragment = ChildDepictionsFragment() + val parentDepictionsFragment = ParentDepictionsFragment() + val wikidataItemName = intent.getStringExtra("wikidataItemName") + val entityId = intent.getStringExtra("entityId") + if (intent != null && wikidataItemName != null) { + val arguments = bundleOf( + "wikidataItemName" to wikidataItemName, + "entityId" to entityId + ) + depictionImagesListFragment!!.arguments = arguments + parentDepictionsFragment.arguments = arguments + childDepictionsFragment.arguments = arguments + } + + viewPagerAdapter!!.setTabs( + R.string.title_for_media to depictionImagesListFragment!!, + R.string.title_for_subcategories to childDepictionsFragment, + R.string.title_for_parent_categories to parentDepictionsFragment + ) + binding!!.viewPager.offscreenPageLimit = 2 + viewPagerAdapter!!.notifyDataSetChanged() + } + + + /** + * Shows media detail fragment when user clicks on any image in the list + */ + override fun onMediaClicked(position: Int) { + binding!!.tabLayout.visibility = View.GONE + binding!!.viewPager.visibility = View.GONE + binding!!.mediaContainer.visibility = View.VISIBLE + if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment!!.isVisible) { + // set isFeaturedImage true for featured images, to include author field on media detail + mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true) + val supportFragmentManager = getSupportFragmentManager() + supportFragmentManager + .beginTransaction() + .replace(R.id.mediaContainer, mediaDetailPagerFragment!!) + .addToBackStack(null) + .commit() + supportFragmentManager.executePendingTransactions() + } + mediaDetailPagerFragment!!.showImage(position) + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * @param i It is the index of which media object is to be returned which is same as + * current index of viewPager. + * @return Media Object + */ + override fun getMediaAtPosition(i: Int): Media? { + return depictionImagesListFragment!!.getMediaAtPosition(i) + } + + /** + * This method is called on backPressed of anyFragment in the activity. + * If condition is called when mediaDetailFragment is opened. + */ + override fun onBackPressed() { + if (supportFragmentManager!!.backStackEntryCount == 1) { + binding!!.tabLayout.visibility = View.VISIBLE + binding!!.viewPager.visibility = View.VISIBLE + binding!!.mediaContainer.visibility = View.GONE + } + super.onBackPressed() + } + + /** + * This method is called on from getCount of MediaDetailPagerFragment + * The viewpager will contain same number of media items as that of media elements in adapter. + * @return Total Media count in the adapter + */ + override fun getTotalMediaCount(): Int = depictionImagesListFragment!!.getTotalMediaCount() + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (getSupportFragmentManager().backStackEntryCount == 1) { + onBackPressed() + onMediaClicked(index) + } + } + + /** + * This function inflates the menu + */ + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val menuInflater = menuInflater + menuInflater.inflate(R.menu.menu_wikidata_item, menu) + + updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item)) + + return super.onCreateOptionsMenu(menu) + } + + /** + * This method handles the logic on item select in toolbar menu + * Currently only 1 choice is available to open Wikidata item details page in browser + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.browser_actions_menu_items -> { + val entityId = intent.getStringExtra("entityId") + val uri = Uri.parse("https://www.wikidata.org/wiki/$entityId") + handleWebUrl(this, uri) + return true + } + + R.id.menu_bookmark_current_item -> { + if (intent.getStringExtra("fragment") != null) { + compositeDisposable!!.add( + depictModel!!.getDepictions( + intent.getStringExtra("entityId")!! + ).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(Consumer> { depictedItems: List -> + val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem( + depictedItems[0]!! + ) + val snackbar = if (bookmarkExists) + Snackbar.make( + findViewById(R.id.toolbar_layout), + R.string.add_bookmark, Snackbar.LENGTH_LONG + ) + else + Snackbar.make( + findViewById(R.id.toolbar_layout), + R.string.remove_bookmark, + Snackbar.LENGTH_LONG + ) + + snackbar.show() + updateBookmarkState(item) + }) + ) + } else { + val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(wikidataItem!!) + val snackbar = if (bookmarkExists) + Snackbar.make( + findViewById(R.id.toolbar_layout), + R.string.add_bookmark, Snackbar.LENGTH_LONG + ) + else + Snackbar.make( + findViewById(R.id.toolbar_layout), R.string.remove_bookmark, + Snackbar.LENGTH_LONG + ) + + snackbar.show() + updateBookmarkState(item) + } + return true + } + + android.R.id.home -> { + onBackPressed() + return true + } + + else -> return super.onOptionsItemSelected(item) + } + } + + private fun updateBookmarkState(item: MenuItem) { + val isBookmarked: Boolean = if (intent.getStringExtra("fragment") != null) { + bookmarkItemsDao!!.findBookmarkItem(intent.getStringExtra("entityId")) + } else { + bookmarkItemsDao!!.findBookmarkItem(wikidataItem!!.id) + } + item.setIcon(if (isBookmarked) { + R.drawable.menu_ic_round_star_filled_24px + } else { + R.drawable.menu_ic_round_star_border_24px + }) + } + + companion object { + /** + * Consumers should be simply using this method to use this activity. + * + * @param context A Context of the application package implementing this class. + * @param depictedItem Name of the depicts for displaying its details + */ + fun startYourself(context: Context, depictedItem: DepictedItem) { + val intent = Intent(context, WikidataItemDetailsActivity::class.java).apply { + putExtra("wikidataItemName", depictedItem.name) + putExtra("entityId", depictedItem.id) + putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem) + } + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java deleted file mode 100644 index 5e674dceb..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import androidx.annotation.NonNull; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.media.MediaClient; -import java.util.Collections; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ExploreMapCalls { - - @Inject - MediaClient mediaClient; - - @Inject - public ExploreMapCalls() { - } - - /** - * Calls method to query Commons for uploads around a location - * - * @param currentLatLng coordinates of search location - * @return list of places obtained - */ - @NonNull - List callCommonsQuery(final LatLng currentLatLng) { - String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude(); - return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt new file mode 100644 index 000000000..6c62d3667 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.explore.map + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.media.MediaClient +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ExploreMapCalls @Inject constructor() { + @Inject + @JvmField + var mediaClient: MediaClient? = null + + /** + * Calls method to query Commons for uploads around a location + * + * @param currentLatLng coordinates of search location + * @return list of places obtained + */ + fun callCommonsQuery(currentLatLng: LatLng): List { + val coordinates = currentLatLng.latitude.toString() + "|" + currentLatLng.longitude + return mediaClient!!.getMediaListFromGeoSearch(coordinates).blockingGet() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java deleted file mode 100644 index feb66bf55..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java +++ /dev/null @@ -1,45 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import android.content.Context; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; -import java.util.List; - -public class ExploreMapContract { - - interface View { - boolean isNetworkConnectionEstablished(); - void populatePlaces(LatLng curlatLng); - void askForLocationPermission(); - void recenterMap(LatLng curLatLng); - void hideBottomDetailsSheet(); - LatLng getMapCenter(); - LatLng getMapFocus(); - LatLng getLastMapFocus(); - void addMarkersToMap(final List nearbyBaseMarkers); - void clearAllMarkers(); - void addSearchThisAreaButtonAction(); - void setSearchThisAreaButtonVisibility(boolean isVisible); - void setProgressBarVisibility(boolean isVisible); - boolean isDetailsBottomSheetVisible(); - boolean isSearchThisAreaButtonVisible(); - Context getContext(); - LatLng getLastLocation(); - void disableFABRecenter(); - void enableFABRecenter(); - void setFABRecenterAction(android.view.View.OnClickListener onClickListener); - boolean backButtonClicked(); - } - - interface UserActions { - void updateMap(LocationServiceManager.LocationChangeType locationChangeType); - void lockUnlockNearby(boolean isNearbyLocked); - void attachView(View view); - void detachView(); - void setActionListeners(JsonKvStore applicationKvStore); - boolean backButtonClicked(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt new file mode 100644 index 000000000..306446a43 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt @@ -0,0 +1,43 @@ +package fr.free.nrw.commons.explore.map + +import android.content.Context +import android.view.View +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType + +class ExploreMapContract { + interface View { + fun isNetworkConnectionEstablished(): Boolean + fun populatePlaces(curlatLng: LatLng?) + fun askForLocationPermission() + fun recenterMap(curLatLng: LatLng?) + fun hideBottomDetailsSheet() + fun getMapCenter(): LatLng? + fun getMapFocus(): LatLng? + fun getLastMapFocus(): LatLng? + fun addMarkersToMap(nearbyBaseMarkers: List?) + fun clearAllMarkers() + fun addSearchThisAreaButtonAction() + fun setSearchThisAreaButtonVisibility(isVisible: Boolean) + fun setProgressBarVisibility(isVisible: Boolean) + fun isDetailsBottomSheetVisible(): Boolean + fun isSearchThisAreaButtonVisible(): Boolean + fun getContext(): Context? + fun getLastLocation(): LatLng? + fun disableFABRecenter() + fun enableFABRecenter() + fun setFABRecenterAction(onClickListener: android.view.View.OnClickListener?) + fun backButtonClicked(): Boolean + } + + interface UserActions { + fun updateMap(locationChangeType: LocationChangeType) + fun lockUnlockNearby(isNearbyLocked: Boolean) + fun attachView(view: View?) + fun detachView() + fun setActionListeners(applicationKvStore: JsonKvStore?) + fun backButtonClicked(): Boolean + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java deleted file mode 100644 index c944f75a1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java +++ /dev/null @@ -1,213 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween; -import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.transition.Transition; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.MapController; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.utils.ImageUtils; -import fr.free.nrw.commons.utils.LocationUtils; -import fr.free.nrw.commons.utils.PlaceUtils; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.inject.Inject; -import timber.log.Timber; - -public class ExploreMapController extends MapController { - - private final ExploreMapCalls exploreMapCalls; - public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used - public LatLng currentLocation; // current location of user - public double latestSearchRadius = 0; // Any last search radius - public double currentLocationSearchRadius = 0; // Search radius of only searches around current location - - - @Inject - public ExploreMapController(ExploreMapCalls explorePlaces) { - this.exploreMapCalls = explorePlaces; - } - - /** - * Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList, - * explorePlaceList and boundaryCoordinates - * - * @param currentLatLng is current geolocation - * @param searchLatLng is the location that we want to search around - * @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around - * current location, false if another location - * @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and - * boundaryCoordinates - */ - public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng, - boolean checkingAroundCurrentLocation) { - - if (searchLatLng == null) { - Timber.d("Loading attractions explore map, but search is null"); - return null; - } - - ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo(); - try { - explorePlacesInfo.currentLatLng = currentLatLng; - latestSearchLocation = searchLatLng; - - List mediaList = exploreMapCalls.callCommonsQuery(searchLatLng); - LatLng[] boundaryCoordinates = {mediaList.get(0).getCoordinates(), // south - mediaList.get(0).getCoordinates(), // north - mediaList.get(0).getCoordinates(), // west - mediaList.get(0).getCoordinates()};// east, init with a random location - - if (searchLatLng != null) { - Timber.d("Sorting places by distance..."); - final Map distances = new HashMap<>(); - for (Media media : mediaList) { - distances.put(media, - computeDistanceBetween(media.getCoordinates(), searchLatLng)); - // Find boundaries with basic find max approach - if (media.getCoordinates().getLatitude() - < boundaryCoordinates[0].getLatitude()) { - boundaryCoordinates[0] = media.getCoordinates(); - } - if (media.getCoordinates().getLatitude() - > boundaryCoordinates[1].getLatitude()) { - boundaryCoordinates[1] = media.getCoordinates(); - } - if (media.getCoordinates().getLongitude() - < boundaryCoordinates[2].getLongitude()) { - boundaryCoordinates[2] = media.getCoordinates(); - } - if (media.getCoordinates().getLongitude() - > boundaryCoordinates[3].getLongitude()) { - boundaryCoordinates[3] = media.getCoordinates(); - } - } - } - explorePlacesInfo.mediaList = mediaList; - explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList); - explorePlacesInfo.boundaryCoordinates = boundaryCoordinates; - - // Sets latestSearchRadius to maximum distance among boundaries and search location - for (LatLng bound : boundaryCoordinates) { - double distance = LocationUtils.calculateDistance(bound.getLatitude(), - bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude()); - if (distance > latestSearchRadius) { - latestSearchRadius = distance; - } - } - - // Our radius searched around us, will be used to understand when user search their own location, we will follow them - if (checkingAroundCurrentLocation) { - currentLocationSearchRadius = latestSearchRadius; - currentLocation = currentLatLng; - } - } catch (Exception e) { - e.printStackTrace(); - } - return explorePlacesInfo; - } - - /** - * Loads attractions from location for map view, we need to return places in Place data type - * - * @return baseMarkerOptions list that holds nearby places with their icons - */ - public static List loadAttractionsFromLocationToBaseMarkerOptions( - LatLng currentLatLng, - final List placeList, - Context context, - NearbyBaseMarkerThumbCallback callback, - ExplorePlacesInfo explorePlacesInfo) { - List baseMarkerList = new ArrayList<>(); - - if (placeList == null) { - return baseMarkerList; - } - - VectorDrawableCompat vectorDrawable = null; - try { - vectorDrawable = VectorDrawableCompat.create( - context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme()); - - } catch (Resources.NotFoundException e) { - // ignore when running tests. - } - if (vectorDrawable != null) { - for (Place explorePlace : placeList) { - final BaseMarker baseMarker = new BaseMarker(); - String distance = formatDistanceBetween(currentLatLng, explorePlace.location); - explorePlace.setDistance(distance); - - baseMarker.setTitle( - explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))); - baseMarker.setPosition( - new fr.free.nrw.commons.location.LatLng( - explorePlace.location.getLatitude(), - explorePlace.location.getLongitude(), 0)); - baseMarker.setPlace(explorePlace); - - Glide.with(context) - .asBitmap() - .load(explorePlace.getThumb()) - .placeholder(R.drawable.image_placeholder_96) - .apply(new RequestOptions().override(96, 96).centerCrop()) - .into(new CustomTarget() { - // We add icons to markers when bitmaps are ready - @Override - public void onResourceReady(@NonNull Bitmap resource, - @Nullable Transition transition) { - baseMarker.setIcon( - ImageUtils.addRedBorder(resource, 6, context)); - baseMarkerList.add(baseMarker); - if (baseMarkerList.size() - == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback - callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, - explorePlacesInfo); - } - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - } - - // We add thumbnail icon for images that couldn't be loaded - @Override - public void onLoadFailed(@Nullable final Drawable errorDrawable) { - super.onLoadFailed(errorDrawable); - baseMarker.fromResource(context, R.drawable.image_placeholder_96); - baseMarkerList.add(baseMarker); - if (baseMarkerList.size() - == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback - callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, - explorePlacesInfo); - } - } - }); - } - } - return baseMarkerList; - } - - interface NearbyBaseMarkerThumbCallback { - - // Callback to notify thumbnails of explore markers are added as icons and ready - void onNearbyBaseMarkerThumbsReady(List baseMarkers, - ExplorePlacesInfo explorePlacesInfo); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt new file mode 100644 index 000000000..0873572d1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt @@ -0,0 +1,219 @@ +package fr.free.nrw.commons.explore.map + +import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.MapController +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.utils.ImageUtils.addRedBorder +import fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween +import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween +import fr.free.nrw.commons.utils.LocationUtils.calculateDistance +import fr.free.nrw.commons.utils.PlaceUtils.mediaToExplorePlace +import timber.log.Timber +import javax.inject.Inject + +class ExploreMapController @Inject constructor( + private val exploreMapCalls: ExploreMapCalls +) : MapController() { + // Can be current and camera target on search this area button is used + private var latestSearchLocation: LatLng? = null + + // Any last search radius + private var latestSearchRadius: Double = 0.0 + + // Search radius of only searches around current location + private var currentLocationSearchRadius: Double = 0.0 + + @JvmField + // current location of user + var currentLocation: LatLng? = null + + /** + * Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList, + * explorePlaceList and boundaryCoordinates + * + * @param currentLatLng is current geolocation + * @param searchLatLng is the location that we want to search around + * @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around + * current location, false if another location + * @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and + * boundaryCoordinates + */ + fun loadAttractionsFromLocation( + currentLatLng: LatLng?, searchLatLng: LatLng?, + checkingAroundCurrentLocation: Boolean + ): ExplorePlacesInfo? { + if (searchLatLng == null) { + Timber.d("Loading attractions explore map, but search is null") + return null + } + + val explorePlacesInfo = ExplorePlacesInfo() + try { + explorePlacesInfo.currentLatLng = currentLatLng + latestSearchLocation = searchLatLng + + val mediaList = exploreMapCalls.callCommonsQuery(searchLatLng) + val boundaryCoordinates = arrayOf( + mediaList[0].coordinates!!, // south + mediaList[0].coordinates!!, // north + mediaList[0].coordinates!!, // west + mediaList[0].coordinates!! + ) // east, init with a random location + + Timber.d("Sorting places by distance...") + val distances: MutableMap = HashMap() + for (media in mediaList) { + distances[media] = computeDistanceBetween(media.coordinates!!, searchLatLng) + // Find boundaries with basic find max approach + if (media.coordinates!!.latitude + < boundaryCoordinates[0]!!.latitude + ) { + boundaryCoordinates[0] = media.coordinates!! + } + if (media.coordinates!!.latitude + > boundaryCoordinates[1]!!.latitude + ) { + boundaryCoordinates[1] = media.coordinates!! + } + if (media.coordinates!!.longitude + < boundaryCoordinates[2]!!.longitude + ) { + boundaryCoordinates[2] = media.coordinates!! + } + if (media.coordinates!!.longitude + > boundaryCoordinates[3]!!.longitude + ) { + boundaryCoordinates[3] = media.coordinates!! + } + } + explorePlacesInfo.mediaList = mediaList + explorePlacesInfo.explorePlaceList = mediaToExplorePlace(mediaList) + explorePlacesInfo.boundaryCoordinates = boundaryCoordinates + + // Sets latestSearchRadius to maximum distance among boundaries and search location + for ((latitude, longitude) in boundaryCoordinates) { + val distance = calculateDistance( + latitude, + longitude, searchLatLng.latitude, searchLatLng.longitude + ) + if (distance > latestSearchRadius) { + latestSearchRadius = distance + } + } + + // Our radius searched around us, will be used to understand when user search their own location, we will follow them + if (checkingAroundCurrentLocation) { + currentLocationSearchRadius = latestSearchRadius + currentLocation = currentLatLng + } + } catch (e: Exception) { + Timber.e(e) + } + return explorePlacesInfo + } + + interface NearbyBaseMarkerThumbCallback { + // Callback to notify thumbnails of explore markers are added as icons and ready + fun onNearbyBaseMarkerThumbsReady( + baseMarkers: List?, + explorePlacesInfo: ExplorePlacesInfo? + ) + } + + companion object { + /** + * Loads attractions from location for map view, we need to return places in Place data type + * + * @return baseMarkerOptions list that holds nearby places with their icons + */ + fun loadAttractionsFromLocationToBaseMarkerOptions( + currentLatLng: LatLng?, + placeList: List?, + context: Context, + callback: NearbyBaseMarkerThumbCallback, + explorePlacesInfo: ExplorePlacesInfo? + ): List { + val baseMarkerList: MutableList = ArrayList() + + if (placeList == null) { + return baseMarkerList + } + + var vectorDrawable: VectorDrawableCompat? = null + try { + vectorDrawable = VectorDrawableCompat.create( + context.resources, R.drawable.ic_custom_map_marker_dark, context.theme + ) + } catch (e: Resources.NotFoundException) { + // ignore when running tests. + } + if (vectorDrawable != null) { + for (explorePlace in placeList) { + val baseMarker = BaseMarker() + val distance = formatDistanceBetween(currentLatLng, explorePlace.location) + explorePlace.setDistance(distance) + + baseMarker.title = + explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")) + baseMarker.position = LatLng( + explorePlace.location.latitude, + explorePlace.location.longitude, 0f + ) + baseMarker.place = explorePlace + + Glide.with(context) + .asBitmap() + .load(explorePlace.thumb) + .placeholder(R.drawable.image_placeholder_96) + .apply(RequestOptions().override(96, 96).centerCrop()) + .into(object : CustomTarget() { + // We add icons to markers when bitmaps are ready + override fun onResourceReady( + resource: Bitmap, + transition: Transition? + ) { + baseMarker.icon = addRedBorder(resource, 6, context) + baseMarkerList.add(baseMarker) + if (baseMarkerList.size == placeList.size) { + // if true, we added all markers to list and can trigger thumbs ready callback + callback.onNearbyBaseMarkerThumbsReady( + baseMarkerList, + explorePlacesInfo + ) + } + } + + override fun onLoadCleared(placeholder: Drawable?) = Unit + + // We add thumbnail icon for images that couldn't be loaded + override fun onLoadFailed(errorDrawable: Drawable?) { + super.onLoadFailed(errorDrawable) + baseMarker.fromResource(context, R.drawable.image_placeholder_96) + baseMarkerList.add(baseMarker) + if (baseMarkerList.size == placeList.size) { + // if true, we added all markers to list and can trigger thumbs ready callback + callback.onNearbyBaseMarkerThumbsReady( + baseMarkerList, + explorePlacesInfo + ) + } + } + }) + } + } + return baseMarkerList + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java deleted file mode 100644 index 364f4d53a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java +++ /dev/null @@ -1,1140 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; -import static fr.free.nrw.commons.utils.GeoCoordinatesKt.handleGeoCoordinates; -import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; -import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl; - -import android.Manifest.permission; -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.location.Location; -import android.location.LocationManager; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.MapController; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentExploreMapBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.ExploreMapRootFragment; -import fr.free.nrw.commons.explore.paging.LiveDataConverter; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationPermissionsHelper; -import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.location.LocationUpdateListener; -import fr.free.nrw.commons.media.MediaClient; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.MapUtils; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.inject.Inject; -import javax.inject.Named; -import org.osmdroid.events.MapEventsReceiver; -import org.osmdroid.events.MapListener; -import org.osmdroid.events.ScrollEvent; -import org.osmdroid.events.ZoomEvent; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.util.constants.GeoConstants; -import org.osmdroid.views.CustomZoomButtonsController; -import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener; -import org.osmdroid.views.overlay.ItemizedOverlayWithFocus; -import org.osmdroid.views.overlay.MapEventsOverlay; -import org.osmdroid.views.overlay.Overlay; -import org.osmdroid.views.overlay.OverlayItem; -import org.osmdroid.views.overlay.ScaleBarOverlay; -import org.osmdroid.views.overlay.ScaleDiskOverlay; -import org.osmdroid.views.overlay.TilesOverlay; -import timber.log.Timber; - -public class ExploreMapFragment extends CommonsDaggerSupportFragment - implements ExploreMapContract.View, LocationUpdateListener, LocationPermissionCallback { - - private BottomSheetBehavior bottomSheetDetailsBehavior; - private BroadcastReceiver broadcastReceiver; - private boolean isNetworkErrorOccurred; - private Snackbar snackbar; - private boolean isDarkTheme; - private boolean isPermissionDenied; - private fr.free.nrw.commons.location.LatLng lastKnownLocation; // last location of user - private fr.free.nrw.commons.location.LatLng lastFocusLocation; // last location that map is focused - public List mediaList; - private boolean recenterToUserLocation; // true is recenter is needed (ie. when current location is in visible map boundaries) - private BaseMarker clickedMarker; - private GeoPoint mapCenter; - private GeoPoint lastMapFocus; - IntentFilter intentFilter = new IntentFilter(MapUtils.NETWORK_INTENT_ACTION); - private Map baseMarkerOverlayMap; - - @Inject - LiveDataConverter liveDataConverter; - @Inject - MediaClient mediaClient; - @Inject - LocationServiceManager locationManager; - @Inject - ExploreMapController exploreMapController; - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - @Inject - BookmarkLocationsDao bookmarkLocationDao; // May be needed in future if we want to integrate bookmarking explore places - @Inject - SystemThemeUtils systemThemeUtils; - LocationPermissionsHelper locationPermissionsHelper; - - // Nearby map state (if we came from Nearby) - private double prevZoom; - private double prevLatitude; - private double prevLongitude; - private boolean recentlyCameFromNearbyMap; - - private ExploreMapPresenter presenter; - - public FragmentExploreMapBinding binding; - - private ActivityResultLauncher activityResultLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - locationPermissionGranted(); - } else { - if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - DialogUtil.showAlertDialog(getActivity(), - getActivity().getString(R.string.location_permission_title), - getActivity().getString(R.string.location_permission_rationale_explore), - getActivity().getString(android.R.string.ok), - getActivity().getString(android.R.string.cancel), - () -> { - askForLocationPermission(); - }, - null, - null - ); - } else { - if (isPermissionDenied) { - locationPermissionsHelper.showAppSettingsDialog(getActivity(), - R.string.explore_map_needs_location); - } - Timber.d("The user checked 'Don't ask again' or denied the permission twice"); - isPermissionDenied = true; - } - } - }); - - @NonNull - public static ExploreMapFragment newInstance() { - ExploreMapFragment fragment = new ExploreMapFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState - ) { - loadNearbyMapData(); - binding = FragmentExploreMapBinding.inflate(getLayoutInflater()); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setSearchThisAreaButtonVisibility(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.tvAttribution.setText( - Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY)); - } else { - binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); - } - initNetworkBroadCastReceiver(); - locationPermissionsHelper = new LocationPermissionsHelper(getActivity(), locationManager, - this); - if (presenter == null) { - presenter = new ExploreMapPresenter(bookmarkLocationDao); - } - setHasOptionsMenu(true); - - isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - isPermissionDenied = false; - presenter.attachView(this); - - initViews(); - presenter.setActionListeners(applicationKvStore); - - org.osmdroid.config.Configuration.getInstance().load(this.getContext(), - PreferenceManager.getDefaultSharedPreferences(this.getContext())); - - binding.mapView.setTileSource(TileSourceFactory.WIKIMEDIA); - binding.mapView.setTilesScaledToDpi(true); - - org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put( - "Referer", "http://maps.wikimedia.org/" - ); - - ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView); - scaleBarOverlay.setScaleBarOffset(15, 25); - Paint barPaint = new Paint(); - barPaint.setARGB(200, 255, 250, 250); - scaleBarOverlay.setBackgroundPaint(barPaint); - scaleBarOverlay.enableScaleBar(); - binding.mapView.getOverlays().add(scaleBarOverlay); - binding.mapView.getZoomController() - .setVisibility(CustomZoomButtonsController.Visibility.NEVER); - binding.mapView.setMultiTouchControls(true); - - if (!isCameFromNearbyMap()) { - binding.mapView.getController().setZoom(ZOOM_LEVEL); - } - - - binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { - @Override - public boolean singleTapConfirmedHelper(GeoPoint p) { - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - binding.mapView.invalidate(); - } else { - Timber.e("CLICKED MARKER IS NULL"); - } - if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { - // Back should first hide the bottom sheet if it is expanded - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } else if (isDetailsBottomSheetVisible()) { - hideBottomDetailsSheet(); - } - return true; - } - - @Override - public boolean longPressHelper(GeoPoint p) { - return false; - } - })); - - binding.mapView.addMapListener(new MapListener() { - @Override - public boolean onScroll(ScrollEvent event) { - if (getLastMapFocus() != null) { - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(binding.mapView.getMapCenter().getLatitude()); - dest_location.setLongitude(binding.mapView.getMapCenter().getLongitude()); - mylocation.setLatitude(getLastMapFocus().getLatitude()); - mylocation.setLongitude(getLastMapFocus().getLongitude()); - Float distance = mylocation.distanceTo(dest_location);//in meters - if (getLastMapFocus() != null) { - if (isNetworkConnectionEstablished() && (event.getX() > 0 - || event.getY() > 0)) { - if (distance > 2000.0) { - setSearchThisAreaButtonVisibility(true); - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } else { - setSearchThisAreaButtonVisibility(false); - } - } - - return true; - } - - @Override - public boolean onZoom(ZoomEvent event) { - return false; - } - - }); - if (!locationPermissionsHelper.checkLocationPermission(getActivity())) { - askForLocationPermission(); - } - } - - @Override - public void onResume() { - super.onResume(); - binding.mapView.onResume(); - presenter.attachView(this); - registerNetworkReceiver(); - if (isResumed()) { - if (locationPermissionsHelper.checkLocationPermission(getActivity())) { - performMapReadyActions(); - } else { - startMapWithoutPermission(); - } - } - } - - @Override - public void onPause() { - super.onPause(); - // unregistering the broadcastReceiver, as it was causing an exception and a potential crash - unregisterNetworkReceiver(); - } - - - /** - * Unregisters the networkReceiver - */ - private void unregisterNetworkReceiver() { - if (getActivity() != null) { - getActivity().unregisterReceiver(broadcastReceiver); - } - } - - private void startMapWithoutPermission() { - lastKnownLocation = MapUtils.getDefaultLatLng(); - moveCameraToPosition( - new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); - presenter.onMapReady(exploreMapController); - } - - private void registerNetworkReceiver() { - if (getActivity() != null) { - getActivity().registerReceiver(broadcastReceiver, intentFilter); - } - } - - private void performMapReadyActions() { - if (isDarkTheme) { - binding.mapView.getOverlayManager().getTilesOverlay() - .setColorFilter(TilesOverlay.INVERT_COLORS); - } - if (applicationKvStore.getBoolean("doNotAskForLocationPermission", false) && - !locationPermissionsHelper.checkLocationPermission(getActivity())) { - isPermissionDenied = true; - } - - lastKnownLocation = getLastLocation(); - - if (lastKnownLocation == null) { - lastKnownLocation = MapUtils.getDefaultLatLng(); - } - - // if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom - if (isCameFromNearbyMap()) { - moveCameraToPosition( - new GeoPoint(prevLatitude, prevLongitude), - prevZoom, - 1L - ); - } else { - moveCameraToPosition( - new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); - } - presenter.onMapReady(exploreMapController); - } - - /** - * Fetch Nearby map camera data from fragment arguments if any. - */ - public void loadNearbyMapData() { - // get fragment arguments - if (getArguments() != null) { - prevZoom = getArguments().getDouble("prev_zoom"); - prevLatitude = getArguments().getDouble("prev_latitude"); - prevLongitude = getArguments().getDouble("prev_longitude"); - } - - setRecentlyCameFromNearbyMap(isCameFromNearbyMap()); - } - - /** - * @return The LatLng from the previous Fragment's map center or (0,0,0) coordinates - * if that information is not available/applicable. - */ - public LatLng getPreviousLatLng() { - return new LatLng(prevLatitude, prevLongitude, (float)prevZoom); - } - - /** - * Checks if fragment arguments contain data from Nearby map, indicating that the user navigated - * from Nearby using 'Show in Explore'. - * - * @return true if user navigated from Nearby map - **/ - public boolean isCameFromNearbyMap() { - return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0; - } - - /** - * Gets the value that indicates if the user navigated from "Show in Explore" in Nearby and - * that the LatLng from Nearby has yet to be searched for map markers. - */ - public boolean recentlyCameFromNearbyMap() { - return recentlyCameFromNearbyMap; - } - - /** - * Sets the value that indicates if the user navigated from "Show in Explore" in Nearby and - * that the LatLng from Nearby has yet to be searched for map markers. - * @param newValue The value to set. - */ - public void setRecentlyCameFromNearbyMap(boolean newValue) { - recentlyCameFromNearbyMap = newValue; - } - - public void loadNearbyMapFromExplore() { - ((MainActivity) getContext()).loadNearbyMapFromExplore( - binding.mapView.getZoomLevelDouble(), - binding.mapView.getMapCenter().getLatitude(), - binding.mapView.getMapCenter().getLongitude() - ); - } - - private void initViews() { - Timber.d("init views called"); - initBottomSheets(); - setBottomSheetCallbacks(); - } - - /** - * a) Creates bottom sheet behaviours from bottom sheet, sets initial states and visibility - * b) Gets the touch event on the map to perform following actions: - * if bottom sheet details are expanded or collapsed hide the bottom sheet details. - */ - @SuppressLint("ClickableViewAccessibility") - private void initBottomSheets() { - bottomSheetDetailsBehavior = BottomSheetBehavior.from( - binding.bottomSheetDetailsBinding.getRoot()); - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - binding.bottomSheetDetailsBinding.getRoot().setVisibility(View.VISIBLE); - } - - /** - * Defines how bottom sheets will act on click - */ - private void setBottomSheetCallbacks() { - binding.bottomSheetDetailsBinding.getRoot().setOnClickListener(v -> { - if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); - } else if (bottomSheetDetailsBehavior.getState() - == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } - }); - } - - @Override - public void onLocationChangedSignificantly(LatLng latLng) { - Timber.d("Location significantly changed"); - if (latLng != null) { - handleLocationUpdate(latLng, LOCATION_SIGNIFICANTLY_CHANGED); - } - } - - @Override - public void onLocationChangedSlightly(LatLng latLng) { - Timber.d("Location slightly changed"); - if (latLng != null) {//If the map has never ever shown the current location, lets do it know - handleLocationUpdate(latLng, LOCATION_SLIGHTLY_CHANGED); - } - } - - private void handleLocationUpdate(final fr.free.nrw.commons.location.LatLng latLng, - final LocationServiceManager.LocationChangeType locationChangeType) { - lastKnownLocation = latLng; - exploreMapController.currentLocation = lastKnownLocation; - presenter.updateMap(locationChangeType); - } - - @Override - public void onLocationChangedMedium(LatLng latLng) { - - } - - @Override - public boolean isNetworkConnectionEstablished() { - return NetworkUtils.isInternetConnectionEstablished(getActivity()); - } - - @Override - public void populatePlaces(LatLng currentLatLng) { - final Observable nearbyPlacesInfoObservable; - if (currentLatLng == null) { - return; - } - if (currentLatLng.equals( - getLastMapFocus())) { // Means we are checking around current location - nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(currentLatLng, - getLastMapFocus(), true); - } else { - nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(getLastMapFocus(), - currentLatLng, false); - } - getCompositeDisposable().add(nearbyPlacesInfoObservable - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(explorePlacesInfo -> { - mediaList = explorePlacesInfo.mediaList; - if (mediaList == null) { - showResponseMessage(getString(R.string.no_pictures_in_this_area)); - } - updateMapMarkers(explorePlacesInfo); - lastMapFocus = new GeoPoint(currentLatLng.getLatitude(), - currentLatLng.getLongitude()); - }, - throwable -> { - Timber.d(throwable); - // Not showing the user, throwable localizedErrorMessage - showErrorMessage(getString(R.string.error_fetching_nearby_places)); - - setProgressBarVisibility(false); - presenter.lockUnlockNearby(false); - })); - if (recenterToUserLocation) { - recenterToUserLocation = false; - } - } - - /** - * Updates map markers according to latest situation - * - * @param explorePlacesInfo holds several information as current location, marker list etc. - */ - private void updateMapMarkers(final MapController.ExplorePlacesInfo explorePlacesInfo) { - presenter.updateMapMarkers(explorePlacesInfo); - } - - private void showErrorMessage(final String message) { - ViewUtil.showLongToast(getActivity(), message); - } - - private void showResponseMessage(final String message) { - ViewUtil.showLongSnackbar(getView(), message); - } - - @Override - public void askForLocationPermission() { - Timber.d("Asking for location permission"); - activityResultLauncher.launch(permission.ACCESS_FINE_LOCATION); - } - - private void locationPermissionGranted() { - isPermissionDenied = false; - applicationKvStore.putBoolean("doNotAskForLocationPermission", false); - lastKnownLocation = locationManager.getLastLocation(); - fr.free.nrw.commons.location.LatLng target = lastKnownLocation; - if (lastKnownLocation != null) { - GeoPoint targetP = new GeoPoint(target.getLatitude(), target.getLongitude()); - mapCenter = targetP; - binding.mapView.getController().setCenter(targetP); - recenterMarkerToPosition(targetP); - moveCameraToPosition(targetP); - } else if (locationManager.isGPSProviderEnabled() - || locationManager.isNetworkProviderEnabled()) { - locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER); - locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); - setProgressBarVisibility(true); - } else { - locationPermissionsHelper.showLocationOffDialog(getActivity(), - R.string.ask_to_turn_location_on_text); - } - presenter.onMapReady(exploreMapController); - registerUnregisterLocationListener(false); - } - - public void registerUnregisterLocationListener(final boolean removeLocationListener) { - MapUtils.registerUnregisterLocationListener(removeLocationListener, locationManager, this); - } - - @Override - public void recenterMap(LatLng currentLatLng) { - // if user has denied permission twice, then show dialog - if (isPermissionDenied) { - if (locationPermissionsHelper.checkLocationPermission(getActivity())) { - // this will run when user has given permission by opening app's settings - isPermissionDenied = false; - recenterMap(currentLatLng); - } else { - askForLocationPermission(); - } - } else { - if (!locationPermissionsHelper.checkLocationPermission(getActivity())) { - askForLocationPermission(); - } else { - locationPermissionGranted(); - } - } - if (currentLatLng == null) { - recenterToUserLocation = true; - return; - } - recenterMarkerToPosition( - new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude())); - binding.mapView.getController() - .animateTo(new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude())); - if (lastMapFocus != null) { - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(binding.mapView.getMapCenter().getLatitude()); - dest_location.setLongitude(binding.mapView.getMapCenter().getLongitude()); - mylocation.setLatitude(lastMapFocus.getLatitude()); - mylocation.setLongitude(lastMapFocus.getLongitude()); - Float distance = mylocation.distanceTo(dest_location);//in meters - if (lastMapFocus != null) { - if (isNetworkConnectionEstablished()) { - if (distance > 2000.0) { - setSearchThisAreaButtonVisibility(true); - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } - - @Override - public void hideBottomDetailsSheet() { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } - - /** - * Same bottom sheet carries information for all nearby places, so we need to pass information - * (title, description, distance and links) to view on nearby marker click - * - * @param place Place of clicked nearby marker - */ - private void passInfoToSheet(final Place place) { - binding.bottomSheetDetailsBinding.directionsButton.setOnClickListener( - view -> handleGeoCoordinates(requireActivity(), - place.getLocation(), binding.mapView.getZoomLevelDouble())); - - binding.bottomSheetDetailsBinding.commonsButton.setVisibility( - place.hasCommonsLink() ? View.VISIBLE : View.GONE); - binding.bottomSheetDetailsBinding.commonsButton.setOnClickListener( - view -> handleWebUrl(getContext(), place.siteLinks.getCommonsLink())); - - int index = 0; - for (Media media : mediaList) { - if (media.getFilename().equals(place.name)) { - int finalIndex = index; - binding.bottomSheetDetailsBinding.mediaDetailsButton.setOnClickListener(view -> { - ((ExploreMapRootFragment) getParentFragment()).onMediaClicked(finalIndex); - }); - } - index++; - } - binding.bottomSheetDetailsBinding.title.setText( - place.name.substring(5, place.name.lastIndexOf("."))); - binding.bottomSheetDetailsBinding.category.setText(place.distance); - // Remove label since it is double information - String descriptionText = place.getLongDescription() - .replace(place.getName() + " (", ""); - descriptionText = (descriptionText.equals(place.getLongDescription()) ? descriptionText - : descriptionText.replaceFirst(".$", "")); - // Set the short description after we remove place name from long description - binding.bottomSheetDetailsBinding.description.setText(descriptionText); - } - - @Override - public void addSearchThisAreaButtonAction() { - binding.searchThisAreaButton.setOnClickListener(presenter.onSearchThisAreaClicked()); - } - - @Override - public void setSearchThisAreaButtonVisibility(boolean isVisible) { - binding.searchThisAreaButton.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - - @Override - public void setProgressBarVisibility(boolean isVisible) { - binding.mapProgressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - - @Override - public boolean isDetailsBottomSheetVisible() { - if (binding.bottomSheetDetailsBinding.getRoot().getVisibility() == View.VISIBLE) { - return true; - } else { - return false; - } - } - - @Override - public boolean isSearchThisAreaButtonVisible() { - return binding.bottomSheetDetailsBinding.getRoot().getVisibility() == View.VISIBLE; - } - - @Override - public LatLng getLastLocation() { - if (lastKnownLocation == null) { - lastKnownLocation = locationManager.getLastLocation(); - } - return lastKnownLocation; - } - - @Override - public void disableFABRecenter() { - binding.fabRecenter.setEnabled(false); - } - - @Override - public void enableFABRecenter() { - binding.fabRecenter.setEnabled(true); - } - - /** - * Adds a markers to the map based on the list of NearbyBaseMarker. - * - * @param nearbyBaseMarkers The NearbyBaseMarker object representing the markers to be added. - */ - @Override - public void addMarkersToMap(List nearbyBaseMarkers) { - clearAllMarkers(); - for (int i = 0; i < nearbyBaseMarkers.size(); i++) { - addMarkerToMap(nearbyBaseMarkers.get(i)); - } - binding.mapView.invalidate(); - } - - /** - * Adds a marker to the map based on the specified NearbyBaseMarker. - * - * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added. - */ - private void addMarkerToMap(BaseMarker nearbyBaseMarker) { - if (isAttachedToActivity()) { - ArrayList items = new ArrayList<>(); - Bitmap icon = nearbyBaseMarker.getIcon(); - Drawable d = new BitmapDrawable(getResources(), icon); - GeoPoint point = new GeoPoint( - nearbyBaseMarker.getPlace().location.getLatitude(), - nearbyBaseMarker.getPlace().location.getLongitude()); - - Media markerMedia = this.getMediaFromImageURL(nearbyBaseMarker.getPlace().pic); - String authorUser = null; - if (markerMedia != null) { - authorUser = markerMedia.getAuthorOrUser(); - // HTML text is sometimes part of the author string and needs to be removed - authorUser = Html.fromHtml(authorUser, Html.FROM_HTML_MODE_LEGACY).toString(); - } - - String title = nearbyBaseMarker.getPlace().name; - // Remove "File:" if present at start - if (title.startsWith("File:")) { - title = title.substring(5); - } - // Remove extensions like .jpg, .jpeg, .png, .svg (case insensitive) - title = title.replaceAll("(?i)\\.(jpg|jpeg|png|svg)$", ""); - title = title.replace("_", " "); - //Truncate if too long because it doesn't fit the screen - if (title.length() > 43) { - title = title.substring(0, 40) + "…"; - } - - OverlayItem item = new OverlayItem(title, authorUser, point); - item.setMarker(d); - items.add(item); - ItemizedOverlayWithFocus overlay = new ItemizedOverlayWithFocus(items, - new OnItemGestureListener() { - @Override - public boolean onItemSingleTapUp(int index, OverlayItem item) { - final Place place = nearbyBaseMarker.getPlace(); - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - bottomSheetDetailsBehavior.setState( - BottomSheetBehavior.STATE_COLLAPSED); - } - clickedMarker = nearbyBaseMarker; - passInfoToSheet(place); - - //Move the overlay to the top so it can be fully seen. - moveOverlayToTop(getOverlay(item)); - return true; - } - - @Override - public boolean onItemLongPress(int index, OverlayItem item) { - return false; - } - }, getContext()); - - if (this.baseMarkerOverlayMap == null) { - this.baseMarkerOverlayMap = new HashMap<>(); - } - this.baseMarkerOverlayMap.put(nearbyBaseMarker, overlay); - - overlay.setFocusItemsOnTap(true); - binding.mapView.getOverlays().add(overlay); // Add the overlay to the map - } - } - - /** - * Moves the specified Overlay above all other Overlays. This prevents other Overlays from - * obstructing it. Upon failure, this method returns early. - * @param overlay The Overlay to move. - */ - private void moveOverlayToTop (Overlay overlay) { - if (overlay == null || binding == null || binding.mapView.getOverlays() == null) { - return; - } - - boolean successfulRemoval = binding.mapView.getOverlays().remove(overlay); - if (!successfulRemoval) { - return; - } - - binding.mapView.getOverlays().add(overlay); - } - - /** - * Performs a linear search for the first Overlay which contains the specified OverlayItem. - * - * @param item The OverlayItem contained within the first target Overlay. - * @return The first Overlay which contains the specified OverlayItem or null if the Overlay - * could not be found. - */ - private Overlay getOverlay (OverlayItem item) { - if (item == null || binding == null || binding.mapView.getOverlays() == null) { - return null; - } - - for (int i = 0; i < binding.mapView.getOverlays().size(); i++) { - if (binding.mapView.getOverlays().get(i) instanceof ItemizedOverlayWithFocus) { - ItemizedOverlayWithFocus overlay = - (ItemizedOverlayWithFocus)binding.mapView.getOverlays().get(i); - - for (int j = 0; j < overlay.size(); j++) { - if (overlay.getItem(j) == item) { - return overlay; - } - } - } - } - - return null; - } - - /** - * Retrieves the specific Media object from the mediaList field. - * @param url The specific Media's image URL. - * @return The Media object that matches the URL or null if it could not be found. - */ - private Media getMediaFromImageURL(String url) { - if (mediaList == null || url == null) { - return null; - } - - for (int i = 0; i < mediaList.size(); i++) { - if (mediaList.get(i) != null && mediaList.get(i).getImageUrl() != null - && mediaList.get(i).getImageUrl().equals(url)) { - return mediaList.get(i); - } - } - - return null; - } - - /** - * Removes a marker from the map based on the specified NearbyBaseMarker. - * - * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be removed. - */ - private void removeMarker(BaseMarker nearbyBaseMarker) { - if (nearbyBaseMarker == null || nearbyBaseMarker.getPlace().getName() == null || - baseMarkerOverlayMap == null || !baseMarkerOverlayMap.containsKey(nearbyBaseMarker)) { - return; - } - - Overlay target = baseMarkerOverlayMap.get(nearbyBaseMarker); - List overlays = binding.mapView.getOverlays(); - - for (int i = 0; i < overlays.size(); i++) { - Overlay overlay = overlays.get(i); - - if (overlay.equals(target)) { - binding.mapView.getOverlays().remove(i); - binding.mapView.invalidate(); - baseMarkerOverlayMap.remove(nearbyBaseMarker); - break; - } - } - } - - /** - * Clears all markers from the map and resets certain map overlays and gestures. After clearing - * markers, it re-adds a scale bar overlay and rotation gesture overlay to the map. - */ - @Override - public void clearAllMarkers() { - if (isAttachedToActivity()) { - binding.mapView.getOverlayManager().clear(); - GeoPoint geoPoint = mapCenter; - if (geoPoint != null) { - List overlays = binding.mapView.getOverlays(); - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this.getContext(), - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - binding.mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - binding.mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this.getContext(), - R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - binding.mapView.getOverlays().add(startMarker); - } - ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView); - scaleBarOverlay.setScaleBarOffset(15, 25); - Paint barPaint = new Paint(); - barPaint.setARGB(200, 255, 250, 250); - scaleBarOverlay.setBackgroundPaint(barPaint); - scaleBarOverlay.enableScaleBar(); - binding.mapView.getOverlays().add(scaleBarOverlay); - binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { - @Override - public boolean singleTapConfirmedHelper(GeoPoint p) { - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - binding.mapView.invalidate(); - } else { - Timber.e("CLICKED MARKER IS NULL"); - } - if (bottomSheetDetailsBehavior.getState() - == BottomSheetBehavior.STATE_EXPANDED) { - // Back should first hide the bottom sheet if it is expanded - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } else if (isDetailsBottomSheetVisible()) { - hideBottomDetailsSheet(); - } - return true; - } - - @Override - public boolean longPressHelper(GeoPoint p) { - return false; - } - })); - binding.mapView.setMultiTouchControls(true); - } - } - - /** - * Recenters the map view to the specified GeoPoint and updates the marker to indicate the new - * position. - * - * @param geoPoint The GeoPoint representing the new center position for the map. - */ - private void recenterMarkerToPosition(GeoPoint geoPoint) { - if (geoPoint != null) { - binding.mapView.getController().setCenter(geoPoint); - List overlays = binding.mapView.getOverlays(); - for (int i = 0; i < overlays.size(); i++) { - if (overlays.get(i) instanceof org.osmdroid.views.overlay.Marker) { - binding.mapView.getOverlays().remove(i); - } else if (overlays.get(i) instanceof ScaleDiskOverlay) { - binding.mapView.getOverlays().remove(i); - } - } - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this.getContext(), - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - binding.mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - binding.mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this.getContext(), R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - binding.mapView.getOverlays().add(startMarker); - } - } - - /** - * Moves the camera of the map view to the specified GeoPoint using an animation. - * - * @param geoPoint The GeoPoint representing the new camera position for the map. - */ - private void moveCameraToPosition(GeoPoint geoPoint) { - binding.mapView.getController().animateTo(geoPoint); - } - - /** - * Moves the camera of the map view to the specified GeoPoint at specified zoom level and speed - * using an animation. - * - * @param geoPoint The GeoPoint representing the new camera position for the map. - * @param zoom Zoom level of the map camera - * @param speed Speed of animation - */ - private void moveCameraToPosition(GeoPoint geoPoint, double zoom, long speed) { - binding.mapView.getController().animateTo(geoPoint, zoom, speed); - } - - @Override - public fr.free.nrw.commons.location.LatLng getLastMapFocus() { - return lastMapFocus == null ? getMapCenter() : new fr.free.nrw.commons.location.LatLng( - lastMapFocus.getLatitude(), lastMapFocus.getLongitude(), 100); - } - - @Override - public fr.free.nrw.commons.location.LatLng getMapCenter() { - fr.free.nrw.commons.location.LatLng latLnge = null; - if (mapCenter != null) { - latLnge = new fr.free.nrw.commons.location.LatLng( - mapCenter.getLatitude(), mapCenter.getLongitude(), 100); - } else { - if (applicationKvStore.getString("LastLocation") != null) { - final String[] locationLatLng - = applicationKvStore.getString("LastLocation").split(","); - lastKnownLocation - = new fr.free.nrw.commons.location.LatLng(Double.parseDouble(locationLatLng[0]), - Double.parseDouble(locationLatLng[1]), 1f); - latLnge = lastKnownLocation; - } else { - latLnge = new fr.free.nrw.commons.location.LatLng(51.506255446947776, - -0.07483536015053005, 1f); - } - } - return latLnge; - } - - @Override - public fr.free.nrw.commons.location.LatLng getMapFocus() { - fr.free.nrw.commons.location.LatLng mapFocusedLatLng = new fr.free.nrw.commons.location.LatLng( - binding.mapView.getMapCenter().getLatitude(), - binding.mapView.getMapCenter().getLongitude(), 100); - return mapFocusedLatLng; - } - - @Override - public void setFABRecenterAction(OnClickListener onClickListener) { - binding.fabRecenter.setOnClickListener(onClickListener); - } - - @Override - public boolean backButtonClicked() { - if (!(bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN)) { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - return true; - } else { - return false; - } - } - - /** - * Adds network broadcast receiver to recognize connection established - */ - private void initNetworkBroadCastReceiver() { - broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - if (getActivity() != null) { - if (NetworkUtils.isInternetConnectionEstablished(getActivity())) { - if (isNetworkErrorOccurred) { - presenter.updateMap(LOCATION_SIGNIFICANTLY_CHANGED); - isNetworkErrorOccurred = false; - } - - if (snackbar != null) { - snackbar.dismiss(); - snackbar = null; - } - } else { - if (snackbar == null) { - snackbar = Snackbar.make(getView(), R.string.no_internet, - Snackbar.LENGTH_INDEFINITE); - setSearchThisAreaButtonVisibility(false); - setProgressBarVisibility(false); - } - - isNetworkErrorOccurred = true; - snackbar.show(); - } - } - } - }; - } - - /** - * helper function to confirm that this fragment has been attached. - **/ - public boolean isAttachedToActivity() { - boolean attached = isVisible() && getActivity() != null; - return attached; - } - - @Override - public void onLocationPermissionDenied(String toastMessage) { - } - - @Override - public void onLocationPermissionGranted() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt new file mode 100644 index 000000000..e64f12db3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt @@ -0,0 +1,1091 @@ +@file:Suppress("DEPRECATION") + +package fr.free.nrw.commons.explore.map + +import android.Manifest.permission +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Color +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.location.Location +import android.location.LocationManager +import android.os.Build +import android.os.Bundle +import android.preference.PreferenceManager +import android.text.Html +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.isVisible +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.MapController.ExplorePlacesInfo +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentExploreMapBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.ExploreMapRootFragment +import fr.free.nrw.commons.explore.paging.LiveDataConverter +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationPermissionsHelper +import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback +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.nearby.Place +import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog +import fr.free.nrw.commons.utils.MapUtils +import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL +import fr.free.nrw.commons.utils.MapUtils.defaultLatLng +import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished +import fr.free.nrw.commons.utils.SystemThemeUtils +import fr.free.nrw.commons.utils.ViewUtil.showLongSnackbar +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import fr.free.nrw.commons.utils.handleGeoCoordinates +import fr.free.nrw.commons.utils.handleWebUrl +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import org.osmdroid.config.Configuration +import org.osmdroid.events.MapEventsReceiver +import org.osmdroid.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.util.constants.GeoConstants +import org.osmdroid.views.CustomZoomButtonsController +import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener +import org.osmdroid.views.overlay.ItemizedOverlayWithFocus +import org.osmdroid.views.overlay.MapEventsOverlay +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.Overlay +import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.ScaleBarOverlay +import org.osmdroid.views.overlay.ScaleDiskOverlay +import org.osmdroid.views.overlay.TilesOverlay +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Named + +class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.View, + LocationUpdateListener, LocationPermissionCallback { + private var bottomSheetDetailsBehavior: BottomSheetBehavior<*>? = null + private var broadcastReceiver: BroadcastReceiver? = null + private var isNetworkErrorOccurred = false + private var snackbar: Snackbar? = null + private var isDarkTheme = false + private var isPermissionDenied = false + private var lastKnownLocation: LatLng? = null // last location of user + private var recenterToUserLocation = false // true is recenter is needed (ie. when current location is in visible map boundaries) + private var clickedMarker: BaseMarker? = null + private var mapCenter: GeoPoint? = null + private var lastMapFocus: GeoPoint? = null + private var intentFilter: IntentFilter = IntentFilter(MapUtils.NETWORK_INTENT_ACTION) + private var baseMarkerOverlayMap: MutableMap? = null + private var locationPermissionsHelper: LocationPermissionsHelper? = null + private var prevZoom = 0.0 + private var prevLatitude = 0.0 + private var prevLongitude = 0.0 + private var recentlyCameFromNearbyMap = false + private var presenter: ExploreMapPresenter? = null + private var binding: FragmentExploreMapBinding? = null + var mediaList: MutableList? = null + private set + + @Inject + lateinit var liveDataConverter: LiveDataConverter + + @Inject + lateinit var mediaClient: MediaClient + + @Inject + lateinit var locationManager: LocationServiceManager + + @Inject + lateinit var exploreMapController: ExploreMapController + + @Inject + @Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + @Inject + lateinit var bookmarkLocationDao: BookmarkLocationsDao // May be needed in future if we want to integrate bookmarking explore places + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + private val activityResultLauncher = registerForActivityResult( + RequestPermission() + ) { isGranted: Boolean? -> + if (isGranted == true) { + locationPermissionGranted() + } else { + if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { + showAlertDialog( + requireActivity(), + requireActivity().getString(R.string.location_permission_title), + requireActivity().getString(R.string.location_permission_rationale_explore), + requireActivity().getString(android.R.string.ok), + requireActivity().getString(android.R.string.cancel), + { askForLocationPermission() }, + null, + null + ) + } else { + if (isPermissionDenied) { + locationPermissionsHelper!!.showAppSettingsDialog( + requireActivity(), + R.string.explore_map_needs_location + ) + } + Timber.d("The user checked 'Don't ask again' or denied the permission twice") + isPermissionDenied = true + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + loadNearbyMapData() + binding = FragmentExploreMapBinding.inflate(getLayoutInflater()) + return binding!!.getRoot() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setSearchThisAreaButtonVisibility(false) + binding!!.tvAttribution.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY) + } else { + Html.fromHtml(getString(R.string.map_attribution)) + } + initNetworkBroadCastReceiver() + locationPermissionsHelper = LocationPermissionsHelper( + requireActivity(), locationManager, + this + ) + if (presenter == null) { + presenter = ExploreMapPresenter(bookmarkLocationDao) + } + setHasOptionsMenu(true) + + isDarkTheme = systemThemeUtils.isDeviceInNightMode() + isPermissionDenied = false + presenter!!.attachView(this) + + initViews() + presenter!!.setActionListeners(applicationKvStore) + + Configuration.getInstance().load( + requireContext(), + PreferenceManager.getDefaultSharedPreferences(requireContext()) + ) + + binding!!.mapView.setTileSource(TileSourceFactory.WIKIMEDIA) + binding!!.mapView.setTilesScaledToDpi(true) + + Configuration.getInstance().additionalHttpRequestProperties.put( + "Referer", "http://maps.wikimedia.org/" + ) + + val scaleBarOverlay = ScaleBarOverlay(binding!!.mapView) + scaleBarOverlay.setScaleBarOffset(15, 25) + val barPaint = Paint() + barPaint.setARGB(200, 255, 250, 250) + scaleBarOverlay.setBackgroundPaint(barPaint) + scaleBarOverlay.enableScaleBar() + binding!!.mapView.overlays.add(scaleBarOverlay) + binding!!.mapView.zoomController + .setVisibility(CustomZoomButtonsController.Visibility.NEVER) + binding!!.mapView.setMultiTouchControls(true) + + if (!isCameFromNearbyMap) { + binding!!.mapView.controller.setZoom(ZOOM_LEVEL.toDouble()) + } + + + binding!!.mapView.overlays.add(MapEventsOverlay(object : MapEventsReceiver { + override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean { + if (clickedMarker != null) { + removeMarker(clickedMarker) + addMarkerToMap(clickedMarker!!) + binding!!.mapView.invalidate() + } else { + Timber.e("CLICKED MARKER IS NULL") + } + if (bottomSheetDetailsBehavior!!.getState() == BottomSheetBehavior.STATE_EXPANDED) { + // Back should first hide the bottom sheet if it is expanded + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + } else if (isDetailsBottomSheetVisible()) { + hideBottomDetailsSheet() + } + return true + } + + override fun longPressHelper(p: GeoPoint?): Boolean = false + })) + + binding!!.mapView.addMapListener(object : MapListener { + override fun onScroll(event: ScrollEvent): Boolean { + if (getLastMapFocus() != null) { + val mylocation = Location("") + val dest_location = Location("") + dest_location.latitude = binding!!.mapView.mapCenter.latitude + dest_location.longitude = binding!!.mapView.mapCenter.longitude + mylocation.latitude = getLastMapFocus()!!.latitude + mylocation.longitude = getLastMapFocus()!!.longitude + val distance = mylocation.distanceTo(dest_location) //in meters + if (getLastMapFocus() != null) { + if (isNetworkConnectionEstablished() && (event.getX() > 0 + || event.getY() > 0) + ) { + setSearchThisAreaButtonVisibility(distance > 2000.0) + } + } else { + setSearchThisAreaButtonVisibility(false) + } + } + + return true + } + + override fun onZoom(event: ZoomEvent?): Boolean = false + }) + if (!locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + askForLocationPermission() + } + } + + override fun onResume() { + super.onResume() + binding!!.mapView.onResume() + presenter!!.attachView(this) + registerNetworkReceiver() + if (isResumed) { + if (locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + performMapReadyActions() + } else { + startMapWithoutPermission() + } + } + } + + override fun onPause() { + super.onPause() + // unregistering the broadcastReceiver, as it was causing an exception and a potential crash + unregisterNetworkReceiver() + } + + + /** + * Unregisters the networkReceiver + */ + private fun unregisterNetworkReceiver() = + activity?.unregisterReceiver(broadcastReceiver) + + private fun startMapWithoutPermission() { + lastKnownLocation = defaultLatLng + moveCameraToPosition( + GeoPoint(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude) + ) + presenter!!.onMapReady(exploreMapController) + } + + private fun registerNetworkReceiver() = + activity?.registerReceiver(broadcastReceiver, intentFilter) + + private fun performMapReadyActions() { + if (isDarkTheme) { + binding!!.mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS) + } + + if (applicationKvStore.getBoolean("doNotAskForLocationPermission", false) && + !locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + isPermissionDenied = true + } + + lastKnownLocation = getLastLocation() + + if (lastKnownLocation == null) { + lastKnownLocation = defaultLatLng + } + + // if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom + if (isCameFromNearbyMap) { + moveCameraToPosition( + GeoPoint(prevLatitude, prevLongitude), + prevZoom, + 1L + ) + } else { + moveCameraToPosition( + GeoPoint(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude) + ) + } + presenter!!.onMapReady(exploreMapController) + } + + /** + * Fetch Nearby map camera data from fragment arguments if any. + */ + fun loadNearbyMapData() { + // get fragment arguments + if (arguments != null) { + with (requireArguments()) { + prevZoom = getDouble("prev_zoom") + prevLatitude = getDouble("prev_latitude") + prevLongitude = getDouble("prev_longitude") + } + } + + setRecentlyCameFromNearbyMap(isCameFromNearbyMap) + } + + /** + * @return The LatLng from the previous Fragment's map center or (0,0,0) coordinates + * if that information is not available/applicable. + */ + val previousLatLng: LatLng + get() = LatLng(prevLatitude, prevLongitude, prevZoom.toFloat()) + + /** + * Checks if fragment arguments contain data from Nearby map, indicating that the user navigated + * from Nearby using 'Show in Explore'. + * + * @return true if user navigated from Nearby map + */ + val isCameFromNearbyMap: Boolean + get() = prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0 + + /** + * Gets the value that indicates if the user navigated from "Show in Explore" in Nearby and + * that the LatLng from Nearby has yet to be searched for map markers. + */ + fun recentlyCameFromNearbyMap(): Boolean = + recentlyCameFromNearbyMap + + /** + * Sets the value that indicates if the user navigated from "Show in Explore" in Nearby and + * that the LatLng from Nearby has yet to be searched for map markers. + * @param newValue The value to set. + */ + fun setRecentlyCameFromNearbyMap(newValue: Boolean) { + recentlyCameFromNearbyMap = newValue + } + + fun loadNearbyMapFromExplore() { + (requireContext() as MainActivity).loadNearbyMapFromExplore( + binding!!.mapView.zoomLevelDouble, + binding!!.mapView.mapCenter.latitude, + binding!!.mapView.mapCenter.longitude + ) + } + + private fun initViews() { + initBottomSheets() + setBottomSheetCallbacks() + } + + /** + * a) Creates bottom sheet behaviours from bottom sheet, sets initial states and visibility + * b) Gets the touch event on the map to perform following actions: + * if bottom sheet details are expanded or collapsed hide the bottom sheet details. + */ + @SuppressLint("ClickableViewAccessibility") + private fun initBottomSheets() { + bottomSheetDetailsBehavior = BottomSheetBehavior.from( + binding!!.bottomSheetDetailsBinding.getRoot() + ) + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + binding!!.bottomSheetDetailsBinding.getRoot().visibility = View.VISIBLE + } + + /** + * Defines how bottom sheets will act on click + */ + private fun setBottomSheetCallbacks() { + binding!!.bottomSheetDetailsBinding.getRoot() + .setOnClickListener { v: View? -> + if (bottomSheetDetailsBehavior!!.getState() == BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_EXPANDED) + } else if (bottomSheetDetailsBehavior!!.getState() + == BottomSheetBehavior.STATE_EXPANDED + ) { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_COLLAPSED) + } + } + } + + override fun onLocationChangedSignificantly(latLng: LatLng) = + handleLocationUpdate(latLng, LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + + override fun onLocationChangedSlightly(latLng: LatLng) = + handleLocationUpdate(latLng, LocationChangeType.LOCATION_SLIGHTLY_CHANGED) + + private fun handleLocationUpdate( + latLng: LatLng?, + locationChangeType: LocationChangeType + ) { + lastKnownLocation = latLng + exploreMapController.currentLocation = lastKnownLocation + presenter!!.updateMap(locationChangeType) + } + + override fun onLocationChangedMedium(latLng: LatLng) = Unit + + override fun isNetworkConnectionEstablished(): Boolean = + isInternetConnectionEstablished(requireActivity()) + + override fun populatePlaces(curlatLng: LatLng?) { + val nearbyPlacesInfoObservable: Observable + if (curlatLng == null) { + return + } + if (curlatLng.equals( + getLastMapFocus() + ) + ) { // Means we are checking around current location + nearbyPlacesInfoObservable = presenter!!.loadAttractionsFromLocation( + curlatLng, + getLastMapFocus(), true + ) + } else { + nearbyPlacesInfoObservable = presenter!!.loadAttractionsFromLocation( + getLastMapFocus(), + curlatLng, false + ) + } + compositeDisposable.add( + nearbyPlacesInfoObservable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + Consumer { explorePlacesInfo: ExplorePlacesInfo? -> + mediaList = explorePlacesInfo!!.mediaList.toMutableList() + if (mediaList!!.isEmpty()) { + showResponseMessage(getString(R.string.no_pictures_in_this_area)) + } + updateMapMarkers(explorePlacesInfo) + lastMapFocus = GeoPoint( + curlatLng.latitude, + curlatLng.longitude + ) + }, + Consumer { throwable: Throwable? -> + Timber.d(throwable) + // Not showing the user, throwable localizedErrorMessage + showErrorMessage(getString(R.string.error_fetching_nearby_places)) + + setProgressBarVisibility(false) + presenter!!.lockUnlockNearby(false) + }) + ) + if (recenterToUserLocation) { + recenterToUserLocation = false + } + } + + /** + * Updates map markers according to latest situation + * + * @param explorePlacesInfo holds several information as current location, marker list etc. + */ + private fun updateMapMarkers(explorePlacesInfo: ExplorePlacesInfo) = + presenter!!.updateMapMarkers(explorePlacesInfo) + + private fun showErrorMessage(message: String) = + showLongToast(requireActivity(), message) + + private fun showResponseMessage(message: String) = + showLongSnackbar(requireView(), message) + + override fun askForLocationPermission() { + Timber.d("Asking for location permission") + activityResultLauncher.launch(permission.ACCESS_FINE_LOCATION) + } + + private fun locationPermissionGranted() { + isPermissionDenied = false + applicationKvStore.putBoolean("doNotAskForLocationPermission", false) + lastKnownLocation = locationManager.getLastLocation() + val target = lastKnownLocation + if (lastKnownLocation != null) { + val targetP = GeoPoint(target!!.latitude, target.longitude) + mapCenter = targetP + binding!!.mapView.controller.setCenter(targetP) + recenterMarkerToPosition(targetP) + moveCameraToPosition(targetP) + } else if (locationManager.isGPSProviderEnabled() + || locationManager.isNetworkProviderEnabled() + ) { + locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) + locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) + setProgressBarVisibility(true) + } else { + locationPermissionsHelper!!.showLocationOffDialog( + requireActivity(), + R.string.ask_to_turn_location_on_text + ) + } + presenter!!.onMapReady(exploreMapController) + registerUnregisterLocationListener(false) + } + + fun registerUnregisterLocationListener(removeLocationListener: Boolean) { + MapUtils.registerUnregisterLocationListener(removeLocationListener, locationManager, this) + } + + override fun recenterMap(curLatLng: LatLng?) { + // if user has denied permission twice, then show dialog + if (isPermissionDenied) { + if (locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + // this will run when user has given permission by opening app's settings + isPermissionDenied = false + recenterMap(curLatLng) + } else { + askForLocationPermission() + } + } else { + if (!locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + askForLocationPermission() + } else { + locationPermissionGranted() + } + } + if (curLatLng == null) { + recenterToUserLocation = true + return + } + recenterMarkerToPosition( + GeoPoint(curLatLng.latitude, curLatLng.longitude) + ) + binding!!.mapView.controller.animateTo( + GeoPoint(curLatLng.latitude, curLatLng.longitude) + ) + if (lastMapFocus != null) { + val mylocation = Location("") + val dest_location = Location("") + dest_location.latitude = binding!!.mapView.mapCenter.latitude + dest_location.longitude = binding!!.mapView.mapCenter.longitude + mylocation.latitude = lastMapFocus!!.latitude + mylocation.longitude = lastMapFocus!!.longitude + val distance = mylocation.distanceTo(dest_location) //in meters + if (lastMapFocus != null) { + if (isNetworkConnectionEstablished()) { + setSearchThisAreaButtonVisibility(distance > 2000.0) + } + } else { + setSearchThisAreaButtonVisibility(false) + } + } + } + + override fun hideBottomDetailsSheet() { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + } + + /** + * Same bottom sheet carries information for all nearby places, so we need to pass information + * (title, description, distance and links) to view on nearby marker click + * + * @param place Place of clicked nearby marker + */ + private fun passInfoToSheet(place: Place) { + binding!!.bottomSheetDetailsBinding.directionsButton.setOnClickListener { + handleGeoCoordinates(requireActivity(), place.getLocation(), binding!!.mapView.zoomLevelDouble) + } + + binding!!.bottomSheetDetailsBinding.commonsButton.visibility = if (place.hasCommonsLink()) View.VISIBLE else View.GONE + binding!!.bottomSheetDetailsBinding.commonsButton.setOnClickListener { + handleWebUrl(requireContext(), place.siteLinks.commonsLink) + } + + var index = 0 + for (media in mediaList!!) { + if (media.filename == place.name) { + val finalIndex = index + binding!!.bottomSheetDetailsBinding.mediaDetailsButton.setOnClickListener { + (parentFragment as ExploreMapRootFragment).onMediaClicked(finalIndex) + } + } + index++ + } + binding!!.bottomSheetDetailsBinding.title.text = place.name.substring(5, place.name.lastIndexOf(".")) + binding!!.bottomSheetDetailsBinding.category.text = place.distance + // Remove label since it is double information + var descriptionText = place.longDescription + .replace(place.getName() + " (", "") + descriptionText = (if (descriptionText == place.longDescription) + descriptionText + else + descriptionText.replaceFirst(".$".toRegex(), "")) + // Set the short description after we remove place name from long description + binding!!.bottomSheetDetailsBinding.description.text = descriptionText + } + + override fun addSearchThisAreaButtonAction() { + binding!!.searchThisAreaButton.setOnClickListener(presenter!!.onSearchThisAreaClicked()) + } + + override fun setSearchThisAreaButtonVisibility(isVisible: Boolean) { + binding!!.searchThisAreaButton.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + override fun setProgressBarVisibility(isVisible: Boolean) { + binding!!.mapProgressBar.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + override fun isDetailsBottomSheetVisible(): Boolean = + binding!!.bottomSheetDetailsBinding.getRoot().isVisible + + override fun isSearchThisAreaButtonVisible(): Boolean = + binding!!.bottomSheetDetailsBinding.getRoot().isVisible + + override fun getLastLocation(): LatLng? { + if (lastKnownLocation == null) { + lastKnownLocation = locationManager.getLastLocation() + } + return lastKnownLocation + } + + override fun disableFABRecenter() { + binding!!.fabRecenter.setEnabled(false) + } + + override fun enableFABRecenter() { + binding!!.fabRecenter.setEnabled(true) + } + + /** + * Adds a markers to the map based on the list of NearbyBaseMarker. + * + * @param nearbyBaseMarkers The NearbyBaseMarker object representing the markers to be added. + */ + override fun addMarkersToMap(nearbyBaseMarkers: List?) { + clearAllMarkers() + nearbyBaseMarkers?.forEach { + addMarkerToMap(it!!) + } + binding!!.mapView.invalidate() + } + + /** + * Adds a marker to the map based on the specified NearbyBaseMarker. + * + * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added. + */ + private fun addMarkerToMap(nearbyBaseMarker: BaseMarker) { + if (isAttachedToActivity) { + val items = mutableListOf() + val d: Drawable = nearbyBaseMarker.icon!!.toDrawable(resources) + val point = GeoPoint( + nearbyBaseMarker.place.location.latitude, + nearbyBaseMarker.place.location.longitude + ) + + val markerMedia = getMediaFromImageURL(nearbyBaseMarker.place.pic) + var authorUser: String? = null + if (markerMedia != null) { + // HTML text is sometimes part of the author string and needs to be removed + authorUser = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(markerMedia.getAuthorOrUser(), Html.FROM_HTML_MODE_LEGACY) + } else { + Html.fromHtml(markerMedia.getAuthorOrUser()) + }.toString() + } + + var title = nearbyBaseMarker.place.name + // Remove "File:" if present at start + if (title.startsWith("File:")) { + title = title.substring(5) + } + // Remove extensions like .jpg, .jpeg, .png, .svg (case insensitive) + title = title.replace("(?i)\\.(jpg|jpeg|png|svg)$".toRegex(), "") + title = title.replace("_", " ") + //Truncate if too long because it doesn't fit the screen + if (title.length > 43) { + title = title.substring(0, 40) + "…" + } + + val item = OverlayItem(title, authorUser, point) + item.setMarker(d) + items.add(item) + val overlay = ItemizedOverlayWithFocus( + items, + object : OnItemGestureListener { + override fun onItemSingleTapUp(index: Int, item: OverlayItem?): Boolean { + val place = nearbyBaseMarker.place + if (clickedMarker != null) { + removeMarker(clickedMarker) + addMarkerToMap(clickedMarker!!) + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_COLLAPSED) + } + clickedMarker = nearbyBaseMarker + passInfoToSheet(place) + + //Move the overlay to the top so it can be fully seen. + moveOverlayToTop(getOverlay(item)) + return true + } + + override fun onItemLongPress(index: Int, item: OverlayItem?): Boolean = false + }, requireContext() + ) + + if (baseMarkerOverlayMap == null) { + baseMarkerOverlayMap = HashMap() + } + baseMarkerOverlayMap!!.put(nearbyBaseMarker, overlay) + + overlay.setFocusItemsOnTap(true) + binding!!.mapView.overlays.add(overlay) // Add the overlay to the map + } + } + + /** + * Moves the specified Overlay above all other Overlays. This prevents other Overlays from + * obstructing it. Upon failure, this method returns early. + * @param overlay The Overlay to move. + */ + private fun moveOverlayToTop(overlay: Overlay?) { + if (overlay == null || binding == null || binding!!.mapView.overlays == null) { + return + } + + val successfulRemoval = binding!!.mapView.overlays.remove(overlay) + if (!successfulRemoval) { + return + } + + binding!!.mapView.overlays.add(overlay) + } + + /** + * Performs a linear search for the first Overlay which contains the specified OverlayItem. + * + * @param item The OverlayItem contained within the first target Overlay. + * @return The first Overlay which contains the specified OverlayItem or null if the Overlay + * could not be found. + */ + private fun getOverlay(item: OverlayItem?): Overlay? { + if (item == null || binding == null || binding!!.mapView.overlays == null) { + return null + } + + for (i in binding!!.mapView.overlays.indices) { + if (binding!!.mapView.overlays[i] is ItemizedOverlayWithFocus<*>) { + val overlay = binding!!.mapView.overlays[i] as ItemizedOverlayWithFocus<*> + + for (j in 0.. { + setARGB(200, 255, 250, 250) + }) + enableScaleBar() + }) + + binding!!.mapView.overlays.add(MapEventsOverlay(object : MapEventsReceiver { + override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean { + if (clickedMarker != null) { + removeMarker(clickedMarker) + addMarkerToMap(clickedMarker!!) + binding!!.mapView.invalidate() + } else { + Timber.e("CLICKED MARKER IS NULL") + } + if (bottomSheetDetailsBehavior!!.getState() == BottomSheetBehavior.STATE_EXPANDED) { + // Back should first hide the bottom sheet if it is expanded + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + } else if (isDetailsBottomSheetVisible()) { + hideBottomDetailsSheet() + } + return true + } + + override fun longPressHelper(p: GeoPoint?): Boolean = false + })) + binding!!.mapView.setMultiTouchControls(true) + } + } + + /** + * Recenters the map view to the specified GeoPoint and updates the marker to indicate the new + * position. + * + * @param geoPoint The GeoPoint representing the new center position for the map. + */ + private fun recenterMarkerToPosition(geoPoint: GeoPoint?) { + if (geoPoint != null) { + binding!!.mapView.controller.setCenter(geoPoint) + val overlays = binding!!.mapView.overlays + for (i in overlays.indices) { + if (overlays[i] is Marker) { + binding!!.mapView.overlays.removeAt(i) + } else if (overlays[i] is ScaleDiskOverlay) { + binding!!.mapView.overlays.removeAt(i) + } + } + val diskOverlay = ScaleDiskOverlay( + requireContext(), + geoPoint, 2000, GeoConstants.UnitOfMeasure.foot + ).apply { + setCirclePaint2(Paint().apply { + setColor(Color.rgb(128, 128, 128)) + this.style = Paint.Style.STROKE + this.strokeWidth = 2f + }) + + setCirclePaint1(Paint().apply { + setColor(Color.argb(40, 128, 128, 128)) + this.style = Paint.Style.FILL_AND_STROKE + }) + setDisplaySizeMin(900) + setDisplaySizeMax(1700) + } + binding!!.mapView.overlays.add(diskOverlay) + + val startMarker = Marker( + binding!!.mapView + ).apply { + setPosition(geoPoint) + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + icon = ContextCompat.getDrawable(requireContext(), R.drawable.current_location_marker) + title = "Your Location" + textLabelFontSize = 24 + } + binding!!.mapView.overlays.add(startMarker) + } + } + + /** + * Moves the camera of the map view to the specified GeoPoint using an animation. + * + * @param geoPoint The GeoPoint representing the new camera position for the map. + */ + private fun moveCameraToPosition(geoPoint: GeoPoint?) { + binding!!.mapView.controller.animateTo(geoPoint) + } + + /** + * Moves the camera of the map view to the specified GeoPoint at specified zoom level and speed + * using an animation. + * + * @param geoPoint The GeoPoint representing the new camera position for the map. + * @param zoom Zoom level of the map camera + * @param speed Speed of animation + */ + private fun moveCameraToPosition(geoPoint: GeoPoint?, zoom: Double, speed: Long) { + binding!!.mapView.controller.animateTo(geoPoint, zoom, speed) + } + + override fun getLastMapFocus(): LatLng? = if (lastMapFocus == null) { + getMapCenter() + } else { + LatLng(lastMapFocus!!.latitude, lastMapFocus!!.longitude, 100f) + } + + override fun getMapCenter(): LatLng? = if (mapCenter != null) { + LatLng(mapCenter!!.latitude, mapCenter!!.longitude, 100f) + } else { + if (applicationKvStore.getString("LastLocation") != null) { + val locationLatLng: Array = + applicationKvStore.getString("LastLocation")!! + .split(",".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + lastKnownLocation = LatLng( + locationLatLng[0]!!.toDouble(), + locationLatLng[1]!!.toDouble(), 1f + ) + lastKnownLocation + } else { + LatLng(51.506255446947776, -0.07483536015053005, 1f) + } + } + + override fun getMapFocus(): LatLng? = LatLng( + binding!!.mapView.mapCenter.latitude, + binding!!.mapView.mapCenter.longitude, 100f + ) + + override fun setFABRecenterAction(onClickListener: View.OnClickListener?) { + binding!!.fabRecenter.setOnClickListener(onClickListener) + } + + override fun backButtonClicked(): Boolean { + if (bottomSheetDetailsBehavior!!.getState() != BottomSheetBehavior.STATE_HIDDEN) { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + return true + } else { + return false + } + } + + /** + * Adds network broadcast receiver to recognize connection established + */ + private fun initNetworkBroadCastReceiver() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (activity != null) { + if (isInternetConnectionEstablished(requireActivity())) { + if (isNetworkErrorOccurred) { + presenter!!.updateMap(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + isNetworkErrorOccurred = false + } + + if (snackbar != null) { + snackbar!!.dismiss() + snackbar = null + } + } else { + if (snackbar == null) { + snackbar = Snackbar.make( + requireView(), R.string.no_internet, + Snackbar.LENGTH_INDEFINITE + ) + setSearchThisAreaButtonVisibility(false) + setProgressBarVisibility(false) + } + + isNetworkErrorOccurred = true + snackbar!!.show() + } + } + } + } + } + + val isAttachedToActivity: Boolean + get() = isVisible && activity != null + + override fun onLocationPermissionDenied(toastMessage: String) = Unit + + override fun onLocationPermissionGranted() = Unit + + companion object { + fun newInstance(): ExploreMapFragment { + val fragment = ExploreMapFragment() + fragment.setRetainInstance(true) + return fragment + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java deleted file mode 100644 index 70f785b40..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java +++ /dev/null @@ -1,237 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA; - - -import android.location.Location; -import android.view.View; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.MapController; -import fr.free.nrw.commons.MapController.ExplorePlacesInfo; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; -import fr.free.nrw.commons.nearby.Place; -import io.reactivex.Observable; -import java.lang.reflect.Proxy; -import java.util.List; -import timber.log.Timber; - -public class ExploreMapPresenter - implements ExploreMapContract.UserActions, - NearbyBaseMarkerThumbCallback { - - BookmarkLocationsDao bookmarkLocationDao; - private boolean isNearbyLocked; - private LatLng currentLatLng; - private ExploreMapController exploreMapController; - - private static final ExploreMapContract.View DUMMY = (ExploreMapContract.View) Proxy - .newProxyInstance( - ExploreMapContract.View.class.getClassLoader(), - new Class[]{ExploreMapContract.View.class}, (proxy, method, args) -> { - if (method.getName().equals("onMyEvent")) { - return null; - } else if (String.class == method.getReturnType()) { - return ""; - } else if (Integer.class == method.getReturnType()) { - return Integer.valueOf(0); - } else if (int.class == method.getReturnType()) { - return 0; - } else if (Boolean.class == method.getReturnType()) { - return Boolean.FALSE; - } else if (boolean.class == method.getReturnType()) { - return false; - } else { - return null; - } - } - ); - private ExploreMapContract.View exploreMapFragmentView = DUMMY; - - public ExploreMapPresenter(BookmarkLocationsDao bookmarkLocationDao) { - this.bookmarkLocationDao = bookmarkLocationDao; - } - - @Override - public void updateMap(LocationChangeType locationChangeType) { - Timber.d("Presenter updates map and list" + locationChangeType.toString()); - if (isNearbyLocked) { - Timber.d("Nearby is locked, so updateMapAndList returns"); - return; - } - - if (!exploreMapFragmentView.isNetworkConnectionEstablished()) { - Timber.d("Network connection is not established"); - return; - } - - /** - * Significant changed - Markers and current location will be updated together - * Slightly changed - Only current position marker will be updated - */ - if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)) { - Timber.d("LOCATION_SIGNIFICANTLY_CHANGED"); - LatLng populateLatLng = exploreMapFragmentView.getMapCenter(); - - //If "Show in Explore" was selected in Nearby, use the previous LatLng - if (exploreMapFragmentView instanceof ExploreMapFragment) { - ExploreMapFragment exploreMapFragment = (ExploreMapFragment)exploreMapFragmentView; - if (exploreMapFragment.recentlyCameFromNearbyMap()) { - //Ensure this LatLng will not be used again if user searches their GPS location - exploreMapFragment.setRecentlyCameFromNearbyMap(false); - - populateLatLng = exploreMapFragment.getPreviousLatLng(); - } - } - - lockUnlockNearby(true); - exploreMapFragmentView.setProgressBarVisibility(true); - exploreMapFragmentView.populatePlaces(populateLatLng); - } else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) { - Timber.d("SEARCH_CUSTOM_AREA"); - lockUnlockNearby(true); - exploreMapFragmentView.setProgressBarVisibility(true); - exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapFocus()); - } else { // Means location changed slightly, ie user is walking or driving. - Timber.d("Means location changed slightly"); - } - } - - /** - * Nearby updates takes time, since they are network operations. During update time, we don't - * want to get any other calls from user. So locking nearby. - * - * @param isNearbyLocked true means lock, false means unlock - */ - @Override - public void lockUnlockNearby(boolean isNearbyLocked) { - this.isNearbyLocked = isNearbyLocked; - if (isNearbyLocked) { - exploreMapFragmentView.disableFABRecenter(); - } else { - exploreMapFragmentView.enableFABRecenter(); - } - } - - @Override - public void attachView(ExploreMapContract.View view) { - exploreMapFragmentView = view; - } - - @Override - public void detachView() { - exploreMapFragmentView = DUMMY; - } - - /** - * Sets click listener of FAB - */ - @Override - public void setActionListeners(JsonKvStore applicationKvStore) { - exploreMapFragmentView.setFABRecenterAction(v -> { - exploreMapFragmentView.recenterMap(currentLatLng); - }); - - } - - @Override - public boolean backButtonClicked() { - return exploreMapFragmentView.backButtonClicked(); - } - - public void onMapReady(ExploreMapController exploreMapController) { - this.exploreMapController = exploreMapController; - if (null != exploreMapFragmentView) { - exploreMapFragmentView.addSearchThisAreaButtonAction(); - initializeMapOperations(); - } - } - - public void initializeMapOperations() { - lockUnlockNearby(false); - updateMap(LOCATION_SIGNIFICANTLY_CHANGED); - } - - public Observable loadAttractionsFromLocation(LatLng currentLatLng, - LatLng searchLatLng, boolean checkingAroundCurrent) { - return Observable - .fromCallable(() -> exploreMapController - .loadAttractionsFromLocation(currentLatLng, searchLatLng, checkingAroundCurrent)); - } - - /** - * Populates places for custom location, should be used for finding nearby places around a - * location where you are not at. - * - * @param explorePlacesInfo This variable has placeToCenter list information and distances. - */ - public void updateMapMarkers( - MapController.ExplorePlacesInfo explorePlacesInfo) { - if (explorePlacesInfo.mediaList != null) { - prepareNearbyBaseMarkers(explorePlacesInfo); - } else { - lockUnlockNearby(false); // So that new location updates wont come - exploreMapFragmentView.setProgressBarVisibility(false); - } - } - - void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) { - exploreMapController - .loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng, - // Curlatlang will be used to calculate distances - (List) explorePlacesInfo.explorePlaceList, - exploreMapFragmentView.getContext(), - this, - explorePlacesInfo); - } - - @Override - public void onNearbyBaseMarkerThumbsReady(List baseMarkers, - ExplorePlacesInfo explorePlacesInfo) { - if (null != exploreMapFragmentView) { - exploreMapFragmentView.addMarkersToMap(baseMarkers); - lockUnlockNearby(false); // So that new location updates wont come - exploreMapFragmentView.setProgressBarVisibility(false); - } - } - - public View.OnClickListener onSearchThisAreaClicked() { - return v -> { - // Lock map operations during search this area operation - exploreMapFragmentView.setSearchThisAreaButtonVisibility(false); - - if (searchCloseToCurrentLocation()) { - updateMap(LOCATION_SIGNIFICANTLY_CHANGED); - } else { - updateMap(SEARCH_CUSTOM_AREA); - } - }; - } - - /** - * Returns true if search this area button is used around our current location, so that we can - * continue following our current location again - * - * @return Returns true if search this area button is used around our current location - */ - public boolean searchCloseToCurrentLocation() { - if (null == exploreMapFragmentView.getLastMapFocus()) { - return true; - } - - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(exploreMapFragmentView.getMapFocus().getLatitude()); - dest_location.setLongitude(exploreMapFragmentView.getMapFocus().getLongitude()); - mylocation.setLatitude(exploreMapFragmentView.getLastMapFocus().getLatitude()); - mylocation.setLongitude(exploreMapFragmentView.getLastMapFocus().getLongitude()); - Float distance = mylocation.distanceTo(dest_location); - - return !(distance > 2000.0 * 3 / 4); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt new file mode 100644 index 000000000..002ff6044 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt @@ -0,0 +1,223 @@ +package fr.free.nrw.commons.explore.map + +import android.location.Location +import android.view.View +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.MapController.ExplorePlacesInfo +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.explore.map.ExploreMapController.Companion.loadAttractionsFromLocationToBaseMarkerOptions +import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType +import fr.free.nrw.commons.nearby.Place +import io.reactivex.Observable +import timber.log.Timber +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import java.util.concurrent.Callable + +class ExploreMapPresenter( + var bookmarkLocationDao: BookmarkLocationsDao +) : ExploreMapContract.UserActions, NearbyBaseMarkerThumbCallback { + + private var isNearbyLocked = false + private val currentLatLng: LatLng? = null + private var exploreMapController: ExploreMapController? = null + private var exploreMapFragmentView: ExploreMapContract.View? = DUMMY + + override fun updateMap(locationChangeType: LocationChangeType) { + Timber.d("Presenter updates map and list$locationChangeType") + if (isNearbyLocked) { + Timber.d("Nearby is locked, so updateMapAndList returns") + return + } + + if (!exploreMapFragmentView!!.isNetworkConnectionEstablished()) { + Timber.d("Network connection is not established") + return + } + + /** + * Significant changed - Markers and current location will be updated together + * Slightly changed - Only current position marker will be updated + */ + if (locationChangeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) { + Timber.d("LOCATION_SIGNIFICANTLY_CHANGED") + var populateLatLng = exploreMapFragmentView!!.getMapCenter() + + //If "Show in Explore" was selected in Nearby, use the previous LatLng + if (exploreMapFragmentView is ExploreMapFragment) { + val exploreMapFragment = exploreMapFragmentView as ExploreMapFragment + if (exploreMapFragment.recentlyCameFromNearbyMap()) { + //Ensure this LatLng will not be used again if user searches their GPS location + exploreMapFragment.setRecentlyCameFromNearbyMap(false) + + populateLatLng = exploreMapFragment.previousLatLng + } + } + + lockUnlockNearby(true) + exploreMapFragmentView!!.setProgressBarVisibility(true) + exploreMapFragmentView!!.populatePlaces(populateLatLng) + } else if (locationChangeType == LocationChangeType.SEARCH_CUSTOM_AREA) { + Timber.d("SEARCH_CUSTOM_AREA") + lockUnlockNearby(true) + exploreMapFragmentView!!.setProgressBarVisibility(true) + exploreMapFragmentView!!.populatePlaces(exploreMapFragmentView!!.getMapFocus()) + } else { // Means location changed slightly, ie user is walking or driving. + Timber.d("Means location changed slightly") + } + } + + /** + * Nearby updates takes time, since they are network operations. During update time, we don't + * want to get any other calls from user. So locking nearby. + * + * @param isNearbyLocked true means lock, false means unlock + */ + override fun lockUnlockNearby(isNearbyLocked: Boolean) { + this.isNearbyLocked = isNearbyLocked + if (isNearbyLocked) { + exploreMapFragmentView!!.disableFABRecenter() + } else { + exploreMapFragmentView!!.enableFABRecenter() + } + } + + override fun attachView(view: ExploreMapContract.View?) { + exploreMapFragmentView = view + } + + override fun detachView() { + exploreMapFragmentView = DUMMY + } + + /** + * Sets click listener of FAB + */ + override fun setActionListeners(applicationKvStore: JsonKvStore?) { + exploreMapFragmentView!!.setFABRecenterAction { + exploreMapFragmentView!!.recenterMap(currentLatLng) + } + } + + override fun backButtonClicked(): Boolean = + exploreMapFragmentView!!.backButtonClicked() + + fun onMapReady(exploreMapController: ExploreMapController?) { + this.exploreMapController = exploreMapController + if (null != exploreMapFragmentView) { + exploreMapFragmentView!!.addSearchThisAreaButtonAction() + initializeMapOperations() + } + } + + fun initializeMapOperations() { + lockUnlockNearby(false) + updateMap(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + } + + fun loadAttractionsFromLocation( + currentLatLng: LatLng?, + searchLatLng: LatLng?, checkingAroundCurrent: Boolean + ): Observable = Observable.fromCallable(Callable { + exploreMapController!!.loadAttractionsFromLocation( + currentLatLng, + searchLatLng, + checkingAroundCurrent + ) + }) + + /** + * Populates places for custom location, should be used for finding nearby places around a + * location where you are not at. + * + * @param explorePlacesInfo This variable has placeToCenter list information and distances. + */ + fun updateMapMarkers( + explorePlacesInfo: ExplorePlacesInfo + ) { + if (explorePlacesInfo.mediaList != null) { + prepareNearbyBaseMarkers(explorePlacesInfo) + } else { + lockUnlockNearby(false) // So that new location updates wont come + exploreMapFragmentView!!.setProgressBarVisibility(false) + } + } + + private fun prepareNearbyBaseMarkers(explorePlacesInfo: ExplorePlacesInfo) { + loadAttractionsFromLocationToBaseMarkerOptions( + explorePlacesInfo.currentLatLng, // Curlatlang will be used to calculate distances + explorePlacesInfo.explorePlaceList, + exploreMapFragmentView!!.getContext()!!, + this, + explorePlacesInfo + ) + } + + override fun onNearbyBaseMarkerThumbsReady( + baseMarkers: List?, + explorePlacesInfo: ExplorePlacesInfo? + ) { + if (null != exploreMapFragmentView) { + exploreMapFragmentView!!.addMarkersToMap(baseMarkers) + lockUnlockNearby(false) // So that new location updates wont come + exploreMapFragmentView!!.setProgressBarVisibility(false) + } + } + + fun onSearchThisAreaClicked(): View.OnClickListener { + return View.OnClickListener { + // Lock map operations during search this area operation + exploreMapFragmentView!!.setSearchThisAreaButtonVisibility(false) + updateMap(if (searchCloseToCurrentLocation()) { + LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED + } else { + LocationChangeType.SEARCH_CUSTOM_AREA + }) + } + } + + /** + * Returns true if search this area button is used around our current location, so that we can + * continue following our current location again + * + * @return Returns true if search this area button is used around our current location + */ + private fun searchCloseToCurrentLocation(): Boolean { + if (null == exploreMapFragmentView!!.getLastMapFocus()) { + return true + } + + val mylocation = Location("").apply { + latitude = exploreMapFragmentView!!.getLastMapFocus()!!.latitude + longitude = exploreMapFragmentView!!.getLastMapFocus()!!.longitude + } + val dest_location = Location("").apply { + latitude = exploreMapFragmentView!!.getMapFocus()!!.latitude + longitude = exploreMapFragmentView!!.getMapFocus()!!.longitude + } + + val distance = mylocation.distanceTo(dest_location) + + return !(distance > 2000.0 * 3 / 4) + } + + companion object { + private val DUMMY = Proxy.newProxyInstance( + ExploreMapContract.View::class.java.classLoader, + arrayOf>(ExploreMapContract.View::class.java) + ) { _: Any?, method: Method, _: Array? -> + when { + method.name == "onMyEvent" -> null + String::class.java == method.returnType -> "" + Int::class.java == method.returnType -> 0 + Int::class.javaPrimitiveType == method.returnType -> 0 + Boolean::class.java == method.returnType -> java.lang.Boolean.FALSE + Boolean::class.javaPrimitiveType == method.returnType -> false + else -> null + } + } as ExploreMapContract.View + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt index f30636db7..21f7a1a22 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt @@ -8,9 +8,9 @@ import android.net.Uri import androidx.core.net.toUri import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.di.CommonsDaggerContentProvider -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.TABLE_NAME /** * This class contains functions for executing queries for diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java deleted file mode 100644 index cee8a25ae..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java +++ /dev/null @@ -1,275 +0,0 @@ -package fr.free.nrw.commons.explore.recentsearches; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.explore.models.RecentSearch; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; - -import timber.log.Timber; - -/** - * This class doesn't execute queries in database directly instead it contains the logic behind - * inserting, deleting, searching data from recent searches database. - **/ -public class RecentSearchesDao { - - private final Provider clientProvider; - - @Inject - public RecentSearchesDao(@Named("recentsearch") Provider clientProvider) { - this.clientProvider = clientProvider; - } - - /** - * This method is called on click of media/ categories for storing them in recent searches - * @param recentSearch a recent searches object that is to be added in SqLite DB - */ - public void save(RecentSearch recentSearch) { - ContentProviderClient db = clientProvider.get(); - try { - if (recentSearch.getContentUri() == null) { - recentSearch.setContentUri(db.insert(RecentSearchesContentProvider.BASE_URI, toContentValues(recentSearch))); - } else { - db.update(recentSearch.getContentUri(), toContentValues(recentSearch), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * This method is called on confirmation of delete recent searches. - * It deletes all recent searches from the database - */ - public void deleteAll() { - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - RecentSearchesContentProvider.BASE_URI, - Table.ALL_FIELDS, - null, - new String[]{}, - Table.COLUMN_LAST_USED + " DESC" - ); - while (cursor != null && cursor.moveToNext()) { - try { - RecentSearch recentSearch = find(fromCursor(cursor).getQuery()); - if (recentSearch.getContentUri() == null) { - throw new RuntimeException("tried to delete item with no content URI"); - } else { - Timber.d("QUERY_NAME %s - delete tried", recentSearch.getContentUri()); - db.delete(recentSearch.getContentUri(), null, null); - Timber.d("QUERY_NAME %s - query deleted", recentSearch.getQuery()); - } - } catch (RemoteException e) { - Timber.e(e, "query deleted"); - throw new RuntimeException(e); - } finally { - db.release(); - } - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - /** - * Deletes a recent search from the database - */ - public void delete(RecentSearch recentSearch) { - - ContentProviderClient db = clientProvider.get(); - try { - if (recentSearch.getContentUri() == null) { - throw new RuntimeException("tried to delete item with no content URI"); - } else { - db.delete(recentSearch.getContentUri(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - - /** - * Find persisted search query in database, based on its name. - * @param name Search query Ex- "butterfly" - * @return recently searched query from database, or null if not found - */ - @Nullable - public RecentSearch find(String name) { - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - RecentSearchesContentProvider.BASE_URI, - Table.ALL_FIELDS, - Table.COLUMN_NAME + "=?", - new String[]{name}, - null); - if (cursor != null && cursor.moveToFirst()) { - return fromCursor(cursor); - } - } catch (RemoteException e) { - // This feels lazy, but to hell with checked exceptions. :) - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return null; - } - - /** - * Retrieve recently-searched queries, ordered by descending date. - * @return a list containing recent searches - */ - @NonNull - public List recentSearches(int limit) { - List items = new ArrayList<>(); - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( RecentSearchesContentProvider.BASE_URI, Table.ALL_FIELDS, - null, new String[]{}, Table.COLUMN_LAST_USED + " DESC"); - // fixme add a limit on the original query instead of falling out of the loop? - while (cursor != null && cursor.moveToNext() && cursor.getPosition() < limit) { - items.add(fromCursor(cursor).getQuery()); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return items; - } - - - /** - * It creates an Recent Searches object from data stored in the SQLite DB by using cursor - * @param cursor - * @return RecentSearch object - */ - @NonNull - @SuppressLint("Range") - RecentSearch fromCursor(Cursor cursor) { - // Hardcoding column positions! - return new RecentSearch( - RecentSearchesContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))), - cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), - new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))) - ); - } - - /** - * This class contains the database table architechture for recent searches, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - private ContentValues toContentValues(RecentSearch recentSearch) { - ContentValues cv = new ContentValues(); - cv.put(RecentSearchesDao.Table.COLUMN_NAME, recentSearch.getQuery()); - cv.put(RecentSearchesDao.Table.COLUMN_LAST_USED, recentSearch.getLastSearched().getTime()); - return cv; - } - - /** - * This class contains the database table architechture for recent searches, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - public static class Table { - public static final String TABLE_NAME = "recent_searches"; - public static final String COLUMN_ID = "_id"; - static final String COLUMN_NAME = "name"; - static final String COLUMN_LAST_USED = "last_used"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_NAME, - COLUMN_LAST_USED, - }; - - static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; - - static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_ID + " INTEGER PRIMARY KEY," - + COLUMN_NAME + " STRING," - + COLUMN_LAST_USED + " INTEGER" - + ");"; - - /** - * This method creates a RecentSearchesTable in SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * This method deletes RecentSearchesTable from SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onDelete(SQLiteDatabase db) { - db.execSQL(DROP_TABLE_STATEMENT); - onCreate(db); - } - - /** - * This method is called on migrating from a older version to a newer version - * @param db SQLiteDatabase - * @param from Version from which we are migrating - * @param to Version to which we are migrating - */ - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from < 6) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 6) { - // table added in version 7 - onCreate(db); - from++; - onUpdate(db, from, to); - return; - } - if (from == 7) { - from++; - onUpdate(db, from, to); - return; - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt new file mode 100644 index 000000000..e1d0740de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt @@ -0,0 +1,180 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.annotation.SuppressLint +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.os.RemoteException +import androidx.core.content.contentValuesOf +import fr.free.nrw.commons.explore.models.RecentSearch +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME +import fr.free.nrw.commons.utils.getInt +import fr.free.nrw.commons.utils.getLong +import fr.free.nrw.commons.utils.getString +import java.util.Date +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider + +/** + * This class doesn't execute queries in database directly instead it contains the logic behind + * inserting, deleting, searching data from recent searches database. + */ +class RecentSearchesDao @Inject constructor( + @param:Named("recentsearch") private val clientProvider: Provider +) { + /** + * This method is called on click of media/ categories for storing them in recent searches + * @param recentSearch a recent searches object that is to be added in SqLite DB + */ + fun save(recentSearch: RecentSearch) { + val db = clientProvider.get() + try { + val contentValues = toContentValues(recentSearch) + if (recentSearch.contentUri == null) { + recentSearch.contentUri = db.insert(BASE_URI, contentValues) + } else { + db.update(recentSearch.contentUri!!, contentValues, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * This method is called on confirmation of delete recent searches. + * It deletes all recent searches from the database + */ + fun deleteAll() { + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, + ALL_FIELDS, + null, + arrayOf(), + "$COLUMN_LAST_USED DESC" + ) + while (cursor != null && cursor.moveToNext()) { + try { + val recentSearch = find(fromCursor(cursor).query) + if (recentSearch!!.contentUri == null) { + throw RuntimeException("tried to delete item with no content URI") + } else { + db.delete(recentSearch.contentUri!!, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + } + } + + /** + * Deletes a recent search from the database + */ + fun delete(recentSearch: RecentSearch) { + val db = clientProvider.get() + try { + if (recentSearch.contentUri == null) { + throw RuntimeException("tried to delete item with no content URI") + } else { + db.delete(recentSearch.contentUri!!, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + + /** + * Find persisted search query in database, based on its name. + * @param name Search query Ex- "butterfly" + * @return recently searched query from database, or null if not found + */ + fun find(name: String): RecentSearch? { + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, + ALL_FIELDS, + "$COLUMN_NAME=?", + arrayOf(name), + null + ) + if (cursor != null && cursor.moveToFirst()) { + return fromCursor(cursor) + } + } catch (e: RemoteException) { + // This feels lazy, but to hell with checked exceptions. :) + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return null + } + + /** + * Retrieve recently-searched queries, ordered by descending date. + * @return a list containing recent searches + */ + fun recentSearches(limit: Int): List { + val items: MutableList = mutableListOf() + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, ALL_FIELDS, + null, arrayOf(), "$COLUMN_LAST_USED DESC" + ) + // fixme add a limit on the original query instead of falling out of the loop? + while (cursor != null && cursor.moveToNext() && cursor.position < limit) { + items.add(fromCursor(cursor).query) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return items + } + + /** + * It creates an Recent Searches object from data stored in the SQLite DB by using cursor + * @param cursor + * @return RecentSearch object + */ + fun fromCursor(cursor: Cursor): RecentSearch = RecentSearch( + uriForId(cursor.getInt(COLUMN_ID)), + cursor.getString(COLUMN_NAME), + Date(cursor.getLong(COLUMN_LAST_USED)) + ) + + /** + * This class contains the database table architechture for recent searches, + * It also contains queries and logic necessary to the create, update, delete this table. + */ + private fun toContentValues(recentSearch: RecentSearch): ContentValues = contentValuesOf( + COLUMN_NAME to recentSearch.query, + COLUMN_LAST_USED to recentSearch.lastSearched.time + ) +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java deleted file mode 100644 index 588f3a25f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java +++ /dev/null @@ -1,149 +0,0 @@ -package fr.free.nrw.commons.explore.recentsearches; - -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.SearchActivity; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; - - -/** - * Displays the recent searches screen. - */ -public class RecentSearchesFragment extends CommonsDaggerSupportFragment { - - @Inject - RecentSearchesDao recentSearchesDao; - List recentSearches; - ArrayAdapter adapter; - - private FragmentSearchHistoryBinding binding; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - binding = FragmentSearchHistoryBinding.inflate(inflater, container, false); - - recentSearches = recentSearchesDao.recentSearches(10); - - if (recentSearches.isEmpty()) { - binding.recentSearchesDeleteButton.setVisibility(View.GONE); - binding.recentSearchesTextView.setText(R.string.no_recent_searches); - } - - binding.recentSearchesDeleteButton.setOnClickListener(v -> { - showDeleteRecentAlertDialog(requireContext()); - }); - - adapter = new ArrayAdapter<>(requireContext(), R.layout.item_recent_searches, - recentSearches); - binding.recentSearchesList.setAdapter(adapter); - binding.recentSearchesList.setOnItemClickListener((parent, view, position, id) -> ( - (SearchActivity) getContext()).updateText(recentSearches.get(position))); - binding.recentSearchesList.setOnItemLongClickListener((parent, view, position, id) -> { - showDeleteAlertDialog(requireContext(), position); - return true; - }); - updateRecentSearches(); - - return binding.getRoot(); - } - - private void showDeleteRecentAlertDialog(@NonNull final Context context) { - new AlertDialog.Builder(context) - .setMessage(getString(R.string.delete_recent_searches_dialog)) - .setPositiveButton(android.R.string.yes, - (dialog, which) -> setDeleteRecentPositiveButton(context, dialog)) - .setNegativeButton(android.R.string.no, null) - .setCancelable(false) - .create() - .show(); - } - - private void setDeleteRecentPositiveButton(@NonNull final Context context, - final DialogInterface dialog) { - recentSearchesDao.deleteAll(); - if (binding != null) { - binding.recentSearchesDeleteButton.setVisibility(View.GONE); - binding.recentSearchesTextView.setText(R.string.no_recent_searches); - Toast.makeText(getContext(), getString(R.string.search_history_deleted), - Toast.LENGTH_SHORT).show(); - recentSearches = recentSearchesDao.recentSearches(10); - adapter = new ArrayAdapter<>(context, R.layout.item_recent_searches, - recentSearches); - binding.recentSearchesList.setAdapter(adapter); - adapter.notifyDataSetChanged(); - } - dialog.dismiss(); - } - - private void showDeleteAlertDialog(@NonNull final Context context, final int position) { - new AlertDialog.Builder(context) - .setMessage(R.string.delete_search_dialog) - .setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT), - ((dialog, which) -> setDeletePositiveButton(context, dialog, position))) - .setNegativeButton(android.R.string.cancel, null) - .setCancelable(false) - .create() - .show(); - } - - private void setDeletePositiveButton(@NonNull final Context context, - final DialogInterface dialog, final int position) { - recentSearchesDao.delete(recentSearchesDao.find(recentSearches.get(position))); - recentSearches = recentSearchesDao.recentSearches(10); - adapter = new ArrayAdapter<>(context, R.layout.item_recent_searches, - recentSearches); - if (binding != null){ - binding.recentSearchesList.setAdapter(adapter); - adapter.notifyDataSetChanged(); - } - dialog.dismiss(); - } - - /** - * This method is called on back press of activity so we are updating the list from database to - * refresh the recent searches list. - */ - @Override - public void onResume() { - updateRecentSearches(); - super.onResume(); - } - - /** - * This method is called when search query is null to update Recent Searches - */ - public void updateRecentSearches() { - recentSearches = recentSearchesDao.recentSearches(10); - adapter.notifyDataSetChanged(); - - if (!recentSearches.isEmpty()) { - if (binding!= null) { - binding.recentSearchesDeleteButton.setVisibility(View.VISIBLE); - binding.recentSearchesTextView.setText(R.string.search_recent_header); - } - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (binding != null) { - binding = null; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt new file mode 100644 index 000000000..c0f1bd5db --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt @@ -0,0 +1,153 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.content.Context +import android.content.DialogInterface +import android.content.DialogInterface.OnClickListener +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemClickListener +import android.widget.AdapterView.OnItemLongClickListener +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import fr.free.nrw.commons.R +import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.SearchActivity +import javax.inject.Inject + +/** + * Displays the recent searches screen. + */ +class RecentSearchesFragment : CommonsDaggerSupportFragment() { + @JvmField + @Inject + var recentSearchesDao: RecentSearchesDao? = null + + private var recentSearches: List = emptyList() + private lateinit var adapter: ArrayAdapter + private var binding: FragmentSearchHistoryBinding? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSearchHistoryBinding.inflate(inflater, container, false) + + recentSearches = recentSearchesDao!!.recentSearches(10) + + if (recentSearches.isEmpty()) { + binding!!.recentSearchesDeleteButton.visibility = View.GONE + binding!!.recentSearchesTextView.setText(R.string.no_recent_searches) + } + + binding!!.recentSearchesDeleteButton.setOnClickListener { v: View? -> + showDeleteRecentAlertDialog(requireContext()) + } + + adapter = ArrayAdapter(requireContext(), R.layout.item_recent_searches, recentSearches) + binding!!.recentSearchesList.adapter = adapter + binding!!.recentSearchesList.onItemClickListener = + OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long -> + (context as SearchActivity).updateText(recentSearches[position]) + } + binding!!.recentSearchesList.onItemLongClickListener = + OnItemLongClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long -> + showDeleteAlertDialog(requireContext(), position) + true + } + updateRecentSearches() + + return binding!!.root + } + + private fun showDeleteRecentAlertDialog(context: Context) { + AlertDialog.Builder(context) + .setMessage(getString(R.string.delete_recent_searches_dialog)) + .setPositiveButton(android.R.string.yes) { dialog: DialogInterface, _: Int -> + setDeleteRecentPositiveButton(context, dialog) + } + .setNegativeButton(android.R.string.no, null) + .setCancelable(false) + .create() + .show() + } + + private fun setDeleteRecentPositiveButton(context: Context, dialog: DialogInterface) { + recentSearchesDao!!.deleteAll() + if (binding != null) { + binding!!.recentSearchesDeleteButton.visibility = View.GONE + binding!!.recentSearchesTextView.setText(R.string.no_recent_searches) + Toast.makeText( + getContext(), getString(R.string.search_history_deleted), + Toast.LENGTH_SHORT + ).show() + recentSearches = recentSearchesDao!!.recentSearches(10) + adapter = ArrayAdapter(context, R.layout.item_recent_searches, recentSearches) + binding!!.recentSearchesList.adapter = adapter + adapter.notifyDataSetChanged() + } + dialog.dismiss() + } + + private fun showDeleteAlertDialog(context: Context, position: Int) { + AlertDialog.Builder(context) + .setMessage(R.string.delete_search_dialog) + .setPositiveButton( + getString(R.string.delete).uppercase(), + { dialog: DialogInterface, _: Int -> + setDeletePositiveButton(context, dialog, position) + } + ) + .setNegativeButton(android.R.string.cancel, null) + .setCancelable(false) + .create() + .show() + } + + private fun setDeletePositiveButton(context: Context, dialog: DialogInterface, position: Int) { + recentSearchesDao!!.delete(recentSearchesDao!!.find(recentSearches[position])!!) + recentSearches = recentSearchesDao!!.recentSearches(10) + adapter = ArrayAdapter( + context, R.layout.item_recent_searches, + recentSearches + ) + if (binding != null) { + binding!!.recentSearchesList.adapter = adapter + adapter.notifyDataSetChanged() + } + dialog.dismiss() + } + + /** + * This method is called on back press of activity so we are updating the list from database to + * refresh the recent searches list. + */ + override fun onResume() { + updateRecentSearches() + super.onResume() + } + + /** + * This method is called when search query is null to update Recent Searches + */ + fun updateRecentSearches() { + recentSearches = recentSearchesDao!!.recentSearches(10) + adapter.notifyDataSetChanged() + + if (recentSearches.isNotEmpty()) { + if (binding != null) { + binding!!.recentSearchesDeleteButton.visibility = View.VISIBLE + binding!!.recentSearchesTextView.setText(R.string.search_recent_header) + } + } + } + + override fun onDestroy() { + super.onDestroy() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt new file mode 100644 index 000000000..e32fc9fa4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt @@ -0,0 +1,71 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.database.sqlite.SQLiteDatabase + +/** + * This class contains the database table architechture for recent searches, It also contains + * queries and logic necessary to the create, update, delete this table. + */ +object RecentSearchesTable { + const val TABLE_NAME: String = "recent_searches" + const val COLUMN_ID: String = "_id" + const val COLUMN_NAME: String = "name" + const val COLUMN_LAST_USED: String = "last_used" + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + @JvmField + val ALL_FIELDS = arrayOf( + COLUMN_ID, + COLUMN_NAME, + COLUMN_LAST_USED, + ) + + const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME" + + const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY,$COLUMN_NAME STRING,$COLUMN_LAST_USED INTEGER);") + + /** + * This method creates a RecentSearchesTable in SQLiteDatabase + * + * @param db SQLiteDatabase + */ + fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT) + + /** + * This method deletes RecentSearchesTable from SQLiteDatabase + * + * @param db SQLiteDatabase + */ + fun onDelete(db: SQLiteDatabase) { + db.execSQL(DROP_TABLE_STATEMENT) + onCreate(db) + } + + /** + * This method is called on migrating from a older version to a newer version + * + * @param db SQLiteDatabase + * @param from Version from which we are migrating + * @param to Version to which we are migrating + */ + fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { + if (from == to) { + return + } + if (from < 6) { + // doesn't exist yet + onUpdate(db, from + 1, to) + return + } + if (from == 6) { + // table added in version 7 + onCreate(db) + onUpdate(db, from + 1, to) + return + } + if (from == 7) { + onUpdate(db, from + 1, to) + return + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt index 48e61051c..d80be9ea2 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt @@ -46,7 +46,7 @@ class ProfileActivity : BaseActivity() { private var contributionsFragment: ContributionsFragment? = null fun setScroll(canScroll: Boolean) { - binding.viewPager.setCanScroll(canScroll) + binding.viewPager.canScroll = canScroll } override fun onRestoreInstanceState(savedInstanceState: Bundle) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt index 5d98ebffb..3cf9d3a65 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt @@ -56,11 +56,7 @@ class UploadProgressActivity : BaseActivity() { override fun onPageSelected(position: Int) { updateMenuItems(position) - if (position == 2) { - binding.uploadProgressViewPager.setCanScroll(false) - } else { - binding.uploadProgressViewPager.setCanScroll(true) - } + binding.uploadProgressViewPager.canScroll = (position != 2) } override fun onPageScrollStateChanged(state: Int) { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt index 69560279b..1fd99bcee 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt @@ -10,6 +10,14 @@ fun Cursor.getStringArray(name: String): List = fun Cursor.getString(name: String): String = getString(getColumnIndex(name)) +@SuppressLint("Range") +fun Cursor.getInt(name: String): Int = + getInt(getColumnIndex(name)) + +@SuppressLint("Range") +fun Cursor.getLong(name: String): Long = + getLong(getColumnIndex(name)) + /** * Converts string to List * @param listString comma separated single string from of list items diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt index e2ef3bb92..85298d0a3 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt @@ -88,14 +88,14 @@ class ExploreFragmentUnitTest { @Throws(Exception::class) fun testSetScrollCaseTrue() { fragment.setScroll(true) - Assert.assertEquals(viewPager.isCanScroll, true) + Assert.assertEquals(viewPager.canScroll, true) } @Test @Throws(Exception::class) fun testSetScrollCaseFalse() { fragment.setScroll(false) - Assert.assertEquals(viewPager.isCanScroll, false) + Assert.assertEquals(viewPager.canScroll, false) } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt index c772f796e..5e128d4ee 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt @@ -20,15 +20,15 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.explore.models.RecentSearch import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_LAST_USED -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_NAME -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onCreate -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onDelete -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onUpdate +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.CREATE_TABLE_STATEMENT +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.DROP_TABLE_STATEMENT +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onCreate +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onDelete +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onUpdate import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt index 8173dfc44..cd1186fc8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt @@ -123,18 +123,4 @@ class RecentSearchesFragmentUnitTest { method.isAccessible = true method.invoke(fragment, context, 0) } - - @Test - @Throws(Exception::class) - fun testSetDeletePositiveButton() { - val method: Method = - RecentSearchesFragment::class.java.getDeclaredMethod( - "setDeletePositiveButton", - Context::class.java, - DialogInterface::class.java, - Int::class.java, - ) - method.isAccessible = true - method.invoke(fragment, context, dialog, 0) - } } From 6dcce45c592f7a4d6d862d49a67239d21058f4ad Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 4 Aug 2025 14:02:18 +0200 Subject: [PATCH 15/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 3 ++- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-ps/strings.xml | 10 +++++++++ app/src/main/res/values-pt/strings.xml | 28 +++++++++++++++++++++++++- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c9c0323c8..2d704c0e3 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -268,6 +268,7 @@ الوصف نقاش المؤلف + الرافع تاريخ الرفع الترخيص الإحداثيات @@ -822,7 +823,7 @@ الإذن مطلوب لهذه الوظيفة تعلم كيفية كتابة وصف مفيد تعلم كيفية كتابة تعليق مفيد - شاهد إنجازاتك + عرض إنجازاتك تعديل الصورة تعديل الموقع تم تحديث الموقع! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0282d99ed..0c5befc26 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -241,7 +241,7 @@ Niet opgegeven Word bètatester Meld u aan voor ons bètakanaal op Google Play en krijg vroegtijdig toegang tot nieuwe functies en bugfixes - 2FA-code + Tweetrapsauthenticatie-code E-mailverificatiecode Wilt u zich echt afmelden? Media-afbeelding is mislukt diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 64a7762a7..4bfaea5ab 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -274,6 +274,15 @@ موبايل له لارې راپورته‌شوی نخشه انځور په ویکي‌اومتوک کې %1$s ته ورگډ شو! + پېژنيال + مټ‌تړوني + شمارنې + مننه ترلاسه‌شو + ټاکلی انځور + انځورنه د \"څېرمه ځايونو\" له لارې + انځورونه راپورته‌شول + انځورونه په څټ‌گرځول‌شوي نه دي + کارول‌شوي انځورونه پای ته رسېږي په: ټاکنيزې‌سيالۍ ښکاره‌کول روانې ټاکنيزې‌سيالۍ وگورئ @@ -301,5 +310,6 @@ ايا دا د منلو وړ دي؟ ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟ که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ. + ځای اومتوکي له ويکي کارنانو سره مرسته کوي چې ستاسو انځور موندلو او لا ډېر گټور کولو کې مرسته کوي.\nستاسو وروستۍ راپورته‌کېدنې ځای نه لري.\nموږ تاسو ته سپارښته کوو چې خپل د ځای ښودنه د کامرې په کاريال په اوڼنو کې بل کړئ.\nله راپورته کولو مو مننه! گڼون diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 721528c7b..659c2705a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -228,6 +228,7 @@ Descrição Discussão Autor + Carregador Data de carregamento Licença Coordenadas @@ -235,6 +236,7 @@ Torne-se um Testador Beta Opte pelo nosso canal beta no Google Play e obtenha acesso antecipado às novas funcionalidades e às correções de erros Código de autenticação de dois fatores + Código de verificação do e-mail Deseja realmente sair? Falha na imagem multimédia Não foi encontrada nenhuma subcategoria. @@ -255,6 +257,7 @@ Sobre Configurações Comentários + Comentários via GitHub Sair Explicação Notificações @@ -294,8 +297,10 @@ Copiar o texto wiki para a área de transferência O texto wiki foi copiado para a área de transferência A identificação de locais próximos pode não funcionar devidamente; o serviço de localização não está disponível. + Internet indisponível. A mostrar apenas os locais em cache. Acesso à localização negado. Para usar esta funcionalidade defina a sua localização manualmente, por favor. É necessária permissão para mostrar uma lista dos locais próximos + É necessária a permissão para exibir uma lista de imagens próximas Indicações Wikidata Wikipédia @@ -364,6 +369,7 @@ Partilhar aplicação Rodar Não foi possível carregar locais próximos + Não há fotografias nesta área Não existem locais próximos Erro ao procurar monumentos próximos. Não há pesquisas recentes @@ -764,11 +770,12 @@ São necessárias permissões para a funcionalidade Aprenda a escrever uma descrição útil Aprenda a escrever uma legenda útil - Ver as suas realizações + Ver as suas realizações Editar imagem Editar localização Localização actualizada! Remover Localização + Remover Aviso de Localização Localização removida! Agradecer ao autor Erro no envio de agradecimento ao autor. @@ -788,5 +795,24 @@ Discussão Outro problema ou informação (por favor, explique em baixo). O seu comentário é publicado na seguinte página da wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + Envios + Pendente + Falhou + Não foi possível carregar os dados do local + Eliminar Pasta + Confirmar Eliminação + Eliminar + Cancelar + Commons + Outras wikis + Utilização de ficheiro + Conta + Legenda + Legenda copiada para a área de transferência + Parabéns, todas as fotografias neste álbum foram envidas ou marcadas como \'não para enviar\'. + Mostrar no Explorador + Mostrar nas Proximidades + Criada e enviada por: %1$s + Criada por %1$s e enviada por %2$s Nomeada para Eliminação From ffb9af1f1c2fc35dcc8a636c77bd683229248d2c Mon Sep 17 00:00:00 2001 From: Sonal Yadav Date: Tue, 5 Aug 2025 09:43:40 +0530 Subject: [PATCH 16/97] Support both "label" and "itemLabel" for NearbyResultItem mapping (#6386) * Support both label and itemLabel for robust NearbyResultItem mapping. * fix code style * Add getOriginalLabel() for Wikidata edits to avoid fallback issues with itemLabel * Fix Wikidata edit failure by resetting hasInvalidLocation flag on upload confirmation --------- Co-authored-by: Sonal Yadav --- .../commons/nearby/model/NearbyResultItem.kt | 13 +++- .../mediaDetails/UploadMediaDetailFragment.kt | 1 + .../main/resources/queries/query_for_item.rq | 59 +++++++------------ 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt index c39d8901d..1d5d7bd80 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt @@ -7,7 +7,8 @@ class NearbyResultItem( private val wikipediaArticle: ResultTuple?, private val commonsArticle: ResultTuple?, private val location: ResultTuple?, - private val label: ResultTuple?, + @field:SerializedName("label") private val label: ResultTuple?, + @field:SerializedName("itemLabel") private val itemLabel: ResultTuple?, @field:SerializedName("streetAddress") private val address: ResultTuple?, private val icon: ResultTuple?, @field:SerializedName("class") private val className: ResultTuple?, @@ -29,7 +30,15 @@ class NearbyResultItem( fun getLocation(): ResultTuple = location ?: ResultTuple() - fun getLabel(): ResultTuple = label ?: ResultTuple() + /** + * Returns label for display (pins, popup), using fallback to itemLabel if needed. + */ + fun getLabel(): ResultTuple = label ?: itemLabel ?: ResultTuple() + + /** + * Returns only the original label field, for Wikidata edits. + */ + fun getOriginalLabel(): ResultTuple = label ?: ResultTuple() fun getIcon(): ResultTuple = icon ?: ResultTuple() diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt index 4a4c13ba7..43a9c3236 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt @@ -821,6 +821,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra { showProgress(false) uploadItem.imageQuality = IMAGE_OK + uploadItem.hasInvalidLocation = false // Reset invalid location flag when user confirms upload }, { presenterCallback!!.deletePictureAtIndex(index) diff --git a/app/src/main/resources/queries/query_for_item.rq b/app/src/main/resources/queries/query_for_item.rq index 2957b9b5b..fdc349d18 100644 --- a/app/src/main/resources/queries/query_for_item.rq +++ b/app/src/main/resources/queries/query_for_item.rq @@ -1,50 +1,32 @@ SELECT ?item - (SAMPLE(?label) AS ?label) - (SAMPLE(?class) AS ?class) - (SAMPLE(?description) AS ?description) - (SAMPLE(?classLabel) AS ?classLabel) - (SAMPLE(?pic) AS ?pic) - (SAMPLE(?destroyed) AS ?destroyed) - (SAMPLE(?endTime) AS ?endTime) - (SAMPLE(?wikipediaArticle) AS ?wikipediaArticle) - (SAMPLE(?commonsArticle) AS ?commonsArticle) - (SAMPLE(?commonsCategory) AS ?commonsCategory) - (SAMPLE(?dateOfOfficialClosure) AS ?dateOfOfficialClosure) - (SAMPLE(?pointInTime) AS ?pointInTime) + ?itemLabel + ?itemDescription + ?class + ?classLabel + ?pic + ?destroyed + ?endTime + ?wikipediaArticle + ?commonsArticle + ?commonsCategory + ?dateOfOfficialClosure + ?pointInTime WHERE { SERVICE { - values ?item { - ${ENTITY} - } + VALUES ?item {${ENTITY}} } - # Get the label in the preferred language of the user, or any other language if no label is available in that language. - OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage. FILTER (lang(?itemLabelPreferredLanguage) = "${LANG}")} - OPTIONAL {?item rdfs:label ?itemLabelAnyLanguage} - BIND(COALESCE(?itemLabelPreferredLanguage, ?itemLabelAnyLanguage, "?") as ?label) + # Get item label/class label/description in the preferred language of the user, or fallback. + SERVICE wikibase:label { bd:serviceParam wikibase:language "${LANG},en,aa,ab,ae,af,ak,am,an,ar,as,av,ay,az,ba,be,bg,bh,bi,bm,bn,bo,br,bs,ca,ce,ch,co,cr,cs,cu,cv,cy,da,de,dv,dz,ee,el,eo,es,et,eu,fa,ff,fi,fj,fo,fr,fy,ga,gd,gl,gn,gu,gv,ha,he,hi,ho,hr,ht,hu,hy,hz,ia,id,ie,ig,ii,ik,io,is,it,iu,ja,jv,ka,kg,ki,kj,kk,kl,km,kn,ko,kr,ks,ku,kv,kw,ky,la,lb,lg,li,ln,lo,lt,lu,lv,mg,mh,mi,mk,ml,mn,mo,mr,ms,mt,my,na,nb,nd,ne,ng,nl,nn,no,ny,oc,oj,om,or,os,pa,pi,pl,ps,pt,qu,rm,rn,ro,ru,rw,sa,sc,sd,se,sg,sh,si,sk,sl,sm,sn,so,sq,sr,ss,st,su,sv,sw,ta,te,tg,th,ti,tk,tl,tn,to,tr,ts,tt,tw,ty,ug,uk,ur,uz,ve,vi,vo,wa,wo,xh,yi,yo,za,zh,zu". } - # Get the description in the preferred language of the user, or any other language if no description is available in that language. - OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage. FILTER (lang(?itemDescriptionPreferredLanguage) = "${LANG}")} - OPTIONAL {?item schema:description ?itemDescriptionAnyLanguage} - BIND(COALESCE(?itemDescriptionPreferredLanguage, ?itemDescriptionAnyLanguage, "?") as ?description) + # Get class (such as forest or bridge) + OPTIONAL {?item p:P31/ps:P31 ?class} - # Get the class label in the preferred language of the user, or any other language if no label is available in that language. - OPTIONAL { - ?item p:P31/ps:P31 ?class. - OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage. FILTER (lang(?classLabelPreferredLanguage) = "${LANG}")} - OPTIONAL {?class rdfs:label ?classLabelAnyLanguage} - BIND(COALESCE(?classLabelPreferredLanguage, ?classLabelAnyLanguage, "?") as ?classLabel) - } - - OPTIONAL { - ?item p:P31/ps:P31 ?class. - } - - # Get picture + # Get picture (items without a picture will be shown in red on the Nearby map) OPTIONAL {?item wdt:P18 ?pic} - # Get existence + # Get existence (whether an item still exists or not) OPTIONAL {?item wdt:P576 ?destroyed} OPTIONAL {?item wdt:P582 ?endTime} OPTIONAL {?item wdt:P3999 ?dateOfOfficialClosure} @@ -64,5 +46,4 @@ WHERE { ?commonsArticle schema:about ?item. ?commonsArticle schema:isPartOf . } -} -GROUP BY ?item \ No newline at end of file +} \ No newline at end of file From d0e95bc3c23137730b501be0fbc711711c3194cc Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 7 Aug 2025 14:02:05 +0200 Subject: [PATCH 17/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-kn/strings.xml | 16 ++++++++++++++++ app/src/main/res/values-qq/strings.xml | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 5e0e0c602..7ca0b0ea0 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -2,6 +2,7 @@ ಕಾಮನ್ಸ್ ಫೇಸ್ಬುಕ್ ಪುಟ + ಕಾಮನ್ಸ್‌ನ ಗಿಟ್‍ಹಬ್ ಮೂಲ ಕೋಡ್ + ಕಾಮನ್ಸ್‌ ಲಾಂಛನ ಕಾಮನ್ಸ್ ಜಾಲತಾಣ + ಸ್ಥಳ ಆಯ್ಕೆಯಿಂದ ನಿರ್ಗಮಿಸಿ ಸಲ್ಲಿಸಿ + ಇನ್ನೊಂದು ವಿವರಣೆಯನ್ನು ಸೇರಿಸಿ + ಹೊಸ ಕೊಡುಗೆಗಳನ್ನು ಸೇರಿಸಿ + ಕ್ಯಾಮೆರಾದಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ + ಫೋಟೋಗಳಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ + ಹಿಂದಿನ ಕೊಡುಗೆಗಳ ಗ್ಯಾಲರಿಯಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ + ತಲೆಬರಹ + ಭಾಷಾ ವಿವರಣೆ + ತಲೆಬರಹ + ವಿವರಣೆ + ಚಿತ್ರ + ಎಲ್ಲಾ + ಮೇಲಕ್ಕೆ ಟಾಗಲ್ ಮಾಡಿ ದಿನದ ಚಿತ್ರ %1$d ಕಡತ ಅಪ್ಲೋಡ್ ಅಗುತ್ತಿದೆ diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 1f80d9946..887b392b7 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -6,6 +6,7 @@ * Annick green * Cabal * Googology +* H78c67c * LeGuyanaisPure * Liuxinyu970226 * Madhurgupta10 @@ -28,7 +29,7 @@ {{Identical|Submit}} {{identical|All}} - Reba ishakiro + 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}} From 5201af70cdf1e30139d6f29f240461f8f3f3591e Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 11 Aug 2025 14:01:53 +0200 Subject: [PATCH 18/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-io/strings.xml | 12 +++ app/src/main/res/values-min/error.xml | 10 +++ app/src/main/res/values-ps/strings.xml | 116 ++++++++++++++++++++++++- app/src/main/res/values-sh/strings.xml | 4 +- app/src/main/res/values-uk/strings.xml | 3 +- 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/values-min/error.xml diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 0c41d5492..8ba3b61d4 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -649,13 +649,18 @@ Pauzanta sendajo... Nuliganta sendajo... Cesar kargajo + Vu kapabligesis l\'uzo di limitizita konekto. Omna senduri pauzesis e durigos nur kande vu deskapabligos ta uzo. Kapabligesis por uzar limitizita konekti. Voluntez skribar kurta titulo deskriptanta quon vua imajo montras. En la deskripto, explikez pro quo la fotografuro esas interesanta, tipala o rara, ed explikez la kuntexto, videbla o ne. Skriptez tan exakta kam posibla. + Voluntez trovar e selektar omna konceptaji quan ca imajo reprezentas. Esez plu preciza kam vu povas. Se ta imajo montras diversa kozi, selektez precize omna ek li. Ne uzez nepreciza deskripturi, se specifika deskripturi existas. + Voluntez selektar la kategorii konvenanta. Diferante de deskripturi, kategorii nur existas en Angla linguo. + En Commons, vua imaji povos riuzesar ed adaptesar da omni. Ka vu deziras renuncar omna autoroyuri? Ka vu deziras ke l\'imajo atribuesos a vu? Ka vu deziras adapti por uzar la sama licenco? Montras Licencizo di \'\'media\'\' Detali pri \'\'media\'\' Vidar kategorio-pagino Vidar pagino dil arkivo + Idiomo di vua interfacio Removar titulo e deskripto Lektez pluse En omna idiomi @@ -676,6 +681,7 @@ Facita Retroirar Bonveno a personalizita selektilo di imaji + Ica selektilo montras quala imaji vu ja sendis a Commons. Ecelanta Ca imajo ja sendesis a Commons. Por teknikala motivi, l\'utensilo \'\'app\'\' ne povas fidinde sendar plua kam %1$d pikturi samatempe. La limito %1$d superesis per %2$d. @@ -767,9 +773,13 @@ %d imajo selektita %d imaji selektita + Voluntez facar kelka komenti Diskuto Dicez irgu pri l\'arkivo \'%1$s\'. Ol esos videbla publike. + \'%1$s\' ne pluse existas, nula imajo povos rekuperesar de ol. \'%1$s\' esas en diferanta loko. + \'%1$s\' esas en diferanta loko. Voluntez mencionar la korekta loko adinfre e, se posibla, skribez la korekta latitudo e longitudo. + Altra problemo od informo (voluntez explikar adinfre). Extinganta la tota sendaji... Arkivi sendita Vartanta @@ -799,6 +809,8 @@ Deskripto-texto Deskripto-texto kopiita a \'\'clipboard\'\' Gratuli! Omna imaji en ca albumo sive sendesis, sive indikesis por ne sendar. + Montrez en Proxima (\'\'Nearby\'\') Kreesis e sendesis da: %1$s Kreita da %1$s e sendita da %2$s + Indikita por Efaco diff --git a/app/src/main/res/values-min/error.xml b/app/src/main/res/values-min/error.xml new file mode 100644 index 000000000..11f57f87d --- /dev/null +++ b/app/src/main/res/values-min/error.xml @@ -0,0 +1,10 @@ + + + + Commons bamasalah + Ups. Ado nan salah! + Caritokan apo nan sanak karajoan, sudah tu bagikan lewat email ka kami. Akan kami bantu mamelokannyo! + Tarimo kasih! + diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 4bfaea5ab..e37dfa4bb 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -233,6 +233,9 @@ ويکيپېډيا ليکنه انځور ډېر تياره دی. انځور ډېر جړ دی. + تېرېدل + ننوتل + آیا تاسو رښتیا غواړئ چې ننوتل پرېږدئ؟ تگلوري ويکي‌اومتوک ويکيپېډيا @@ -274,15 +277,39 @@ موبايل له لارې راپورته‌شوی نخشه انځور په ویکي‌اومتوک کې %1$s ته ورگډ شو! + پوښتنه + پايله + پرله‌پورې + سم ځواب + ناسم ځواب + کاریال وېشل + تاوول + ايا تاسو ډاډه ياست چې دا راپورته‌کول ناگارل غواړئ؟ + ايا تاسو غواړئ چې دا لټون ړنگ کړئ؟ + د پلټنې پېښليک ړنگ شو + ړنگولو ته نومول + ړنگول + لاسته‌راوړنې پېژنيال مټ‌تړوني شمارنې - مننه ترلاسه‌شو + ترلاسه‌شوې مننې ټاکلی انځور انځورنه د \"څېرمه ځايونو\" له لارې + کچه %d + %s (کچه %s) انځورونه راپورته‌شول انځورونه په څټ‌گرځول‌شوي نه دي کارول‌شوي انځورونه + تېروتنه رامنځته شوه! + راتلونکی + مخکنی + انځورونه + وېشنيزې + په کتاب‌نښو کې ورگډول/لرې‌کول + کتاب‌‌نښې + تاسو هېڅ کتاب‌نښې نه دې ورگډې‌کړې + کتاب‌نښې پای ته رسېږي په: ټاکنيزې‌سيالۍ ښکاره‌کول روانې ټاکنيزې‌سيالۍ وگورئ @@ -310,6 +337,93 @@ ايا دا د منلو وړ دي؟ ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟ که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ. + ليکوال + لمېسل‌رېښتې + ځای + کامرې نمونه + د لړۍ شمېرې + پوستغالی + د رسنيو ځای ته لاسرسی رد شو + کاريال د...لارې وېشل + انځور مالومات + هېڅ وېشنيزې ونه موندل شوې + نښان + انځور وېشل په وسيله د + تياره + روښانه + ځای‌ښودنه بلول + نور بارول + تاييدول + لارښوونې + ۱. لاندې ويکي‌ليک وکاروئ: + درول‌ + بياپيلول + درول‌شوی + نور + کتاب‌‌نښې + لاسته‌راوړنې + سرمشريزه + درجه: + شمېر: + درجه + کارن + شمېر + د سرمشريزې ځان‌بڼې په توگه اوڼل + ځان‌بڼې په توگه اوڼل کېږي، مهرباني وکړئ په تمه شئ + ځان‌بڼې ټولگه + ځان‌بڼې په توگه اوڼل + کلنی + اوونيز + هرمهاله + راپورته‌کول + څېرمه + کارول‌شوی + زما رتبه + محدودې نښلېدا ونگ‌ډول چارن‌شوی! + ښه انځورونه + انځور ځی + نښکه + څرگنداوی + توکي + دوديز ټاکونکی + انځورونه نشته + وشو + پر شا کېدل + دوديزه انځور پاکوونکي ته ښه راغلاست + په‌زړه‌پورې + تړل + کارن پېژنيال کتل + وېشنيزې سمول + پلي‌کول + له‌سره‌اوڼل ځای اومتوکي له ويکي کارنانو سره مرسته کوي چې ستاسو انځور موندلو او لا ډېر گټور کولو کې مرسته کوي.\nستاسو وروستۍ راپورته‌کېدنې ځای نه لري.\nموږ تاسو ته سپارښته کوو چې خپل د ځای ښودنه د کامرې په کاريال په اوڼنو کې بل کړئ.\nله راپورته کولو مو مننه! + هېڅ ځای ونه موندل شو + ځای ورگډول + سپيناوی + وسيلې نوم + له غبرگون ورکولو مو مننه + انځور سمول + ځای سمول + ځای هم‌مهاله‌شو + ځای لرې‌کول + ځای گواښنه لرې‌کول + راپورته‌کېدنې + په تمه + پاتې راغلی + ځای اومتوکي نشي بارېدای + ړنگول تایید کړئ + ړنگول + ناگارل + د بارولو پرمهال تېروتنه + هېڅ کاره‌ونه ونه موندل شوه + خونديځ + نورې ويکي‌گانې + دوتنې کارېدنې + يواړخيزه‌وېب‌کتنې‌چاره گڼون + گڼون له منځه وړل + د گڼون له منځه وړلو گواښنه + نيونگ + نيونگ ټينگدړې ته ولمېسل شو + ړنگولو ته نومول‌شوې diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 9fbec8e17..305fc9a7e 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -94,7 +94,7 @@ Otpremi Pretraži kategorije Snimi - Preučitaj + Osvježi Lista (Još uvijek nema postavljenih datoteka) Nema kategorija što odgovoraju %1$s @@ -154,7 +154,7 @@ Nema opisa Nema razgovora Nepoznata licenca - Preučitaj + Osvježi Potrebno ovlaštenje: Čitanje vanjske memorije. Aplikacija ne može pristupiti galeriji bez toga. Potrebno ovlaštenje: Pisanje u vanjskoj memoriji. Aplikacija ne može pristupiti kameri/galeriji bez toga. U redu diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ce7963b60..6c5d79273 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -243,6 +243,7 @@ Опис Обговорення Автор + Завантажувач Дата завантаження Ліцензія Координати @@ -792,7 +793,7 @@ Для роботи потрібні дозволи Дізнайтеся, як написати корисний опис Дізнайтеся, як написати корисний підпис - Перегляньте свої досягнення + Перегляньте свої досягнення Редагувати зображення Редагувати розташування Розташування оновлено From 1d7d2801e41d3303a25eba9ad2b93d1896f90454 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 14 Aug 2025 14:01:59 +0200 Subject: [PATCH 19/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-fi/strings.xml | 121 ++++++++++++++++++------- app/src/main/res/values-ps/strings.xml | 3 + 2 files changed, 91 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index e640bf981..8d66a8267 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -30,6 +30,9 @@ Poistu sijainnin valitsimesta Lähetä Lisää toinen kuvaus + Lisää uusi tiedosto + Lisää uusi tiedosto kameralla + Lisää uusi tiedosto kuvista Kuvatekstit Kielen kuvaus Kuvateksti @@ -45,7 +48,7 @@ (%1$d) (%1$d) - Aloitetaan latauksia + Aloitetaan tallennuksia Käsitellään %d tallennus Käsitellään %d tallennusta @@ -87,7 +90,7 @@ Poistetaanko akun optimointi käytöstä? Tunnistautuminen epäonnistui, kirjaudu uudelleen sisään Tallentaminen aloitettiin! - Lataus jonossa (rajoitettu yhteystila käytössä) + Tallennus on jonossa (rajoitettu yhteystila käytössä) %1$s tallennettiin! Napauta katsoaksesi tallennusta Kopioidaan palvelimelle: %s @@ -106,6 +109,8 @@ Ota kuva Lähistöllä Omat tallennukset + Kopioi linkki + Linkki on kopioitu leikepöydälle. Jaa Näytä tiedostosivu Kuvateksti (vaaditaan) @@ -122,13 +127,14 @@ Muutokset Tallenna Etsi luokkia - Hae kohteita, joita mediasi kuvaa (vuori, Taj Mahal jne.) + Hae kohteita, joita mediasi esittää (vuori, Taj Mahal jne.) Tallenna + Ylivuotovalikko Päivitä Lista (Ei vielä tallennuksia) Luokkaa %1$s ei löytynyt - Wikidata-kohteita ei löytynyt + Hakusanaa %1$s vastaavia Wikidata-kohteita ei löytynyt %1$s ei ole lapsiluokkia %1$s ei ole vanhempia luokkia Lisää luokkia tehdäksesi kuvistasi enemmän löydettäviä Wikimedia Commonssissa.\nAloita kirjoittaminen lisätäksesi luokkia. @@ -136,6 +142,7 @@ Asetukset Rekisteröidy Suositellut kuvat + Mukautettu valitsin Luokka Vertaisarviointi Tietoja @@ -151,7 +158,7 @@ Et ole vielä tallentanut kuvia. Yritä uudelleen Peruuta - Lisäämällä kuvan, ilmoitan tämän olevan oma työ ja että se ei sisällä tekijänoikeuden alaista materiaalia tai selfietä ja muuten noudattaa <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Commons policies</a>. + Lisäämällä kuvan, ilmoitan tämän olevan oma työ ja että se ei sisällä tekijänoikeuden alaista materiaalia tai selfietä ja muuten noudattaa <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Commonsin käytäntöjä</a>. Lataa Oletuslisenssi Käytä edellistä otsikkoa ja kuvausta @@ -174,7 +181,7 @@ ÄLÄ tallenna seuraavia: Selfiet tai kuvat ystävistäsi Kuvia, jotka olet ladannut Internetistä - Kuvakaappaukset omistamistasi sovelluksista + Kuvakaappaukset muiden omistamista sovelluksista Tallennusesimerkki: Otsikko: Sydneyn oopperatalo Kuvaus: Sydneyn oopperatalo lahden toiselta puolelta katsottuna @@ -200,15 +207,17 @@ Pyydetään sijaintilupaa OK Varoitus + Vastaava tiedostonimi löytyi Tallenna Kyllä Ei Kuvateksti Otsikko - Kuvaukset + Esittää-tunnisteet Kuvaus Keskustelu Tekijä + Tallentaja Tallennuspäivämäärä Lisenssi Koordinaatit @@ -216,6 +225,7 @@ Ryhdy beetatestaajaksi Valitse beeta-kanavamme Google Playssa ja hanki varhainen pääsy uusiin ominaisuuksiin ja virheenkorjauksiin Kaksivaiheisen tunnistautumisen koodi + Sähköpostivahvistuskoodi Haluatko varmasti kirjautua ulos? Mediakuva epäonnistui Alaluokkia ei löytynyt @@ -236,6 +246,7 @@ Tietoja Asetukset Palaute + Palaute GitHubissa Kirjaudu ulos Opas Ilmoitukset @@ -270,14 +281,14 @@ Ohita Kirjaudu sisään Haluatko todella ohittaa kirjautumisen? - Sinun täytyy kirjautua sisään tallentaaksesi kuvia tulevaisuudessa. + Sinun täytyy kirjautua sisään tallentaaksesi kuvia tulevaisuudessa. Kirjaudu sisään käyttääksesi tätä ominaisuutta Kopioi wikiteksti leikepöydälle Wikiteksti kopioitiin leikepöydälle - Nearby ei välttämättä toimi, sillä sijainti ei käytettävissä. + Lähistöllä-toiminto ei välttämättä toimi kunnolla, sillä sijainti ei käytettävissä. Sijainnin käyttö kielletty. Aseta sijaintisi manuaalisesti käyttääksesi tätä ominaisuutta. - Lupa vaaditaan läheisten paikkojen luettelon näyttämiseen - Lupa vaaditaan läheltä otettujen kuvien luettelon näyttämiseen + Lupa vaaditaan lähellä olevien paikkojen luettelon näyttämiseen + Lupa vaaditaan lähellä otettujen kuvien luettelon näyttämiseen Reitit Wikidata Wikipedia @@ -312,7 +323,7 @@ Äskettäiset haut: Äskettäin haetut kyselyt Luokkia ladattaessa tapahtui virhe. - Virhe ladattaessa kuvauksia. + Virhe ladattaessa esittää-tunnisteita. Media Luokat Kohteet @@ -327,8 +338,8 @@ Onko tämä kuva OK tallennettavaksi? Kysymys Tulos - Jos jatkat poistettavien kuvien lataamista, tilisi todennäköisesti kielletään. Haluatko varmasti lopettaa tietokilpailun? - Yli %1$s tallentamistasi kuvista on poistettu. Mikäli jatkat poistamista vaativien kuvien lataamista, tilisi todennäköisesti estetään.\n\nHaluatko tutustua oppaaseen uudelleen ja tehdä sen jälkeen tietovisan oppiaksesi minkälaisia kuvia saa ja ei saa tallentaa? + Jos jatkat poistettavien kuvien tallentamista, tunnuksesi tullaan todennäköisesti estämään. Haluatko varmasti lopettaa tietokilpailun? + Yli %1$s tallentamistasi kuvista on poistettu. Mikäli jatkat poistamista vaativien kuvien tallentamista, tunnuksesi tullaan todennäköisesti estämään.\n\nHaluatko tutustua oppaaseen uudelleen ja tehdä sen jälkeen tietovisan oppiaksesi, minkälaisia kuvia saa ja ei saa tallentaa? Selfieillä ei ole paljoa arvoa tietosanakirjassa. Älä tallenna kuvaa itsestäsi, ellei sinusta jo ole Wikipedia-artikkelia. Monumenteista ja maisemista otetut kuvat ovat hyväksyttäviä tallennettavaksi useimmissa maissa. Huomaa kuitenkin että ulkotiloihin sijoitetut väliaikaiset tilataideteokset ovat usein suojattu tekijänoikeudella ja niistä otettuja kuvia ei usein saa tallentaa. Kuvakaappauksia sivustoista tulkitaan jäljennöksiksi ja ovat täten sivuston kopiosuojan piirissä. Kuvia voidaan käyttää asianmukaisella tekijältä saadulta luvalla. Ilman kyseistä lupaa, mitä tahansa heidän materiaalistaan tuottamaa tuotosta tulkitaan alkuperäisen tekijän näkökulmasta luvattomaksi kopioksi. @@ -350,17 +361,18 @@ Virhe läheisiä monumentteja haettaessa. Ei viimeaikaisia hakuja Haluatko varmasti tyhjentää hakuhistoriasi? - Haluatko varmasti peruuttaa tämän latauksen? + Haluatko varmasti peruuttaa tämän tallennuksen? Haluatko poistaa tämän haun? Hakuhistoria poistettu Ehdota poistettavaksi Poista Saavutukset Profiili + Merkit Tilastot Kiitos vastaanotettu Suositellut kuvat - Kuvia läheltä + \"Lähistöllä\"-kuvat Taso %d %s (taso %s) Kuvia tallennettu @@ -369,9 +381,9 @@ Jaa saavutuksesi ystäviesi kanssa! Tasosi nousee, kun täytät nämä vaatimukset. Tilastot-osion kohteita ei lasketa tasoosi. vähimmäisvaatimus: - Lähetettyjen kuvien määrä Commonsiin minkä tahansa latausohjelmiston kautta + Lähetettyjen kuvien määrä Commonsiin minkä tahansa tallennusohjelmiston kautta Niiden kuvien prosenttiosuus, jotka olet ladannut Commonsiin ja joita ei poistettu - Wikimedia-artikkeleissa käytettyjen Commonsiin lataamiesi kuvien määrä + Wikimedia-artikkeleissa käytettyjen Commonsiin tallentamiesi kuvien määrä Tapahtui virhe! Commons-ilmoitus Käytä mukautettua tekijän nimeä @@ -385,7 +397,7 @@ Näytä sovelluksen sisäinen ilmoitus lähinnä kuvia tarvitsevasta paikasta Lista Tallennuslupa - Tarvitsemme luvan käyttääksesi laitteen ulkoista tallennustilaa kuvien lataamista varten. + Tarvitsemme luvan käyttääksesi laitteen ulkoista tallennustilaa kuvien tallentamista varten. Et enää näe lähellä olevia paikkoja, jotka tarvitsevat kuvia. Voit kuitenkin halutessasi ottaa tämän ilmoituksen uudelleen käyttöön asetuksissa. Vaihe %1$d %2$d: %3$s Seuraava @@ -394,6 +406,7 @@ Laitteestasi ei löydy yhteensopivaa karttasovellusta. Asenna karttasovellus käyttääksesi tätä toimintoa. Kuvat Sijainnit + Luokat Lisää kirjanmerkkeihin/Poista kirjanmerkeistä Kirjanmerkit Et ole lisännyt yhtään kirjanmerkkejä @@ -408,8 +421,8 @@ Tervetuloa Commonsiin!\n\nTallenna ensimmäinen mediasi koskettamalla lisäyspainiketta. Luokkia ei valittu Kuvat, jotka eivät ole luokissa, ovat harvoin käyttökelpoisia. Haluatko varmasti jatkaa valitsematta luokkia? - Kuvauksia ei valittu - Kuvat, joissa on kuvatekstejä, löytyvät helpommin ja todennäköisemmin niitä käytetään. Haluatko varmasti jatkaa valitsematta kuvatekstejä? + Esittää-tunnisteita ei valittu + Kuvat, joissa on esittää-tunnisteita, löytyvät helpommin ja niitä käytetään todennäköisemmin. Haluatko varmasti jatkaa valitsematta esittää-tunnisteita? Peruuta tallennus Takaisin-napin painaminen peruuttaa tämän tallennuksen ja poistaa tallentamasi tiedot Jatka tallennusta @@ -467,6 +480,7 @@ Sinulla ei ole lukemattomia ilmoituksia Sinulla ei ole luettuja ilmoituksia Jaa lokit käyttämällä + Tarkista sähköpostilaatikkosi Näytä luetut Näytä lukemattomat Kuvien valinnassa tapahtui virhe @@ -485,11 +499,11 @@ Linssin malli Sarjanumerot Ohjelmisto - Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s + Tallenna kuvia Wikimedia Commonsiin suoraan puhelimeltasi. Lataa Commons-sovellus nyt: %1$s Jaa sovellus... Kuvan tiedot Luokkia ei löytynyt - Kuvauksia ei löytynyt + Esittää-tunnisteita ei löytynyt Peruutettu tallennus Miksi %1$s tulisi poistaa? %1$s oli lähettänyt: %2$s @@ -505,6 +519,7 @@ Lehdistökuva Satunnainen kuva internetistä Logo + Panoraamavapauden rikkomus Koska se on Yritetään päivittää luokkia. Luokan päivitys @@ -515,6 +530,9 @@ Ei voitu lisätä luokkia. Päivitetään luokkia + Yritetään päivittää esittää-tunnisteita. + Muokkaa esittää-tunnisteita + Esittää-tunnisteita ei voitu lisätä. Yritetään päivittää koordinaatit. Koordinaattien päivitys Kuvaus päivitetty @@ -526,7 +544,7 @@ Koordinaatteja ei voitu lisätä. Kuvauksia ei voitu lisätä. Kuvatekstiä ei voitu lisätä. - Koordinaattien haku epäonnistui. + Kuvan koordinaatteja ei tallennettu Kuvauksia ei voitu hakea. Muokkaa kuvauksia ja kuvatekstejä Jaa kuva @@ -543,8 +561,13 @@ Silta, museo, hotelli jne. Jokin meni pieleen kirjautumisessa. Sinun on nollattava salasanasi! MEDIA - Lähipaikka löytyi - Onko tämä kuva paikasta %1$s? + ALALUOKAT + YLÄLUOKAT + ALALUOKAT + YLÄLUOKAT + Lähistöllä-paikka löytyi + Ovatko nämä kuvia paikasta %1$s? + Onko tämä kuva paikasta %1$s? Kirjanmerkit Asetukset Poistettu kirjanmerkeistä @@ -592,7 +615,7 @@ Viikoittain Koko ajalta Lähetä - Lähistöltä + Lähistöllä Käyttöjä Sijani Rajoitettu yhteystila päällä! @@ -605,9 +628,9 @@ Peruuta tallennus Rajoitettu yhteystila on päällä. Kirjoita lyhyt kuvateksti. Kerro miksi kuva on kiinnostava, tyypillinen tai harvinainen ja selitä asiayhteys, näkyy se kuvassa tai ei. Käytä mahdollisimman tarkkaa terminologiaa. - Etsi ja valitse kaikki tämän kuvan kuvaamat käsitteet. Ole mahdollisimman tarkka. Mikäli kuvattuna on monta kohdetta, valitse ne kaikki kohtuullisuuden rajoissa. Älä valitse yleisiä tunnisteita mikäli tarkempia on saatavilla. - Valitse sopivat luokat. Toisin kuin kuvaukset, luokkien nimet ovat vain englanniksi. - Kuka tahansa saa käyttää ja muokata Commonsiin lataamiasi kuvia. Haluatko luovuttaa kuviesi kaikki oikeudet? Haluatko tulla nimetyksi kuvien tekijänä? Haluatko kuviesi muokattujen versioiden julkaistavan samalla lisenssillä? + Etsi ja valitse kaikki käsitteet, joita tämä kuva esittää. Ole mahdollisimman tarkka. Mikäli kuvattuna on monta kohdetta, valitse ne kaikki kohtuullisuuden rajoissa. Älä valitse yleisiä tunnisteita, mikäli tarkempia on saatavilla. + Valitse sopivat luokat. Toisin kuin esittää-tunnisteet, luokkien nimet ovat vain englanniksi. + Kuka tahansa saa käyttää ja muokata Commonsiin tallentamiasi kuvia. Haluatko luovuttaa kuviesi kaikki oikeudet? Haluatko tulla nimetyksi kuvien tekijänä? Haluatko kuviesi muokattujen versioiden julkaistavan samalla lisenssillä? Esittää Median lisenssi Median tiedot @@ -629,18 +652,19 @@ Ei kuvia Valmis Takaisin - Mahtava + Mahtavaa Tämä kuva on jo ladattu Commonsiin. LUE LISÄÄ Tarvitaan käyttöoikeus Käyttäjän muokkaukset: %s Käyttäjän saavutukset: %s - Näytä käyttäjäsivu + Näytä käyttäjäprofiili + Muokkaa esittää-tunnisteita Muokkaa luokkia Lisäasetukset Käytä Nollaa - Sijaintitiedot auttavat wikin muokkaajia löytämään kuvasi, mikä tekee siitä paljon hyödyllisemmän.\nViimeaikaisissa tallennuksissasi ei ole sijaintia.\nSuosittelemme, että otat sijainnin käyttöön kamerasovelluksesi asetuksista.\nKiitos latauksesta! + Sijaintitiedot auttavat wikin muokkaajia löytämään kuvasi, mikä tekee siitä paljon hyödyllisemmän.\nViimeaikaisissa tallennuksissasi ei ole sijaintia.\nSuosittelemme, että otat sijainnin käyttöön kamerasovelluksesi asetuksista.\nKiitos kuvien tallentamista! Paikkaa ei löytynyt Lisää paikka Tiedot @@ -669,6 +693,7 @@ Näytä omat saavutukset Muokkaa kuvaa Muokkaa sijaintia + Sijainti päivitetty! Poista sijainti Sijainti poistettu! Kiitä tekijää @@ -681,4 +706,34 @@ %d kuva valittu %d kuvaa valittu + Keskustelu + Oletko varma, että haluat peruuttaa kaikki tallennukset? + Peruutetaan kaikki tallennukset... + Tallennukset + Odottavat + Epäonnistuneet + Poista kansio + Vahvista poisto + Oletko varma, että haluat poistaa kansion %1$s, jossa on %2$d kohdetta? + Poista + Peruuta + Kansio %1$s poistettu + Kansion %1$s poistaminen epäonnistui + Tästä paikasta ei ole vielä kuvaa. Ota ihmeessä kuva! + Tästä paikasta on jo kuva. + Tarkistetaan, onko tästä paikasta kuvaa. + Virhe ladattaessa + Commons + Muut wikit + Tiedoston käyttö + Tunnus + Hävitä käyttäjätunnus + Varoitus hävittämisestä + Hävittäminen on <b>viimeinen keino</b> ja sitä tulee <b>käyttää vain, jos haluat lopettaa muokkaamisen lopullisesti</b> ja samalla myös piilottaa mahdollisimman paljon aiemmista toimistasi.<br/><br/>Käyttäjätunnuksen poistaminen Wikimedia Commonsissa tapahtuu muuttamalla tunnuksesi nimeä niin, etteivät muut pysty tunnistamaan muokkauksiasi. Tätä toimea kutsutaan käyttäjätunnuksen hävittämiseksi. <b>Hävittäminen ei takaa täyttä anonyymiyttä, eikä se poista hankkeisiin tekemiäsi muokkauksia</b>. + Kuvateksti + Kuvateksti kopioitu leikepöydälle + Näytä Tutki-välilehdellä + Luonut ja tallentanut: %1$s + Luonut %1$s ja tallentanut %2$s + Ehdotettu poistettavaksi diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index e37dfa4bb..bf757875d 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -348,6 +348,7 @@ انځور مالومات هېڅ وېشنيزې ونه موندل شوې نښان + بریالیتوب انځور وېشل په وسيله د تياره روښانه @@ -392,6 +393,7 @@ دوديزه انځور پاکوونکي ته ښه راغلاست په‌زړه‌پورې تړل + اجازې ته اړتيا لري کارن پېژنيال کتل وېشنيزې سمول پلي‌کول @@ -400,6 +402,7 @@ هېڅ ځای ونه موندل شو ځای ورگډول سپيناوی + اندرويد بلبڼه وسيلې نوم له غبرگون ورکولو مو مننه انځور سمول From 5bdfbf5f6f678090195dad9611577a6b23553f8c Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Sat, 16 Aug 2025 15:06:57 +0530 Subject: [PATCH 20/97] fix: NPE when changing theme while on profile screen (#6398) --- .../free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt index e77c24c8d..5dcdf283b 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt @@ -311,7 +311,7 @@ class LeaderboardFragment : CommonsDaggerSupportFragment() { } private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener { - override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) = + override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, i: Int, l: Long) = handler() override fun onNothingSelected(p0: AdapterView<*>?) = Unit From 5a6b3cbf09ee3d3fe6d75a1a5e76dd0e380930b0 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 18 Aug 2025 14:02:18 +0200 Subject: [PATCH 21/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-el/strings.xml | 6 ++++-- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-mnw/strings.xml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index bb7a9ea98..a95b2f890 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -232,6 +232,7 @@ Περιγραφή Συζήτηση Συγγραφέας + Ανεβαστής Ημερομηνία μεταφόρτωσης Άδεια Συντεταγμένες @@ -433,7 +434,7 @@ Συνειδητοποίησα ότι είναι κακό για την ιδιωτικότητά μου Άλλαξα γνώμη, δε θέλω να προβάλλεται πλέον δημόσια Συγγνώμη, αυτή η φωτογραφία δεν είναι ενδιαφέρουσα για μια εγκυκλοπαίδεια - Ανέβηκε από εμένα στο %1$s, χρησιμοποιήθηκε σε %2$d άρθρο/α + Ανέβηκε από εμένα στο %1$s, χρησιμοποιήθηκε σε τουλάχιστον %2$d άρθρο/α. Καλώς ήρθατε στα Commons!\n\nΑνεβάστε τα πρώτα σας πολυμέσα πατώντας το κουμπί της προσθήκης. Δεν επιλέχθηκαν κατηγορίες Οι εικόνες χωρίς κατηγορίες χρησιμοποιούνται σπάνια. Θέλετε πράγματι να συνεχίσετε δίχως να επιλέξετε κατηγορίες; @@ -773,7 +774,7 @@ Απαιτούνται δικαιώματα για τη λειτουργικότητα Μάθετε πώς να γράψετε μια χρήσιμη περιγραφή Μάθετε πώς να γράψετε μια χρήσιμη λεζάντα - Δείτε τα επιτεύγματά σας + Δείτε τα επιτεύγματά σας Επεξεργασία εικόνας Επεξεργασία τοποθεσίας Η τοποθεσία ενημερώθηκε! @@ -834,4 +835,5 @@ Εμφάνιση στα Κοντινά Δημιουργήθηκε και μεταφορτώθηκε από: %1$s Δημιουργήθηκε από %1$s και μεταφορτώθηκε από %2$s + Προτάθηκε για Διαγραφή diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 75cb51542..1b7b19b3e 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -271,7 +271,7 @@ Texto wiki copiado ó portapapeis A localización non está dispoñible. A identificación de sitios próximos pode non funcionar correctamente. Precísase permiso para amosar unha lista de lugares preto de aquí - COMO CHEGAR + Indicacións WIKIDATA Wikipedia COMMONS diff --git a/app/src/main/res/values-mnw/strings.xml b/app/src/main/res/values-mnw/strings.xml index a6c18bca3..4d7b2ed37 100644 --- a/app/src/main/res/values-mnw/strings.xml +++ b/app/src/main/res/values-mnw/strings.xml @@ -226,7 +226,7 @@ ဗဒင်ဏအ် ဟွံကၠောန်ကမၠောန် ဗွဲဓမ္မတာ၊ ဒၞာဲဒတန် ဟွံသၟဟ်အစောမ်။ အာတ်မိက်ဒၟံင် အခေါင် သွက်ဂွံထ္ၜး စရင်ဒၞာဲဒတန် ဗဒင်ဗဒင် စမၞောန်ဂမၠိုင် - ဝဳကဳဒါတာ + ဝဳကဳတင်ဂၞင် ဝဳကဳပဳဒဳယာ ခမ်မောန် ကဵုင္ၚုဟ် ကုပိုယ် From a892aa6deee67651e8ff65a522175db2866f1864 Mon Sep 17 00:00:00 2001 From: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:16:02 +0530 Subject: [PATCH 22/97] 6357: Fix java.lang.SecurityException for multi-uploads (#6402) * Fix java.lang.SecurityException for ACTION_OPEN_DOCUMENT * Handle SecurityException in case of multi-upload * Remove unused import * Clean up code * Clean up code * Handle SecurityException for other upload methods * Release persisted URI permissions for successful uploads * Remove persistable permission for custom picker as it's not required * Remove persistable permission for in-app camera as it's not required --- .../free/nrw/commons/filepicker/FilePicker.kt | 18 ++++++++++++++---- .../nrw/commons/upload/worker/UploadWorker.kt | 6 ++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt index bb0a371e1..a7e3a671d 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt @@ -296,10 +296,19 @@ object FilePicker : Constants { * https://github.com/commons-app/apps-android-commons/issues/6357 */ private fun takePersistableUriPermissions(context: Context, result: ActivityResult) { - result.data?.data?.also { uri -> - val takeFlags: Int = (Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - context.contentResolver.takePersistableUriPermission(uri, takeFlags) + result.data?.let { intentData -> + val takeFlags: Int = (Intent.FLAG_GRANT_READ_URI_PERMISSION) + // Persist the URI permission for all URIs in the clip data + // if multiple images are selected, + // or for the single URI if only one image is selected + intentData.clipData?.let { clipData -> + for (i in 0 until clipData.itemCount) { + context.contentResolver.takePersistableUriPermission( + clipData.getItemAt(i).uri, takeFlags) + } + } ?: intentData.data?.let { uri -> + context.contentResolver.takePersistableUriPermission(uri, takeFlags) + } } } @@ -358,6 +367,7 @@ object FilePicker : Constants { callbacks: Callbacks ) { if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) { + takePersistableUriPermissions(activity, result) try { val files = getFilesFromGalleryPictures(result.data, activity) callbacks.onImagesPicked(files, ImageSource.GALLERY, restoreType(activity)) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index c8a1d9b98..21db20f1b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -393,6 +393,12 @@ class UploadWorker( makeWikiDataEdit(uploadResult, contribution) } showSuccessNotification(contribution) + if (appContext.contentResolver.persistedUriPermissions.any { + it.uri == contribution.contentUri }) { + appContext.contentResolver.releasePersistableUriPermission( + contribution.contentUri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } } else { Timber.e("Stash Upload failed") showFailedNotification(contribution) From b8a558303b67e5f627070b653135eb553edb90af Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 21 Aug 2025 14:02:13 +0200 Subject: [PATCH 23/97] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-gl/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-nqo/strings.xml | 1 + app/src/main/res/values-ps/strings.xml | 22 ++++++++ app/src/main/res/values-pt/strings.xml | 67 ++++++++++++++----------- 5 files changed, 62 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1b7b19b3e..8025813fc 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -354,6 +354,7 @@ Imaxes destacadas Imaxes vía \"Lugares próximos\" Nivel %d + %s (nivel %s) Imaxes cargadas Imaxes non revertidas Imaxes usadas diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d297d07ac..8872daaa0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -141,7 +141,7 @@ %1$s와(과) 일치하는 분류를 찾을 수 없습니다 %1$s에 대한 위키데이터 검색 결과가 없습니다 %1$s에 자식 클래스가 없습니다 - %1$s에 부모 클래스가 없습니다 + %1$s에 상위 클래스가 없습니다 위키미디어 공용에서 그림을 더 찾기 쉽게 만들기 위해 분류를 추가합니다.\n분류를 추가하려면 입력을 시작하세요. 분류 설정 diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 99923ca5f..d8b764a67 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -322,6 +322,7 @@ ߖߌ߬ߦߊ߬ߓߍ߫ ߟߊߓߊ߯ߙߕߊ ߟߎ߬ ߖߌ߬ߦߊ߬ߓߍ ߞߊߕߙߍ߬ \"ߛߌ߰ߢߐ߲߰ ߦߙߐ\" ߡߊ߬ ߞߊߓߋ + %s (ߞߊߓߋ %s) ߖߌ߬ߦߊ߬ߓߍ ߓߘߊ߫ ߟߊߦߟߍ߬ ߖߌ߬ߦߊ߬ߓߍ ߡߊ߫ ߖߏ߰ߛߌ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߊߓߊ߯ߙߊߣߍ߲ ߠߎ߬ diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index bf757875d..f67fd0a18 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -302,14 +302,29 @@ انځورونه په څټ‌گرځول‌شوي نه دي کارول‌شوي انځورونه تېروتنه رامنځته شوه! + د ليکوال نوم دوديزول + ونډې + څېرمه + خبرتياوې + خبرتیاوې (لوستل‌شوې) + څېرمه خبرتياوې ښکاره‌کول + لړليک + زخيره کولو اجازه راتلونکی مخکنی انځورونه + ځايونه وېشنيزې په کتاب‌نښو کې ورگډول/لرې‌کول کتاب‌‌نښې تاسو هېڅ کتاب‌نښې نه دې ورگډې‌کړې کتاب‌نښې + په تېروتنې سره مې راپورته‌کړی دی + زه نه پوهېدم چې دا به ټولو ته ښکاره شي + زه پوه شوم چې دا زما د پټنتيا لپاره بد دی + زما اند توپير وکړ، زه نه غواړم چې دا نور په ټوليزه توگه ښکاره شي + په بښنې سره دا انځور د يو پوهنغونډ لپاره خواپورې نه دی + هېڅ وېشنيزې نه دې ټاکل شوې پای ته رسېږي په: ټاکنيزې‌سيالۍ ښکاره‌کول روانې ټاکنيزې‌سيالۍ وگورئ @@ -337,6 +352,8 @@ ايا دا د منلو وړ دي؟ ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟ که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ. + بل انځور + هو، ولې نه ليکوال لمېسل‌رېښتې ځای @@ -348,7 +365,12 @@ انځور مالومات هېڅ وېشنيزې ونه موندل شوې نښان + نيونگ هم‌مهالول بریالیتوب + همغږيتوبونه %1$s ورگډ شول. + څرگنداوي ورگډل شول. + نيونگ ورگډ شو. + همغږيتوبونه نشي ورگډېدای. انځور وېشل په وسيله د تياره روښانه diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 659c2705a..c58323b30 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -44,29 +44,29 @@ Estado do Local Imagem do Dia - a carregar %1$d ficheiro - a carregar %1$d ficheiros + a enviar %1$d ficheiro + a enviar %1$d ficheiros (%1$d) (%1$d) - A iniciar carregamentos + A iniciar envios - A processar %d carregamento - A processar %d carregamentos + A processar %d envio + A processar %d envios - %d carregamento - %d carregamentos + %d envio + %d envios Esta imagem será licenciada com a %1$s Estas imagens serão licenciadas com a %1$s - %1$d carregamento - %1$d carregamentos + %1$d envio + %1$d envios A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo @@ -83,8 +83,8 @@ Envio em progresso Nome de utilizador Palavra-passe - Entrar na sua conta da wiki Commons Beta - Entrar + Inicie a sessão na sua conta de Commons Beta + Iniciar sessão Esqueceu-se da palavra-passe? Registar-se A iniciar sessão @@ -99,27 +99,27 @@ O carregamento de mais de três imagens funciona de maneira mais fiável quando a otimização da bateria está desligada. Desligue a otimização da bateria para a aplicação Commons nas configurações, de forma a ter uma experiência de carregamento mais fluida. \n\nPossíveis passos para desativar a otimização da bateria:\n\nEtapa 1: premir o botão \'Configurações\' abaixo.\n\nEtapa 2: mudar de \'Não otimizado\' para \'Todas as aplicações\'.\n\nEtapa 3: pesquisar \"Commons\" ou \"fr.free.nrw.commons\".\n\nEtapa 4: premir esta e selecionar \'Não otimizar\'.\n\nEtapa 5: pressionar \'Concluído\'. Falha na autenticação. Por favor faça login novamente. Carregamento iniciado! - Fila de carregamento (modo de ligação limitada ativado) - %1$s carregado! - Toque para ver o seu carregamento - A carregar o ficheiro %s - A carregar %1$s - A terminar o carregamento de %1$s - O carregamento de %1$s falhou - Carregamento de %1$s em pausa + Envio em fila (modo de ligação limitada ativado) + %1$s enviado! + Toque para ver o seu envio + A enviar o ficheiro %s + A enviar %1$s + A terminar o envio de %1$s + O envio de %1$s falhou + Envio de %1$s pausado Tocar para ver Tocar para ver - Carregamentos recentes - Em espera - Falhado - %1$d%% transferido - A carregar + Meus Envios Recentes + Em fila + Falhou + %1$d%% concluído + A enviar Da galeria - Tirar foto + Tirar fotografia Nas redondezas - Carregamentos - Copiar ligação - A ligação foi copiada para a área de transferência + Meus envios + Copiar hiperligação + A hiperligação foi copiada para a área de transferência Partilhar Ver página do ficheiro Legenda (obrigatória) @@ -132,13 +132,14 @@ Tem de fornecer o seu código de autenticação de dois fatores. Foi enviado um código de verificação de autenticação para o seu endereço de correio eletrónico. Por favor, forneça o código para iniciar a sessão. O início de sessão falhou - Carregar + Enviar Dê um nome a este conjunto Modificações Carregar Pesquisar categorias Procurar elementos que o seu conteúdo multimédia retrata (montanha, o Taj Mahal, etc.) Gravar + Menu de fluxo Atualizar Lista (Ainda não foi carregado nenhum ficheiro) @@ -148,7 +149,7 @@ %1$s não tem nenhuma classe progenitora Adicione categorias para tornar as suas imagens mais fáceis de encontrar na wiki Wikimedia Commons.\nComece a escrever para adicionar categorias. Categorias - Configurações + Definições Registar-se Imagens destacadas Seletor personalizado @@ -784,17 +785,21 @@ Ficheiro guardado com sucesso Deseja abrir o ficheiro GPX? Deseja abrir o ficheiro KML? + Não foi possível guardar o ficheiro KML. + Não foi possível guardar o ficheiro GPX. Guardar Ficheiro KML Guardar Ficheiro GPX %d imagem selecionada %d imagens selecionadas + Nota sobre múltiplos envios Reporte um problema sobre este item na Wikidados Por favor, insira alguns comentários Discussão Outro problema ou informação (por favor, explique em baixo). O seu comentário é publicado na seguinte página da wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + A cancelar todos os envios... Envios Pendente Falhou @@ -803,6 +808,8 @@ Confirmar Eliminação Eliminar Cancelar + Erro ao carregar + Não foram encontradas utilizações Commons Outras wikis Utilização de ficheiro From 718c466505317e14b8d03301e35d590ddd83b4f3 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Sat, 23 Aug 2025 12:27:37 +0530 Subject: [PATCH 24/97] Bump target sdk to API 35 and make the app UI compatible with edge to edge (#6393) * chore: upgrade target SDK and refactor function signatures to resolve build issues * chore: bump android gradle plugin version * chore(ui): add extension functions for applying edge to edge insets * fix: apply system bar top and bottom insets for edge to edge * fix: force edge to edge for backward compatibility and consistent UI * fix: apply top bar insets as padding and make the status bar color white Since the toolbars have primary color as bg, we should make the status bar white * chore: bump robolectric version for API 35 compatibility * fix: preserve existing margins when adding new insets * feat(customselector): improve RecyclerView edge-to-edge inset handling It allows the last item to sits above the navigation bar while preserving edge-to-edge appearance. * feat(notification): improve RecyclerView edge-to-edge insets handling Also, refactor LocationPicker and DescriptionEdit activities to use extension functions and reduce duplication * fix(quiz): enable and handle edge-to-edge insets and status icon colors * fix: bottom insets not dispatched on all API versions consistently Upgraded core-ktx version installCompatInsetsDispatch wasn't available on current version * fix: return fallback value when versionName is null Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: resolve compilation errors * docs: add KDoc for edge-to-edge insets utility functions * fix(SearchActivity): apply insets for system bars * fix(util): add utility function to handle keyboard insets with animation * fix(upload): handle keyboard insets for upload media detail card view * fix(login): hadle IME insets and make edge-to-edge backward compatible --------- Co-authored-by: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/build.gradle.kts | 4 +- .../java/fr/free/nrw/commons/AboutActivity.kt | 2 + .../fr/free/nrw/commons/WelcomeActivity.kt | 2 + .../fr/free/nrw/commons/auth/LoginActivity.kt | 10 + .../free/nrw/commons/auth/SignupActivity.kt | 2 + .../category/CategoryDetailsActivity.kt | 2 + .../nrw/commons/contributions/MainActivity.kt | 2 + .../ui/selector/CustomSelectorActivity.kt | 6 + .../ui/selector/FolderFragment.kt | 2 + .../ui/selector/ImageFragment.kt | 2 + .../description/DescriptionEditActivity.kt | 7 + .../nrw/commons/explore/SearchActivity.kt | 2 + .../depictions/WikidataItemDetailsActivity.kt | 2 + .../locationpicker/LocationPickerActivity.kt | 7 + .../notification/NotificationActivity.kt | 6 + .../nrw/commons/profile/ProfileActivity.kt | 2 + .../fr/free/nrw/commons/quiz/QuizActivity.kt | 7 + .../nrw/commons/quiz/QuizResultActivity.kt | 7 + .../free/nrw/commons/review/ReviewActivity.kt | 2 + .../nrw/commons/settings/SettingsActivity.kt | 2 + .../fr/free/nrw/commons/theme/BaseActivity.kt | 2 + .../free/nrw/commons/upload/UploadActivity.kt | 2 + .../commons/upload/UploadProgressActivity.kt | 2 + .../mediaDetails/UploadMediaDetailFragment.kt | 2 + .../fr/free/nrw/commons/utils/ConfigUtils.kt | 4 +- .../free/nrw/commons/utils/EdgeToEdgeUtils.kt | 229 ++++++++++++++++++ .../fr/free/nrw/commons/utils/ImageUtils.kt | 24 +- .../main/res/layout/activity_notification.xml | 1 + .../res/layout/fragment_custom_selector.xml | 1 + .../fragment_upload_media_detail_fragment.xml | 1 + .../res/layout/toolbar_location_picker.xml | 3 +- app/src/main/res/values/ids.xml | 7 + app/src/main/res/values/styles.xml | 3 + gradle/libs.versions.toml | 6 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 35 files changed, 348 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/EdgeToEdgeUtils.kt create mode 100644 app/src/main/res/values/ids.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 674a6473f..32f2ee415 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,12 +18,12 @@ if (isRunningOnTravisAndIsNotPRBuild) { android { namespace = "fr.free.nrw.commons" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "fr.free.nrw.commons" minSdk = 21 - targetSdk = 34 + targetSdk = 35 versionCode = 1055 versionName = "5.6.1" diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt index ebbb4097a..865ad3ddb 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt @@ -19,6 +19,7 @@ import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog import java.util.Collections import androidx.core.net.toUri +import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets import fr.free.nrw.commons.utils.handleWebUrl import fr.free.nrw.commons.utils.setUnderlinedText @@ -47,6 +48,7 @@ class AboutActivity : BaseActivity() { */ binding = ActivityAboutBinding.inflate(layoutInflater) val view: View = binding!!.root + applyEdgeToEdgeTopInsets(binding!!.toolbarLayout) setContentView(view) setSupportActionBar(binding!!.toolbarBinding.toolbar) diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt index 439ed1e92..0882ba117 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt @@ -9,6 +9,7 @@ import fr.free.nrw.commons.databinding.ActivityWelcomeBinding import fr.free.nrw.commons.databinding.PopupForCopyrightBinding import fr.free.nrw.commons.quiz.QuizActivity import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour class WelcomeActivity : BaseActivity() { @@ -23,6 +24,7 @@ class WelcomeActivity : BaseActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityWelcomeBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding!!.welcomePager.rootView) setContentView(binding!!.root) isQuiz = intent?.extras?.getBoolean("isQuiz", false) ?: false diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt index 7a665197b..688f508ae 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -22,6 +22,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDelegate import androidx.core.app.NavUtils import androidx.core.content.ContextCompat +import androidx.core.view.WindowCompat import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.R @@ -32,11 +33,13 @@ import fr.free.nrw.commons.contributions.MainActivity import fr.free.nrw.commons.databinding.ActivityLoginBinding import fr.free.nrw.commons.di.ApplicationlessInjection import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.utils.AbstractTextWatcher import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour import fr.free.nrw.commons.utils.SystemThemeUtils import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard +import fr.free.nrw.commons.utils.handleKeyboardInsets import fr.free.nrw.commons.utils.handleWebUrl import io.reactivex.disposables.CompositeDisposable import timber.log.Timber @@ -79,7 +82,14 @@ class LoginActivity : AccountAuthenticatorActivity() { delegate.installViewFactory() delegate.onCreate(savedInstanceState) + WindowCompat.getInsetsController(window, window.decorView) + .isAppearanceLightStatusBars = !isDarkTheme + + WindowCompat.setDecorFitsSystemWindows(window, false) + binding = ActivityLoginBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding!!.root) + binding?.aboutPrivacyPolicy?.handleKeyboardInsets() with(binding!!) { setContentView(root) diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt index 5b48ecd8f..22f557bcd 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt @@ -10,6 +10,7 @@ import android.widget.Toast import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.R import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import timber.log.Timber class SignupActivity : BaseActivity() { @@ -21,6 +22,7 @@ class SignupActivity : BaseActivity() { Timber.d("Signup Activity started") webView = WebView(this) + applyEdgeToEdgeAllInsets(webView!!) with(webView!!) { setContentView(this) webViewClient = MyWebViewClient() diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt index c998f96ac..fefe462a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt @@ -23,6 +23,7 @@ import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment import fr.free.nrw.commons.media.MediaDetailPagerFragment import fr.free.nrw.commons.media.MediaDetailProvider import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.utils.handleWebUrl import fr.free.nrw.commons.wikidata.model.WikiSite import fr.free.nrw.commons.wikidata.model.page.PageTitle @@ -57,6 +58,7 @@ class CategoryDetailsActivity : BaseActivity(), binding = ActivityCategoryDetailsBinding.inflate(layoutInflater) val view = binding.root + applyEdgeToEdgeAllInsets(view) setContentView(view) supportFragmentManager = getSupportFragmentManager() viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt index b9fa3e395..5c2c44ab5 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt @@ -34,6 +34,7 @@ import fr.free.nrw.commons.quiz.QuizChecker import fr.free.nrw.commons.settings.SettingsFragment import fr.free.nrw.commons.startWelcome import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.upload.UploadProgressActivity import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest import fr.free.nrw.commons.utils.ViewUtilWrapper @@ -112,6 +113,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = MainBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding!!.root) setContentView(binding!!.root) setSupportActionBar(binding!!.toolbarBinding.toolbar) tabLayout = binding!!.fragmentMainNavTabLayout diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 7e7d7e4cd..2534b4aeb 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat +import androidx.core.view.ViewGroupCompat import androidx.lifecycle.ViewModelProvider import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.database.NotForUploadStatus @@ -56,6 +57,8 @@ import fr.free.nrw.commons.media.ZoomableActivity import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.upload.FileUtilsWrapper import fr.free.nrw.commons.utils.CustomSelectorUtils +import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets +import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -198,6 +201,9 @@ class CustomSelectorActivity : .fillMaxWidth(), ) } + ViewGroupCompat.installCompatInsetsDispatch(binding.root) + applyEdgeToEdgeTopInsets(toolbarBinding.toolbarLayout) + bottomSheetBinding.bottomLayout.applyEdgeToEdgeBottomPaddingInsets() val view = binding.root setContentView(view) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index 6ca2b06e4..0c3c5bdd0 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -18,6 +18,7 @@ import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.upload.FileProcessor +import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets import javax.inject.Inject /** @@ -99,6 +100,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { selectorRV = binding?.selectorRv loader = binding?.loader with(binding?.selectorRv) { + this?.applyEdgeToEdgeBottomPaddingInsets() this?.layoutManager = gridLayoutManager this?.setHasFixedSize(true) this?.adapter = folderAdapter diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index 6e08e30f1..4f37106cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -41,6 +41,7 @@ import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper +import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -217,6 +218,7 @@ class ImageFragment : imageAdapter.setSingleSelection(singleSelection) gridLayoutManager = GridLayoutManager(context, getSpanCount()) with(binding?.selectorRv) { + this?.applyEdgeToEdgeBottomPaddingInsets() this?.layoutManager = gridLayoutManager this?.setHasFixedSize(true) this?.adapter = imageAdapter diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 44cefe4d5..89d43845b 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -7,6 +7,7 @@ import android.speech.RecognizerIntent import android.view.View import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.WindowCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.free.nrw.commons.CommonsApplication @@ -20,9 +21,11 @@ import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomInsets import fr.free.nrw.commons.upload.UploadMediaDetail import fr.free.nrw.commons.upload.UploadMediaDetailAdapter import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog +import fr.free.nrw.commons.utils.applyEdgeToEdgeTopPaddingInsets import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.Consumer import io.reactivex.schedulers.Schedulers @@ -87,6 +90,10 @@ class DescriptionEditActivity : super.onCreate(savedInstanceState) binding = ActivityDescriptionEditBinding.inflate(layoutInflater) + applyEdgeToEdgeBottomInsets(binding.btnEditSubmit) + WindowCompat.getInsetsController(window, window.decorView) + .isAppearanceLightStatusBars = false + binding.toolbar.applyEdgeToEdgeTopPaddingInsets() setContentView(binding.root) val bundle = intent.extras diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt index 7b7bb2cd5..0d7dfd218 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt @@ -22,6 +22,7 @@ import fr.free.nrw.commons.media.MediaDetailProvider import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.utils.FragmentUtils.isFragmentUIActive import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import io.reactivex.android.schedulers.AndroidSchedulers import timber.log.Timber import java.util.Date @@ -48,6 +49,7 @@ class SearchActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallba override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivitySearchBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding!!.root) setContentView(binding!!.root) title = getString(R.string.title_activity_search) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt index 4696ae8d4..32af67e95 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt @@ -24,6 +24,7 @@ import fr.free.nrw.commons.media.MediaDetailProvider import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.upload.structure.depictions.DepictModel import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.utils.handleWebUrl import fr.free.nrw.commons.wikidata.WikidataConstants import io.reactivex.android.schedulers.AndroidSchedulers @@ -55,6 +56,7 @@ class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, Categor super.onCreate(savedInstanceState) binding = ActivityWikidataItemDetailsBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding!!.root) setContentView(binding!!.root) supportFragmentManager = getSupportFragmentManager() viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager()) diff --git a/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt b/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt index a8b6ddf26..e4fedf2e4 100644 --- a/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt @@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.IntentCompat import androidx.core.os.BundleCompat import androidx.core.text.HtmlCompat +import androidx.core.view.WindowCompat import com.google.android.material.floatingactionbutton.FloatingActionButton import fr.free.nrw.commons.CameraPosition import fr.free.nrw.commons.CommonsApplication @@ -44,6 +45,8 @@ import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Compani import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM import fr.free.nrw.commons.utils.DialogUtil import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL +import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomInsets +import fr.free.nrw.commons.utils.applyEdgeToEdgeTopPaddingInsets import fr.free.nrw.commons.utils.handleGeoCoordinates import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -330,6 +333,9 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { */ private fun getToolbarUI() { val toolbar: ConstraintLayout = findViewById(R.id.location_picker_toolbar) + WindowCompat.getInsetsController(window, window.decorView) + .isAppearanceLightStatusBars = false + toolbar.applyEdgeToEdgeTopPaddingInsets() largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view) smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view) toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.primaryColor)) @@ -460,6 +466,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { */ private fun addPlaceSelectedButton() { placeSelectedButton = findViewById(R.id.location_chosen_button) + applyEdgeToEdgeBottomInsets(placeSelectedButton) placeSelectedButton.setOnClickListener { placeSelected() } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt index 76975964b..4a43bf470 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt @@ -8,6 +8,7 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import androidx.core.view.ViewGroupCompat import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar @@ -19,8 +20,10 @@ import fr.free.nrw.commons.databinding.ActivityNotificationBinding import fr.free.nrw.commons.notification.models.Notification import fr.free.nrw.commons.notification.models.NotificationType import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets import fr.free.nrw.commons.utils.NetworkUtils import fr.free.nrw.commons.utils.ViewUtil +import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets import fr.free.nrw.commons.utils.handleWebUrl import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers @@ -56,6 +59,9 @@ class NotificationActivity : BaseActivity() { super.onCreate(savedInstanceState) isRead = intent.getStringExtra("title") == "read" binding = ActivityNotificationBinding.inflate(layoutInflater) + ViewGroupCompat.installCompatInsetsDispatch(binding.root) + applyEdgeToEdgeTopInsets(binding.toolbar.toolbar) + binding.listView.applyEdgeToEdgeBottomPaddingInsets() setContentView(binding.root) mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag( tagNotificationWorkerFragment diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt index d80be9ea2..c368d6cd4 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt @@ -21,6 +21,7 @@ import fr.free.nrw.commons.databinding.ActivityProfileBinding import fr.free.nrw.commons.profile.achievements.AchievementsFragment import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.utils.DialogUtil import java.io.File import java.io.FileOutputStream @@ -61,6 +62,7 @@ class ProfileActivity : BaseActivity() { super.onCreate(savedInstanceState) binding = ActivityProfileBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding.root) setContentView(binding.root) setSupportActionBar(binding.toolbarBinding.toolbar) diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt index e65b819e5..11fd1e6a6 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt @@ -3,9 +3,11 @@ package fr.free.nrw.commons.quiz import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.facebook.drawee.drawable.ProgressBarDrawable @@ -15,6 +17,7 @@ import fr.free.nrw.commons.databinding.ActivityQuizBinding import java.util.ArrayList import fr.free.nrw.commons.R +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets class QuizActivity : AppCompatActivity() { @@ -37,7 +40,11 @@ class QuizActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() binding = ActivityQuizBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding.root) + WindowCompat.getInsetsController(window, window.decorView) + .isAppearanceLightStatusBars = true setContentView(binding.root) quizController.initialize(this) diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt index 81372b4a6..6979edd15 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt @@ -12,9 +12,11 @@ import android.view.MenuItem import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat import fr.free.nrw.commons.databinding.ActivityQuizResultBinding import java.io.File @@ -22,6 +24,7 @@ import java.io.FileOutputStream import fr.free.nrw.commons.R import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets /** @@ -35,7 +38,11 @@ class QuizResultActivity : AppCompatActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() binding = ActivityQuizResultBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding!!.root) + WindowCompat.getInsetsController(window, window.decorView) + .isAppearanceLightStatusBars = true setContentView(binding?.root) setSupportActionBar(binding?.toolbar?.toolbar) diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt index 20f289f8f..42a75aeac 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -16,6 +16,7 @@ import fr.free.nrw.commons.databinding.ActivityReviewBinding import fr.free.nrw.commons.delete.DeleteHelper import fr.free.nrw.commons.media.MediaDetailFragment import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.utils.DialogUtil import fr.free.nrw.commons.utils.ViewUtil import io.reactivex.android.schedulers.AndroidSchedulers @@ -73,6 +74,7 @@ class ReviewActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityReviewBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding.root) setContentView(binding.root) setSupportActionBar(binding.toolbarBinding?.toolbar) diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt index 91c88d7b0..233e688f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.MenuItem import fr.free.nrw.commons.databinding.ActivitySettingsBinding import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets /** @@ -21,6 +22,7 @@ class SettingsActivity : BaseActivity() { super.onCreate(savedInstanceState) binding = ActivitySettingsBinding.inflate(layoutInflater) val view = binding.root + applyEdgeToEdgeAllInsets(view) setContentView(view) setSupportActionBar(binding.toolbarBinding.toolbar) diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt index d2d936460..d317a7d35 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import android.os.Bundle import android.util.DisplayMetrics import android.view.WindowManager +import androidx.activity.enableEdgeToEdge import javax.inject.Inject import javax.inject.Named import fr.free.nrw.commons.R @@ -36,6 +37,7 @@ abstract class BaseActivity : CommonsDaggerAppCompatActivity() { 1f ) adjustFontScale(resources.configuration, fontScale) + enableEdgeToEdge() } override fun onResume() { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt index 38e7dace8..74597bc14 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt @@ -38,6 +38,7 @@ import fr.free.nrw.commons.mwapi.UserClient import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import fr.free.nrw.commons.upload.ThumbnailsAdapter.OnThumbnailDeletedListener import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment import fr.free.nrw.commons.upload.depicts.DepictsFragment @@ -177,6 +178,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C presenter?.setupBasicKvStoreFactory { BasicKvStore(this@UploadActivity, it) } _binding = ActivityUploadBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(_binding!!.root, false) setContentView(binding.root) // Overrides the back button to make sure the user is prepared to lose their progress diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt index 3cf9d3a65..665f106e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt @@ -10,6 +10,7 @@ import fr.free.nrw.commons.ViewPagerAdapter import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.databinding.ActivityUploadProgressBinding import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets import javax.inject.Inject /** @@ -35,6 +36,7 @@ class UploadProgressActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityUploadProgressBinding.inflate(layoutInflater) + applyEdgeToEdgeAllInsets(binding.root) setContentView(binding.root) viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager) binding.uploadProgressViewPager.setAdapter(viewPagerAdapter) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt index 43a9c3236..6ece53170 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt @@ -50,6 +50,7 @@ import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import fr.free.nrw.commons.utils.handleKeyboardInsets import timber.log.Timber import java.io.File import java.util.ArrayList @@ -153,6 +154,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false) + _binding!!.mediaDetailCardView.handleKeyboardInsets() return binding.root } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt index 332c8d023..95fa62a20 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt @@ -12,9 +12,9 @@ object ConfigUtils { val isBetaFlavour: Boolean = BuildConfig.FLAVOR == "beta" @JvmStatic - private fun Context.getVersionName(): String = + private fun Context.getVersionName(): String? = try { - packageManager.getPackageInfo(packageName, 0).versionName + packageManager.getPackageInfo(packageName, 0).versionName ?: BuildConfig.VERSION_NAME } catch (e: PackageManager.NameNotFoundException) { BuildConfig.VERSION_NAME } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/EdgeToEdgeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/EdgeToEdgeUtils.kt new file mode 100644 index 000000000..d0c2b12e8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/EdgeToEdgeUtils.kt @@ -0,0 +1,229 @@ +package fr.free.nrw.commons.utils + +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.marginBottom +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import fr.free.nrw.commons.R + +/** + * Applies edge-to-edge system bar insets to a [View]’s margins using a custom adjustment block. + * + * Stores the initial margins to ensure inset calculations are additive, and applies the provided + * [block] with an [InsetsAccumulator] containing initial and system bar inset values. + * + * @param typeMask The type of window insets to apply. Defaults to [WindowInsetsCompat.Type.systemBars]. + * @param shouldConsumeInsets If `true`, the insets are consumed and not propagated to child views. + * @param block Lambda applied to update [MarginLayoutParams] using the accumulated insets. + */ +fun View.applyEdgeToEdgeInsets( + typeMask: Int = WindowInsetsCompat.Type.systemBars(), + shouldConsumeInsets: Boolean = true, + block: MarginLayoutParams.(InsetsAccumulator) -> Unit +) { + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + val insets = windowInsets.getInsets(typeMask) + + val initialTop = if (view.getTag(R.id.initial_margin_top) != null) { + view.getTag(R.id.initial_margin_top) as Int + } else { + view.setTag(R.id.initial_margin_top, view.marginTop) + view.marginTop + } + + val initialBottom = if (view.getTag(R.id.initial_margin_bottom) != null) { + view.getTag(R.id.initial_margin_bottom) as Int + } else { + view.setTag(R.id.initial_margin_bottom, view.marginBottom) + view.marginBottom + } + + val initialLeft = if (view.getTag(R.id.initial_margin_left) != null) { + view.getTag(R.id.initial_margin_left) as Int + } else { + view.setTag(R.id.initial_margin_left, view.marginLeft) + view.marginLeft + } + + val initialRight = if (view.getTag(R.id.initial_margin_right) != null) { + view.getTag(R.id.initial_margin_right) as Int + } else { + view.setTag(R.id.initial_margin_right, view.marginRight) + view.marginRight + } + + val accumulator = InsetsAccumulator( + initialTop, + insets.top, + initialBottom, + insets.bottom, + initialLeft, + insets.left, + initialRight, + insets.right + ) + + view.updateLayoutParams { + apply { block(accumulator) } + } + + if(shouldConsumeInsets) WindowInsetsCompat.CONSUMED else windowInsets + } +} + +/** + * Applies edge-to-edge system bar insets to the top padding of the view. + * + * @param typeMask The type of window insets to apply. Defaults to [WindowInsetsCompat.Type.systemBars]. + */ +fun View.applyEdgeToEdgeTopPaddingInsets( + typeMask: Int = WindowInsetsCompat.Type.systemBars(), +) { + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + val insets = windowInsets.getInsets(typeMask) + + view.updatePadding( + left = insets.left, + right = insets.right, + top = insets.top + ) + + WindowInsetsCompat.CONSUMED + } +} + +/** + * Applies edge-to-edge system bar insets to the bottom padding of the view. + * + * @param typeMask The type of window insets to apply. Defaults to [WindowInsetsCompat.Type.systemBars]. + */ +fun View.applyEdgeToEdgeBottomPaddingInsets( + typeMask: Int = WindowInsetsCompat.Type.systemBars(), +) { + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + val insets = windowInsets.getInsets(typeMask) + + view.updatePadding( + left = insets.left, + right = insets.right, + bottom = insets.bottom + ) + + WindowInsetsCompat.CONSUMED + } +} + +/** + * Applies system bar insets to all margins (top, bottom, left, right) of the view. + * + * @param view The target view. + * @param shouldConsumeInsets If `true`, the insets are consumed and not propagated to child views. + */ +fun applyEdgeToEdgeAllInsets( + view: View, + shouldConsumeInsets: Boolean = true +) = view.applyEdgeToEdgeInsets(shouldConsumeInsets = shouldConsumeInsets) { insets -> + leftMargin = insets.left + rightMargin = insets.right + topMargin = insets.top + bottomMargin = insets.bottom +} + +/** + * Applies system bar insets to the top and horizontal margins of the view. + * + * @param view The target view. + */ +fun applyEdgeToEdgeTopInsets(view: View) = view.applyEdgeToEdgeInsets { insets -> + leftMargin = insets.left + rightMargin = insets.right + topMargin = insets.top +} + +/** + * Applies system bar insets to the bottom and horizontal margins of the view. + * + * @param view The target view. + */ +fun applyEdgeToEdgeBottomInsets(view: View) = view.applyEdgeToEdgeInsets { insets -> + leftMargin = insets.left + rightMargin = insets.right + bottomMargin = insets.bottom +} + +/** + * Adjusts a [View]'s bottom margin dynamically to account for the on-screen keyboard (IME), + * ensuring the view remains visible above the keyboard during transitions. + * + * Preserves the initial margin, adjusts during IME visibility changes, + * and accounts for navigation bar insets to avoid double offsets. + */ +fun View.handleKeyboardInsets() { + var existingBottomMargin = 0 + + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + existingBottomMargin = if (view.getTag(R.id.initial_margin_bottom) != null) { + view.getTag(R.id.initial_margin_bottom) as Int + } else { + view.setTag(R.id.initial_margin_bottom, view.marginBottom) + view.marginBottom + } + + WindowInsetsCompat.CONSUMED + } + + // Animate during IME transition + ViewCompat.setWindowInsetsAnimationCallback( + this, + object : WindowInsetsAnimationCompat.Callback( + DISPATCH_MODE_CONTINUE_ON_SUBTREE + ) { + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: MutableList + ): WindowInsetsCompat { + val lp = layoutParams as MarginLayoutParams + val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + + // Avoid extra space due to system nav bar when the keyboard is shown + val imeBottomMargin = imeInsets.bottom - navBarInsets.bottom + + lp.bottomMargin = if(imeVisible && imeBottomMargin >= existingBottomMargin) + imeBottomMargin + existingBottomMargin + else existingBottomMargin + + layoutParams = lp + return WindowInsetsCompat.CONSUMED + } + } + ) +} + +/** + * Holds both initial margin values and system bar insets, providing summed values + * for each side (top, bottom, left, right) to apply in layout updates. + */ +data class InsetsAccumulator( + private val initialTop: Int, + private val insetTop: Int, + private val initialBottom: Int, + private val insetBottom: Int, + private val initialLeft: Int, + private val insetLeft: Int, + private val initialRight: Int, + private val insetRight: Int +) { + val top = initialTop + insetTop + val bottom = initialBottom + insetBottom + val left = initialLeft + insetLeft + val right = initialRight + insetRight +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt index ebff3d054..fa538bb21 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt @@ -24,6 +24,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers import timber.log.Timber +import androidx.core.graphics.createBitmap /** * Created by blueSir9 on 3/10/17. @@ -307,16 +308,19 @@ object ImageUtils { * * @return */ @JvmStatic - fun addRedBorder(bitmap: Bitmap, borderSize: Int, context: Context): Bitmap { - val bmpWithBorder = Bitmap.createBitmap( - bitmap.width + borderSize * 2, - bitmap.height + borderSize * 2, - bitmap.config - ) - val canvas = Canvas(bmpWithBorder) - canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)) - canvas.drawBitmap(bitmap, borderSize.toFloat(), borderSize.toFloat(), null) - return bmpWithBorder + fun addRedBorder(bitmap: Bitmap, borderSize: Int, context: Context): Bitmap? { + return bitmap.config?.let { config -> + val bmpWithBorder = + createBitmap( + width = bitmap.width + borderSize * 2, + height = bitmap.height + borderSize * 2, + config = config + ) + val canvas = Canvas(bmpWithBorder) + canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)) + canvas.drawBitmap(bitmap, borderSize.toFloat(), borderSize.toFloat(), null) + return bmpWithBorder + } } /** diff --git a/app/src/main/res/layout/activity_notification.xml b/app/src/main/res/layout/activity_notification.xml index a8b60dea3..800c8aa0b 100644 --- a/app/src/main/res/layout/activity_notification.xml +++ b/app/src/main/res/layout/activity_notification.xml @@ -35,6 +35,7 @@ android:scrollbars="vertical" android:fadeScrollbars="false" android:scrollbarThumbVertical="@color/primaryColor" + android:clipToPadding="false" android:scrollbarSize="@dimen/dimen_6"/> diff --git a/app/src/main/res/layout/fragment_custom_selector.xml b/app/src/main/res/layout/fragment_custom_selector.xml index 03381fd24..b016b6605 100644 --- a/app/src/main/res/layout/fragment_custom_selector.xml +++ b/app/src/main/res/layout/fragment_custom_selector.xml @@ -34,6 +34,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/switchWidget" + android:clipToPadding="false" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 02c314a4a..cdc8ce387 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -123,6 +123,9 @@ @drawable/ic_arrow_back_black false false + + @android:color/transparent + @android:color/transparent