Async folder loading. Optimized search result loading.
This commit is contained in:
parent
6749661b0b
commit
6f0d39a457
2 changed files with 123 additions and 46 deletions
|
|
@ -3,7 +3,6 @@ package com.kabouzeid.gramophone.ui.activities;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.app.LoaderManager;
|
import android.support.v4.app.LoaderManager;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v4.view.MenuItemCompat;
|
import android.support.v4.view.MenuItemCompat;
|
||||||
|
|
@ -11,6 +10,7 @@ import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.SearchView;
|
import android.support.v7.widget.SearchView;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
@ -33,9 +33,11 @@ import java.util.List;
|
||||||
|
|
||||||
import butterknife.Bind;
|
import butterknife.Bind;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import hugo.weaving.DebugLog;
|
||||||
|
|
||||||
public class SearchActivity extends AbsMusicServiceActivity implements SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<List<Object>> {
|
public class SearchActivity extends AbsMusicServiceActivity implements SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<List<Object>> {
|
||||||
public static final String TAG = SearchActivity.class.getSimpleName();
|
public static final String TAG = SearchActivity.class.getSimpleName();
|
||||||
|
public static final String QUERY = "query";
|
||||||
private static final int LOADER_ID = 1;
|
private static final int LOADER_ID = 1;
|
||||||
|
|
||||||
@Bind(R.id.recycler_view)
|
@Bind(R.id.recycler_view)
|
||||||
|
|
@ -48,7 +50,9 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
SearchView searchView;
|
SearchView searchView;
|
||||||
|
|
||||||
private SearchAdapter adapter;
|
private SearchAdapter adapter;
|
||||||
|
private String query;
|
||||||
|
|
||||||
|
@DebugLog
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -81,9 +85,24 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
|
|
||||||
setUpToolBar();
|
setUpToolBar();
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
query = savedInstanceState.getString(QUERY);
|
||||||
|
}
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(LOADER_ID, null, this);
|
getSupportLoaderManager().initLoader(LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putString(QUERY, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
private void setUpToolBar() {
|
private void setUpToolBar() {
|
||||||
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
|
toolbar.setBackgroundColor(ThemeStore.primaryColor(this));
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
|
@ -91,6 +110,7 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DebugLog
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
getMenuInflater().inflate(R.menu.menu_search, menu);
|
getMenuInflater().inflate(R.menu.menu_search, menu);
|
||||||
|
|
@ -98,7 +118,7 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
final MenuItem searchItem = menu.findItem(R.id.search);
|
final MenuItem searchItem = menu.findItem(R.id.search);
|
||||||
searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
|
searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
|
||||||
searchView.setQueryHint(getString(R.string.search_hint));
|
searchView.setQueryHint(getString(R.string.search_hint));
|
||||||
searchView.setOnQueryTextListener(this);
|
searchView.setMaxWidth(Integer.MAX_VALUE);
|
||||||
|
|
||||||
MenuItemCompat.expandActionView(searchItem);
|
MenuItemCompat.expandActionView(searchItem);
|
||||||
MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
|
MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
|
||||||
|
|
@ -114,6 +134,14 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
searchView.setQuery(query, false);
|
||||||
|
searchView.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
searchView.setOnQueryTextListener(SearchActivity.this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
return super.onCreateOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,18 +154,14 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
}
|
}
|
||||||
|
|
||||||
private void search(@NonNull String query) {
|
private void search(@NonNull String query) {
|
||||||
Loader loader = getSupportLoaderManager().getLoader(LOADER_ID);
|
this.query = query;
|
||||||
AsyncSearchResultLoader asyncSearchResultLoader = (AsyncSearchResultLoader) loader;
|
getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
|
||||||
asyncSearchResultLoader.setQuery(query);
|
|
||||||
asyncSearchResultLoader.forceLoad();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMediaStoreChanged() {
|
public void onMediaStoreChanged() {
|
||||||
super.onMediaStoreChanged();
|
super.onMediaStoreChanged();
|
||||||
Loader loader = getSupportLoaderManager().getLoader(LOADER_ID);
|
getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
|
||||||
AsyncSearchResultLoader asyncSearchResultLoader = (AsyncSearchResultLoader) loader;
|
|
||||||
asyncSearchResultLoader.forceLoad();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -146,6 +170,7 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DebugLog
|
||||||
@Override
|
@Override
|
||||||
public boolean onQueryTextChange(String newText) {
|
public boolean onQueryTextChange(String newText) {
|
||||||
search(newText);
|
search(newText);
|
||||||
|
|
@ -161,7 +186,7 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Loader<List<Object>> onCreateLoader(int id, Bundle args) {
|
public Loader<List<Object>> onCreateLoader(int id, Bundle args) {
|
||||||
return new AsyncSearchResultLoader(this);
|
return new AsyncSearchResultLoader(this, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -175,21 +200,17 @@ public class SearchActivity extends AbsMusicServiceActivity implements SearchVie
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class AsyncSearchResultLoader extends WrappedAsyncTaskLoader<List<Object>> {
|
private static class AsyncSearchResultLoader extends WrappedAsyncTaskLoader<List<Object>> {
|
||||||
private String query;
|
private final String query;
|
||||||
|
|
||||||
public AsyncSearchResultLoader(Context context) {
|
public AsyncSearchResultLoader(Context context, String query) {
|
||||||
super(context);
|
super(context);
|
||||||
setUpdateThrottle(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuery(@Nullable String query) {
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Object> loadInBackground() {
|
public List<Object> loadInBackground() {
|
||||||
List<Object> results = new ArrayList<>();
|
List<Object> results = new ArrayList<>();
|
||||||
if (query != null && !query.trim().equals("")) {
|
if (!TextUtils.isEmpty(query)) {
|
||||||
List songs = SongLoader.getSongs(getContext(), query);
|
List songs = SongLoader.getSongs(getContext(), query);
|
||||||
if (!songs.isEmpty()) {
|
if (!songs.isEmpty()) {
|
||||||
results.add(getContext().getResources().getString(R.string.songs));
|
results.add(getContext().getResources().getString(R.string.songs));
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.AppBarLayout;
|
import android.support.design.widget.AppBarLayout;
|
||||||
import android.support.design.widget.CoordinatorLayout;
|
import android.support.design.widget.CoordinatorLayout;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v4.app.LoaderManager;
|
||||||
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
|
|
@ -39,6 +41,7 @@ import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
|
||||||
import com.kabouzeid.gramophone.interfaces.CabHolder;
|
import com.kabouzeid.gramophone.interfaces.CabHolder;
|
||||||
import com.kabouzeid.gramophone.loader.SongLoader;
|
import com.kabouzeid.gramophone.loader.SongLoader;
|
||||||
import com.kabouzeid.gramophone.loader.SortedCursor;
|
import com.kabouzeid.gramophone.loader.SortedCursor;
|
||||||
|
import com.kabouzeid.gramophone.misc.WrappedAsyncTaskLoader;
|
||||||
import com.kabouzeid.gramophone.model.Song;
|
import com.kabouzeid.gramophone.model.Song;
|
||||||
import com.kabouzeid.gramophone.ui.activities.MainActivity;
|
import com.kabouzeid.gramophone.ui.activities.MainActivity;
|
||||||
import com.kabouzeid.gramophone.ui.fragments.mainactivity.AbsMainActivityFragment;
|
import com.kabouzeid.gramophone.ui.fragments.mainactivity.AbsMainActivityFragment;
|
||||||
|
|
@ -51,6 +54,7 @@ import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileFilter;
|
import java.io.FileFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
@ -61,9 +65,11 @@ import java.util.List;
|
||||||
import butterknife.Bind;
|
import butterknife.Bind;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
public class FoldersFragment extends AbsMainActivityFragment implements MainActivity.MainActivityFragmentCallbacks, CabHolder, BreadCrumbLayout.SelectionCallback, SongFileAdapter.Callbacks, AppBarLayout.OnOffsetChangedListener {
|
public class FoldersFragment extends AbsMainActivityFragment implements MainActivity.MainActivityFragmentCallbacks, CabHolder, BreadCrumbLayout.SelectionCallback, SongFileAdapter.Callbacks, AppBarLayout.OnOffsetChangedListener, LoaderManager.LoaderCallbacks<List<File>> {
|
||||||
public static final String TAG = FoldersFragment.class.getSimpleName();
|
public static final String TAG = FoldersFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int LOADER_ID = 1;
|
||||||
|
|
||||||
protected static final String PATH = "path";
|
protected static final String PATH = "path";
|
||||||
protected static final String CRUMBS = "crumbs";
|
protected static final String CRUMBS = "crumbs";
|
||||||
|
|
||||||
|
|
@ -101,43 +107,41 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCrumb(BreadCrumbLayout.Crumb crumb, boolean addToHistory) {
|
public void setCrumb(BreadCrumbLayout.Crumb crumb, boolean addToHistory) {
|
||||||
|
if (crumb == null) return;
|
||||||
saveScrollPosition();
|
saveScrollPosition();
|
||||||
updateAdapter(crumb.getFile());
|
|
||||||
breadCrumbs.setActiveOrAdd(crumb, false);
|
breadCrumbs.setActiveOrAdd(crumb, false);
|
||||||
if (addToHistory)
|
if (addToHistory) {
|
||||||
breadCrumbs.addHistory(crumb);
|
breadCrumbs.addHistory(crumb);
|
||||||
crumb = breadCrumbs.findCrumb(crumb.getFile()); // get the real reference so we can restore previous scroll states
|
}
|
||||||
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(crumb.getScrollPosition(), 0);
|
getLoaderManager().restartLoader(LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveScrollPosition() {
|
private void saveScrollPosition() {
|
||||||
if (breadCrumbs.size() > 0) {
|
BreadCrumbLayout.Crumb crumb = getActiveCrumb();
|
||||||
BreadCrumbLayout.Crumb crumb = breadCrumbs.getCrumb(breadCrumbs.getActiveIndex());
|
if (crumb != null) {
|
||||||
crumb.setScrollPosition(((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition());
|
crumb.setScrollPosition(((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private BreadCrumbLayout.Crumb getActiveCrumb() {
|
||||||
|
return breadCrumbs != null && breadCrumbs.size() > 0 ? breadCrumbs.getCrumb(breadCrumbs.getActiveIndex()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putParcelable(CRUMBS, breadCrumbs.getStateWrapper());
|
outState.putParcelable(CRUMBS, breadCrumbs.getStateWrapper());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAdapter(File directory) {
|
@Override
|
||||||
List<File> files = sort(listFiles(directory, getFileFilter()));
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
if (adapter == null) {
|
super.onActivityCreated(savedInstanceState);
|
||||||
adapter = new SongFileAdapter(getMainActivity(), files, R.layout.item_list, this, this);
|
if (savedInstanceState == null) {
|
||||||
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
setCrumb(new BreadCrumbLayout.Crumb(tryGetCanonicalFile((File) getArguments().getSerializable(PATH))), true);
|
||||||
@Override
|
|
||||||
public void onChanged() {
|
|
||||||
super.onChanged();
|
|
||||||
checkIsEmpty();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
checkIsEmpty();
|
|
||||||
} else {
|
} else {
|
||||||
adapter.swapDataSet(files);
|
breadCrumbs.restoreFromStateWrapper((BreadCrumbLayout.SavedStateWrapper) savedInstanceState.getParcelable(CRUMBS));
|
||||||
|
getLoaderManager().initLoader(LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,13 +162,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
setUpToolbar();
|
setUpToolbar();
|
||||||
setUpBreadCrumbs();
|
setUpBreadCrumbs();
|
||||||
setUpRecyclerView();
|
setUpRecyclerView();
|
||||||
|
setUpAdapter();
|
||||||
if (savedInstanceState == null) {
|
|
||||||
setCrumb(new BreadCrumbLayout.Crumb(tryGetCanonicalFile((File) getArguments().getSerializable(PATH))), true);
|
|
||||||
} else {
|
|
||||||
breadCrumbs.restoreFromStateWrapper((BreadCrumbLayout.SavedStateWrapper) savedInstanceState.getParcelable(CRUMBS));
|
|
||||||
setCrumb(breadCrumbs.getCrumb(breadCrumbs.getActiveIndex()), true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setUpAppbarColor() {
|
private void setUpAppbarColor() {
|
||||||
|
|
@ -194,6 +192,19 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
appbar.addOnOffsetChangedListener(this);
|
appbar.addOnOffsetChangedListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setUpAdapter() {
|
||||||
|
adapter = new SongFileAdapter(getMainActivity(), new LinkedList<File>(), R.layout.item_list, this, this);
|
||||||
|
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
super.onChanged();
|
||||||
|
checkIsEmpty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
checkIsEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
@ -463,7 +474,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private List<File> listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) {
|
private static List<File> listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) {
|
||||||
List<File> fileList = new LinkedList<>();
|
List<File> fileList = new LinkedList<>();
|
||||||
File[] found = directory.listFiles(fileFilter);
|
File[] found = directory.listFiles(fileFilter);
|
||||||
if (found != null) {
|
if (found != null) {
|
||||||
|
|
@ -605,4 +616,49 @@ public class FoldersFragment extends AbsMainActivityFragment implements MainActi
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateAdapter(@NonNull List<File> files) {
|
||||||
|
adapter.swapDataSet(files);
|
||||||
|
BreadCrumbLayout.Crumb crumb = getActiveCrumb();
|
||||||
|
if (crumb != null && recyclerView != null) {
|
||||||
|
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(crumb.getScrollPosition(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Loader<List<File>> onCreateLoader(int id, Bundle args) {
|
||||||
|
return new AsyncFileLoader(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(Loader<List<File>> loader, List<File> data) {
|
||||||
|
updateAdapter(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoaderReset(Loader<List<File>> loader) {
|
||||||
|
updateAdapter(new LinkedList<File>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AsyncFileLoader extends WrappedAsyncTaskLoader<List<File>> {
|
||||||
|
private WeakReference<FoldersFragment> fragmentWeakReference;
|
||||||
|
|
||||||
|
public AsyncFileLoader(FoldersFragment foldersFragment) {
|
||||||
|
super(foldersFragment.getActivity());
|
||||||
|
fragmentWeakReference = new WeakReference<>(foldersFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<File> loadInBackground() {
|
||||||
|
FoldersFragment foldersFragment = fragmentWeakReference.get();
|
||||||
|
File directory = null;
|
||||||
|
if (foldersFragment != null) {
|
||||||
|
BreadCrumbLayout.Crumb crumb = foldersFragment.getActiveCrumb();
|
||||||
|
if (crumb != null) {
|
||||||
|
directory = crumb.getFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return directory != null ? foldersFragment.sort(listFiles(directory, foldersFragment.getFileFilter())) : new LinkedList<File>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue