added themes manager ui (beta)

This commit is contained in:
Vologhat
2022-07-31 20:22:06 +03:00
parent 937c7efb03
commit db83fbff52
10 changed files with 800 additions and 1 deletions
@@ -0,0 +1,164 @@
package ru.vtosters.lite.ui.adapters;
import static ru.vtosters.lite.utils.AndroidUtils.dp2px;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
import static ru.vtosters.lite.utils.ThemesUtils.getAccentColor;
import static ru.vtosters.lite.utils.ThemesUtils.getSTextAttr;
import static ru.vtosters.lite.utils.ThemesUtils.getTextAttr;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vk.core.util.ToastUtils;
import org.json.JSONException;
import java.io.IOException;
import ru.vtosters.lite.ui.components.ThemesManager;
import ru.vtosters.lite.ui.items.Theme;
import ru.vtosters.lite.ui.views.NoTouchRadioButton;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.LifecycleUtils;
public class ThemesAdapter extends RecyclerView.Adapter<ThemesAdapter.ThemeViewHolder> {
public ThemesManager mManager = ThemesManager.getInstance();
@NonNull
@Override
public ThemeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
var container = LayoutInflater.from(parent.getContext()).inflate(
AndroidUtils.getIdentifier("theme_manager_item", "layout"), parent, false
);
return new ThemeViewHolder(container);
}
@Override
public void onBindViewHolder(@NonNull ThemeViewHolder holder, int pos) {
holder.bind(mManager.getThemeByIndex(pos));
}
@Override
public int getItemCount() {
return mManager.getThemesCount();
}
public class ThemeViewHolder extends RecyclerView.ViewHolder {
private ConstraintLayout mContainer;
private TextView mName;
private TextView mAuthor;
private NoTouchRadioButton mRadioButton;
public ThemeViewHolder(@NonNull View view) {
super(view);
mContainer = (ConstraintLayout) view;
mName = view.findViewById(AndroidUtils.getIdentifier("name", "id"));
mAuthor = view.findViewById(AndroidUtils.getIdentifier("author", "id"));
mRadioButton = view.findViewById(AndroidUtils.getIdentifier("selection", "id"));
}
public void bind(Theme theme) {
var isCurrentTheme = mManager.isCurrentTheme(theme);
if (!isCurrentTheme)
mContainer.setOnClickListener(v -> {
mManager.setTheme(theme);
LifecycleUtils.restartApplication();
});
if (!"default".equals(theme.id))
mContainer.setOnLongClickListener(v -> {
var titles = new CharSequence[] { "Редактировать название", "Поделиться темой", "Удалить тему" };
new VkAlertDialog.Builder(v.getContext())
.setItems(titles, ((dialog, which) -> {
switch (which) {
case 0:
showEditThemeNameDialog(v.getContext(), theme);
break;
case 1:
try {
mManager.share(v.getContext(), theme);
} catch (Exception e) {
e.printStackTrace();
ToastUtils.a("Не удалось поделиться темой");
}
break;
case 2:
mManager.delete(theme);
if (isCurrentTheme)
LifecycleUtils.restartApplication();
else
notifyDataSetChanged();
break;
}
}))
.create()
.show();
return true;
});
mRadioButton.setChecked(isCurrentTheme);
mName.setText(theme.name);
mAuthor.setText(theme.author);
}
private void showEditThemeNameDialog(Context context, Theme theme) {
LinearLayout linearLayout = new LinearLayout(getGlobalContext());
final EditText editText = new EditText(getGlobalContext());
editText.setText(theme.name);
editText.setHint("Название темы");
editText.setTextColor(getTextAttr());
editText.setHintTextColor(getSTextAttr());
editText.setBackgroundTintList(ColorStateList.valueOf(getAccentColor()));
linearLayout.addView(editText);
editText.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
ViewGroup.MarginLayoutParams margin = ((ViewGroup.MarginLayoutParams) editText.getLayoutParams());
margin.setMargins(dp2px(20f), 0, dp2px(20f), 0);
editText.setLayoutParams(margin);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Редактирование названия темы");
builder.setView(linearLayout);
builder.setPositiveButton("OK", (dialog, which) -> {
var newName = editText.getText().toString();
if (TextUtils.isEmpty(newName)) {
ToastUtils.a("Название не может быть пустым");
return;
}
if (newName.equals(theme.name)) {
ToastUtils.a("Вы не изменили название");
return;
}
theme.setName(newName);
try {
mManager.save(theme);
notifyDataSetChanged();
} catch (IOException | JSONException e) {
e.printStackTrace();
ToastUtils.a("Не удалось изменить название темы");
}
});
var alert = builder.create();
alert.show();
alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor());
}
}
}
@@ -0,0 +1,228 @@
package ru.vtosters.lite.ui.components;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.os.StrictMode;
import android.util.Log;
import com.vtosters.lite.auth.VKAccountManager;
import org.json.JSONException;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import ru.vtosters.lite.ui.items.Theme;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
public class ThemesManager {
private List<Theme> themes = new ArrayList<>();
private static File themesDir;
private static ThemesManager instance;
public static ThemesManager getInstance() {
if (instance == null)
instance = new ThemesManager();
return instance;
}
public ThemesManager() {
init();
}
private void init() {
themes = new ArrayList<>();
themes.add(new Theme().setName("Тема по умолчанию")
.setAuthor("Разработчики ВК")
.setDarkMode(false)
.setId("default"));
themesDir = new File(Environment.getExternalStorageDirectory(), "VTL/themes");
if (!themesDir.exists()) {
themesDir.mkdirs();
return;
}
// Support themes from Sova V RE (.svt)
File[] files = themesDir.listFiles((dir, name) -> name.endsWith(".vtlt") || name.endsWith(".svt"));
if (files == null || files.length == 0)
return;
for (File file : files) {
try {
var theme = read(file);
Log.d("ThemesManager", theme.toString());
themes.add(theme);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void reload() {
init();
}
/*
* Theme file storing method from Sova V RE is storing json clearly or compressed in gzip.
* If first byte byte is 0x1 or 0x2 json has been compressed with gzip,
* but if first byte is 0x2 next int is length of json string. */
private Theme read(InputStream fis) throws IOException {
InputStream is = fis;
var first = is.read();
if (first > 0x0 && first < 0x3) {
var gzis = new GZIPInputStream(is);
var dis = new DataInputStream(gzis);
if (first == 0x2)
dis.readInt();
is = dis;
}
return Theme.fromJson(IOUtils.readAllLines(is).trim());
}
private Theme read(File file) throws IOException {
return read(new FileInputStream(file));
}
public File getThemeFile(Theme theme) throws IOException {
if (!themesDir.exists()) {
themesDir.mkdirs();
throw new FileNotFoundException("theme file not found");
}
var themeFile = new File(themesDir, theme.id + ".svt");
if (!themeFile.exists()) {
themeFile = new File(themesDir, theme.id + ".vtlt");
if (!themeFile.exists())
themeFile.createNewFile();
}
return themeFile;
}
public void create(String name, boolean darkMode) throws JSONException, IOException {
var id = name.toLowerCase().replace(" ", "-");
if (hasTheme(id)) {
int i = 1;
while (hasTheme(id + "-" + i)) {
i++;
}
id += "-" + i;
}
var userProfile = VKAccountManager.d().e1();
Theme theme = new Theme()
.setName(name)
.setAuthor(userProfile.c + " " + userProfile.e)
.setDarkMode(darkMode)
.setId(id);
themes.add(theme);
save(theme);
}
public void save(Theme theme) throws IOException, JSONException {
var buffer = theme.toJson().toString().getBytes(StandardCharsets.UTF_8);
var fos = new FileOutputStream(getThemeFile(theme));
// writing with data length
fos.write(0x2);
var gzos = new GZIPOutputStream(fos);
var dos = new DataOutputStream(gzos);
dos.writeInt(buffer.length);
dos.write(buffer);
dos.close();
}
public void delete(Theme theme) {
try {
getThemeFile(theme).delete();
} catch (IOException e) {
e.printStackTrace();
}
themes.remove(theme);
}
public void copy(ContentResolver contentResolver, Uri uri) throws IOException, JSONException {
var theme = read(contentResolver.openInputStream(uri));
if (isCurrentTheme(theme)) {
int i = 1;
while (hasTheme(theme.id + "-" + i)) {
i++;
}
theme.id += "-" + i;
}
themes.add(theme);
save(theme);
}
public void share(Context context, Theme theme) throws Exception {
var vmPolicy = StrictMode.getVmPolicy();
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build());
var intent = new Intent(Intent.ACTION_SEND)
.setType("file/*")
.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(getThemeFile(theme)));
context.startActivity(Intent.createChooser(intent, ""));
StrictMode.setVmPolicy(vmPolicy);
}
public void setTheme(Theme theme) {
AndroidUtils.getPreferences()
.edit()
.putString("vtl_theme_id", theme.id)
.commit();
}
public Theme getThemeByIndex(int i) {
return themes.get(i);
}
public Theme getCurrentTheme() {
var currentThemeId = AndroidUtils.getPreferences().getString("vtl_theme_id", "default");
for (Theme theme : themes) {
if (currentThemeId.equals(theme.id))
return theme;
}
AndroidUtils.getPreferences()
.edit()
.putString("vtl_theme_id", "default")
.commit();
return getCurrentTheme();
}
public int getThemesCount() {
return themes.size();
}
public boolean isCurrentTheme(Theme theme) {
return isCurrentTheme(theme.id);
}
public boolean isCurrentTheme(String id) {
return id.equals(getCurrentTheme().id);
}
public boolean hasTheme(Theme theme) {
return hasTheme(theme.id);
}
public boolean hasTheme(String id) {
for (Theme theme : themes) {
if (id.equals(theme.id))
return true;
}
return false;
}
}
@@ -12,11 +12,12 @@ import android.os.Bundle;
import androidx.preference.Preference;
import com.vk.navigation.Navigator;
import com.vk.newsfeed.NewsfeedSettingsFragment;
import com.vtosters.lite.general.fragments.MaterialPreferenceToolbarFragment;
import ru.vtosters.lite.ui.components.ThemesManager;
import ru.vtosters.lite.utils.LifecycleUtils;
public class ThemesFragment extends MaterialPreferenceToolbarFragment {
@Override
@@ -27,6 +28,17 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
}
private void prefs() {
var themesPreference = findPreference("themes");
var themesManager = ThemesManager.getInstance();
if (themesManager.getThemesCount() > 0)
themesPreference.setSummary(themesManager.getCurrentTheme().name);
themesPreference.setOnPreferenceClickListener(preference -> {
var intent = new Navigator(ThemesManagerFragment.class).b(getContext());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
return false;
});
findPreference("navbar").setOnPreferenceClickListener(new restart());
findPreference("milkshake").setOnPreferenceClickListener(new restart());
findPreference("darktheme").setOnPreferenceChangeListener(new restartdark());
@@ -0,0 +1,170 @@
package ru.vtosters.lite.ui.fragments;
import static ru.vtosters.lite.utils.AndroidUtils.dp2px;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
import static ru.vtosters.lite.utils.AndroidUtils.getIdentifier;
import static ru.vtosters.lite.utils.ThemesUtils.getAccentColor;
import static ru.vtosters.lite.utils.ThemesUtils.getSTextAttr;
import static ru.vtosters.lite.utils.ThemesUtils.getTextAttr;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.vk.core.util.ToastUtils;
import org.json.JSONException;
import java.io.IOException;
import ru.vtosters.lite.ui.adapters.ThemesAdapter;
import ru.vtosters.lite.ui.components.ThemesManager;
import ru.vtosters.lite.utils.LayoutUtils;
public class ThemesManagerFragment extends BaseToolbarFragment {
private static final int IMPORT_REQUEST_CODE = 0x1;
private ThemesAdapter mAdapter;
private ThemesManager mManager = ThemesManager.getInstance();
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
switch (requestCode) {
case IMPORT_REQUEST_CODE:
if (resultCode != Activity.RESULT_OK) {
ToastUtils.a("Не удалось импортировать тему");
return;
}
try {
var data = intent.getData();
if (data != null) {
mManager.copy(getContext().getContentResolver(), data);
mAdapter.notifyDataSetChanged();
} else {
throw new IOException("Error while getting file path");
}
} catch (IOException | JSONException e) {
e.printStackTrace();
ToastUtils.a("Не удалось импортировать тему");
}
break;
}
}
@Override
public View onCreateContent(@NonNull LayoutInflater inflater, @Nullable Bundle bundle) {
setTitle("Темы");
var container = new FrameLayout(getContext());
mAdapter = new ThemesAdapter();
var recyclerView = new RecyclerView(getContext());
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
container.addView(recyclerView, LayoutUtils.createFrame(-1, -1));
var buttonsContainer = new LinearLayout(getContext());
buttonsContainer.setOrientation(LinearLayout.HORIZONTAL);
var params = LayoutUtils.createFrame(-2, -2);
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
params.setMargins(0, 0, 0, dp2px(12f));
container.addView(buttonsContainer, params);
var addNewTheme = new FloatingActionButton(getContext());
addNewTheme.setImageResource(getIdentifier("ic_add_24", "drawable"));
addNewTheme.setBackgroundTintList(ColorStateList.valueOf(getAccentColor()));
addNewTheme.setOnClickListener(v -> {
LinearLayout linearLayout = new LinearLayout(getGlobalContext());
final EditText editText = new EditText(getGlobalContext());
editText.setHint("Название новой темы");
editText.setTextColor(getTextAttr());
editText.setHintTextColor(getSTextAttr());
editText.setBackgroundTintList(ColorStateList.valueOf(getAccentColor()));
linearLayout.addView(editText);
editText.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
ViewGroup.MarginLayoutParams margin = ((ViewGroup.MarginLayoutParams) editText.getLayoutParams());
margin.setMargins(dp2px(20f), 0, dp2px(20f), 0);
editText.setLayoutParams(margin);
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("Новая тема");
builder.setView(linearLayout);
builder.setPositiveButton("OK", (dialog, which) -> {
var name = editText.getText().toString();
if (TextUtils.isEmpty(name)) {
ToastUtils.a("Название не может быть пустым");
return;
}
try {
mManager.create(name, false);
mAdapter.notifyDataSetChanged();
} catch (IOException | JSONException e) {
e.printStackTrace();
ToastUtils.a("Не удалось изменить название темы");
}
});
var alert = builder.create();
alert.show();
alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor());
mAdapter.notifyDataSetChanged();
});
buttonsContainer.addView(addNewTheme, LayoutUtils.createLinear(-2, -2));
buttonsContainer.addView(new View(getContext()), LayoutUtils.createLinear(dp2px(5), 0));
var importTheme = new FloatingActionButton(getContext());
importTheme.setImageResource(getIdentifier("ic_import", "drawable"));
importTheme.setBackgroundTintList(ColorStateList.valueOf(getAccentColor()));
importTheme.setImageTintList(ColorStateList.valueOf(Color.WHITE));
importTheme.setOnClickListener(v -> {
var intent = new Intent(Intent.ACTION_GET_CONTENT)
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(Intent.createChooser(intent, "Выберите тему (.vtlt, .svt)"), IMPORT_REQUEST_CODE);
ToastUtils.a("Возможен импорт тем из стороннего мода Sova V RE");
});
buttonsContainer.addView(importTheme, LayoutUtils.createLinear(-2, -2));
return container;
}
@Override
public void onResume() {
super.onResume();
mManager.reload();
mAdapter.notifyDataSetChanged();
}
}
@@ -0,0 +1,115 @@
package ru.vtosters.lite.ui.items;
import android.graphics.Color;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class Theme {
public String author;
public String name;
public boolean isDarkMode;
public String id;
private Map<String, Integer> colors = new HashMap<>();
public Theme setAuthor(String author) {
this.author = author;
return this;
}
public Theme setName(String name) {
this.name = name;
return this;
}
public Theme setDarkMode(boolean b) {
this.isDarkMode = b;
return this;
}
public Theme setId(String id) {
this.id = id;
return this;
}
public Theme setColors(Map<String, Integer> colors) {
this.colors.putAll(colors);
return this;
}
public Theme setColor(String colorName, int color) {
this.colors.put(colorName, color);
return this;
}
public Theme setColor(String colorName, String color) {
this.colors.put(colorName, Color.parseColor(color));
return this;
}
public static Theme valuesOf(String author, String name, boolean darkMode, String id, Map<String, Integer> colors) {
return new Theme()
.setAuthor(author)
.setName(name)
.setDarkMode(darkMode)
.setId(id)
.setColors(colors);
}
public static Theme fromJson(String json) {
try {
return fromJson(new JSONObject(json));
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
public static Theme fromJson(JSONObject json) {
var colors = new HashMap<String, Integer>();
var colorsJson = json.optJSONObject("colors");
var names = colorsJson.names();
for (int i = 0; i < colorsJson.length(); i++) {
var name = names.optString(i);
var color = Color.parseColor("#" + colorsJson.optString(name));
colors.put(name, color);
}
return valuesOf(
json.optString("author"),
json.optString("name"),
json.optBoolean("dark"),
json.optString("id"),
colors
);
}
public JSONObject toJson() throws JSONException {
var colorsJson = new JSONObject();
for (var entry : colors.entrySet())
colorsJson.put(entry.getKey(), Integer.toHexString(entry.getValue()));
return new JSONObject()
.put("author", this.author)
.put("name", this.name)
.put("dark", this.isDarkMode)
.put("id", this.id)
.put("colors", colorsJson);
}
@Override
public String toString() {
return "Theme{" +
"author='" + this.author + '\'' +
", name='" + this.name + '\'' +
", isDarkMode=" + this.isDarkMode +
", colors=" + this.colors +
'}';
}
public int getColor(String name) {
return colors.get(name);
}
}
@@ -0,0 +1,26 @@
package ru.vtosters.lite.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.appcompat.widget.AppCompatRadioButton;
public class NoTouchRadioButton extends AppCompatRadioButton {
public NoTouchRadioButton(Context context) {
super(context);
}
public NoTouchRadioButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoTouchRadioButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return false;
}
}
@@ -71,10 +71,34 @@ public class IOUtils {
}
public static void writeToFile(File file, byte[] content) throws IOException {
if (!file.exists())
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
fos.write(content);
}
public static void copyFileToDirectory(File dest, File targetDir) throws IOException {
if (!targetDir.exists())
targetDir.mkdirs();
var path = dest.getAbsolutePath();
var target = new File(targetDir, path.substring(path.lastIndexOf("/")));
copyFile(dest, target);
}
public static void copyFile(File dest, File target) throws IOException {
var parent = target.getParentFile();
if (!parent.exists())
parent.getParentFile().mkdirs();
writeToFile(target, readAllBytes(dest));
}
public static void copyFile(InputStream is, File target) throws IOException {
var parent = target.getParentFile();
if (!parent.exists())
parent.getParentFile().mkdirs();
writeToFile(target, readAllBytes(is));
}
public static void deleteRecursive(File f) {
deleteRecursive(f, true);
}
@@ -1,9 +1,12 @@
package ru.vtosters.lite.utils;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import androidx.constraintlayout.widget.ConstraintLayout;
public class LayoutUtils {
public static int MATCH_PARENT = -1;
@@ -21,4 +24,12 @@ public class LayoutUtils {
public static FrameLayout.LayoutParams createFrame(int w, int h) {
return new FrameLayout.LayoutParams(w, h);
}
public static ViewGroup.LayoutParams createViewGroup(int w, int h) {
return new ViewGroup.LayoutParams(w, h);
}
public static ConstraintLayout.LayoutParams createConstraint(int w, int h) {
return new ConstraintLayout.LayoutParams(w, h);
}
}
+9
View File
@@ -0,0 +1,9 @@
<vector android:height="24dp"
android:viewportHeight="17"
android:viewportWidth="17"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#ff000000"
android:pathData="M2.8,0.7c-0.4,0.4 -0.4,1 0,1.4l2.7,2.7l1.4,-1.4L4.2,0.7C3.8,0.3 3.2,0.3 2.8,0.7zM11.8,12.4H9.5V6.8l4.7,-4.7c0.4,-0.4 0.4,-1 0,-1.4c-0.4,-0.4 -1,-0.4 -1.4,0L8.1,5.4C7.7,5.8 7.5,6.3 7.5,6.8v5.6H5.2c-0.4,0 -0.7,0.5 -0.4,0.8l3.3,3.3c0.2,0.2 0.5,0.2 0.7,0l3.3,-3.3C12.5,12.9 12.3,12.4 11.8,12.4z" />
</vector>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp">
<TextView
android:id="@id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?attr/text_primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/author" />
<TextView
android:id="@id/author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/text_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/name"
app:layout_constraintBottom_toBottomOf="parent" />
<ru.vtosters.lite.ui.views.NoTouchRadioButton
android:id="@id/selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>