mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-30 14:23:55 +01:00
Replace temporary fixes completely
This commit is contained in:
parent
73c16aea41
commit
c25f79c49a
3 changed files with 29 additions and 238 deletions
|
|
@ -23,8 +23,6 @@ import android.graphics.drawable.Drawable;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
@ -56,7 +54,6 @@ import androidx.appcompat.app.AlertDialog.Builder;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.LifecycleOwnerKt;
|
import androidx.lifecycle.LifecycleOwnerKt;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
|
@ -97,7 +94,6 @@ import fr.free.nrw.commons.nearby.PlacesRepository;
|
||||||
import fr.free.nrw.commons.nearby.WikidataFeedback;
|
import fr.free.nrw.commons.nearby.WikidataFeedback;
|
||||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
||||||
import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback;
|
import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback;
|
||||||
import fr.free.nrw.commons.nearby.helper.JustExperimenting;
|
|
||||||
import fr.free.nrw.commons.nearby.model.BottomSheetItem;
|
import fr.free.nrw.commons.nearby.model.BottomSheetItem;
|
||||||
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
|
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
|
||||||
import fr.free.nrw.commons.upload.FileUtils;
|
import fr.free.nrw.commons.upload.FileUtils;
|
||||||
|
|
@ -113,16 +109,12 @@ import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -158,8 +150,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
|
|
||||||
FragmentNearbyParentBinding binding;
|
FragmentNearbyParentBinding binding;
|
||||||
|
|
||||||
// private JustExperimenting justExperimenting;
|
|
||||||
|
|
||||||
public final MapEventsOverlay mapEventsOverlay = new MapEventsOverlay(new MapEventsReceiver() {
|
public final MapEventsOverlay mapEventsOverlay = new MapEventsOverlay(new MapEventsReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
||||||
|
|
@ -353,8 +343,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
binding = FragmentNearbyParentBinding.inflate(inflater, container, false);
|
binding = FragmentNearbyParentBinding.inflate(inflater, container, false);
|
||||||
view = binding.getRoot();
|
view = binding.getRoot();
|
||||||
|
|
||||||
// justExperimenting = new JustExperimenting(this);
|
|
||||||
|
|
||||||
initNetworkBroadCastReceiver();
|
initNetworkBroadCastReceiver();
|
||||||
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao, placesRepository, nearbyController);
|
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao, placesRepository, nearbyController);
|
||||||
progressDialog = new ProgressDialog(getActivity());
|
progressDialog = new ProgressDialog(getActivity());
|
||||||
|
|
@ -1363,7 +1351,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
? getTextBetweenParentheses(
|
? getTextBetweenParentheses(
|
||||||
updatedPlace.getLongDescription()) : updatedPlace.getLongDescription());
|
updatedPlace.getLongDescription()) : updatedPlace.getLongDescription());
|
||||||
marker.showInfoWindow();
|
marker.showInfoWindow();
|
||||||
// justExperimenting.handlePlaceClicked(updatedPlace);
|
presenter.handlePinClicked(updatedPlace);
|
||||||
savePlaceToDatabase(place);
|
savePlaceToDatabase(place);
|
||||||
Drawable icon = ContextCompat.getDrawable(getContext(),
|
Drawable icon = ContextCompat.getDrawable(getContext(),
|
||||||
getIconFor(updatedPlace, isBookMarked));
|
getIconFor(updatedPlace, isBookMarked));
|
||||||
|
|
@ -1464,20 +1452,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Place getPlaceFromRepository(String entityID) {
|
|
||||||
return placesRepository.fetchPlace(entityID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Place> getPlacesFromController(List<Place> places) {
|
|
||||||
List<Place> results = new ArrayList<Place>();
|
|
||||||
try {
|
|
||||||
results = nearbyController.getPlaces(places);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.tag("Nearby Pin Details").e(e);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void savePlaceToDatabase(Place place) {
|
public void savePlaceToDatabase(Place place) {
|
||||||
compositeDisposable.add(placesRepository
|
compositeDisposable.add(placesRepository
|
||||||
.save(place)
|
.save(place)
|
||||||
|
|
@ -1953,18 +1927,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
binding.map.getOverlays().addAll(newMarkers);
|
binding.map.getOverlays().addAll(newMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds multiple markers representing places to the map and handles item gestures.
|
|
||||||
*
|
|
||||||
* @param nearbyBaseMarkers The list of Place objects containing information about the
|
|
||||||
* locations.
|
|
||||||
*/
|
|
||||||
private void addMarkersToMap(List<BaseMarker> nearbyBaseMarkers) {
|
|
||||||
Timber.tag("temptagtwo").e("another n+1 C 2: " + nearbyBaseMarkers.size());
|
|
||||||
for(int i = 0; i< nearbyBaseMarkers.size(); i++){
|
|
||||||
addMarkerToMap(nearbyBaseMarkers.get(i).getPlace(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts text between the first occurrence of '(' and its corresponding ')' in the input
|
* Extracts text between the first occurrence of '(' and its corresponding ')' in the input
|
||||||
|
|
@ -2257,7 +2219,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
binding.map.invalidate();
|
binding.map.invalidate();
|
||||||
GeoPoint geoPoint = mapCenter;
|
GeoPoint geoPoint = mapCenter;
|
||||||
if (geoPoint != null) {
|
if (geoPoint != null) {
|
||||||
List<Overlay> overlays = binding.map.getOverlays();
|
|
||||||
ScaleDiskOverlay diskOverlay =
|
ScaleDiskOverlay diskOverlay =
|
||||||
new ScaleDiskOverlay(this.getContext(),
|
new ScaleDiskOverlay(this.getContext(),
|
||||||
geoPoint, 2000, UnitOfMeasure.foot);
|
geoPoint, 2000, UnitOfMeasure.foot);
|
||||||
|
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
package fr.free.nrw.commons.nearby.helper
|
|
||||||
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import fr.free.nrw.commons.location.LatLng
|
|
||||||
import fr.free.nrw.commons.nearby.MarkerPlaceGroup
|
|
||||||
import fr.free.nrw.commons.nearby.Place
|
|
||||||
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.osmdroid.views.overlay.Marker
|
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
|
||||||
|
|
||||||
class JustExperimenting(frag: NearbyParentFragment) {
|
|
||||||
private val scope = frag.viewLifecycleOwner.lifecycleScope
|
|
||||||
|
|
||||||
private var skippedCount = 0
|
|
||||||
private val skipLimit = 2
|
|
||||||
private val skipDelayMs = 1000L
|
|
||||||
|
|
||||||
private var markersStateChannel = Channel<List<Marker>>(Channel.CONFLATED)
|
|
||||||
private val markerBaseDataChannel = Channel<ArrayList<MarkerPlaceGroup>>(Channel.CONFLATED)
|
|
||||||
|
|
||||||
private val clickedPlaces = CopyOnWriteArrayList<Place>()
|
|
||||||
fun handlePlaceClicked(place: Place) {
|
|
||||||
clickedPlaces.add(place)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadNewMarkers(es: ArrayList<MarkerPlaceGroup>) = scope.launch {
|
|
||||||
markerBaseDataChannel.send(es)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateMarkersState(markers: List<Marker>) {
|
|
||||||
markersStateChannel.send(markers)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
var pinStateUpdateJob: Job? = null
|
|
||||||
for (markers in markersStateChannel) {
|
|
||||||
pinStateUpdateJob?.cancel()
|
|
||||||
pinStateUpdateJob = launch {
|
|
||||||
if (markers.isEmpty()) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
if (skippedCount++ < skipLimit) {
|
|
||||||
delay(skipDelayMs)
|
|
||||||
}
|
|
||||||
skippedCount = 0
|
|
||||||
// frag.replaceMarkerOverlays(markers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
var loadPinDetailsJob: Job? = null
|
|
||||||
for (markerBaseDataList in markerBaseDataChannel) {
|
|
||||||
loadPinDetailsJob?.cancel()
|
|
||||||
loadPinDetailsJob = launch {
|
|
||||||
loadPinsDetails(frag, markerBaseDataList, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frag.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
|
||||||
performCleanup()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadPinsDetails(
|
|
||||||
frag: NearbyParentFragment,
|
|
||||||
markerBaseDataList: ArrayList<MarkerPlaceGroup>,
|
|
||||||
scope: CoroutineScope
|
|
||||||
) {
|
|
||||||
// make sure the grey pins are loaded immediately:
|
|
||||||
skippedCount = skipLimit
|
|
||||||
updateMarkersState(markerBaseDataList.map {
|
|
||||||
frag.convertToMarker(it.place, it.isBookmarked)
|
|
||||||
})
|
|
||||||
|
|
||||||
// now load the pin details:
|
|
||||||
clickedPlaces.clear()
|
|
||||||
var clickedPlacesIndex = 0
|
|
||||||
markerBaseDataList.sortBy {
|
|
||||||
it.place.getDistanceInDouble(frag.mapFocus)
|
|
||||||
}
|
|
||||||
val updatedMarkers = ArrayList<Marker>(markerBaseDataList.size)
|
|
||||||
markerBaseDataList.forEach {
|
|
||||||
updatedMarkers.add(frag.convertToMarker(it.place, it.isBookmarked))
|
|
||||||
}
|
|
||||||
|
|
||||||
val batchSize = 3
|
|
||||||
var currentIndex = 0
|
|
||||||
val endIndex = markerBaseDataList.lastIndex
|
|
||||||
while (currentIndex <= endIndex) {
|
|
||||||
scope.ensureActive()
|
|
||||||
val toUpdateMarkersFrom = currentIndex
|
|
||||||
|
|
||||||
val placesToFetch = mutableListOf<Int>()
|
|
||||||
while (currentIndex <= endIndex && placesToFetch.size < batchSize) {
|
|
||||||
val existingPlace = markerBaseDataList[currentIndex].place
|
|
||||||
if (existingPlace.name != "") {
|
|
||||||
++currentIndex
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val repoPlace = withContext(Dispatchers.IO) {
|
|
||||||
frag.getPlaceFromRepository(existingPlace.entityID)
|
|
||||||
}
|
|
||||||
if (repoPlace != null && repoPlace.name != "") {
|
|
||||||
markerBaseDataList[currentIndex] =
|
|
||||||
MarkerPlaceGroup(markerBaseDataList[currentIndex].isBookmarked, repoPlace)
|
|
||||||
++currentIndex
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
placesToFetch.add(currentIndex)
|
|
||||||
++currentIndex
|
|
||||||
}
|
|
||||||
if (placesToFetch.isNotEmpty()) {
|
|
||||||
val fetchedPlaces = withContext(Dispatchers.IO) {
|
|
||||||
frag.getPlacesFromController(placesToFetch.map {
|
|
||||||
markerBaseDataList[it].place
|
|
||||||
})
|
|
||||||
}
|
|
||||||
scope.ensureActive()
|
|
||||||
for (fetchedPlace in fetchedPlaces) {
|
|
||||||
for (index in placesToFetch) { // nesting okay here as batch size is small
|
|
||||||
val existingPlace = markerBaseDataList[index].place
|
|
||||||
if (existingPlace.siteLinks.wikidataLink ==
|
|
||||||
fetchedPlace.siteLinks.wikidataLink
|
|
||||||
) {
|
|
||||||
fetchedPlace.location = existingPlace.location
|
|
||||||
fetchedPlace.distance = existingPlace.distance
|
|
||||||
fetchedPlace.isMonument = existingPlace.isMonument
|
|
||||||
markerBaseDataList[index] = MarkerPlaceGroup(
|
|
||||||
markerBaseDataList[index].isBookmarked, fetchedPlace
|
|
||||||
)
|
|
||||||
frag.savePlaceToDatabase(fetchedPlace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (i in toUpdateMarkersFrom..<currentIndex) {
|
|
||||||
updatedMarkers[i] = frag.convertToMarker(
|
|
||||||
markerBaseDataList[i].place, markerBaseDataList[i].isBookmarked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (clickedPlacesIndex < clickedPlaces.size) {
|
|
||||||
val clickedPlacesBacklog = hashMapOf<LatLng, Place>()
|
|
||||||
while (clickedPlacesIndex < clickedPlaces.size) {
|
|
||||||
clickedPlacesBacklog.put(
|
|
||||||
clickedPlaces[clickedPlacesIndex].location,
|
|
||||||
clickedPlaces[clickedPlacesIndex]
|
|
||||||
)
|
|
||||||
++clickedPlacesIndex
|
|
||||||
}
|
|
||||||
for (i in currentIndex..endIndex) {
|
|
||||||
if (clickedPlacesBacklog.containsKey(markerBaseDataList[i].place.location)) {
|
|
||||||
markerBaseDataList[i] = MarkerPlaceGroup(
|
|
||||||
markerBaseDataList[i].isBookmarked,
|
|
||||||
clickedPlacesBacklog[markerBaseDataList[i].place.location]
|
|
||||||
)
|
|
||||||
updatedMarkers[i] = frag.convertToMarker(
|
|
||||||
markerBaseDataList[i].place, markerBaseDataList[i].isBookmarked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateMarkersState(updatedMarkers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performCleanup() {
|
|
||||||
markersStateChannel.close()
|
|
||||||
markerBaseDataChannel.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// private val mapEventsOverlay = frag.mapEventsOverlay
|
|
||||||
// fun getBaseOverlays(view: MapView): List<Overlay> = listOf(
|
|
||||||
// // distance scale
|
|
||||||
// ScaleBarOverlay(view).apply {
|
|
||||||
// setScaleBarOffset(15, 25)
|
|
||||||
// setBackgroundPaint(Paint().apply { setARGB(200, 255, 250, 250) })
|
|
||||||
// enableScaleBar()
|
|
||||||
// },
|
|
||||||
// // map events overlay:
|
|
||||||
// mapEventsOverlay
|
|
||||||
// )
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -32,6 +32,8 @@ import timber.log.Timber
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.lang.reflect.Proxy
|
import java.lang.reflect.Proxy
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet
|
||||||
|
|
||||||
class NearbyParentFragmentPresenter
|
class NearbyParentFragmentPresenter
|
||||||
(
|
(
|
||||||
|
|
@ -51,6 +53,11 @@ class NearbyParentFragmentPresenter
|
||||||
|
|
||||||
private var nearbyParentFragmentView: NearbyParentFragmentContract.View = DUMMY
|
private var nearbyParentFragmentView: NearbyParentFragmentContract.View = DUMMY
|
||||||
|
|
||||||
|
private val clickedPlaces = CopyOnWriteArrayList<Place>()
|
||||||
|
fun handlePinClicked(place: Place) {
|
||||||
|
clickedPlaces.add(place)
|
||||||
|
}
|
||||||
|
|
||||||
private var loadPlacesDataAyncJob: Job? = null
|
private var loadPlacesDataAyncJob: Job? = null
|
||||||
private object LoadPlacesAsyncOptions {
|
private object LoadPlacesAsyncOptions {
|
||||||
val batchSize = 3
|
val batchSize = 3
|
||||||
|
|
@ -235,6 +242,8 @@ class NearbyParentFragmentPresenter
|
||||||
updatePlaceGroupsToControllerAndRender(nearbyPlaceGroups)
|
updatePlaceGroupsToControllerAndRender(nearbyPlaceGroups)
|
||||||
|
|
||||||
loadPlacesDataAyncJob = scope?.launch(Dispatchers.IO) {
|
loadPlacesDataAyncJob = scope?.launch(Dispatchers.IO) {
|
||||||
|
clickedPlaces.clear() // clear past clicks
|
||||||
|
var clickedPlacesIndex = 0
|
||||||
val updatedGroups = nearbyPlaceGroups.toMutableList()
|
val updatedGroups = nearbyPlaceGroups.toMutableList()
|
||||||
// first load cached places:
|
// first load cached places:
|
||||||
val indicesToUpdate = mutableListOf<Int>()
|
val indicesToUpdate = mutableListOf<Int>()
|
||||||
|
|
@ -305,6 +314,25 @@ class NearbyParentFragmentPresenter
|
||||||
placesRepository.save(finalPlaceGroup.place)
|
placesRepository.save(finalPlaceGroup.place)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// handle any places clicked
|
||||||
|
if (clickedPlacesIndex < clickedPlaces.size) {
|
||||||
|
val clickedPlacesBacklog = hashMapOf<LatLng, Place>()
|
||||||
|
while (clickedPlacesIndex < clickedPlaces.size) {
|
||||||
|
clickedPlacesBacklog.put(
|
||||||
|
clickedPlaces[clickedPlacesIndex].location,
|
||||||
|
clickedPlaces[clickedPlacesIndex]
|
||||||
|
)
|
||||||
|
++clickedPlacesIndex
|
||||||
|
}
|
||||||
|
for ((index, group) in updatedGroups.withIndex()) {
|
||||||
|
if (clickedPlacesBacklog.containsKey(group.place.location)) {
|
||||||
|
updatedGroups[index] = MarkerPlaceGroup(
|
||||||
|
updatedGroups[index].isBookmarked,
|
||||||
|
clickedPlacesBacklog[group.place.location]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
schedulePlacesUpdate(updatedGroups)
|
schedulePlacesUpdate(updatedGroups)
|
||||||
if (collectCount++ == totalBatches) {
|
if (collectCount++ == totalBatches) {
|
||||||
break
|
break
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue