diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cde699ce..99e24fe55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Wikimedia Commons for Android +## v2.11.0 +- Refactored upload process, explore/media details, and peer review to use MVP architecture +- Refactored all AsyncTasks to use RxAndroid +- Partial migration to Retrofit +- Allow users to remove EXIF tags from their uploads if desired +- Multiple crash and bug fixes + ## v2.10.2 - Fixed remaining issues with date image taken - Fixed database crash diff --git a/app/build.gradle b/app/build.gradle index 7a540a648..8ceb6b543 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,8 +83,6 @@ dependencies { implementation "androidx.cardview:cardview:1.0.0" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "androidx.exifinterface:exifinterface:1.0.0" - //swipe_layout - implementation 'com.daimajia.swipelayout:library:1.2.0@aar' //metadata extractor implementation 'com.drewnoakes:metadata-extractor:2.11.0' } @@ -93,8 +91,8 @@ android { buildToolsVersion "28.0.3" defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 243 - versionName '2.10.2' + versionCode 475 + versionName '2.11.0' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion 19 targetSdkVersion 28 diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java index 9b084da49..21c39bf97 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java @@ -48,8 +48,21 @@ public class CategoriesModel{ */ public Comparator sortBySimilarity(final String filter) { Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter); - return (firstItem, secondItem) -> stringSimilarityComparator - .compare(firstItem.getName(), secondItem.getName()); + return (firstItem, secondItem) -> { + //if the category is selected, it should get precedence + if (null != firstItem && firstItem.isSelected()) { + if (null != secondItem && secondItem.isSelected()) { + return stringSimilarityComparator + .compare(firstItem.getName(), secondItem.getName()); + } + return -1; + } + if (null != secondItem && secondItem.isSelected()) { + return 1; + } + return stringSimilarityComparator + .compare(firstItem.getName(), secondItem.getName()); + }; } /** @@ -255,4 +268,38 @@ public class CategoriesModel{ this.categoriesCache.clear(); this.selectedCategories.clear(); } + + /** + * Search for categories + */ + public Observable searchCategories(String query, List imageTitleList) { + if (TextUtils.isEmpty(query)) { + return gpsCategories() + .concatWith(titleCategories(imageTitleList)) + .concatWith(recentCategories()); + } + + return mwApi + .searchCategories(query, SEARCH_CATS_LIMIT) + .map(s -> new CategoryItem(s, false)); + } + + /** + * Returns default categories + */ + public Observable getDefaultCategories(List titleList) { + Observable directCategories = directCategories(); + if (hasDirectCategories()) { + Timber.d("Image has direct Categories"); + return directCategories + .concatWith(gpsCategories()) + .concatWith(titleCategories(titleList)) + .concatWith(recentCategories()); + } else { + Timber.d("Image has no direct Categories"); + return gpsCategories() + .concatWith(titleCategories(titleList)) + .concatWith(recentCategories()); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java new file mode 100644 index 000000000..ec00043d6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java @@ -0,0 +1,723 @@ +package fr.free.nrw.commons.nearby; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.snackbar.Snackbar; +import com.google.gson.Gson; + +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationUpdateListener; +import fr.free.nrw.commons.utils.FragmentUtils; +import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.utils.PermissionUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import fr.free.nrw.commons.wikidata.WikidataEditListener; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION; +import static fr.free.nrw.commons.contributions.MainActivity.NEARBY_TAB_POSITION; +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.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED; + +public class NearbyFragment extends CommonsDaggerSupportFragment + implements LocationUpdateListener, + WikidataEditListener.WikidataP18EditListener { + + @BindView(R.id.progressBar) + ProgressBar progressBar; + @BindView(R.id.bottom_sheet) + LinearLayout bottomSheet; + @BindView(R.id.bottom_sheet_details) + LinearLayout bottomSheetDetails; + @BindView(R.id.transparentView) + View transparentView; + @BindView(R.id.container_sheet) + FrameLayout frameLayout; + @BindView(R.id.loading_nearby_list) + ConstraintLayout loading_nearby_layout; + + @Inject + LocationServiceManager locationManager; + @Inject + NearbyController nearbyController; + @Inject + WikidataEditListener wikidataEditListener; + @Inject Gson gson; + + public NearbyMapFragment nearbyMapFragment; + private NearbyListFragment nearbyListFragment; + private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); + private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); + private Bundle bundle; + private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet + private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet + + private LatLng curLatLng; + private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed + public View view; + private Snackbar snackbar; + + private LatLng lastKnownLocation; + private LatLng customLatLng; + + private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; + private BroadcastReceiver broadcastReceiver; + + private boolean onOrientationChanged = false; + private boolean populateForCurrentLocation = false; + private boolean isNetworkErrorOccured = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_nearby, container, false); + ButterKnife.bind(this, view); + + /*// Resume the fragment if exist + resumeFragment();*/ + bundle = new Bundle(); + initBottomSheetBehaviour(); + this.view = view; + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (savedInstanceState != null) { + onOrientationChanged = true; + } + } + + /** + * Hide or expand bottom sheet according to states of all sheets + */ + public void listOptionMenuItemClicked() { + if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){ + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + }else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){ + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + } + + /** + * Resume fragments if they exists + */ + private void resumeFragment() { + // Find the retained fragment on activity restarts + nearbyMapFragment = getMapFragment(); + nearbyListFragment = getListFragment(); + addNetworkBroadcastReceiver(); + } + + /** + * Returns the map fragment added to child fragment manager previously, if exists. + */ + private NearbyMapFragment getMapFragment() { + return (NearbyMapFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT); + } + + private void removeMapFragment() { + if (nearbyMapFragment != null) { + FragmentManager fm = getFragmentManager(); + fm.beginTransaction().remove(nearbyMapFragment).commit(); + nearbyMapFragment = null; + } + } + + + /** + * Returns the list fragment added to child fragment manager previously, if exists. + */ + private NearbyListFragment getListFragment() { + return (NearbyListFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT); + } + + private void removeListFragment() { + if (nearbyListFragment != null) { + FragmentManager fm = getFragmentManager(); + fm.beginTransaction().remove(nearbyListFragment).commit(); + nearbyListFragment = null; + } + } + + /** + * Initialize bottom sheet behaviour (sheet for map list.) Set height 9/16 of all window. + * Add callback for bottom sheet changes, so that we can sync it with bottom sheet for details + * (sheet for nearby details) + */ + private void initBottomSheetBehaviour() { + + transparentView.setAlpha(0); + bottomSheet.getLayoutParams().height = getActivity().getWindowManager() + .getDefaultDisplay().getHeight() / 16 * 9; + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); + bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + + @Override + public void onStateChanged(View bottomSheet, int unusedNewState) { + prepareViewsForSheetPosition(); + } + + @Override + public void onSlide(View bottomSheet, float slideOffset) { + + } + }); + + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehaviorForDetails = BottomSheetBehavior.from(bottomSheetDetails); + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); + } + + /** + * Sets camera position, zoom level according to sheet positions + */ + private void prepareViewsForSheetPosition() { + // TODO + } + + @Override + public void onLocationChangedSignificantly(LatLng latLng) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } + + @Override + public void onLocationChangedSlightly(LatLng latLng) { + refreshView(LOCATION_SLIGHTLY_CHANGED); + } + + + @Override + public void onLocationChangedMedium(LatLng latLng) { + // For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change + refreshView(LOCATION_SLIGHTLY_CHANGED); + } + + @Override + public void onWikidataEditSuccessful() { + // Do not refresh nearby map if we are checking other areas with search this area button + if (nearbyMapFragment != null && !nearbyMapFragment.searchThisAreaModeOn) { + refreshView(MAP_UPDATED); + } + } + + /** + * This method should be the single point to load/refresh nearby places + * + * @param locationChangeType defines if location changed significantly or slightly + */ + private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { + Timber.d("Refreshing nearby places"); + if (lockNearbyView) { + return; + } + + if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) { + hideProgressBar(); + return; + } + + registerLocationUpdates(); + LatLng lastLocation = locationManager.getLastLocation(); + + if (curLatLng != null && curLatLng.equals(lastLocation) + && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed + // Two exceptional cases to refresh nearby map manually. + if (!onOrientationChanged) { + return; + } + + } + curLatLng = lastLocation; + + if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) { + curLatLng = lastKnownLocation; + } + + if (curLatLng == null) { + Timber.d("Skipping update of nearby places as location is unavailable"); + return; + } + + /* + onOrientation changed is true whenever activities orientation changes. After orientation + change we want to refresh map significantly, doesn't matter if location changed significantly + or not. Thus, we included onOrientationChanged boolean to if clause + */ + if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) + || locationChangeType.equals(PERMISSION_JUST_GRANTED) + || locationChangeType.equals(MAP_UPDATED) + || onOrientationChanged) { + progressBar.setVisibility(View.VISIBLE); + + //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found + String gsonCurLatLng = gson.toJson(curLatLng); + bundle.clear(); + bundle.putString("CurLatLng", gsonCurLatLng); + + compositeDisposable.add(Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLng, curLatLng, false, true)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::populatePlaces, + throwable -> { + Timber.d(throwable); + showErrorMessage(getString(R.string.error_fetching_nearby_places)); + progressBar.setVisibility(View.GONE); + })); + + } else if (locationChangeType + .equals(LOCATION_SLIGHTLY_CHANGED) && nearbyMapFragment != null) { + String gsonCurLatLng = gson.toJson(curLatLng); + bundle.putString("CurLatLng", gsonCurLatLng); + updateMapFragment(false,true, null, null); + } + + if (nearbyMapFragment != null && nearbyMapFragment.searchThisAreaButton != null) { + nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE); + } + } + + /** + * This method should be used with "Search this are button". This method will search nearby + * points around any custom location (target location when user clicked on search this area) + * button. It populates places for custom location. + * @param customLatLng Custom area which we will search around + */ + void refreshViewForCustomLocation(LatLng customLatLng, boolean refreshForCurrentLocation) { + if (customLatLng == null) { + // If null, return + return; + } + + populateForCurrentLocation = refreshForCurrentLocation; + this.customLatLng = customLatLng; + compositeDisposable.add(Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLng, customLatLng, false, populateForCurrentLocation)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::populatePlacesFromCustomLocation, + throwable -> { + Timber.d(throwable); + showErrorMessage(getString(R.string.error_fetching_nearby_places)); + })); + + if (nearbyMapFragment != null) { + nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE); + } + } + + /** + * Populates places for custom location, should be used for finding nearby places around a + * location where you are not at. + * @param nearbyPlacesInfo This variable has place list information and distances. + */ + private void populatePlacesFromCustomLocation(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + if (nearbyMapFragment != null) { + nearbyMapFragment.searchThisAreaButtonProgressBar.setVisibility(View.GONE); + } + + if (nearbyMapFragment != null && curLatLng != null) { + if (!populateForCurrentLocation) { + nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList); + } else { + updateMapFragment(true,true, customLatLng, nearbyPlacesInfo); + } + updateListFragmentForCustomLocation(nearbyPlacesInfo.placeList); + } + } + + /** + * Turns nearby place lists and boundary coordinates into gson and update map and list fragments + * accordingly + * @param nearbyPlacesInfo a variable holds both nearby place list and boundary coordinates + */ + private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + Timber.d("Populating nearby places"); + List placeList = nearbyPlacesInfo.placeList; + LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; + String gsonPlaceList = gson.toJson(placeList); + String gsonCurLatLng = gson.toJson(curLatLng); + String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); + + if (placeList.size() == 0) { + ViewUtil.showShortSnackbar(view.findViewById(R.id.container), R.string.no_nearby); + } + + bundle.putString("PlaceList", gsonPlaceList); + bundle.putString("BoundaryCoord", gsonBoundaryCoordinates); + + // First time to init fragments + if (nearbyMapFragment == null) { + Timber.d("Init map fragment for the first time"); + lockNearbyView(true); + setMapFragment(); + setListFragment(); + hideProgressBar(); + lockNearbyView(false); + } else { + // There are fragments, just update the map and list + Timber.d("Map fragment already exists, just update the map and list"); + updateMapFragment(false,false, null, null); + updateListFragment(); + } + } + + /** + * Lock nearby view updates while updating map or list. Because we don't want new update calls + * when we already updating for old location update. + * @param lock true if we should lock nearby map + */ + private void lockNearbyView(boolean lock) { + if (lock) { + lockNearbyView = true; + locationManager.unregisterLocationManager(); + locationManager.removeLocationListener(this); + } else { + lockNearbyView = false; + registerLocationUpdates(); + locationManager.addLocationListener(this); + } + } + + /** + * Updates map fragment, + * For slight update: camera follows users location + * For significant update: nearby markers are removed and new markers added again + * Slight updates stop if user is checking another area of map + * + * @param updateViaButton search this area button is clicked + * @param isSlightUpdate Means no need to update markers, just follow user location with camera + * @param customLatLng Will be used for updates for other locations than users current location. + * Ie. when we use search this area feature + * @param nearbyPlacesInfo Includes nearby places list and boundary coordinates + */ + private void updateMapFragment(boolean updateViaButton, boolean isSlightUpdate, @Nullable LatLng customLatLng, @Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + /* + Significant update means updating nearby place markers. Slightly update means only + updating current location marker and camera target. + We update our map Significantly on each 1000 meter change, but we can't never know + the frequency of nearby places. Thus we check if we are close to the boundaries of + our nearby markers, we update our map Significantly. + */ + NearbyMapFragment nearbyMapFragment = getMapFragment(); + + if (nearbyMapFragment != null && !nearbyMapFragment.isCurrentLocationMarkerVisible() && !onOrientationChanged) { + Timber.d("Do not update the map, user is not seeing current location marker" + + " means they are checking around and moving on map"); + return; + } + + + if (nearbyMapFragment != null && curLatLng != null) { + hideProgressBar(); // In case it is visible (this happens, not an impossible case) + /* + * If we are close to nearby places boundaries, we need a significant update to + * get new nearby places. Check order is south, north, west, east + * */ + if (nearbyMapFragment.boundaryCoordinates != null + && !nearbyMapFragment.checkingAround + && !nearbyMapFragment.searchThisAreaModeOn + && !onOrientationChanged + && (curLatLng.getLatitude() < nearbyMapFragment.boundaryCoordinates[0].getLatitude() + || curLatLng.getLatitude() > nearbyMapFragment.boundaryCoordinates[1].getLatitude() + || curLatLng.getLongitude() < nearbyMapFragment.boundaryCoordinates[2].getLongitude() + || curLatLng.getLongitude() > nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { + // populate places + compositeDisposable.add(Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLng, curLatLng, false, updateViaButton)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::populatePlaces, + throwable -> { + Timber.d(throwable); + showErrorMessage(getString(R.string.error_fetching_nearby_places)); + progressBar.setVisibility(View.GONE); + })); + nearbyMapFragment.setBundleForUpdates(bundle); + nearbyMapFragment.updateMapSignificantlyForCurrentLocation(); + updateListFragment(); + return; + } + + if (updateViaButton) { + nearbyMapFragment.updateMapSignificantlyForCustomLocation(customLatLng, nearbyPlacesInfo.placeList); + return; + } + + /* + If this is the map update just after orientation change, then it is not a slight update + anymore. We want to significantly update map after each orientation change + */ + if (onOrientationChanged) { + isSlightUpdate = false; + onOrientationChanged = false; + } + + if (isSlightUpdate) { + nearbyMapFragment.setBundleForUpdates(bundle); + nearbyMapFragment.updateMapSlightly(); + } else { + nearbyMapFragment.setBundleForUpdates(bundle); + nearbyMapFragment.updateMapSignificantlyForCurrentLocation(); + updateListFragment(); + } + } else { + lockNearbyView(true); + setMapFragment(); + setListFragment(); + hideProgressBar(); + lockNearbyView(false); + } + } + + /** + * Updates already existing list fragment with bundle includes nearby places and boundary + * coordinates + */ + private void updateListFragment() { + nearbyListFragment.setBundleForUpdates(bundle); + nearbyListFragment.updateNearbyListSignificantly(); + } + + /** + * Updates nearby list for custom location, will be used with search this area method. When you + * want to search for a place where you are not at. + * @param placeList List of places around your manually chosen target location from map. + */ + private void updateListFragmentForCustomLocation(List placeList) { + nearbyListFragment.updateNearbyListSignificantlyForCustomLocation(placeList); + } + + /** + * Calls fragment for map view. + */ + private void setMapFragment() { + FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction(); + nearbyMapFragment = new NearbyMapFragment(); + nearbyMapFragment.setArguments(bundle); + fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT); + fragmentTransaction.commitAllowingStateLoss(); + } + + /** + * Calls fragment for list view. + */ + private void setListFragment() { + loading_nearby_layout.setVisibility(View.GONE); + frameLayout.setVisibility(View.VISIBLE); + FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction(); + nearbyListFragment = new NearbyListFragment(); + nearbyListFragment.setArguments(bundle); + fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT); + initBottomSheetBehaviour(); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + fragmentTransaction.commitAllowingStateLoss(); + } + + /** + * Hides progress bar + */ + private void hideProgressBar() { + if (progressBar != null) { + progressBar.setVisibility(View.GONE); + } + } + + /** + * This method first checks if the location permissions has been granted and then register the location manager for updates. + */ + private void registerLocationUpdates() { + locationManager.registerLocationManager(); + } + + private void showErrorMessage(String message) { + ViewUtil.showLongToast(getActivity(), message); + } + + /** + * Adds network broadcast receiver to recognize connection established + */ + private void addNetworkBroadcastReceiver() { + if (!FragmentUtils.isFragmentUIActive(this)) { + return; + } + + if (broadcastReceiver != null) { + return; + } + + IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION); + + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getActivity() != null) { + if (NetworkUtils.isInternetConnectionEstablished(getActivity())) { + if (isNetworkErrorOccured) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + isNetworkErrorOccured = false; + } + + if (snackbar != null) { + snackbar.dismiss(); + snackbar = null; + } + } else { + if (snackbar == null) { + snackbar = Snackbar.make(view, R.string.no_internet, Snackbar.LENGTH_INDEFINITE); + if (nearbyMapFragment != null && nearbyMapFragment.searchThisAreaButton != null) { + nearbyMapFragment.searchThisAreaButton.setVisibility(View.GONE); + } + } + + isNetworkErrorOccured = true; + snackbar.show(); + } + } + } + }; + + getActivity().registerReceiver(broadcastReceiver, intentFilter); + } + + @Override + public void onResume() { + super.onResume(); + // Resume the fragment if exist + if (((MainActivity) getActivity()).viewPager.getCurrentItem() == NEARBY_TAB_POSITION) { + checkPermissionsAndPerformAction(this::resumeFragment); + } else { + resumeFragment(); + } + } + + /** + * Perform nearby operations on nearby tab selected + * @param onOrientationChanged pass orientation changed info to fragment + */ + public void onTabSelected(boolean onOrientationChanged) { + Timber.d("On nearby tab selected"); + this.onOrientationChanged = onOrientationChanged; + checkPermissionsAndPerformAction(this::performNearbyOperations); + } + + private void checkPermissionsAndPerformAction(Runnable runnable) { + PermissionUtils.checkPermissionsAndPerformAction(getActivity(), + Manifest.permission.ACCESS_FINE_LOCATION, + runnable, + () -> ((MainActivity) getActivity()).viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION), + R.string.location_permission_title, + R.string.location_permission_rationale_nearby); + } + + /** + * Calls nearby operations in required order. + */ + private void performNearbyOperations() { + locationManager.addLocationListener(this); + registerLocationUpdates(); + lockNearbyView = false; + addNetworkBroadcastReceiver(); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + wikidataEditListener.setAuthenticationStateListener(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + wikidataEditListener.setAuthenticationStateListener(null); + } + + @Override + public void onDetach() { + super.onDetach(); + snackbar = null; + broadcastReceiver = null; + wikidataEditListener.setAuthenticationStateListener(null); + } + + @Override + public void onPause() { + super.onPause(); + // this means that this activity will not be recreated now, user is leaving it + // or the activity is otherwise finishing + if(getActivity().isFinishing()) { + // we will not need this fragment anymore, this may also be a good place to signal + // to the retained fragment object to perform its own cleanup. + //removeMapFragment(); + removeListFragment(); + + } + if (broadcastReceiver != null) { + getActivity().unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + if (locationManager != null) { + locationManager.removeLocationListener(this); + locationManager.unregisterLocationManager(); + } + } + + + /** + * Centers the map in nearby fragment to a given place + * @param place is new center of the map + */ + public void centerMapToPlace(Place place) { + if (nearbyMapFragment != null) { + nearbyMapFragment.centerMapToPlace(place); + } + } + + public boolean isBottomSheetExpanded() { return bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED; + } +} + + diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java new file mode 100644 index 000000000..67c41c951 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -0,0 +1,1100 @@ +package fr.free.nrw.commons.nearby; + +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.IconFactory; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.MarkerOptions; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.constants.Style; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.MapboxMapOptions; +import com.mapbox.mapboxsdk.plugins.localization.LocalizationPlugin; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; +import butterknife.BindView; +import butterknife.ButterKnife; +import dagger.android.support.DaggerFragment; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; +import fr.free.nrw.commons.contributions.ContributionController; +import fr.free.nrw.commons.kvstore.JsonKvStore; +import fr.free.nrw.commons.utils.LocationUtils; +import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.utils.UiUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import timber.log.Timber; + +import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; +import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; + +public class NearbyMapFragment extends DaggerFragment { + + public MapView mapView; + private List baseMarkerOptions; + private fr.free.nrw.commons.location.LatLng curLatLng; + public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates; + private List bookmarkedplaces; + + @BindView(R.id.bottom_sheet) + View bottomSheetList; + + @BindView(R.id.bottom_sheet_details) + View bottomSheetDetails; + + @BindView(R.id.wikipediaButton) + LinearLayout wikipediaButton; + + @BindView(R.id.wikidataButton) + LinearLayout wikidataButton; + + @BindView(R.id.directionsButton) + LinearLayout directionsButton; + + @BindView(R.id.commonsButton) + LinearLayout commonsButton; + + @BindView(R.id.bookmarkButton) + LinearLayout bookmarkButton; + + @BindView(R.id.fab_plus) + FloatingActionButton fabPlus; + + @BindView(R.id.fab_camera) + FloatingActionButton fabCamera; + + @BindView(R.id.fab_gallery) + FloatingActionButton fabGallery; + + @BindView(R.id.fab_recenter) + FloatingActionButton fabRecenter; + + @BindView(R.id.transparentView) + View transparentView; + + @BindView(R.id.description) + TextView description; + + @BindView(R.id.title) + TextView title; + + @BindView(R.id.category) + TextView distance; + + @BindView(R.id.icon) + ImageView icon; + + @BindView(R.id.bookmarkButtonImage) + ImageView bookmarkButtonImage; + + @BindView(R.id.wikidataButtonText) + TextView wikidataButtonText; + + @BindView(R.id.wikipediaButtonText) + TextView wikipediaButtonText; + + @BindView(R.id.commonsButtonText) + TextView commonsButtonText; + + @BindView(R.id.directionsButtonText) + TextView directionsButtonText; + + @BindView(R.id.search_this_area_button) + Button searchThisAreaButton; + + @BindView(R.id.search_this_area_button_progress_bar) + ProgressBar searchThisAreaButtonProgressBar; + + private BottomSheetBehavior bottomSheetListBehavior; + private BottomSheetBehavior bottomSheetDetailsBehavior; + + private boolean isFabOpen = false; + private Animation rotate_backward; + private Animation fab_close; + private Animation fab_open; + private Animation rotate_forward; + + private Place place; + private Marker selected; + private Marker currentLocationMarker; + public MapboxMap mapboxMap; + private PolygonOptions currentLocationPolygonOptions; + + + private boolean isBottomListSheetExpanded; + private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06; + private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; + + public boolean searchThisAreaModeOn = false; + public boolean checkingAround = false; + + private Bundle bundleForUpdates;// Carry information from activity about changed nearby places and current location + private boolean searchedAroundCurrentLocation = true; + + @Inject @Named("default_preferences") JsonKvStore applicationKvStore; + @Inject BookmarkLocationsDao bookmarkLocationDao; + @Inject ContributionController controller; + @Inject Gson gson; + + private static final double ZOOM_LEVEL = 14f; + + public NearbyMapFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Timber.d("Nearby map fragment created"); + + Bundle bundle = this.getArguments(); + if (bundle != null) { + String gsonPlaceList = bundle.getString("PlaceList"); + String gsonLatLng = bundle.getString("CurLatLng"); + Type listType = new TypeToken>() { + }.getType(); + String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord"); + List placeList = gson.fromJson(gsonPlaceList, listType); + Type curLatLngType = new TypeToken() { + }.getType(); + Type gsonBoundaryCoordinatesType = new TypeToken() {}.getType(); + curLatLng = gson.fromJson(gsonLatLng, curLatLngType); + baseMarkerOptions = NearbyController + .loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, + placeList, + getActivity(), + bookmarkLocationDao.getAllBookmarksLocations()); + boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType); + } + if (curLatLng != null) { + Mapbox.getInstance(getActivity(), + getString(R.string.mapbox_commons_app_token)); + Mapbox.getTelemetry().setUserTelemetryRequestState(false); + } + setRetainInstance(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Timber.d("onCreateView called"); + if (curLatLng != null) { + Timber.d("curLatLng found, setting up map view..."); + setupMapView(savedInstanceState); + } + setHasOptionsMenu(false); + + return mapView; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + this.getView().setFocusableInTouchMode(true); + this.getView().requestFocus(); + this.getView().setOnKeyListener((v, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior + .STATE_EXPANDED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + return true; + } else if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior + .STATE_COLLAPSED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + mapView.getMapAsync(MapboxMap::deselectMarkers); + selected = null; + return true; + } + } + return false; + }); + } + + + /** + * Updates map slightly means it doesn't updates all nearby markers around. It just updates + * location tracker marker of user. + */ + public void updateMapSlightly() { + Timber.d("updateMapSlightly called, bundle is:"+ bundleForUpdates); + if (mapboxMap != null) { + if (bundleForUpdates != null) { + String gsonLatLng = bundleForUpdates.getString("CurLatLng"); + Type curLatLngType = new TypeToken() {}.getType(); + curLatLng = gson.fromJson(gsonLatLng, curLatLngType); + } + updateMapToTrackPosition(); + } + + } + + /** + * Updates map significantly means it updates nearby markers and location tracker marker. It is + * called when user is out of boundaries (south, north, east or west) of markers drawn by + * previous nearby call. + */ + public void updateMapSignificantlyForCurrentLocation() { + Timber.d("updateMapSignificantlyForCurrentLocation called, bundle is:"+ bundleForUpdates); + if (mapboxMap != null) { + if (bundleForUpdates != null) { + String gsonPlaceList = bundleForUpdates.getString("PlaceList"); + String gsonLatLng = bundleForUpdates.getString("CurLatLng"); + String gsonBoundaryCoordinates = bundleForUpdates.getString("BoundaryCoord"); + Type listType = new TypeToken>() {}.getType(); + List placeList = gson.fromJson(gsonPlaceList, listType); + Type curLatLngType = new TypeToken() {}.getType(); + Type gsonBoundaryCoordinatesType = new TypeToken() {}.getType(); + curLatLng = gson.fromJson(gsonLatLng, curLatLngType); + baseMarkerOptions = NearbyController + .loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, + placeList, + getActivity(), + bookmarkLocationDao.getAllBookmarksLocations()); + boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType); + } + mapboxMap.clear(); + addCurrentLocationMarker(mapboxMap); + updateMapToTrackPosition(); + // We are trying to find nearby places around our current location, thus custom parameter is nullified + addNearbyMarkersToMapBoxMap(null); + } + } + + /** + * Initialize all views. + * TODO: View elements that are part of NearbyFragment should ideally be not accessed directly in NearbyMapFragment. + */ + private void initViews() { + Timber.d("initViews called"); + View view = ((NearbyFragment)getParentFragment()).view; + ButterKnife.bind(this, view); + + bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList); + bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails); + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetDetails.setVisibility(View.VISIBLE); + + fab_open = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_open); + fab_close = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_close); + rotate_forward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_forward); + rotate_backward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_backward); + + + } + + /** + * Will be used for map vew updates for custom locations (ie. with search this area method). + * Clears the map, adds current location marker, adds nearby markers around custom location, + * re-enables map gestures which was locked during place load, remove progress bar. + * @param customLatLng custom location that we will search around + * @param placeList places around of custom location + */ + public void updateMapSignificantlyForCustomLocation(fr.free.nrw.commons.location.LatLng customLatLng, List placeList) { + List customBaseMarkerOptions = NearbyController + .loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, // Curlatlang will be used to calculate distances + placeList, + getActivity(), + bookmarkLocationDao.getAllBookmarksLocations()); + mapboxMap.clear(); + // We are trying to find nearby places around our custom searched area, thus custom parameter is nonnull + addNearbyMarkersToMapBoxMap(customBaseMarkerOptions); + addCurrentLocationMarker(mapboxMap); + // Re-enable mapbox gestures on custom location markers load + mapboxMap.getUiSettings().setAllGesturesEnabled(true); + searchThisAreaButtonProgressBar.setVisibility(View.GONE); + } + // Only update current position marker and camera view + + private void updateMapToTrackPosition() { + + if (currentLocationMarker != null) { + LatLng curMapBoxLatLng = new LatLng(curLatLng.getLatitude(),curLatLng.getLongitude()); + ValueAnimator markerAnimator = ObjectAnimator.ofObject(currentLocationMarker, "position", + new LatLngEvaluator(), currentLocationMarker.getPosition(), + curMapBoxLatLng); + markerAnimator.setDuration(1000); + markerAnimator.start(); + + List circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(), + curLatLng.getAccuracy() * 2, 100); + if (currentLocationPolygonOptions != null){ + mapboxMap.removePolygon(currentLocationPolygonOptions.getPolygon()); + currentLocationPolygonOptions = new PolygonOptions() + .addAll(circle) + .strokeColor(Color.parseColor("#55000000")) + .fillColor(Color.parseColor("#11000000")); + mapboxMap.addPolygon(currentLocationPolygonOptions); + } + + // Make camera to follow user on location change + CameraPosition position ; + + // Do not update camera position is search this area mode on + if (!searchThisAreaModeOn) { + if (ViewUtil.isPortrait(getActivity())){ + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, + curMapBoxLatLng.getLongitude()) + : curMapBoxLatLng ) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + ZOOM_LEVEL // zoom level is fixed when bottom sheet is expanded + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + }else { + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE, + curMapBoxLatLng.getLongitude()) + : curMapBoxLatLng ) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + ZOOM_LEVEL // zoom level is fixed when bottom sheet is expanded + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + } + + mapboxMap.animateCamera(CameraUpdateFactory + .newCameraPosition(position), 1000); + } + } + } + + /** + * Sets click listeners of FABs, and 2 bottom sheets + */ + private void setListeners() { + fabPlus.setOnClickListener(view -> { + if (applicationKvStore.getBoolean("login_skipped", false)) { + // prompt the user to login + new AlertDialog.Builder(getContext()) + .setMessage(R.string.login_alert_message) + .setPositiveButton(R.string.login, (dialog, which) -> { + // logout of the app + BaseLogoutListener logoutListener = new BaseLogoutListener(); + CommonsApplication app = (CommonsApplication) getActivity().getApplication(); + app.clearApplicationData(getContext(), logoutListener); + + }) + .show(); + }else { + animateFAB(isFabOpen); + } + }); + + bottomSheetDetails.setOnClickListener(view -> { + if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } else { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + + fabRecenter.setOnClickListener(view -> { + if (curLatLng != null) { + mapView.getMapAsync(mapboxMap -> { + CameraPosition position; + + if (ViewUtil.isPortrait(getActivity())){ + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, + curLatLng.getLongitude()) + : new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude(), 0)) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + ZOOM_LEVEL + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + }else { + position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE, + curLatLng.getLongitude()) + : new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude(), 0)) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + ZOOM_LEVEL + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + } + + mapboxMap.animateCamera(CameraUpdateFactory + .newCameraPosition(position), 1000); + + }); + } + }); + + bottomSheetDetailsBehavior.setBottomSheetCallback(new BottomSheetBehavior + .BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + prepareViewsForSheetPosition(newState); + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + if (slideOffset >= 0) { + transparentView.setAlpha(slideOffset); + if (slideOffset == 1) { + transparentView.setClickable(true); + } else if (slideOffset == 0) { + transparentView.setClickable(false); + } + } + } + }); + + bottomSheetListBehavior.setBottomSheetCallback(new BottomSheetBehavior + .BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + if (newState == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + + } + }); + + // Remove button text if they exceed 1 line or if internal layout has not been built + // Only need to check for directions button because it is the longest + if (directionsButtonText.getLineCount() > 1 || directionsButtonText.getLineCount() == 0) { + wikipediaButtonText.setVisibility(View.GONE); + wikidataButtonText.setVisibility(View.GONE); + commonsButtonText.setVisibility(View.GONE); + directionsButtonText.setVisibility(View.GONE); + } + title.setOnLongClickListener(view -> { + Utils.copy("place",title.getText().toString(),getContext()); + Toast.makeText(getContext(),"Text copied to clipboard",Toast.LENGTH_SHORT).show(); + return true; + } + ); + title.setOnClickListener(view -> { + if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } else { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + } + + /** + * Sets up map view of first time it created, it passes MapBoxMap options and style assets. + * @param savedInstanceState bundle coming from Nearby Fragment + */ + private void setupMapView(Bundle savedInstanceState) { + Timber.d("setupMapView called"); + boolean isDarkTheme = applicationKvStore.getBoolean("theme", false); + MapboxMapOptions options = new MapboxMapOptions() + .compassGravity(Gravity.BOTTOM | Gravity.LEFT) + .compassMargins(new int[]{12, 0, 0, 24}) + .styleUrl(isDarkTheme ? Style.DARK : Style.OUTDOORS) + .logoEnabled(true) + .attributionEnabled(true) + .camera(new CameraPosition.Builder() + .target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())) + .zoom(ZOOM_LEVEL) + .build()); + + if (!getParentFragment().getActivity().isFinishing()) { + mapView = new MapView(getParentFragment().getActivity(), options); + // create map + mapView.onCreate(savedInstanceState); + mapView.getMapAsync(mapboxMap -> { + LocalizationPlugin localizationPlugin = new LocalizationPlugin(mapView, mapboxMap); + + try { + localizationPlugin.matchMapLanguageWithDeviceDefault(); + } catch (RuntimeException exception) { + Timber.d(exception.toString()); + } + + NearbyMapFragment.this.mapboxMap = mapboxMap; + addMapMovementListeners(); + updateMapSignificantlyForCurrentLocation(); + }); + } + } + + /** + * Adds map movement listener to understand swiping with fingers. So that we can display search + * this area button to search nearby places for other locations + */ + private void addMapMovementListeners() { + + mapboxMap.addOnCameraMoveListener(() -> { + + if (NearbyController.currentLocation != null) { // If our nearby markers are calculated at least once + + if (searchThisAreaButton.getVisibility() == View.GONE && NetworkUtils.isInternetConnectionEstablished(getContext())) { + searchThisAreaButton.setVisibility(View.VISIBLE); + } + double distance = mapboxMap.getCameraPosition().target + .distanceTo(new LatLng(NearbyController.currentLocation.getLatitude() + , NearbyController.currentLocation.getLongitude())); + + if (distance > NearbyController.searchedRadius*1000*3/4) { //Convert to meter, and compare if our distance is bigger than 3/4 or our searched area + checkingAround = true; + if (!searchThisAreaModeOn) { // If we are changing mode, then change click action + searchThisAreaModeOn = true; + searchThisAreaButton.setOnClickListener(view -> { + searchThisAreaModeOn = true; + // Lock map operations during search this area operation + mapboxMap.getUiSettings().setAllGesturesEnabled(false); + searchThisAreaButtonProgressBar.setVisibility(View.VISIBLE); + searchThisAreaButton.setVisibility(View.GONE); + searchedAroundCurrentLocation = false; + ((NearbyFragment)getParentFragment()) + .refreshViewForCustomLocation(LocationUtils + .mapBoxLatLngToCommonsLatLng(mapboxMap.getCameraPosition().target), false); + }); + } + + } else { + checkingAround = false; + if (searchThisAreaModeOn) { + searchThisAreaModeOn = false; // This flag will help us to understand should we folor users location or not + searchThisAreaButton.setOnClickListener(view -> { + searchThisAreaModeOn = true; + // Lock map operations during search this area operation + mapboxMap.getUiSettings().setAllGesturesEnabled(false); + searchThisAreaButtonProgressBar.setVisibility(View.GONE); + fabRecenter.callOnClick(); + searchThisAreaButton.setVisibility(View.GONE); + searchedAroundCurrentLocation = true; + ((NearbyFragment)getParentFragment()) + .refreshViewForCustomLocation(LocationUtils + .mapBoxLatLngToCommonsLatLng(mapboxMap.getCameraPosition().target), true); + }); + } + if (searchedAroundCurrentLocation) { + searchThisAreaButton.setVisibility(View.GONE); + } + } + } + }); + } + + /** + * onLogoutComplete is called after shared preferences and data stored in local database are cleared. + */ + private class BaseLogoutListener implements CommonsApplication.LogoutListener { + @Override + public void onLogoutComplete() { + Timber.d("Logout complete callback received."); + Intent nearbyIntent = new Intent( getActivity(), LoginActivity.class); + nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(nearbyIntent); + getActivity().finish(); + } + } + + /** + * Adds a marker for the user's current position. Adds a + * circle which uses the accuracy * 2, to draw a circle + * which represents the user's position with an accuracy + * of 95%. + * + * Should be called only on creation of mapboxMap, there + * is other method to update markers location with users + * move. + */ + private void addCurrentLocationMarker(MapboxMap mapboxMap) { + Timber.d("addCurrentLocationMarker is called"); + if (currentLocationMarker != null) { + currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel + } + + Icon icon = IconFactory.getInstance(getContext()).fromResource(R.drawable.current_location_marker); + + MarkerOptions currentLocationMarkerOptions = new MarkerOptions() + .position(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())); + currentLocationMarkerOptions.setIcon(icon); // Set custom icon + + currentLocationMarker = mapboxMap.addMarker(currentLocationMarkerOptions); + + List circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(), + curLatLng.getAccuracy() * 2, 100); + + currentLocationPolygonOptions = new PolygonOptions() + .addAll(circle) + .strokeColor(Color.parseColor("#55000000")) + .fillColor(Color.parseColor("#11000000")); + mapboxMap.addPolygon(currentLocationPolygonOptions); + } + + /** + * Checks whether current location marker is in visible region or not + * @return true if point is in region + */ + public boolean isCurrentLocationMarkerVisible() { + if (currentLocationMarker != null) { + return mapboxMap.getProjection().getVisibleRegion().latLngBounds.contains(currentLocationMarker.getPosition()); + } else { + return false; + } + } + + /** + * Adds markers for nearby places to mapbox map + */ + private void addNearbyMarkersToMapBoxMap(@Nullable List customNearbyBaseMarker) { + List baseMarkerOptions; + Timber.d("addNearbyMarkersToMapBoxMap is called"); + if (customNearbyBaseMarker != null) { + // If we try to update nearby points for a custom location choosen from map (we are not there) + baseMarkerOptions = customNearbyBaseMarker; + } else { + // If we try to display nearby markers around our curret location + baseMarkerOptions = this.baseMarkerOptions; + } + mapboxMap.addMarkers(baseMarkerOptions); + mapboxMap.setOnInfoWindowCloseListener(marker -> { + if (marker == selected) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + }); + mapView.getMapAsync(mapboxMap -> { + mapboxMap.addMarkers(baseMarkerOptions); + fabRecenter.setVisibility(View.VISIBLE); + mapboxMap.setOnInfoWindowCloseListener(marker -> { + if (marker == selected) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + }); + + mapboxMap.setOnMarkerClickListener(marker -> { + + if (marker instanceof NearbyMarker) { + this.selected = marker; + NearbyMarker nearbyMarker = (NearbyMarker) marker; + Place place = nearbyMarker.getNearbyBaseMarker().getPlace(); + passInfoToSheet(place); + bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + return false; + }); + + }); + } + + + /** + * Creates a series of points that create a circle on the map. + * Takes the center latitude, center longitude of the circle, + * the radius in meter and the number of nodes of the circle. + * + * @return List List of LatLng points of the circle. + */ + private List createCircleArray( + double centerLat, double centerLong, float radius, int nodes) { + List circle = new ArrayList<>(); + float radiusKilometer = radius / 1000; + double radiusLong = radiusKilometer + / (111.320 * Math.cos(centerLat * Math.PI / 180)); + double radiusLat = radiusKilometer / 110.574; + + for (int i = 0; i < nodes; i++) { + double theta = ((double) i / (double) nodes) * (2 * Math.PI); + double nodeLongitude = centerLong + radiusLong * Math.cos(theta); + double nodeLatitude = centerLat + radiusLat * Math.sin(theta); + circle.add(new LatLng(nodeLatitude, nodeLongitude)); + } + return circle; + } + + /** + * If nearby details bottom sheet state is collapsed: show fab plus + * If nearby details bottom sheet state is expanded: show fab plus + * If nearby details bottom sheet state is hidden: hide all fabs + * @param bottomSheetState + */ + public void prepareViewsForSheetPosition(int bottomSheetState) { + + switch (bottomSheetState) { + case (BottomSheetBehavior.STATE_COLLAPSED): + closeFabs(isFabOpen); + if (!fabPlus.isShown()) showFAB(); + this.getView().requestFocus(); + break; + case (BottomSheetBehavior.STATE_EXPANDED): + this.getView().requestFocus(); + break; + case (BottomSheetBehavior.STATE_HIDDEN): + mapView.getMapAsync(MapboxMap::deselectMarkers); + transparentView.setClickable(false); + transparentView.setAlpha(0); + closeFabs(isFabOpen); + hideFAB(); + if (this.getView() != null) { + this.getView().requestFocus(); + } + break; + } + } + + /** + * Hides all fabs + */ + private void hideFAB() { + + removeAnchorFromFABs(fabPlus); + fabPlus.hide(); + + removeAnchorFromFABs(fabCamera); + fabCamera.hide(); + + removeAnchorFromFABs(fabGallery); + fabGallery.hide(); + + } + + /* + * We are not able to hide FABs without removing anchors, this method removes anchors + * */ + private void removeAnchorFromFABs(FloatingActionButton floatingActionButton) { + //get rid of anchors + //Somehow this was the only way https://stackoverflow.com/questions/32732932 + // /floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone + CoordinatorLayout.LayoutParams param = (CoordinatorLayout.LayoutParams) floatingActionButton + .getLayoutParams(); + param.setAnchorId(View.NO_ID); + // If we don't set them to zero, then they become visible for a moment on upper left side + param.width = 0; + param.height = 0; + floatingActionButton.setLayoutParams(param); + } + + private void showFAB() { + + addAnchorToBigFABs(fabPlus, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet_details).getId()); + fabPlus.show(); + + addAnchorToSmallFABs(fabGallery, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view).getId()); + + addAnchorToSmallFABs(fabCamera, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view1).getId()); + } + + + /* + * Add anchors back before making them visible again. + * */ + private void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) { + CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams + (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); + params.setAnchorId(anchorID); + params.anchorGravity = Gravity.TOP|Gravity.RIGHT|Gravity.END; + floatingActionButton.setLayoutParams(params); + } + + /* + * Add anchors back before making them visible again. Big and small fabs have different anchor + * gravities, therefore the are two methods. + * */ + private void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) { + CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams + (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); + params.setAnchorId(anchorID); + params.anchorGravity = Gravity.CENTER_HORIZONTAL; + floatingActionButton.setLayoutParams(params); + } + + /** + * 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(Place place) { + this.place = place; + updateBookmarkButtonImage(this.place); + + bookmarkButton.setOnClickListener(view -> { + boolean isBookmarked = bookmarkLocationDao.updateBookmarkLocation(this.place); + updateBookmarkButtonImage(this.place); + updateMarker(isBookmarked, this.place); + }); + + wikipediaButton.setVisibility(place.hasWikipediaLink()?View.VISIBLE:View.GONE); + wikipediaButton.setOnClickListener(view -> openWebView(this.place.siteLinks.getWikipediaLink())); + + wikidataButton.setVisibility(place.hasWikidataLink()?View.VISIBLE:View.GONE); + wikidataButton.setOnClickListener(view -> openWebView(this.place.siteLinks.getWikidataLink())); + + directionsButton.setOnClickListener(view -> Utils.handleGeoCoordinates(getActivity(), this.place.getLocation())); + + commonsButton.setVisibility(this.place.hasCommonsLink()?View.VISIBLE:View.GONE); + commonsButton.setOnClickListener(view -> openWebView(this.place.siteLinks.getCommonsLink())); + + icon.setImageResource(this.place.getLabel().getIcon()); + + title.setText(this.place.name); + distance.setText(this.place.distance); + description.setText(this.place.getLongDescription()); + + fabCamera.setOnClickListener(view -> { + if (fabCamera.isShown()) { + Timber.d("Camera button tapped. Place: %s", this.place.toString()); + storeSharedPrefs(); + controller.initiateCameraPick(getActivity()); + } + }); + + fabGallery.setOnClickListener(view -> { + if (fabGallery.isShown()) { + Timber.d("Gallery button tapped. Place: %s", this.place.toString()); + storeSharedPrefs(); + controller.initiateGalleryPick(getActivity(), false); + } + }); + } + + public void updateBookmarkButtonImage(Place place) { + int bookmarkIcon; + if (bookmarkLocationDao.findBookmarkLocation(place)) { + bookmarkIcon = R.drawable.ic_round_star_filled_24px; + } else { + bookmarkIcon = R.drawable.ic_round_star_border_24px; + } + if (bookmarkButtonImage != null) { + bookmarkButtonImage.setImageResource(bookmarkIcon); + } + } + + void storeSharedPrefs() { + Timber.d("Store place object %s", place.toString()); + applicationKvStore.putJson(PLACE_OBJECT, place); + } + + private void openWebView(Uri link) { + Utils.handleWebUrl(getContext(), link); + } + + /** + * Starts animation of fab plus (turning on opening) and other FABs + * @param isFabOpen state of FAB buttons, open when clicked on fab button, closed on other click + */ + private void animateFAB(boolean isFabOpen) { + this.isFabOpen = !isFabOpen; + if (fabPlus.isShown()){ + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGallery.startAnimation(fab_close); + fabCamera.hide(); + fabGallery.hide(); + } else { + fabPlus.startAnimation(rotate_forward); + fabCamera.startAnimation(fab_open); + fabGallery.startAnimation(fab_open); + fabCamera.show(); + fabGallery.show(); + } + this.isFabOpen=!isFabOpen; + } + } + + /** + * Hides camera and gallery FABs, turn back plus FAB + * @param isFabOpen + */ + private void closeFabs ( boolean isFabOpen){ + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGallery.startAnimation(fab_close); + fabCamera.hide(); + fabGallery.hide(); + this.isFabOpen = !isFabOpen; + } + } + + /** + * This bundle is sent whenever and update for nearby map comes, not for recreation, for updates + */ + public void setBundleForUpdates(Bundle bundleForUpdates) { + this.bundleForUpdates = bundleForUpdates; + } + + @Override + public void onStart() { + if (mapView != null) { + mapView.onStart(); + } + super.onStart(); + } + + @Override + public void onPause() { + if (mapView != null) { + mapView.onPause(); + } + bookmarkedplaces = bookmarkLocationDao.getAllBookmarksLocations(); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + if (mapView != null) { + mapView.onResume(); + } + if (mapboxMap != null) { + mapboxMap.getUiSettings().setAllGesturesEnabled(true); + } + searchThisAreaModeOn = false; + checkingAround = false; + searchedAroundCurrentLocation = true; + boundaryCoordinates = null; + initViews(); + setListeners(); + transparentView.setClickable(false); + transparentView.setAlpha(0); + if (bookmarkedplaces != null) { + for (Place place : bookmarkedplaces) { + if (!bookmarkLocationDao.findBookmarkLocation(place)) { + updateMarker(false, place); + } + } + } + } + + @Override + public void onStop() { + if (mapView != null) { + mapView.onStop(); + } + super.onStop(); + } + + @Override + public void onDestroyView() { + if (mapView != null) { + mapView.onDestroy(); + } + selected = null; + currentLocationMarker = null; + + super.onDestroyView(); + } + + private static class LatLngEvaluator implements TypeEvaluator { + // Method is used to interpolate the marker animation. + private LatLng latLng = new LatLng(); + + @Override + public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) { + latLng.setLatitude(startValue.getLatitude() + + ((endValue.getLatitude() - startValue.getLatitude()) * fraction)); + latLng.setLongitude(startValue.getLongitude() + + ((endValue.getLongitude() - startValue.getLongitude()) * fraction)); + return latLng; + } + } + + /** + * Centers the map in nearby fragment to a given place + * @param place is new center of the map + */ + public void centerMapToPlace(Place place) { + mapView.getMapAsync(mapboxMap1 -> { + CameraPosition position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(place.location.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE, + place.getLocation().getLongitude()) + : new LatLng(place.getLocation().getLatitude(), place.getLocation().getLongitude(), 0)) // Sets the new camera position + .zoom(isBottomListSheetExpanded ? + ZOOM_LEVEL + :mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000); + }); + } + + + public void updateMarker(boolean isBookmarked, Place place) { + + VectorDrawableCompat vectorDrawable; + if (isBookmarked) { + vectorDrawable = VectorDrawableCompat.create( + getContext().getResources(), R.drawable.ic_custom_bookmark_marker, getContext().getTheme() + ); + } + else { + vectorDrawable = VectorDrawableCompat.create( + getContext().getResources(), R.drawable.ic_custom_map_marker, getContext().getTheme() + ); + } + for(Marker marker: mapboxMap.getMarkers()){ + if(marker.getTitle()!=null && marker.getTitle().equals(place.getName())){ + + Bitmap icon = UiUtils.getBitmap(vectorDrawable); + + String distance = formatDistanceBetween(curLatLng, place.location); + place.setDistance(distance); + + NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); + nearbyBaseMarker.title(place.name); + nearbyBaseMarker.position( + new com.mapbox.mapboxsdk.geometry.LatLng( + place.location.getLatitude(), + place.location.getLongitude())); + nearbyBaseMarker.place(place); + nearbyBaseMarker.icon(IconFactory.getInstance(getContext()) + .fromBitmap(icon)); + marker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon)); + } + } + + } + + +} + diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNotificationCardView.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNotificationCardView.java index b5bc8b219..30acedb8a 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNotificationCardView.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNotificationCardView.java @@ -17,6 +17,8 @@ import fr.free.nrw.commons.utils.SwipableCardView; import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; +import static fr.free.nrw.commons.contributions.MainActivity.NEARBY_TAB_POSITION; + /** * Custom card view for nearby notification card view on main screen, above contributions list */ @@ -66,7 +68,6 @@ public class NearbyNotificationCardView extends SwipableCardView { progressBar = rootView.findViewById(R.id.progressBar); - setActionListeners(); } @Override @@ -81,8 +82,16 @@ public class NearbyNotificationCardView extends SwipableCardView { } - private void setActionListeners() { - this.setOnClickListener(view -> ((MainActivity)getContext()).viewPager.setCurrentItem(1)); + private void setActionListeners(Place place) { + this.setOnClickListener(view -> { + MainActivity m = (MainActivity) getContext(); + + // Change to nearby tab + m.viewPager.setCurrentItem(NEARBY_TAB_POSITION); + + // Center the map to the place + ((NearbyFragment) m.contributionsActivityPagerAdapter.getItem(NEARBY_TAB_POSITION)).centerMapToPlace(place); + }); } @Override public boolean onSwipe(View view) { @@ -120,6 +129,7 @@ public class NearbyNotificationCardView extends SwipableCardView { contentLayout.setVisibility(VISIBLE); // Make progress bar invisible once data is ready progressBar.setVisibility(GONE); + setActionListeners(place); // And content views visible since they are ready notificationTitle.setVisibility(VISIBLE); notificationDistance.setVisibility(VISIBLE); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 189c181b5..9470aa71f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -116,9 +116,12 @@ public class PlaceRenderer extends Renderer { ((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(lastPosition, buttonLayout.getHeight()); } } - + if (onBookmarkClick == null) { + ((NearbyFragment) fragment.getParentFragment()).centerMapToPlace(place); + } }; view.setOnClickListener(listener); + view.requestFocus(); view.setOnFocusChangeListener((view1, hasFocus) -> { if (!hasFocus && buttonLayout.isShown()) { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index b1f775a5b..505de1682 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -225,6 +225,7 @@ public class NotificationActivity extends NavigationBaseActivity { public void notificationClicked(Notification notification) { Timber.d("Notification clicked %s", notification.link); handleUrl(notification.link); + removeNotification(notification); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index f891306b9..df8388cee 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -9,7 +9,6 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; -import com.daimajia.swipe.SwipeLayout; import com.google.android.material.animation.ArgbEvaluatorCompat; import com.pedrogomez.renderers.Renderer; @@ -32,12 +31,8 @@ public class NotificationRenderer extends Renderer { TextView time; @BindView(R.id.icon) ImageView icon; - @BindView(R.id.swipeLayout) - SwipeLayout swipeLayout; - @BindView(R.id.bottom) - LinearLayout bottomLayout; - @BindView(R.id.notification_view) - RelativeLayout notificationView; + /*@BindView(R.id.bottom) + LinearLayout bottomLayout;*/ private NotificationClicked listener; private boolean isarchivedvisible = false; @@ -53,13 +48,6 @@ public class NotificationRenderer extends Renderer { listener.notificationClicked(getContent()); } - @OnClick(R.id.bottom) - void onBottomLayoutClicked(){ - Notification notification = getContent(); - Timber.d("NotificationID: %s", notification.notificationId); - listener.markNotificationAsRead(notification); - } - @Override protected void setUpView(View rootView) { @@ -74,21 +62,6 @@ public class NotificationRenderer extends Renderer { protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false); ButterKnife.bind(this, inflatedView); - if (isarchivedvisible) { - swipeLayout.setSwipeEnabled(false); - }else { - swipeLayout.setSwipeEnabled(true); - } - swipeLayout.addDrag(SwipeLayout.DragEdge.Top, bottomLayout); - swipeLayout.addRevealListener(R.id.bottom_wrapper_child1, (child, edge, fraction, distance) -> { - View star = child.findViewById(R.id.star); - float d = child.getHeight() / 2 - star.getHeight() / 2; - star.setTranslationY(d * fraction); - star.setScaleX(fraction + 0.6f); - star.setScaleY(fraction + 0.6f); - int c = ArgbEvaluatorCompat.getInstance().evaluate(fraction, Color.parseColor("#dddddd"), Color.parseColor("#90960a0a")); - child.setBackgroundColor(c); - }); return inflatedView; } diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java index 938b6f30d..1ebebb7b7 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRemoteDataSource.java @@ -11,10 +11,8 @@ import fr.free.nrw.commons.upload.UploadModel; import fr.free.nrw.commons.upload.UploadModel.UploadItem; import io.reactivex.Observable; import io.reactivex.Single; - import java.util.Comparator; import java.util.List; - import javax.inject.Inject; import javax.inject.Singleton; @@ -167,7 +165,7 @@ public class UploadRemoteDataSource { } /** - * ask the UplaodModel for the image quality of the UploadItem + * ask the UploadModel for the image quality of the UploadItem * * @param uploadItem * @param shouldValidateTitle @@ -176,4 +174,21 @@ public class UploadRemoteDataSource { public Single getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) { return uploadModel.getImageQuality(uploadItem, shouldValidateTitle); } + + /** + * Ask the CategoriesModel to search categories + * @param query + * @param imageTitleList + * @return + */ + public Observable searchCategories(String query, List imageTitleList) { + return categoriesModel.searchCategories(query, imageTitleList); + } + + /** + * Ask the CategoriesModel for default categories + */ + public Observable getDefaultCategories(List imageTitleList) { + return categoriesModel.getDefaultCategories(imageTitleList); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java index dbd0f6134..9a157edce 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -8,10 +8,8 @@ import fr.free.nrw.commons.upload.SimilarImageInterface; import fr.free.nrw.commons.upload.UploadModel.UploadItem; import io.reactivex.Observable; import io.reactivex.Single; - import java.util.Comparator; import java.util.List; - import javax.inject.Inject; import javax.inject.Singleton; @@ -262,4 +260,18 @@ public class UploadRepository { public void setSelectedLicense(String licenseName) { localDataSource.setSelectedLicense(licenseName); } + + /** + * Ask the RemoteDataSource to search for categories + */ + public Observable searchCategories(String query, List imageTitleList) { + return remoteDataSource.searchCategories(query, imageTitleList); + } + + /** + * Ask the RemoteDataSource to get default categories + */ + public Observable getDefaultCategories(List imageTitleList) { + return remoteDataSource.getDefaultCategories(imageTitleList); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 176697f9a..109dbc178 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -1,15 +1,12 @@ package fr.free.nrw.commons.settings; import android.Manifest; -import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.preference.EditTextPreference; -import android.preference.MultiSelectListPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; import android.preference.SwitchPreference; import android.text.Editable; import android.text.TextWatcher; @@ -31,6 +28,7 @@ import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.logging.CommonsLogSender; +import fr.free.nrw.commons.ui.LongTitlePreferences.LongTitleMultiSelectListPreference; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.upload.Language; @@ -70,8 +68,9 @@ public class SettingsFragment extends PreferenceFragment { return true; }); - MultiSelectListPreference multiSelectListPref = (MultiSelectListPreference) findPreference("manageExifTags"); + LongTitleMultiSelectListPreference multiSelectListPref = (LongTitleMultiSelectListPreference) findPreference("manageExifTags"); if (multiSelectListPref != null) { + defaultKvStore.putJson(Prefs.MANAGED_EXIF_TAGS, multiSelectListPref.getValues()); multiSelectListPref.setOnPreferenceChangeListener((preference, newValue) -> { defaultKvStore.putJson(Prefs.MANAGED_EXIF_TAGS, newValue); return true; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java index badcd48f8..946118c3a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java @@ -147,7 +147,6 @@ public class DescriptionsAdapter extends RecyclerView.Adapter getExifTagsToRedact(Context context) { Type setType = new TypeToken>() {}.getType(); - Set prefManageEXIFTags = defaultKvStore.getJson(Prefs.MANAGED_EXIF_TAGS, setType); + Set selectedExifTags = defaultKvStore.getJson(Prefs.MANAGED_EXIF_TAGS, setType); Set redactTags = new HashSet<>(Arrays.asList( context.getResources().getStringArray(R.array.pref_exifTag_values))); - Timber.d(redactTags.toString()); - if (prefManageEXIFTags != null) redactTags.removeAll(prefManageEXIFTags); + if (selectedExifTags != null) redactTags.removeAll(selectedExifTags); + else redactTags.clear(); return redactTags; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java index f050568a8..256c841b9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java @@ -139,14 +139,7 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter { String languageCode = LangCodeUtils.fixLanguageCode(languageCodesList.get(position)); final String languageName = StringUtils.capitalize(languageNamesList.get(position)); - if(savedLanguageValue.equals("")){ - savedLanguageValue = Locale.getDefault().getLanguage(); - } - if (!isDropDownView) { - if( !dropDownClicked){ - languageCode = LangCodeUtils.fixLanguageCode(savedLanguageValue); - } view.setVisibility(View.GONE); if (languageCode.length() > 2) tvLanguage.setText(languageCode.substring(0, 2)); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java index a0a776246..f690d1de6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.java @@ -80,6 +80,9 @@ public class CategoriesPresenter implements CategoriesContract.UserActionListene .observeOn(ioScheduler) .concatWith( repository.searchAll(query, imageTitleList) + .mergeWith(repository.searchCategories(query, imageTitleList)) + .concatWith(TextUtils.isEmpty(query) ? repository + .getDefaultCategories(imageTitleList) : Observable.empty()) ) .filter(categoryItem -> !repository.containsYear(categoryItem.getName())) .distinct(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java index 48485c17a..77fb6ecb3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java @@ -89,7 +89,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate @Override public void onResume() { super.onResume(); - if (presenter != null && isVisible && (categories == null || categories.isEmpty())) { + if (presenter != null && isVisible) { presenter.searchForCategories(null); } } @@ -193,7 +193,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate super.setUserVisibleHint(isVisibleToUser); isVisible = isVisibleToUser; - if (presenter != null && isResumed() && (categories == null || categories.isEmpty())) { + if (presenter != null && isResumed()) { presenter.searchForCategories(null); } } diff --git a/app/src/main/res/layout/item_notification.xml b/app/src/main/res/layout/item_notification.xml index 92e7e8bff..4c7cbbd38 100644 --- a/app/src/main/res/layout/item_notification.xml +++ b/app/src/main/res/layout/item_notification.xml @@ -1,80 +1,51 @@ - + + + + - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:layout_alignTop="@id/time" + android:layout_marginLeft="16dp" + android:layout_marginStart="16dp" + android:layout_toEndOf="@id/icon" + android:layout_toLeftOf="@id/time" + android:layout_toRightOf="@id/icon" + android:layout_toStartOf="@id/time" + android:ellipsize="end" + android:layout_alignParentTop="true" + android:textAppearance="@style/TextAppearance.AppCompat.Body2" + android:padding="12dp" + /> \ No newline at end of file diff --git a/app/src/main/res/values-as/error.xml b/app/src/main/res/values-as/error.xml new file mode 100644 index 000000000..0f4812f25 --- /dev/null +++ b/app/src/main/res/values-as/error.xml @@ -0,0 +1,9 @@ + + + + ক\'ম\'ঞ্চ ক্রেশ্ব হৈছে + উস্। কিবা সমস্যা হ\'ল! + ধন্যবাদ! + diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index d66590636..a9fff4898 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -1,25 +1,46 @@ + - কমন্স - ছেটিং - সদস্যনাম - গুপ্তশব্দ - প্ৰৱেশ + অন্বেষণ কৰক + অন্বেষণ কৰক + ৰূপ + সাধাৰণ + প্ৰতিক্ৰিয়া + গোপনিয়তা + অৱস্থান + ক\'ম\'ঞ্চ + + ছেটিংছ + ক\'ম\'ঞ্চত আপল\'ড কৰক + ব্যৱহাৰকাৰীনাম + পাছৱৰ্ড + আপোনাৰ ক\'ম\'ঞ্চ বিটা একাউণ্টত লগইন কৰক + লগইন কৰক + পাছৱৰ্ড পাহৰিলে? পঞ্জীয়ন কৰক - প্ৰৱেশ হৈ আছে + লগইন হৈ আছে অনুগ্ৰহ কৰি অপেক্ষা কৰক... - প্ৰৱেশ সফল হ\'ল! - প্ৰৱেশ বিফল হৈছে! + লগইন সফল হ\'ল! + লগইন বিফল হৈছে! ফাইল পোৱা নগ\'ল। অনুগ্ৰহ কৰি আন এটা ফাইল চেষ্টা কৰক। - প্ৰমাণীকৰণ ব্যৰ্থ হৈছে! + প্ৰমাণীকৰণ ব্যৰ্থ হৈছে, অনুগ্ৰহ কৰি আকৌ প্ৰৱেশ কৰক আপল\'ড আৰম্ভ হৈছে! - %1$s আপল\'ড হৈছে! + %1$s আপল\'ড কৰা হ\'ল! আপোনাৰ আপল\'ড চাবলৈ টেপ্‌ কৰক %1$s আপল\'ড আৰম্ভ হৈছে %1$s আপল\'ড হৈছে %1$s আপল\'ড শেষ হৈছে %1$s আপল\'ড ব্যৰ্থ হৈছে চাবৰ বাবে স্পৰ্শ কৰক + + %1$dটা ফাইল আপল\'ড হৈছে + %1$dটা ফাইল আপল\'ড হৈছে + মোৰ শেহতীয়া আপল\'ড অপে‌ক্ষাৰত ব্যৰ্থ হৈছে @@ -31,8 +52,13 @@ মোৰ আপল\'ডসমূহ শ্বেয়াৰ কৰক ব্ৰাউজাৰত চাওক - শিৰোনামা + শীৰ্ষক (প্ৰয়োজনীয়) + অনুগ্ৰহ কৰি এই ফাই‍লটোৰ এটা শিৰোনাম দিয়ক বিৱৰণ + প্ৰৱেশ ব্যৰ্থ হৈছে - নে\'টৱৰ্ক সমস্যা + প্ৰৱেশ ব্যৰ্থ হৈছে - অনুগ্ৰহ কৰি আপোনাৰ সদস্যনাম আৰু গুপ্তশব্দ পৰীক্ষা কৰক + অত্যাধিক অসফল প্ৰচেষ্টা। অনুগ্ৰহ কৰি অলপ সময়ৰ পাছত পুনৰ চেষ্টা কৰক। + ক্ষমা কৰিব, এই ব্যৱহাৰকাৰী গৰাকীক ক\'ম\'ঞ্চত বাৰণ কৰা হৈছে প্ৰৱেশ ব্যৰ্থ হৈছে আপল’ড কৰক পৰিৱৰ্তনসমূহ @@ -40,13 +66,89 @@ শ্ৰেণী সন্ধান কৰক সাঁচি থওক ৰিফ্ৰেচ + তালিকা আপোনাৰ সঁজুলিত GPS নিষ্ক্ৰিয় হৈ আছে। আপুনি ইয়াক সক্ৰিয় কৰিব খুজিছে নেকি? GPS সক্ৰিয় কৰক এতিয়ালৈকে কোনো আপল\'ড নাই শ্ৰেণী ছেটিংছ পঞ্জীয়ন কৰক + নিৰ্বাচিত ছবি + শ্ৰেণী বিষয়ে + <u>গোপনিয়তা নীতি</u> + প্ৰতিক্ৰিয়া প্ৰেৰণ কৰক (ইমেইল যোগে) + কোনো ইমেইল ক্লায়েন্ট ইনষ্টল কৰা নাই + প্ৰথম চিংকৰ বাবে অপেক্ষাৰত... + আপুনি এতিয়ালৈকে কোনো ফটো আপল\'ড কৰা নাই। পুনৰ চেষ্টা কৰক বাতিল কৰক + ডাউনল’ড কৰক + নিশা ম\'ড + ক\'লা থীম ব্যৱহাৰ কৰক + অনুগ্ৰহ কৰি আপল\'ড নকৰিব: + আপোনাৰ বন্ধুসকলৰ ছেল্ফি বা ছবি + আপুনি ইণ্টাৰনেটৰ পৰা ডাউনল\'ড কৰা ছবিসমূহ + আপল\'ডৰ উদাহৰণ: + শীৰ্ষক: ছিডনী অপেৰা হাউছ + আপুনি বুজিছে বুলি ভাবেনে? + হয়! + <u>অধিক তথ্য</u> + ল\'ড হৈ আছে… + একো চয়ন কৰা নাই + একো আলোচনা নাই + অজ্ঞাত লাইচেঞ্চ + সতেজ কৰক + ঠিক আছে + নিকটৱৰ্তী ঠাইসমূহ + কোনো নিকটৱৰ্তী ঠাই পোৱা নাই + সকীয়নি + হয় + নহয় + শীৰ্ষক + আলোচনা + আপল\'ড কৰা তাৰিখ + লাইচেঞ্চ + স্থানাংকসমূহ + একো প্ৰদান কৰা নাই + এগৰাকী বিটা টেষ্টাৰ হওঁক + শূণ্য বৈধ নহয় + অবৈধ ইনপুট + এটা বৈধ সংখ্যা প্ৰবিষ্ট কৰক + আপল\'ডৰ সীমা ০ হ\'ব নোৱাৰে + শেহতীয়া আপল\'ডৰ সীমা + পটভূমিৰ ছবি + মিডিয়াৰ ছবি বিফল হ\'ল + কোনো ছবি পোৱা নাই + খোলক + বন্ধ কৰক + নিকটৱৰ্তী + ছেটিংছ + প্ৰতিক্ৰিয়া + জাননী + ছবিখন ধোঁৱা-ধোঁৱা। + অনুমতি দিয়ক + ত্ৰুটি! URL পোৱা নাই + বাদ দিয়ক + ক\'ম\'ঞ্চ + দিনটোৰ ছবিখন + দিনটোৰ ছবিখন + ক\'ম\'ঞ্চত সন্ধান কৰক + বিলোপ কৰক + পৰিসংখ্যা + স্তৰ + ত্ৰুটি সংঘটিত হ\'ল! + জাননী + দাখিল কৰক + হয়, দাখিল কৰক + %1$s কিয় বিলোপ কৰা হ\'ব লাগে? + সফল + বিফল হ\'ল + এটা ছেল্ফি + ধোঁৱা-ধোঁৱা + অৰ্থহীন + অনান্য + ল\'গ\' + অন্য + কাৰণ এয়া diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index ba18dcdd0..964995205 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -169,6 +169,7 @@ Pidiendo permisu d\'almacenamientu Permisu riquíu: llectura d\'almacenamientu esternu. L\'aplicación nun puede entrar na to galería ensin él. Permisu necesariu: Escritura n\'almacenamientu esternu. L\'aplicación nun puede aportar a la cámara/galería ensin él. + Pidiendo permisu d\'allugamientu Permisu opcional: llograr l\'allugamientu actual pa suxerir categoríes Aceutar Llugares cercanos @@ -319,8 +320,8 @@ Asocedió un error al cargar les subcategoríes. Multimedia Categoríes - DESTACADA - XUBÍO DENDE\'L MÓVIL + Destacada + Xubío dende\'l móvil Añadióse correutamente la imaxe a %1$s en Wikidata. Nun pudo anovase la entidá de Wikidata correspondiente. Poner como fondu @@ -406,6 +407,14 @@ Marcadores Nun añadisti nengún marcador Marcadores + Principió la collecha de rexistros. Reanicia l\'aplicación, realiza l\'acción que desees rexistrar y, dempués, toca «Unviar ficheru de rexistru» nuevamente + Xubilu por error + Nun sabía que diba ser visible\'n público + Decatéme que ye malo pa la mio privacidá + Cambié d\'opinión, nun quiero que siga siendo visible públicamente + Sentímoslo, esta imaxe nun ye interesante pa una enciclopedia + Xubíla yo mesmu\'l %1$s, usada en %2$d artículu(os). + ¡Afáyate\'n Commons.\n\nCarga\'l to primer ficheru tocando nel botón amestar. Mundial América Europa @@ -420,6 +429,17 @@ (Pa toles imaxes del conxuntu) Buscar nesta área Solicitú de permisu + ¿Quies qu\'usemos el to allugamientu actual p\'amosate\'l llugar más cercanu que precisa imaxes? + Nun ye posible amosar el sitiu más cercanu que precisa semeyes ensin permisos d\'allugamientu + Nun entrugar más esto + Amosar permisu d\'allugamientu + Pidir permisu d\'allugamientu cuando se precise pa la función de ver la tarxeta de notificación cercana. + Daqué salió mal, nun pudimos recibir los tos llogros + Remata el: + Amosar campañes + Ver les campañes en cursu + Yá nun verás más les campañes. Sicasí, si quies puedes reactivar esti avisu na configuración. + Esta función rique conexón de rede, comprueba la configuración de conexón. Fecho Avisando al usuariu na páxina d\'alderique Añadiendo ficheru al rexistru de solicitúes de desaniciu @@ -432,5 +452,70 @@ Agradecer Agradecer Unviar agradecimientu por %1$s + Paez bien + Non, fora de contestu + Paez bien + Non, vulneración de derechos d\'autor + Paez bien + Sí, por qué non + Imaxe siguiente + Faciendo clic nesti botón recibirás otra imaxe de Wikimedia Commons xubida apocayá + Puedes revisar imaxes y ameyorar la calidá de Wikimedia Commoms.\nLos cuatro parámetros de revisión son: \n - ¿Ésta imaxe tien rellación col contestu? \n - ¿La imaxe cumple les riegles de copyright? \n - ¿La imaxe ta correchamente categorizada? \n - Si too ta correuto, tamién puedes dar les gracies al collaborador. + + Recibiendo\'l conteníu compartíu. El procesamientu de la imaxe puede tardar ciertu tiempu, dependiendo del tamañu de la imaxe y del to preséu + Recibiendo\'l conteníu compartíu. El procesamientu de les imaxes puede tardar ciertu tiempu, dependiendo del tamañu de les imaxes y del to preséu + + Nun s\'usó nenguna imaxe + Nun se revertió nenguna imaxe + Nun se xubió nenguna imaxe + Nun tienes avisos ensin lleer + Nun tienes avisos archivaos + Compartir rexistros usando + Ver los archivaos + Ver los nun lleíos + Asocedió un error al escoyer les imaxes + Escueyi les imaxes pa xubir + Espera… + Les imaxes destacaes son creaciones de talentosos fotógrafos y ilustradores que la comunidá de Wikimedia Commons reconoció como les de mayor calidá del sitiu. + Les imaxes xubíes via Llugares Cercanos son les imaxes que se cargaron descubriendo llugares nel mapa. + Esta función permite que los editores unvien una nota Gracies a los usuarios que realicen ediciones útiles usando un pequeñu enllaz d\'agradecimientu na páxina d\'historial o na de diferencies. + Copiar el títulu y descripción anteriores + Fai clic para reutilizar el títulu y descripción qu\'escribisti na imaxe anterior y cambialu p\'adautalu a la imaxe actual + Exemplos d\'imaxes bones pa xubir a Commons + Exemplos d\'imaxes que nun tienen de xubise Saltar esta imaxe + Falló la descarga. Nun podemos descargar el ficheru ensin permisu d\'almacenamientu esternu. + Alministrar etiquetes EXIF + Escoyer qué etiquetes EXIF tienen de caltenese nes cargues + Autor + Derechos d\'autor + Allugamientu + Modelu de la cámara + Modelu de la lente + Númberos de serie + Software + Xubi semeyes a Wikimedia Commons nel to móvil, descarga l\'aplicación de Commons: %1$s + Compartir l\'aplicación per... + Información de la imaxe + Nun s\'alcontró nenguna categoría + Carga encaboxada + Nun hai datos sobro\'l títulu o la descripción de la imaxe anterior + ¿Por qué tien de desaniciase %1$s? + %1$s xubióse por: %2$s + Idioma predetermináu de la descripción + Tentando marcar %1$s pal so desaniciu + Marcando pa desaniciar + Correuto + Marcóse correutamente %1$s pa desaniciar. + Falló + Non pudo pidise\'l desaniciu. + Un autorretratu + Borrosa + Xirigonza + Otru + Semeya de la prensa + Semeya tomada d\'internet al debalu + Logo + Otros + Porque ye diff --git a/app/src/main/res/values-b+tg+Cyrl/error.xml b/app/src/main/res/values-b+tg+Cyrl/error.xml index 6348c138a..07b2a2352 100644 --- a/app/src/main/res/values-b+tg+Cyrl/error.xml +++ b/app/src/main/res/values-b+tg+Cyrl/error.xml @@ -1,7 +1,11 @@ + Хаиогии Викианбор + Упс. Чизе хато пеш рафт! + Ба мо бигуед, ки шумо чӣ кор кардед, ба мо дар имайл нависед. Мо ба шумо кӯмак мерасонем. Ташаккур! diff --git a/app/src/main/res/values-b+tg+Cyrl/strings.xml b/app/src/main/res/values-b+tg+Cyrl/strings.xml index a6f8cc285..f0f464f65 100644 --- a/app/src/main/res/values-b+tg+Cyrl/strings.xml +++ b/app/src/main/res/values-b+tg+Cyrl/strings.xml @@ -1,10 +1,21 @@ + - Анбори Викимедиа + Умумӣ + Пешниҳод + Ҳарими хусусӣ + Мавқеъ + Викианбор Танзимот + Ирсолнамоӣ ба Викианбор Номи корбар Гузарвожа Вуруд + Гузарвожаро фаромӯш кардед? Дар ҳоли вуруд Лутфан мунтазир шавед… Вуруд бо муваффақият! @@ -17,10 +28,11 @@ Дар ҳоли боркунӣ Аз Нигористон Гирифтани акс + Наздикӣ Боргузориҳои ман Бо ҳам дидан Дидан дар Мурургар - Унвон + Унвон Тавзеҳот Вуруд номуваффақ шуд Боркунӣ @@ -28,8 +40,11 @@ Боркунӣ Ҷустуҷӯи гурӯҳҳо Захира + Азнавкунӣ + Рӯйхат Гурӯҳҳо Танзимот + Гурӯҳ Дар бораи Дар бораи Ирсоли Пешниҳод (тавассути Email) @@ -59,9 +74,89 @@ Гурӯҳҳо Дар ҳоли кушодашавӣ… Ҳеҷ яке интихоб нашудааст + Тавзеҳот нест + Тавзеҳот нест Иҷозатномаи ношинос + Азнавкунӣ + Бошад + Ҳушдор + Бале + На + Унвон + Тавсифот + Баҳс + Муаллиф + Рӯзи боркунӣ Иҷозатнома Координатҳо + Пешниҳод нашудааст + Логотипи Викианбор + Сомонаи Викианбор + Викианбор дар Фейсбук + Аксе пайдо нашуд + Ягон зергурӯҳе пайдо нашуд + Ба Википедиа хуш омадед. + Пӯшидан + Кушодан + Пӯшидан + Хона + Боркунӣ + Наздикӣ + Дар бораи + Танзимот + Пешниҳод + Баромад + Огоҳиҳо + Баргузида + Тафтиш Унсури Викидода Хатогӣ ҳангоми кэшкунии тасвир + Вуруд + Хондани мақола + Ҷиҳат + Викидода + Википедиа + Викианбор + Забонҳо + Лағв + Дубора саъй кунед + Гирифтан! + Ҷустуҷӯ + Ҷустуҷӯи Викианбор + Ҷустуҷӯ + Ҷусторҳои охир: + Расона + Гурӯҳҳо + Баргузида + Пурсиш + Савол + Натиҷа + Ҳазв + Омор + Сатҳ + Рӯйхат + Баъдӣ + Пешина + Ирсол + Маркаҳои китоб + Маркаҳои китоб + Расмҳо + Ҷойҳо + Илова/Ҳазв намудани маркаҳои китоб + Маркаҳои китоб + Маркаҳои китоб + Шабакаи дунявӣ + Амрико + Урупо + Шарқи наздик + Африқо + Осиё + Баҳри Ором + Ягон гурӯҳе интихоб нашуд + Бале, ирсолнамоӣ + На, ба ақиб + Анҷом шуд + Гузаштани ин акс + Забони шарҳ ба таври пешфарз + Муваффақият diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 3b61eb395..004e57183 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -204,6 +204,8 @@ УИКИПЕДИЯ ОБЩОМЕДИЯ Отказ + Търсене + Търсене ИЗОБРАЖЕНИЯ/ЗАПИСИ КАТЕГОРИИ Няма последни търсения @@ -215,6 +217,7 @@ Персонализираното авторско име, което ще се използва вместо потребителското ви име при качване Известия (архивирани) Списък + Следваща Изпращане Америка Европа diff --git a/app/src/main/res/values-ca/error.xml b/app/src/main/res/values-ca/error.xml index 6752fa848..14b176cac 100644 --- a/app/src/main/res/values-ca/error.xml +++ b/app/src/main/res/values-ca/error.xml @@ -4,8 +4,8 @@ * XVEC --> - El Commons s\'ha penjat - Uups ! Quelcom ha anat malament ! - Explica\'ns que estaves fent, després comparteix-ho mitjançant email amb nosaltres. Ens ajudaràs a arreglar-ho! + El Commons ha fallat + Ups! Quelcom ha anat malament! + Expliqueu-nos què féieu, després compartiu-nos-ho per correu electrònic. Ens ajudareu a arreglar-ho! Gràcies! diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 66f8b6956..d23ea88fa 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -10,14 +10,21 @@ * XVEC --> + Explora + Explora Aparença + General + Comentaris Privadesa Ubicació Commons Configuració + Carrega a Commons Nom d\'usuari Contrasenya + Inicieu una sessió al vostre compte beta de Commons Inicia una sessió + Heu oblidat la contrasenya? Registre S’està iniciant la sessió Espereu… @@ -33,8 +40,8 @@ Acabant la càrrega al servidor de %1$s Error al carregar %1$s Prem per veure-ho - - s\'està carregant 1 fitxer + + s\'està carregant %1$d fitxer s\'estan carregant %1$d fitxers Les meves càrregues recents @@ -48,34 +55,38 @@ Les meves càrregues Comparteix Mostra al navegador - Títol + Títol (obligatori) + Proporcioneu un títol per al fitxer Descripció No s\'ha pogut iniciar la sessió – error de xarxa + No es pot iniciar la sessió - comproveu el nom d\'usuari i la contrasenya Massa intents erronis – Proveu-ho de nou d\'aquí uns minuts. Ho sentim, aquest usuari ha estat blocat a Commons Heu de proporcionar el vostre codi d\'autenticació de dos factors. Ha fallat l\'inici de sessió Carrega Anomena aquest conjunt + Proporcioneu un nom per al conjunt Modificacions Carrega Categories de cerca Desa Refresca + Llista El vostre dispositiu no té el GPS habilitat. El voleu habilitar? Habilita el GPS Encara no hi ha cap càrrega - + \@string/contributions_subtitle_zero - %1$d càrrega - %1$d càrregues + (%1$d) + (%1$d) S\'està iniciant %1$d càrrega S\'estan iniciant %1$d càrregues - - 1 càrrega + + %1$d càrrega %1$d càrregues No s\'ha trobat cap categoria que coincideixi amb %1$s @@ -83,10 +94,14 @@ Categories Paràmetres Registre + Imatges destacades + Categoria + Revisió per parells Quant a L’aplicació de codi obert Wikimedia Commons fou creada per, i rep manteniment de, cessionaris i voluntaris de la comunitat de Wikimedia. La Fundació Wikimedia no està involucrada en la creació, el desenvolupament ni el manteniment de l’aplicació. - Codi a <a href=\"https://github.com/commons-app/apps-android-commons\">GitHub</a>. Informes d\'error a <a href=\" https://github.com/commons-app/apps-android-commons/issues\">Github</a>. - Wikimedia:Commons-android-texts-sobre privacitat/ca + Crea una <a href=\"%1$s\">incidència de GitHub</a> nova per a informes d\'error i suggeriments. + <u>Política de privadesa</u> + <u>Crèdits</u> Quant a Envia comentaris (per correu) No hi ha cap client de correu instal·lat @@ -95,10 +110,16 @@ Encara no heu carregat cap foto. Reintenta Cancel·la - Aquesta imatge es llicenciarà sota %1$s + + Aquesta imatge quedarà sota llicència %1$s + Aquestes imatges quedaran sota llicència %1$s + + En trametre aquesta imatge declaro que és fruit del meu treball, que no conté material amb drets d\'autor o amb autofotos, i que s\'adhereix a les <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">polítiques de Wikimedia Commons</a>. Baixa Llicència per defecte Utilitza títol i descripció anteriors + Obté la ubicació actual automàticament + Recupera la ubicació actual si la imatge no està geoetiquetada, i n\'etiqueta la imatge geogràficament. Avís: això revelarà la vostra ubicació actual. Mode nocturn Utilitza el tema fosc Reconeixement-CompartirIgual 4.0 @@ -121,44 +142,77 @@ CC BY-SA 4.0 CC BY 4.0 CC Zero + Wikimedia Commons hostatja la majoria d\'imatges que s\'utilitzen a la Viquipèdia. + Les vostres imatges ajuden a educar gent d\'arreu del món! + Carregueu només imatges que hàgiu pres o creat: + Objectes naturals (flors, animals, muntanyes) + Objectes útils (bicicletes, estacions de tren) + Gent famosa (el vostre batlle, atletes olímpiques que hàgiu conegut) NO carregueu: + Autofotos o fotos de les amistats Imatges baixades des de la Internet + Captures de pantalla d\'aplicacions privatives Exemple de càrrega: + Títol: Òpera de Sydney + Descripció: Òpera de Sydney vista des de l\'altre costat de la badia Doneu les vostres imatges. Ajudeu a donar vida als articles de la Viquipèdia! Les imatges de la Viquipèdia vénen de la Wikimedia Commons. Les vostres imatges ajuden a educar gent d\'arreu del món. No utilitzis material amb copyright que puguis trobar a Internet, ni tampoc imatges de pòsters, portades de llibres, etc. Creieu que ho heu entès? Sí! + <u>Més Informació</u> Categories Carregant… No se n\'ha seleccionat cap Sense descripció + Cap discussió Llicència desconeguda Refresca + S\'estan sol·licitant permisos d\'emmagatzematge + Sol·licitud de permisos d\'ubicació + Permís opcional: obtén la ubicació actual per al suggeriment de categories D\'acord Llocs propers No s\'han trobat llocs propers Avís + El fitxer ja existeix a Commons. Esteu segur que voleu procedir? No Títol Descripció Discussió + Autor Data de càrrega Llicència Coordenades No s\'ha proporcionat cap Codi 2FA + El meu límit de càrrega recent Límit màxim + El zero no és vàlid + Entrada no vàlida + No es poden mostrar més de 500 + Introduïu un nombre vàlid + El límit de càrrega no pot ser 0 + Límit de càrrega recent + Actualment no es permet l\'autenticació de dos factors. Realment voleu finalitzar la sessió? Logo de Commons Lloc web del Commons Pàgina del Facebook del Commons Codi font del Commons a GitHub + Imatge de fons + Ha fallat la imatge multimèdia No s’ha trobat cap imatge + No s\'han trobat subcategories + No s\'ha trobat cap categoria pare Carrega la imatge + Mont Zao Llames + Pont Rainbow + Tulipa + No autofotos Imatge privativa Benvinguts a la Viquipèdia Casa d\'Òpera de Sydney @@ -174,49 +228,200 @@ Finalitza la sessió Tutorial Notificacions + Destacat + Revisa + No poden mostrar-se els llocs propers sense permisos d\'ubicació no s\'ha trobat cap descripció Article del fitxer a Commons Element del Wikidata + Article de Viquipèdia Error mentre es carregaven les fotografies + Problemes potencials d\'aquesta imatge: + La imatge és massa fosca. + La imatge és borrosa. + La imatge ja és a Commons. + Aquesta imatge s\'ha pres a una altra ubicació. + Encara voleu carregar la imatge? Dóna permís Utilitza l’emmagatzematge extern + Desa les fotos fetes amb la càmera integrada del dispositiu Entreu en el vostre compte Envia el fitxer de registre - Envia el registre als desenvolupadors per correu + Envia el fitxer de registre a l\'equip de desenvolupament per correu per ajudar a depurar problemes amb l\'aplicació. Nota: els registres poden incorporar informació identificadora + No s\'ha trobat cap navegador web per obrir l\'URL + Error! No s\'ha trobat l\'URL + Nomina per a supressió + S\'ha nominat la imatge per a supressió. + <u>Vegeu la pàgina web per a més detalls</u> + Mostra en el navegador + Omet + Inicia una sessió + Realment voleu ometre l\'inici de sessió? + Caldrà que inicieu una sessió per carregar imatges en un futur. + Inicieu una sessió per utilitzar aquesta característica + Copia el wikitext al portaretalls + S\'ha copiat el wikitext al portaretalls La ubicació no ha canviat. La ubicació no és disponible. Com anar-hi Llegeix l’article + Us donem la benvinguda a Wikimedia Commons, %1$s! Ens alegra que hàgiu vingut. Gràcies per fer una modificació - %1$s us ha mencionat a %2$s. + %1$s us ha mencionat a %2$s. + Commuta la vista + Direccions Wikidata Viquipèdia Commons - Preguntes freqüents + <u>Valoreu-nos</u> + <u>PMF</u> + Omet la guia + Internet no disponible + Internet disponible + No s\'ha trobat cap notificació + <u>Tradueix</u> + Llengües + Procedeix + Cancel·la + Torna-ho a provar + Entesos No s’ha trobat cap imatge. + S\'ha produït un error en carregar les imatges. + Carregada per: %1$s + Esteu blocat d\'editar a Commons + Imatge del dia + Imatge del dia + Cerca + Cerca a Commons + Cerca + Cerques recents: + Consultes de cerca recents Multimèdia Categories Destacats Penjats mitjançant el mòbil + Defineix com a fons d\'escriptori + Qüestionari + Esteu d\'acord en carregar la imatge? + Pregunta + Resultat + Les autofotos no tenen molt valor enciclopèdic. No carregueu una foto vostra a menys que ja tingueu un article vostre a la Viquipèdia. + Continua + Resposta correcta + Resposta incorrecta + Esteu d\'acord en carregar la captura de pantalla? + Comparteix l\'aplicació + S\'ha produït un error en recuperar els llocs propers. + + Afegeix una descripció No hi ha cap cerca recent + Esteu segur que voleu esborrar el vostre historial de cerca? Voleu suprimir aquesta cerca? + S\'ha eliminat l\'historial de cerca + Nomina per a supressió Suprimeix + Assoliments Estadístiques + Agraïments rebuts + Imatges destacades + Imatges per proximitat Nivell + Imatges carregades + Imatges no revertides + Imatges utilitzades + S\'ha produït un error! + Notificació de Commons + Nom de l\'autor personalitzat + Contribucions + A prop + Notificacions + No s\'han trobat llocs propers prop vostre + Llista + Permissos d\'emmagatzematge + Pas %1$d de %2$d + Següent + Anterior + Envia + Adreces d\'interès + Adreces d\'interès + Imatges + Ubicacions + Afegeix/suprimeix de les adreces d\'interès + Adreces d\'interès + No heu afegit cap adreça d\'interès + Adreces d\'interès + Ho he carregat per error + No sabia que seria visible públicament + Me n\'he adonat que no era bo per a la meva privadesa + He canviat d\'opinió i no vull que continuï sent visible públicament + Ho sentim, la imatge no sembla interessant per a una enciclopèdia + Arreu del món + Amèrica + Europa + Orient mitjà + Àfrica + Àsia + Pacífic + No s\'ha seleccionat cap categoria + Sí, envia + No, vés enrere + Cerca en aquesta àrea + Sol·licitud de permisos + No ho tornis a demanar + Mostra el permís d\'ubicació + Finalitza el: + Mostra les campanyes Fet - NO, VIOLACIÓ DE DRETS D’AUTOR + No n\'estic segur + Té categorizació correcta? + S\'adiu al tema? + Voleu agrair al col·laborador? + Oh, no està ni tan sols categoritzat! + Aquesta imatge està sota les categories %1$s. + No, mala categorització + Sembla bé + Sembla bé + No, vulneració de drets d\'autor + Sembla bé I tant Imatge següent + No s\'ha utilitzat cap imatge + No s\'ha revertit cap imatge + No s\'ha carregat cap imatge No teniu cap notificació sense llegir No teniu cap notificació arxivada + Comparteix els registres utilitzant + Mostra els arxivats + Trieu les imatges per carregar + Espereu… + Copia el títol i descripció anteriors Exemples d’imatges adequades per a Commons Exemples d’imatges que no s’han de pujar + Omet aquesta imatge + Gestiona les etiquetes EXIF + Seleccioneu quines etiquetes EXIF voleu conservar en les càrregues Autor Drets d’autor Ubicació Model de la càmera + Model de lent Números de sèrie Programari + Comparteix l\'aplicació a través de... + Informació de la imatge No s’ha trobat cap categoria S’ha cancel·lat la pujada + No hi ha dades del títol o de la descripció anteriors de la imatge + Per què s\'hauria de suprimir %1$s? + Llengua per defecte de la descripció + Nomina per a supressió + Èxit + Una autofoto + Borrosa + Sense sentit + Altre + Foto de premsa + Foto aleatòria d\'Internet + Logotip + Altra + Perquè sí diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5ffb27d5b..15bf7568f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -32,7 +32,7 @@ Nahrát na Commons Uživatelské jméno Heslo - Přihlásit se do svého Commons Beta účtu + Přihlásit se do svého Commons beta účtu Přihlásit se Zapomněli jste heslo? Zaregistrovat se diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 801ac97ad..23fa6f611 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -16,6 +16,7 @@ Aparencia Xeral Comentarios + Privacidade Localización Commons @@ -97,6 +98,7 @@ Rexistrarse Imaxes destacadas Categoría + Revisión por pares Acerca de A aplicación Wikimedia Commons é unha aplicación de código aberto creada e mantida polos cesionarios e voluntarios da comunidade de Wikimedia. A Fundación Wikimedia non está involucrada na creación, desenvolvemento ou mantemento da aplicación. Crear unha nova <a href=\"%1$s\">incidencia</a> para informar de problemas e suxestións. @@ -167,11 +169,13 @@ Cargando… Ningunha seleccionada Sen descrición + Sen conversas Licenza descoñecida Refrescar Pedindo permiso de almacenamento Permiso necesarioː ler un almacenamento externo. A aplicación non pode acceder á súa galería sen isto. - Permiso necesario: Escribir en almacenamento externo. A aplicación non pode acceder á súa cámara sen el. + Permiso necesario: Escribir en almacenamento externo. A aplicación non pode acceder á súa cámara/galería sen el. + Pedindo Permiso de Localización Permiso opcionalː obter a localización actual para suxerir categorías Aceptar Lugares próximos @@ -182,6 +186,7 @@ Non Título Descrición + Conversa Autor Data de suba Licenza @@ -197,7 +202,7 @@ Non se poden amosar máis de 500 Indique un número válido O límite de subas non pode ser 0 - Establecer o límite de subas recentes + Establecer o límite de subas recentes Actualmente non se permite a autenticación de dous factores. Está seguro de querer saír? Logo de Commons @@ -232,6 +237,7 @@ Titorial Notificacións Destacados + Revisar Os sitios situados preto non poden visualizarse sen permisos de localización non se atopou descrición Páxina do ficheiro en Commons @@ -247,7 +253,9 @@ A imaxe está borrosa. A imaxe xa está en Commons. Esta imaxe foi realizada nunha localización diferente. + Por favor sube so fotografías feitas por ti mesmo. Non subas imaxes ou fotografías que atopes nas contas de Facebook de outros. Aínda quere subir esta imaxe? + Por favor suba so fotografías feitas por vostede mesmo. Non suba imaxes ou fotografías que descargara da Internet. Outorgar permiso Usar o almacenamento externo Gardar as imaxes capturadas coa cámara do seu dispositivo @@ -259,6 +267,8 @@ Nomear para borrado Esta imaxe foi nomeada para borrar. <u>Consulte a páxina web para ter máis para detalles</u> + Nomeando %1$d para borrado. + Nomeando ficheiro para borradoː %1$s Ver en navegador Omitir Acceder ao sistema @@ -270,23 +280,25 @@ A localización non cambiou. A localización non está dispoñible. Precísase permiso para amosar unha lista de lugares preto de aquí - OBTER DIRECCIÓNS - LER ARTIGO + Obter direccións + Ler artigo Benvido a Wikimedia Commons, %1$s! Alegrámonos de que estea aquí. %1$s deixoulle unha mensaxe na súa páxina de conversa Grazas por realizar unha edición %1$s mencionouno en %2$s. Cambiar modo de visualización - COMO CHEGAR - WIKIDATA - WIKIPEDIA - COMMONS + Como chegar + Wikidata + Wikipedia + Commons <u>Avalíenos</u> <u>FAQ</u> Saltar titorial Internet non dispoñible Internet dispoñible Erro ó recuperar as notificacións + Houbo un erro ó recuperar a imaxe a revisar. Prema en refrescar para tentalo de novo. + Houbo un erro ó obter as categorías de imaxes a revisar. Prema en refrescar para tentalo de novo. Non se atopou ningunha notificación <u>Traducir</u> Linguas @@ -295,7 +307,7 @@ Cancelar Reintentar Entendido! - Hai sitios preto de vostede que precisan fotos para ilustrar os seus artigos de Wikipedia + Hai sitios preto de vostede que precisan fotos para ilustrar os seus artigos de Wikipedia.\n\nPremendo en \"Procurar nesta área\" bloquea a localización do mapa e comeza unha procura de lugares na área circundante. Premendo neste botón aparecerá unha lista destes lugares Pode cargar unha imaxe de calquera lugar dende a galería ou a cámara Non se atopou ningunha imaxeǃ @@ -312,8 +324,10 @@ Consultas buscadas recentemente Houbo un erro ó cargar categorías. Houbo un erro ó cargar subcategorías. - MULTIMEDIA - CATEGORÍAS + Multimedia + Categorías + Destacadas + Cargada vía móbil A imaxe engadiuse con éxito a %1$s en Wikidata! Fallou a actualización da entidade do Wikidata correspondente! Poñer como imaxe de fondo @@ -344,14 +358,16 @@ + Engadir descrición Non hai procuras recentes Está seguro de querer borrar o seu historial de procuras? + Queres borrar esta procura? Eliminouse o historial de procuras Nomear para borrado + Borrar Logros - ESTATÍSTICAS + Estatísticas Agradecementos recibidos Imaxes destacadas Imaxes vía \"Lugares próximos\" - NIVEL + Nivel Imaxes cargadas Imaxes non revertidas Imaxes usadas @@ -403,8 +419,8 @@ Decateime de que prexudica a miña privacidade Cambiei de idea, non quero que siga sendo visible de forma pública Desculpas, esta imaxe non é interesante para unha enciclopedia - Cargada por min o - Benvido a Commonsǃ\n\nCargue o seu primeiro ficheiro premendo na icona da cámara ou da galería enriba. + Cargada por min o %1$s, usada en %2$d artigo(s). + Dámoslle a benvida ó Commonsǃ\n\nCargue o seu primeiro ficheiro premendo no botón Engadir. Mundial América Europa @@ -429,6 +445,19 @@ Esta función require conexión de rede, verifique a súa configuración de conexión. Erro na suba debido a problemas co identificador de edición. Peche a sesión e volva a entrar. Houbo un erro ó procesar a imaxe. Por favor, ténteo de novoǃ + Feito + Non seguro + Enviando agradecementos: Éxito + Enviando agradecementos + Enviando agradecementos + Enviando agradecementos por %1$s + Parece ben + Non, fóra de alcance + Parece ben + Non, violación de copyright + Parece ben + Si, por que non + Seguinte imaxe Recepción de contido compartido. O procesamento da imaxe pode tardar certo tempo, dependendo do tamaño da imaxe e do seu dispositivo Recepción de contido compartido. O procesamento das imaxes pode tardar certo tempo, dependendo do tamaño das imaxes e do seu dispositivo @@ -436,12 +465,35 @@ Ningunha imaxe usada Ningunha imaxe revertida Ningunha imaxe subida - Non ten ningunha notificación sen ler - Non ten notificacións arquivadas + Non ten ningunha notificación sen ler + Non ten notificacións arquivadas Compartir os rexistros usando Ver arquivadas Ver as non lidas Houbo un erro ó escoller as imaxes Escoller imaxes a subir Por favor, agarde… + Saltar esta imaxe + Autor + Dereitos de autoría + Localización + Modelo da cámara + Modelo de lente + Números de serie + Software + Compartir a aplicación vía... + Información da imaxe + Non se atoparon categorías + Cancelouse a carga + Todo correcto + Fallou + Un autorretrato + Borrosa + Sen sentido + Outro + Foto de prensa + Foto aleatoria de internet + Logo + Outro + Porque é diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 0af5b7979..e5ca8d0a3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2,6 +2,7 @@ @@ -25,6 +26,7 @@ Commons Ustawienia + Prześlij do Commons Nazwa użytkownika Hasło Zaloguj się @@ -60,6 +62,7 @@ Udostępnij Zobacz w przeglądarce Tytuł (wymagany) + Podaj tytuł tego pliku Opis Nie można zalogować - błąd sieci Nie można się zalogować - sprawdź swoją nazwę użytkownika i hasło diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3ca2a7d71..1ef6351bd 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -492,17 +492,17 @@ Логотипы, снимки экрана, постеры фильмов часто нарушают копирайт.\n Нажмите НЕТ, чтобы номинировать это изображение к удалению. %1$s получит от вас благодарность Эй, да тут даже категорий нет! - Это изображение отнесено к %1$s {{PLURAL:%1$s|категории|категориям}}. + Это изображение отнесено к следующим категориям: %1$s Файл не уместен на Викискладе, поскольку Это нарушение авторских прав, поскольку Неверно категоризировано - Выглядит подходяще - Не уместно - Выглядит подходяще + Уместен + Нет + Уместен Нет, это нарушение авторского права - Выглядит подходяще - Да, подходит - Следующее изображение + Уместен + Да + Следующий файл Нажатие этой кнопки приводит к показу следующего недавно загруженного изображения с Викисклада Вы можете проверить файлы, и, таким образом, улучшить их качество на Викискладе.\n Есть четыре проверки:\n - Уместен ли файл на Викискладе?\n - Соответствует ли файл правилам об авторском праве?\n - Правильно ли файл категоризирован?\n - Если всё хорошо, можете поблагодарить загрузившего этот файл. diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index f404e74ab..4ffec41e0 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -131,7 +131,7 @@ ڪڪ ٻڙي وڪيپيڊيا تي استعمال ٿيندڙ گھڻن عڪسن جي وڪيميڊيا العام ميزباني ڪري ٿو. توھان جا عڪس سڄي دنيا جي ماڻھن کي تعليم يافتا ڪرڻ ۾ مدد ڪن ٿا - براءِ مھرباني اھي تصويرون چاڙھيو مڪمل طور تي توھان پاران ڪڍيل يا تخليقيل آھن: + براءِ مھرباني اھي تصويرون چاڙھيو مڪمل طور تي توھان پاران ڪڍيل يا سرجيل آھن: قدرتي شيون (گل، جانور، جبل) استعمال جوڳيون شيون (سائيڪلون، ريل اسٽيشنون) مشھور شخصيتون (توھان جو ناظم، اولمپڪ رانديگر جنھن سان توھان مليو) @@ -254,5 +254,6 @@ ڳولا سوانح ڊاٿي وئي انگ اکر خلل ٿيو! + ڀاڱيداريون اسان کي اوهان جي ڊوائس جي اسٽورج جي اجازت گھرجي جيئن تصويرون چاڙهي سگھجن. diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 0873cb36b..54cb187b9 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -11,6 +11,7 @@ Pidangan Umum Eupan balik + Privasi Lokasi Commons diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 6fea0083f..00c959d14 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -533,7 +533,11 @@ %1$s laddas upp av: %2$s Standardspråk för beskrivning Försöker nominera %1$s för radering + Nominerar för radering + Genomfördes + Nominaterades %1$s för radering. Misslyckades + Kunde inte begära radering. Ett självporträtt Suddig Strunt @@ -542,4 +546,5 @@ Slumpbild från Internet Logotyp Annat + Eftersom den är diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 802ecc0f4..ec35107a1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -10,6 +10,7 @@ * Hydra * Kuailong * Liuxinyu970226 +* Looong * Qiyue2001 * Shizhao * Wxyveronica @@ -49,7 +50,7 @@ 点击查看您的上传 开始%1$s的上传 %1$s正在上传 - 完成上传%1$s + 正在完成上传%1$s 上传%1$s失败 点击查看 @@ -113,7 +114,7 @@ 维基共享资源应用是由维基媒体社区的受助者和志愿者创建和维护的开源应用。维基媒体基金会不参与该应用程序的创立,开发或维护。 创建新的<a href=\"%1$s\">GitHub问题</a>以发送错误报告和建议。 <u>隐私政策</u> - <u>制作人员</u> + <u>信用</u> 关于 发送反馈(通过电子邮件) 未安装电子邮件客户端 @@ -134,9 +135,9 @@ 如果图片没有地理标记,以及地理标签图片的话,就取得当前位置。警告:这将暴露您的当前位置。 夜间模式 使用黑暗主题 - 署名-相同方式共享4.0 + 署名-ShareAlike4.0 署名4.0 - 署名-相同方式共享3.0 + 署名-ShareAlike3.0 署名3.0 CC0 CC BY-SA 3.0 @@ -154,20 +155,20 @@ CC BY-SA 4.0 CC BY 4.0 CC0 - 维基共享资源存储大量用于维基百科的图片。 + 维基共享资源存储大部分用于维基百科的图片。 您的图像会帮助教育全世界的人! 请上传完全由您拍摄或创作的图片: 自然物(花、动物、山峰) 实用对象(自行车、火车站) - 著名人物(您会见的市长、奥运运动员) + 著名人物(您见过的市长、奥运会运动员) 请不要上传: 您朋友的自拍照或图片 您从互联网下载的图片 - 专利应用的截图 + 专门应用的截图 示例上传: 标题:悉尼歌剧院 说明:穿过海湾看到的悉尼歌剧院 - 分类:来自西侧的悉尼歌剧院、悉尼歌剧院外观 + 分类:尼歌剧院西侧、悉尼歌剧院远景 贡献您的图像。使维基百科的条目更加生动! 维基百科上的图像来自维基共享资源。 您的图像可以帮助教育世界各地的人。 @@ -212,7 +213,7 @@ 无法显示超过500张 输入有效数字 上传数量限制不能是 0 - 设置上传限制 + 最近上传限制 目前不支持双因素验证。 您真的想要退出么? 共享资源标志 @@ -246,9 +247,9 @@ 退出 教程 通知 - 特色 + 特色的 复核 - 附近地点不能在没有位置权限的情况下显示 + 没有位置权限的情况下不能显示附近地点 找不到描述 共享资源文件页面 维基数据项 @@ -268,7 +269,7 @@ 请仅上传由您自己拍摄的图像。请勿上传您从互联网下载的图像。 提供权限 使用外部存储 - 在您的设备上,使用应用中的照相机保存照片 + 保存有您的设备的内部照相机应用拍摄的照片 登录您的账户 发送日志文件 通过电子邮件发送日志文件给开发人员,以帮助调试应用程序的问题。注意:日志可能包含标识信息。 @@ -307,6 +308,8 @@ 互联网不可用 互联网可用 检索通知时出错 + 获取审查图片错误。按刷新键重试。 + 获取审查图片类别错误。按刷新按键重试。 找不到通知 <u>翻译</u> 语言 @@ -315,7 +318,7 @@ 取消 重试 明白了! - 这些是您附近需要图片以阐明维基百科条目的地方 + 这些是您附近需要图片以阐明维基百科条目的地方。\n点击“检索这个区域”来锁定地图并启动定位功能来检索附近位置。 点按此按钮会出现这些地点的列表 您可以从您的图库或照相机中上传任意地点的图片 找不到图片! @@ -366,6 +369,7 @@ + 添加描述 还没有最近搜索 您确认要清除您的搜索历史? + 你想删除本次检索吗? 搜索历史已删除 提名删除 删除 @@ -455,10 +459,24 @@ 这个功能需要网络连接,请检查你的网络设置。 因为编辑令牌失效上传失败。请尝试登出后重新登录。 处理图像时出错。请再试一次! + 获取编辑标记 + 添加类别检查模板 + 请求对%1$s进行类别检查 + 请求类别检查 + 已请求类别检查 + 类别检查请求不起作用 + %1$成功请求类别检查 + %1$不能请求类别检查 + 正在请求对%1$进行类别检查 + 添加文件的删除信息 完成 在讨论页上通知用户 + 正在添加文件到删除请求的日志文件中。 + 创建删除请求子页面 不确定 发送感谢成功 + 成功的给%1$发送感谢 + 给%1$发送感谢失败 发送感谢失败 发送感谢 发送感谢 @@ -467,8 +485,11 @@ 它是否被正确分类? 它是否在维基共享资源的收录范围内? 您是否要感谢贡献者? + 如果该图片无用,点击否来提议删除该文件。 标志、屏幕截图和电影海报通常侵犯版权。\n提名删除候选请点击“否”。 + 你的感谢会鼓励%1 该文件未被分类! + 该图片是在%1类别下 该文件不在收录范围内,原因是 该文件侵犯版权,原因是 否,分类错误 @@ -479,38 +500,62 @@ 看起来没问题 是,为什么不呢 下一张图片 + 点击这个按钮会给你另外一个来自于Wikimedia Common上传的图片 + 你可以审查图片病提高Wikimedia Commoms的质量。\n审查的四个参数是:\n-图片尺寸是否在规定的范围\n-图片是否遵循版权规则\n-图片是否正确分类\n-如果一切正常你是否也可以感谢贡献者 正在处理数据。处理时间取决于图片的尺寸和您的设备 没有被使用的图片 没有被撤回的图片 还没有上传图片 - 您没有未读通知 - 您没有已存档的通知 + 您没有任何未读通知 + 您没有已存档的通知 分享日志于 查看已存档 查看未读 选择图片时出错 选择要上传的图片 请稍候…… + 特征图片是Wikimedia Commons社区选出的网站上的最高质量的图片中的一部分,是来自于技术高超的摄影师和绘画师。 + 通过附近位置上传的图片是指那些使用地图上发现位置功能上传的图片 + 这些功能允许编辑人员给那些做出了有用编辑的用户发送感谢通知-感谢通知通过使用在历史页面或差分页面上的一个小的感谢链接实现的。 复制先前的标题及描述 点击复用您在先前图片中填写的标题及描述并稍加修改来匹配当前图像。 + 上传好图片到Commons的例子 + 不能上传图片的例子 跳过该图片 + 下载失败!!在没有外部存储权限时我们不能下载该文件。 + 管理EXIF标签 + 选择保存在上传中的EXIF标签 作者 著作权 位置 相机型号 + 镜面模型 序列号 软件 + 上传照片到手机上的Wikimedia Commons应用 下载Commons应用:%1 分享到... 图像信息 找不到分类 取消上传 + 没有之前图片的标题或描述对应的数据 %1$s为何应被删除? + %1通过%2上传 + 默认描述语言 + 尝试将%1作为删除的提案 + 正在提出删除准备 + 成功 + 成功提出将%1删除 + 失败了 + 不能请求删除 自拍 模糊 无意义 其他 + 按压图片 + 来自网络的随机图片 标志 其他 + 由于他是 diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index cdeaa8366..051cc19ec 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -70,6 +70,7 @@ diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt index eae8defc5..c54e9c2d9 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt @@ -57,7 +57,9 @@ class CategoriesPresenterTest { fun searchForCategoriesTest() { Mockito.`when`(repository?.sortBySimilarity(ArgumentMatchers.anyString())).thenReturn(Comparator { _, _ -> 1 }) Mockito.`when`(repository?.selectedCategories).thenReturn(categoryItems) - Mockito.`when`(repository?.searchAll(ArgumentMatchers.anyString(), ArgumentMatchers.anyList())).thenReturn(Observable.empty()) + Mockito.`when`(repository?.searchAll(ArgumentMatchers.anyString(), ArgumentMatchers.anyList())).thenReturn(testObservable) + Mockito.`when`(repository?.searchCategories(ArgumentMatchers.anyString(), ArgumentMatchers.anyList())).thenReturn(testObservable) + Mockito.`when`(repository?.getDefaultCategories(ArgumentMatchers.anyList())).thenReturn(testObservable) categoriesPresenter?.searchForCategories("test") verify(view)?.showProgress(true) verify(view)?.showError(null) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt index b54ed3b6d..9800d5f92 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -64,7 +64,6 @@ class UploadPresenterTest { verify(repository)?.prepareService() verify(view)?.showProgress(false) verify(view)?.showMessage(ArgumentMatchers.any(Int::class.java)) - verify(view)?.finish() true } }