Infinite mod list implementation

This commit is contained in:
BuildTools 2023-08-17 15:52:08 +03:00 committed by ArtDev
parent 04d6900bc4
commit e21be296ec
9 changed files with 272 additions and 99 deletions

View File

@ -2,9 +2,7 @@ package net.kdt.pojavlaunch.fragments;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
@ -18,23 +16,14 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.kdt.pojavlaunch.PojavApplication;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter;
import net.kdt.pojavlaunch.modloaders.modpacks.SelfReferencingFuture;
import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi;
import net.kdt.pojavlaunch.modloaders.modpacks.api.CurseforgeApi;
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi;
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModrinthApi;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.profiles.VersionSelectorDialog;
import java.util.Arrays;
import java.util.concurrent.Future;
public class SearchModFragment extends Fragment {
public class SearchModFragment extends Fragment implements ModItemAdapter.SearchResultCallback {
public static final String TAG = "SearchModFragment";
private View mOverlay;
@ -53,13 +42,12 @@ public class SearchModFragment extends Fragment {
private RecyclerView mRecyclerview;
private ModItemAdapter mModItemAdapter;
private ProgressBar mSearchProgressBar;
private Future<?> mSearchFuture;
private TextView mStatusTextView;
private ColorStateList mDefaultTextColor;
private ModpackApi modpackApi;
private SearchFilters mSearchFilters;
private final SearchFilters mSearchFilters;
public SearchModFragment(){
super(R.layout.fragment_mod_search);
@ -71,7 +59,7 @@ public class SearchModFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
// You can only access resources after attaching to current context
mModItemAdapter = new ModItemAdapter(getResources(), modpackApi);
mModItemAdapter = new ModItemAdapter(getResources(), modpackApi, this);
mOverlayTopCache = getResources().getDimension(R.dimen.fragment_padding_medium);
mOverlay = view.findViewById(R.id.search_mod_overlay);
@ -96,15 +84,19 @@ public class SearchModFragment extends Fragment {
}));
mSearchEditText.setOnEditorActionListener((v, actionId, event) -> {
if(mSearchFuture != null && !mSearchFuture.isCancelled()) {
mSearchFuture.cancel(true);
}
mSearchProgressBar.setVisibility(View.VISIBLE);
mSearchFilters.name = mSearchEditText.getText().toString();
mSearchFuture = new SelfReferencingFuture(new SearchModTask(mSearchFilters))
.startOnExecutor(PojavApplication.sExecutorService);
mModItemAdapter.performSearchQuery(mSearchFilters);
return true;
});
mOverlay.post(()->{
int overlayHeight = mOverlay.getHeight();
mRecyclerview.setPadding(mRecyclerview.getPaddingLeft(),
mRecyclerview.getPaddingTop() + overlayHeight,
mRecyclerview.getPaddingRight(),
mRecyclerview.getPaddingBottom());
});
}
@Override
@ -113,35 +105,25 @@ public class SearchModFragment extends Fragment {
mRecyclerview.removeOnScrollListener(mOverlayPositionListener);
}
class SearchModTask implements SelfReferencingFuture.FutureInterface {
@Override
public void onSearchFinished() {
mSearchProgressBar.setVisibility(View.GONE);
mStatusTextView.setVisibility(View.GONE);
}
private final SearchFilters mTaskFilters;
SearchModTask(SearchFilters mSearchFilters) {
this.mTaskFilters = mSearchFilters;
}
@Override
public void run(Future<?> myFuture) {
ModItem[] items = modpackApi.searchMod(mTaskFilters);
Log.d(SearchModFragment.class.toString(), Arrays.toString(items));
Tools.runOnUiThread(() -> {
ModItem[] localItems = items;
if(myFuture.isCancelled()) return;
mSearchProgressBar.setVisibility(View.GONE);
if(localItems == null) {
mStatusTextView.setVisibility(View.VISIBLE);
mStatusTextView.setTextColor(Color.RED);
mStatusTextView.setText(R.string.search_modpack_error);
}else if(localItems.length == 0) {
mStatusTextView.setVisibility(View.VISIBLE);
mStatusTextView.setTextColor(mDefaultTextColor);
mStatusTextView.setText(R.string.search_modpack_no_result);
localItems = null;
}else{
mStatusTextView.setVisibility(View.GONE);
}
mModItemAdapter.setModItems(localItems, mSelectedVersion.getText().toString());
});
@Override
public void onSearchError(int error) {
mSearchProgressBar.setVisibility(View.GONE);
mStatusTextView.setVisibility(View.VISIBLE);
switch(error) {
case ERROR_INTERNAL:
mStatusTextView.setTextColor(Color.RED);
mStatusTextView.setText(R.string.search_modpack_error);
break;
case ERROR_NO_RESULTS:
mStatusTextView.setTextColor(mDefaultTextColor);
mStatusTextView.setText(R.string.search_modpack_no_result);
break;
}
}
}

View File

@ -29,23 +29,103 @@ import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache;
import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.Future;
public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHolder> {
public class ModItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0];
private static final int VIEW_TYPE_MOD_ITEM = 0;
private static final int VIEW_TYPE_LOADING = 1;
/* Used when versions haven't loaded yet, default text to reduce layout shifting */
private final SimpleArrayAdapter<String> mLoadingAdapter = new SimpleArrayAdapter<>(Collections.singletonList("Loading"));
private final ModIconCache mIconCache = new ModIconCache();
private final SearchResultCallback mSearchResultCallback;
private ModItem[] mModItems;
private final ModpackApi mModpackApi;
/* Cache for ever so slightly rounding the image for the corner not to stick out of the layout */
private final float mCornerDimensionCache;
private Future<?> mTaskInProgress;
private SearchFilters mSearchFilters;
private SearchResult mCurrentResult;
public ModItemAdapter(Resources resources, ModpackApi api, SearchResultCallback callback) {
mCornerDimensionCache = resources.getDimension(R.dimen._1sdp) / 250;
mModpackApi = api;
mModItems = new ModItem[]{};
mSearchResultCallback = callback;
}
public void performSearchQuery(SearchFilters searchFilters) {
if(mTaskInProgress != null) {
mTaskInProgress.cancel(true);
mTaskInProgress = null;
}
this.mSearchFilters = searchFilters;
mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, null))
.startOnExecutor(PojavApplication.sExecutorService);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(viewGroup.getContext());
View view;
switch (viewType) {
case VIEW_TYPE_MOD_ITEM:
// Create a new view, which defines the UI of the list item
view = layoutInflater.inflate(R.layout.view_mod, viewGroup, false);
return new ViewHolder(view);
case VIEW_TYPE_LOADING:
// Create a new view, which is actually just the progress bar
view = layoutInflater.inflate(R.layout.view_loading, viewGroup, false);
return new LoadingViewHolder(view);
default:
throw new RuntimeException("Unimplemented view type!");
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case VIEW_TYPE_MOD_ITEM:
((ModItemAdapter.ViewHolder)holder).setStateLimited(mModItems[position]);
break;
case VIEW_TYPE_LOADING:
loadMoreResults();
break;
default:
throw new RuntimeException("Unimplemented view type!");
}
}
@Override
public int getItemCount() {
if(mModItems.length == 0) return 0;
return mModItems.length+1;
}
private void loadMoreResults() {
if(mTaskInProgress != null) return;
mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, mCurrentResult))
.startOnExecutor(PojavApplication.sExecutorService);
}
@Override
public int getItemViewType(int position) {
if(position < mModItems.length) return VIEW_TYPE_MOD_ITEM;
return VIEW_TYPE_LOADING;
}
/**
* Basic viewholder with expension capabilities
*/
@ -228,38 +308,67 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
}
}
public ModItemAdapter(Resources resources, ModpackApi api) {
mCornerDimensionCache = resources.getDimension(R.dimen._1sdp) / 250;
mModpackApi = api;
mModItems = new ModItem[]{};
/**
* The view holder used to hold the progress bar at the end of the list
*/
private static class LoadingViewHolder extends RecyclerView.ViewHolder {
public LoadingViewHolder(View view) {
super(view);
}
}
@SuppressLint("NotifyDataSetChanged")
public void setModItems(ModItem[] items, String targetMcVersion){
// TODO: Use targetMcVersion to affect default selected modpack version
if(items != null) mModItems = items;
else mModItems = MOD_ITEMS_EMPTY;
notifyDataSetChanged();
private class SearchApiTask implements SelfReferencingFuture.FutureInterface {
private final SearchFilters mSearchFilters;
private final SearchResult mPreviousResult;
private SearchApiTask(SearchFilters searchFilters, SearchResult previousResult) {
this.mSearchFilters = searchFilters;
this.mPreviousResult = previousResult;
}
@SuppressLint("NotifyDataSetChanged")
@Override
public void run(Future<?> myFuture) {
SearchResult result = mModpackApi.searchMod(mSearchFilters, mPreviousResult);
ModItem[] resultModItems = result != null ? result.results : null;
Tools.runOnUiThread(() -> {
if(myFuture.isCancelled()) return;
if(resultModItems == null) {
mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_INTERNAL);
}else if(resultModItems.length == 0) {
mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_NO_RESULTS);
}else{
mSearchResultCallback.onSearchFinished();
}
mCurrentResult = result;
if(resultModItems == null) {
mModItems = MOD_ITEMS_EMPTY;
mTaskInProgress = null;
notifyDataSetChanged();
return;
}
if(mPreviousResult != null) {
ModItem[] newModItems = new ModItem[resultModItems.length + mModItems.length];
System.arraycopy(mModItems, 0, newModItems, 0, mModItems.length);
System.arraycopy(resultModItems, 0, newModItems, mModItems.length, resultModItems.length);
mModItems = newModItems;
mTaskInProgress = null;
notifyItemChanged(mModItems.length);
notifyItemRangeInserted(mModItems.length+1, newModItems.length);
return;
}
mModItems = resultModItems;
mTaskInProgress = null;
notifyDataSetChanged();
});
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
// Create a new view, which defines the UI of the list item
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.view_mod, viewGroup, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
viewHolder.setStateLimited(mModItems[position]);
}
@Override
public int getItemCount() {
return mModItems.length;
public interface SearchResultCallback {
int ERROR_INTERNAL = 0;
int ERROR_NO_RESULTS = 1;
void onSearchFinished();
void onSearchError(int error);
}
}

View File

@ -7,6 +7,7 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import java.util.Arrays;
import java.util.concurrent.Callable;
@ -21,22 +22,36 @@ public class CommonApi implements ModpackApi {
private final ModpackApi mModrinthApi = new ModrinthApi();
private final ModpackApi[] mModpackApis = new ModpackApi[]{mModrinthApi, mCurseforgeApi};
@Override
public ModItem[] searchMod(SearchFilters searchFilters) {
ModItem[][] items = new ModItem[mModpackApis.length][];
public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) {
CommonApiSearchResult commonApiSearchResult = (CommonApiSearchResult) previousPageResult;
// If there are no previous page results, create a new array. Otherwise, use the one from the previous page
SearchResult[] results = commonApiSearchResult == null ?
new SearchResult[mModpackApis.length] : commonApiSearchResult.searchResults;
int totalSize = 0;
int totalTotalSize = 0;
Future<?>[] futures = new Future<?>[mModpackApis.length];
for(int i = 0; i < mModpackApis.length; i++) {
futures[i] = PojavApplication.sExecutorService.submit(new ApiDownloadTask(i, searchFilters));
// If there is an array and its length is zero, this means that we've exhausted the results for this
// search query and we don't need to actually do the search
if(results[i] != null && results[i].results.length == 0) continue;
futures[i] = PojavApplication.sExecutorService.submit(new ApiDownloadTask(i, searchFilters,
results[i]));
}
if(Thread.interrupted()) {
cancelAllFutures(futures);
return null;
}
// Count up all the results
for(int i = 0; i < mModpackApis.length; i++) {
Future<?> future = futures[i];
if(future == null) continue;
try {
items[i] = (ModItem[]) futures[i].get();
totalSize += items[i].length;
SearchResult searchResult = results[i] = (SearchResult) future.get();
totalSize += searchResult.results.length;
totalTotalSize += searchResult.totalResultCount;
}catch (Exception e) {
cancelAllFutures(futures);
e.printStackTrace();
@ -46,14 +61,22 @@ public class CommonApi implements ModpackApi {
// Then build an array with all the mods
ModItem[] concatenatedItems = new ModItem[totalSize];
int copyOffset = 0;
for(ModItem[] apiItems : items) {
System.arraycopy(apiItems, 0, concatenatedItems, copyOffset, apiItems.length);
copyOffset += apiItems.length;
for(SearchResult result : results) {
ModItem[] searchResults = result.results;
// If the length is zero, we don't need to perform needless copies
if(searchResults.length == 0) continue;
System.arraycopy(searchResults, 0, concatenatedItems, copyOffset, searchResults.length);
copyOffset += searchResults.length;
}
if(Thread.interrupted()) return null;
Arrays.sort(concatenatedItems, (modItem, t1) -> modItem.title.compareToIgnoreCase(t1.title));
if(Thread.interrupted()) return null;
return concatenatedItems;
// Recycle or create new search result
if(commonApiSearchResult == null) commonApiSearchResult = new CommonApiSearchResult();
commonApiSearchResult.searchResults = results;
commonApiSearchResult.totalResultCount = totalTotalSize;
commonApiSearchResult.results = concatenatedItems;
return commonApiSearchResult;
}
@Override
@ -84,18 +107,24 @@ public class CommonApi implements ModpackApi {
}
}
private class ApiDownloadTask implements Callable<ModItem[]> {
private class ApiDownloadTask implements Callable<SearchResult> {
private final int mModApi;
private final SearchFilters mSearchFilters;
private final SearchResult mPreviousPageResult;
private ApiDownloadTask(int modApi, SearchFilters searchFilters) {
private ApiDownloadTask(int modApi, SearchFilters searchFilters, SearchResult previousPageResult) {
this.mModApi = modApi;
this.mSearchFilters = searchFilters;
this.mPreviousPageResult = previousPageResult;
}
@Override
public ModItem[] call() {
return mModpackApis[mModApi].searchMod(mSearchFilters);
public SearchResult call() {
return mModpackApis[mModApi].searchMod(mSearchFilters, mPreviousPageResult);
}
}
class CommonApiSearchResult extends SearchResult {
SearchResult[] searchResults = new SearchResult[mModpackApis.length];
}
}

View File

@ -14,6 +14,7 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.CurseManifest;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.utils.FileUtils;
import net.kdt.pojavlaunch.utils.ZipUtils;
@ -40,17 +41,21 @@ public class CurseforgeApi implements ModpackApi{
private final ApiHandler mApiHandler = new ApiHandler("https://api.curseforge.com/v1", "$2a$10$Vxkj4kH1Ekf8EsS4Mx8b2eVTHsht107Lk2erVEUtnbqvojsLy.jYq");
@Override
public ModItem[] searchMod(SearchFilters searchFilters) {
public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) {
CurseforgeSearchResult curseforgeSearchResult = (CurseforgeSearchResult) previousPageResult;
HashMap<String, Object> params = new HashMap<>();
params.put("gameId", CURSEFORGE_MINECRAFT_GAME_ID);
params.put("classId", searchFilters.isModpack ? CURSEFORGE_MODPACK_CLASS_ID : CURSEFORGE_MOD_CLASS_ID);
params.put("searchFilter", searchFilters.name);
if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty())
params.put("gameVersion", searchFilters.mcVersion);
if(previousPageResult != null)
params.put("index", curseforgeSearchResult.previousOffset);
JsonObject response = mApiHandler.get("mods/search", params, JsonObject.class);
if(response == null) return null;
JsonArray dataArray = response.getAsJsonArray("data");
if(dataArray == null) return null;
JsonObject paginationInfo = response.getAsJsonObject("pagination");
ArrayList<ModItem> modItemList = new ArrayList<>(dataArray.size());
for(int i = 0; i < dataArray.size(); i++) {
JsonObject dataElement = dataArray.get(i).getAsJsonObject();
@ -69,7 +74,12 @@ public class CurseforgeApi implements ModpackApi{
dataElement.getAsJsonObject("logo").get("thumbnailUrl").getAsString());
modItemList.add(modItem);
}
return modItemList.toArray(new ModItem[0]);
if(curseforgeSearchResult == null) curseforgeSearchResult = new CurseforgeSearchResult();
curseforgeSearchResult.results = modItemList.toArray(new ModItem[0]);
curseforgeSearchResult.totalResultCount = paginationInfo.get("totalCount").getAsInt();
curseforgeSearchResult.previousOffset += dataArray.size();
return curseforgeSearchResult;
}
@Override
@ -215,4 +225,8 @@ public class CurseforgeApi implements ModpackApi{
if(manifest.minecraft.modLoaders.length < 1) return false;
return true;
}
class CurseforgeSearchResult extends SearchResult {
int previousOffset;
}
}

View File

@ -10,6 +10,7 @@ import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import java.io.File;
@ -18,11 +19,20 @@ import java.io.File;
*/
public interface ModpackApi {
/**
* @param searchFilters Filters
* @param offset the offset into the list of search results
* @return the list of mod items from specified offset
*/
SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult);
/**
* @param searchFilters Filters
* @return A list of mod items
*/
ModItem[] searchMod(SearchFilters searchFilters);
default SearchResult searchMod(SearchFilters searchFilters) {
return searchMod(searchFilters, null);
}
/**
* Fetch the mod details

View File

@ -11,6 +11,7 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper;
import net.kdt.pojavlaunch.utils.ZipUtils;
@ -27,7 +28,8 @@ public class ModrinthApi implements ModpackApi{
}
@Override
public ModItem[] searchMod(SearchFilters searchFilters) {
public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) {
ModrinthSearchResult modrinthSearchResult = (ModrinthSearchResult) previousPageResult;
HashMap<String, Object> params = new HashMap<>();
// Build the facets filters
@ -39,7 +41,9 @@ public class ModrinthApi implements ModpackApi{
facetString.append("]");
params.put("facets", facetString.toString());
params.put("query", searchFilters.name);
params.put("limit", 100);
params.put("limit", 50);
if(modrinthSearchResult != null)
params.put("offset", modrinthSearchResult.previousOffset);
JsonObject response = mApiHandler.get("search", params, JsonObject.class);
if(response == null) return null;
@ -58,8 +62,11 @@ public class ModrinthApi implements ModpackApi{
hit.get("icon_url").getAsString()
);
}
return items;
if(modrinthSearchResult == null) modrinthSearchResult = new ModrinthSearchResult();
modrinthSearchResult.previousOffset += responseHits.size();
modrinthSearchResult.results = items;
modrinthSearchResult.totalResultCount = response.get("total_hits").getAsInt();
return modrinthSearchResult;
}
@Override
@ -124,4 +131,8 @@ public class ModrinthApi implements ModpackApi{
return createInfo(modrinthIndex);
}
}
class ModrinthSearchResult extends SearchResult {
int previousOffset;
}
}

View File

@ -0,0 +1,6 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
public class SearchResult {
public int totalResultCount;
public ModItem[] results;
}

View File

@ -101,7 +101,7 @@
android:id="@+id/search_mod_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/_65sdp"
android:paddingTop="@dimen/_15sdp"
android:clipToPadding="false"
app:layout_constraintBottom_toBottomOf="parent"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:indeterminate="true">
</ProgressBar>