mirror of
https://codeberg.org/tom79/Fedilab.git
synced 2024-12-23 17:20:04 +02:00
parent
58c3a5fc35
commit
b584a37281
10 changed files with 160 additions and 109 deletions
|
@ -156,6 +156,9 @@ public class FilterActivity extends BaseActivity implements FilterAdapter.Delete
|
|||
popupAddFilterBinding.actionRemove.setChecked(true);
|
||||
}
|
||||
}
|
||||
if (filterParams.keywords == null) {
|
||||
filterParams.keywords = new ArrayList<>();
|
||||
}
|
||||
|
||||
KeywordAdapter keywordAdapter = new KeywordAdapter(filterParams.keywords);
|
||||
popupAddFilterBinding.lvKeywords.setAdapter(keywordAdapter);
|
||||
|
|
|
@ -46,15 +46,11 @@ public interface MastodonFiltersService {
|
|||
@Path("id") String id);
|
||||
|
||||
//Add a filter
|
||||
@FormUrlEncoded
|
||||
@Headers({"Accept: application/json"})
|
||||
@POST("filters")
|
||||
Call<Filter> addFilter(
|
||||
@Header("Authorization") String token,
|
||||
@Field("title") String title,
|
||||
@Field("expires_in") Long expires_in,
|
||||
@Field("filter_action") String filter_action,
|
||||
@Field("context[]") List<String> context,
|
||||
@Field("keywords_attributes[]") List<Filter.KeywordsParams> keywordsAttributes
|
||||
@Body Filter.FilterParams filter
|
||||
);
|
||||
|
||||
//Edit a filter
|
||||
|
@ -64,15 +60,6 @@ public interface MastodonFiltersService {
|
|||
@Header("Authorization") String token,
|
||||
@Path("id") String id,
|
||||
@Body Filter.FilterParams filter
|
||||
/*@Path("id") String id,
|
||||
@Field("title") String title,
|
||||
@Field("expires_in") Date expires_in,
|
||||
@Field("filter_action") String filter_action,
|
||||
@Field("context[]") List<String> context,
|
||||
@Field("keywords_attributes[]") List<Filter.KeywordsAttributes> keywords
|
||||
@Field("keywords_attributes[][id]") List<String> keywordId,
|
||||
@Field("keywords_attributes[][keyword]") List<String> keywords,
|
||||
@Field("keywords_attributes[][whole_word]") List<Boolean> wholeWords*/
|
||||
);
|
||||
|
||||
//Remove a filter
|
||||
|
|
|
@ -65,7 +65,7 @@ public class Filter implements Serializable {
|
|||
public List<String> context;
|
||||
@SerializedName("whole_word")
|
||||
public boolean whole_word;
|
||||
@SerializedName("expires_in")
|
||||
@SerializedName("expires_at")
|
||||
public Date expires_at;
|
||||
@SerializedName("filter_action")
|
||||
public String filter_action;
|
||||
|
|
|
@ -39,7 +39,7 @@ public class Notification {
|
|||
public Status status;
|
||||
@SerializedName("cached")
|
||||
public boolean cached;
|
||||
|
||||
public Filter filteredByApp;
|
||||
public PositionFetchMore positionFetchMore = PositionFetchMore.BOTTOM;
|
||||
public transient List<Notification> relatedNotifications;
|
||||
public boolean isFetchMore;
|
||||
|
|
|
@ -92,8 +92,8 @@ public class Status implements Serializable, Cloneable {
|
|||
public Card card;
|
||||
@SerializedName("poll")
|
||||
public Poll poll;
|
||||
/* @SerializedName("filtered")
|
||||
public Filter.FilterResult filtered;*/
|
||||
@SerializedName("filtered")
|
||||
public List<Filter.FilterResult> filtered;
|
||||
@SerializedName("pleroma")
|
||||
public Pleroma pleroma;
|
||||
@SerializedName("cached")
|
||||
|
@ -114,7 +114,7 @@ public class Status implements Serializable, Cloneable {
|
|||
public transient int cursorPosition = 0;
|
||||
public transient boolean submitted = false;
|
||||
public transient boolean spoilerChecked = false;
|
||||
|
||||
public Filter filteredByApp;
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
boolean same = false;
|
||||
|
|
|
@ -113,50 +113,40 @@ public class TimelineHelper {
|
|||
}
|
||||
if (filter.keywords != null && filter.keywords.size() > 0) {
|
||||
for (Filter.KeywordsAttributes filterKeyword : filter.keywords) {
|
||||
String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).matches() ? "\\b" : "";
|
||||
String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).matches() ? "\\b" : "";
|
||||
Pattern p;
|
||||
if (filterKeyword.whole_word) {
|
||||
Pattern p = Pattern.compile("(^|\\W)(" + Pattern.quote(filterKeyword.keyword) + ")($|\\W)", Pattern.CASE_INSENSITIVE);
|
||||
for (Status status : statuses) {
|
||||
String content;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString();
|
||||
Matcher m = p.matcher(content);
|
||||
if (m.find()) {
|
||||
statusesToRemove.add(status);
|
||||
continue;
|
||||
}
|
||||
if (status.spoiler_text != null) {
|
||||
String spoilerText;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString();
|
||||
Matcher ms = p.matcher(spoilerText);
|
||||
if (ms.find()) {
|
||||
statusesToRemove.add(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
p = Pattern.compile(sb + "(" + Pattern.quote(filterKeyword.keyword) + ")" + eb, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
|
||||
} else {
|
||||
for (Status status : statuses) {
|
||||
String content;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString();
|
||||
if (content.contains(filterKeyword.keyword)) {
|
||||
p = Pattern.compile("#" + Pattern.quote(filterKeyword.keyword), Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
for (Status status : statuses) {
|
||||
String content;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString();
|
||||
Matcher m = p.matcher(content);
|
||||
if (m.find()) {
|
||||
if (filter.filter_action.equalsIgnoreCase("warn")) {
|
||||
status.filteredByApp = filter;
|
||||
} else {
|
||||
statusesToRemove.add(status);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.spoiler_text != null) {
|
||||
String spoilerText;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString();
|
||||
if (spoilerText.contains(filterKeyword.keyword)) {
|
||||
continue;
|
||||
}
|
||||
if (status.spoiler_text != null) {
|
||||
String spoilerText;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString();
|
||||
Matcher ms = p.matcher(spoilerText);
|
||||
if (ms.find()) {
|
||||
if (filter.filter_action.equalsIgnoreCase("warn")) {
|
||||
status.filteredByApp = filter;
|
||||
} else {
|
||||
statusesToRemove.add(status);
|
||||
}
|
||||
}
|
||||
|
@ -203,60 +193,46 @@ public class TimelineHelper {
|
|||
//Expired filter
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!filter.context.contains("notification")) continue;
|
||||
if (filter.keywords != null && filter.keywords.size() > 0) {
|
||||
for (Filter.KeywordsAttributes filterKeyword : filter.keywords) {
|
||||
String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).matches() ? "\\b" : "";
|
||||
String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).matches() ? "\\b" : "";
|
||||
Pattern p;
|
||||
if (filterKeyword.whole_word) {
|
||||
Pattern p = Pattern.compile("(^|\\W)(" + Pattern.quote(filterKeyword.keyword) + ")($|\\W)", Pattern.CASE_INSENSITIVE);
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.status == null) {
|
||||
continue;
|
||||
}
|
||||
String content;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content).toString();
|
||||
Matcher m = p.matcher(content);
|
||||
if (m.find()) {
|
||||
notificationToRemove.add(notification);
|
||||
continue;
|
||||
}
|
||||
if (notification.status.spoiler_text != null) {
|
||||
String spoilerText;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text).toString();
|
||||
Matcher ms = p.matcher(spoilerText);
|
||||
if (ms.find()) {
|
||||
notificationToRemove.add(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
p = Pattern.compile(sb + "(" + Pattern.quote(filterKeyword.keyword) + ")" + eb, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
|
||||
} else {
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.status == null) {
|
||||
continue;
|
||||
}
|
||||
String content;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content).toString();
|
||||
if (content.contains(filterKeyword.keyword)) {
|
||||
p = Pattern.compile("#" + Pattern.quote(filterKeyword.keyword), Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.status == null) {
|
||||
continue;
|
||||
}
|
||||
String content;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
content = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.content : notification.status.content).toString();
|
||||
Matcher m = p.matcher(content);
|
||||
if (m.find()) {
|
||||
if (filter.filter_action.equalsIgnoreCase("warn")) {
|
||||
notification.filteredByApp = filter;
|
||||
} else {
|
||||
notificationToRemove.add(notification);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (notification.status.spoiler_text != null) {
|
||||
String spoilerText;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text).toString();
|
||||
if (spoilerText.contains(filterKeyword.keyword)) {
|
||||
continue;
|
||||
}
|
||||
if (notification.status.spoiler_text != null) {
|
||||
String spoilerText;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||
else
|
||||
spoilerText = Html.fromHtml(notification.status.reblog != null ? notification.status.reblog.spoiler_text : notification.status.spoiler_text).toString();
|
||||
Matcher ms = p.matcher(spoilerText);
|
||||
if (ms.find()) {
|
||||
if (filter.filter_action.equalsIgnoreCase("warn")) {
|
||||
notification.filteredByApp = filter;
|
||||
} else {
|
||||
notificationToRemove.add(notification);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ import app.fedilab.android.client.entities.app.StatusDraft;
|
|||
import app.fedilab.android.client.entities.app.Timeline;
|
||||
import app.fedilab.android.databinding.DrawerStatusArtBinding;
|
||||
import app.fedilab.android.databinding.DrawerStatusBinding;
|
||||
import app.fedilab.android.databinding.DrawerStatusFilteredBinding;
|
||||
import app.fedilab.android.databinding.DrawerStatusHiddenBinding;
|
||||
import app.fedilab.android.databinding.DrawerStatusNotificationBinding;
|
||||
import app.fedilab.android.databinding.DrawerStatusReportBinding;
|
||||
|
@ -147,6 +148,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
public static final int STATUS_HIDDEN = 0;
|
||||
public static final int STATUS_VISIBLE = 1;
|
||||
public static final int STATUS_ART = 2;
|
||||
public static final int STATUS_FILTERED = 3;
|
||||
private final List<Status> statusList;
|
||||
private final boolean minified;
|
||||
private final Timeline.TimeLineEnum timelineType;
|
||||
|
@ -2102,7 +2104,12 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
if (timelineType == Timeline.TimeLineEnum.ART) {
|
||||
return STATUS_ART;
|
||||
} else {
|
||||
return isVisible(timelineType, statusList.get(position)) ? STATUS_VISIBLE : STATUS_HIDDEN;
|
||||
if (statusList.get(position).filteredByApp != null) {
|
||||
return STATUS_FILTERED;
|
||||
} else {
|
||||
return isVisible(timelineType, statusList.get(position)) ? STATUS_VISIBLE : STATUS_HIDDEN;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2116,6 +2123,9 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
} else if (viewType == STATUS_ART) { //Art statuses
|
||||
DrawerStatusArtBinding itemBinding = DrawerStatusArtBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new StatusViewHolder(itemBinding);
|
||||
} else if (viewType == STATUS_FILTERED) { //Art statuses
|
||||
DrawerStatusFilteredBinding itemBinding = DrawerStatusFilteredBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new StatusViewHolder(itemBinding);
|
||||
} else { //Classic statuses
|
||||
if (!minified) {
|
||||
DrawerStatusBinding itemBinding = DrawerStatusBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
|
@ -2152,6 +2162,13 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class);
|
||||
SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class);
|
||||
statusManagement(context, statusesVM, searchVM, holder, this, statusList, status, timelineType, minified, canBeFederated, fetchMoreCallBack);
|
||||
} else if (viewHolder.getItemViewType() == STATUS_FILTERED) {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
holder.bindingFiltered.filteredText.setText(context.getString(R.string.filtered_by, status.filteredByApp.title));
|
||||
holder.bindingFiltered.displayButton.setOnClickListener(v -> {
|
||||
status.filteredByApp = null;
|
||||
notifyItemChanged(position);
|
||||
});
|
||||
} else if (viewHolder.getItemViewType() == STATUS_ART) {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
MastodonHelper.loadPPMastodon(holder.bindingArt.artPp, status.account);
|
||||
|
@ -2251,6 +2268,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
DrawerStatusReportBinding bindingReport;
|
||||
DrawerStatusNotificationBinding bindingNotification;
|
||||
DrawerStatusArtBinding bindingArt;
|
||||
DrawerStatusFilteredBinding bindingFiltered;
|
||||
|
||||
StatusViewHolder(DrawerStatusBinding itemView) {
|
||||
super(itemView.getRoot());
|
||||
|
@ -2279,6 +2297,11 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
super(itemView.getRoot());
|
||||
bindingArt = itemView;
|
||||
}
|
||||
|
||||
StatusViewHolder(DrawerStatusFilteredBinding itemView) {
|
||||
super(itemView.getRoot());
|
||||
bindingFiltered = itemView;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ public class FiltersVM extends AndroidViewModel {
|
|||
MastodonFiltersService mastodonFiltersService = initV2(instance);
|
||||
new Thread(() -> {
|
||||
Filter filter = null;
|
||||
Call<Filter> addFilterCall = mastodonFiltersService.addFilter(token, filterParams.title, filterParams.expires_in, filterParams.filter_action, filterParams.context, filterParams.keywords);
|
||||
Call<Filter> addFilterCall = mastodonFiltersService.addFilter(token, filterParams);
|
||||
if (addFilterCall != null) {
|
||||
try {
|
||||
Response<Filter> addFiltersResponse = addFilterCall.execute();
|
||||
|
|
60
app/src/main/res/layout/drawer_status_filtered.xml
Normal file
60
app/src/main/res/layout/drawer_status_filtered.xml
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Copyright 2022 Thomas Schneider
|
||||
|
||||
This file is a part of Fedilab
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||
see <http://www.gnu.org/licenses>
|
||||
-->
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/cardview_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/card_margin"
|
||||
android:layout_marginTop="@dimen/card_margin"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:padding="5dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/filtered_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/display_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Filter: The special" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/display_button"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/show_anyway"
|
||||
android:textColor="@color/cyanea_accent_dark_reference"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/filtered_text"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:strokeColor="@color/cyanea_accent_dark_reference" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
|
@ -2000,4 +2000,6 @@
|
|||
<string name="keyword_or_phrase">Keyword or phrase</string>
|
||||
<string name="delete_keyword">Delete keyword</string>
|
||||
<string name="add_keyword">Add keyword</string>
|
||||
<string name="show_anyway">Show aniway</string>
|
||||
<string name="filtered_by">Filtered: %1$s</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue