UI update: Snackbars + NRO Metadata

This UI updates adds Snackbar notifications for actions and extracts metadata in the form of an icon, name and author from NRO files to display in the game list.

Co-Authored-By: Ryan Teal <zephyren25@users.noreply.github.com>
This commit is contained in:
◱ PixelyIon 2019-07-04 00:14:28 +05:30
parent 4921bb41ea
commit ae6dddf9f6
8 changed files with 186 additions and 126 deletions

View File

@ -41,5 +41,5 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0-beta01' implementation 'androidx.preference:preference:1.1.0-beta01'
implementation 'com.squareup.picasso:picasso:2.4.0' implementation 'com.google.android.material:material:1.0.0'
} }

View File

@ -0,0 +1,104 @@
package gq.cyuubi.lightswitch;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.io.File;
import java.util.ArrayList;
class DataModel {
File file;
TitleEntry meta;
int index;
public DataModel(File file) {
this.file = file;
index = file.getName().lastIndexOf(".");
meta = NroMeta.GetNroTitle(getPath());
}
public Bitmap getIcon() {
return meta.getIcon();
}
public String getTitle() {
return meta.getName() + " (" + getType() + ")";
}
public String getFileName() {
return file.getName();
}
public String getAuthor() {
return meta.getAuthor();
}
public String getType() {
return file.getName().substring(index + 1).toUpperCase();
}
public String getPath() {
return file.getAbsolutePath();
}
}
public class FileAdapter extends ArrayAdapter<DataModel> implements View.OnClickListener {
Context mContext;
private ArrayList<DataModel> dataSet;
public FileAdapter(Context context, @NonNull ArrayList<DataModel> data) {
super(context, R.layout.file_item, data);
this.dataSet = new ArrayList<>();
this.mContext = context;
}
@Override
public void onClick(View v) {
int position = (Integer) v.getTag();
Object object = getItem(position);
DataModel dataModel = (DataModel) object;
switch (v.getId()) {
case R.id.icon:
break;
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
DataModel dataModel = getItem(position);
ViewHolder viewHolder;
if (convertView == null) {
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.file_item, parent, false);
viewHolder.icon = convertView.findViewById(R.id.icon);
viewHolder.txtTitle = convertView.findViewById(R.id.text_title);
viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.txtTitle.setText(dataModel.getTitle());
viewHolder.txtSub.setText(dataModel.getAuthor());
viewHolder.icon.setImageBitmap(dataModel.getIcon());
return convertView;
}
private static class ViewHolder {
ImageView icon;
TextView txtTitle;
TextView txtSub;
}
}

View File

@ -1,22 +1,17 @@
package gq.cyuubi.lightswitch; package gq.cyuubi.lightswitch;
import android.Manifest; import android.Manifest;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
@ -24,82 +19,12 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.material.snackbar.Snackbar;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
class DataModel {
File file;
int index;
public DataModel(File file) {
this.file = file;
index = file.getName().lastIndexOf(".");
}
public String getTitle() {
return getName() + "(" + getType() + ")";
}
public String getName() {
String name = "";
for (String str_i : file.getName().substring(0, index).split("_")) {
name += str_i.substring(0, 1).toUpperCase() + str_i.substring(1) + " ";
}
return name;
}
public String getType() {
return file.getName().substring(index + 1).toUpperCase();
}
public String getPath() {
return file.getAbsolutePath();
}
}
class FileAdapter extends ArrayAdapter<DataModel> {
Context mContext;
private ArrayList<DataModel> dataSet;
public FileAdapter(Context context, @NonNull ArrayList<DataModel> data) {
super(context, android.R.layout.simple_list_item_2, data);
this.dataSet = new ArrayList<>();
this.mContext = context;
}
@Override
public void add(DataModel object) {
super.add(object);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
DataModel dataModel = getItem(position);
ViewHolder viewHolder;
if (convertView == null) {
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(android.R.layout.simple_list_item_2, parent, false);
viewHolder.txtTitle = convertView.findViewById(android.R.id.text1);
viewHolder.txtPath = convertView.findViewById(android.R.id.text2);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.txtTitle.setText(dataModel.getTitle());
viewHolder.txtPath.setText(dataModel.getPath());
return convertView;
}
private static class ViewHolder {
TextView txtTitle;
TextView txtPath;
}
}
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
static { static {
@ -109,6 +34,11 @@ public class MainActivity extends AppCompatActivity {
SharedPreferences sharedPreferences; SharedPreferences sharedPreferences;
FileAdapter adapter; FileAdapter adapter;
private void notifyUser(String text) {
Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show();
// Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show();
}
private List<File> findFile(String ext, File file, @Nullable List<File> files) { private List<File> findFile(String ext, File file, @Nullable List<File> files) {
if (files == null) { if (files == null) {
files = new ArrayList<>(); files = new ArrayList<>();
@ -159,7 +89,9 @@ public class MainActivity extends AppCompatActivity {
game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() { game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
loadFile(((DataModel) parent.getItemAtPosition(position)).getPath()); String path = ((DataModel) parent.getItemAtPosition(position)).getPath();
notifyUser(getString(R.string.launch_string) + " " + path);
loadFile(path);
} }
}); });
refresh_files(); refresh_files();
@ -178,6 +110,7 @@ public class MainActivity extends AppCompatActivity {
startActivity(new Intent(this, SettingsActivity.class)); startActivity(new Intent(this, SettingsActivity.class));
return true; return true;
case R.id.action_refresh: case R.id.action_refresh:
notifyUser(getString(R.string.refresh_string));
refresh_files(); refresh_files();
return true; return true;
default: default:

View File

@ -1,30 +1,34 @@
package gq.cyuubi.lightswitch; package gq.cyuubi.lightswitch;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log; import android.util.Log;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
final class TitleEntry { final class TitleEntry {
private final String name; private final String name;
private final String author; private final String author;
private final Bitmap icon;
public TitleEntry(String name, String author) { public TitleEntry(String name, String author, Bitmap icon) {
this.name = name; this.name = name;
this.author = author; this.author = author;
this.icon = icon;
} }
public String Name() { public String getName() {
return name; return name;
} }
public String Author() { public String getAuthor() {
return author; return author;
} }
public Bitmap getIcon() {
return icon;
}
} }
public class NroMeta { public class NroMeta {
@ -36,56 +40,34 @@ public class NroMeta {
f.seek(asetOffset); // Skip to the offset specified by NroHeader.size f.seek(asetOffset); // Skip to the offset specified by NroHeader.size
byte[] buffer = new byte[4]; byte[] buffer = new byte[4];
f.read(buffer); f.read(buffer);
if(!(new String(buffer).equals("ASET"))) if (!(new String(buffer).equals("ASET")))
return null; return null;
f.skipBytes(0x14); f.skipBytes(0x4);
long iconOffset = Long.reverseBytes(f.readLong());
int iconSize = Integer.reverseBytes(f.readInt());
if (iconOffset == 0 || iconSize == 0)
throw new IOException();
f.seek(asetOffset + iconOffset);
byte[] iconData = new byte[iconSize];
f.read(iconData);
Bitmap icon = BitmapFactory.decodeByteArray(iconData, 0, iconSize);
f.seek(asetOffset + 0x18);
long nacpOffset = Long.reverseBytes(f.readLong()); long nacpOffset = Long.reverseBytes(f.readLong());
long nacpSize = Long.reverseBytes(f.readLong()); long nacpSize = Long.reverseBytes(f.readLong());
if(nacpOffset == 0 || nacpSize == 0) if (nacpOffset == 0 || nacpSize == 0)
return null; return null;
f.seek(asetOffset + nacpOffset); f.seek(asetOffset + nacpOffset);
byte[] name = new byte[0x200]; byte[] name = new byte[0x200];
f.read(name); f.read(name);
byte[] author = new byte[0x100]; byte[] author = new byte[0x100];
f.read(author); f.read(author);
return new TitleEntry(new String(name).trim(), new String(author).trim()); return new TitleEntry(new String(name).trim(), new String(author).trim(), icon);
} } catch (IOException e) {
catch(IOException e) {
Log.e("app_process64", "Error while loading ASET: " + e.getMessage()); Log.e("app_process64", "Error while loading ASET: " + e.getMessage());
return null; return null;
} }
} }
public static void LoadImage(String file, ImageView target, MainActivity context) {
try {
RandomAccessFile f = new RandomAccessFile(file, "r");
f.seek(0x18); // Skip to NroHeader.size
int asetOffset = Integer.reverseBytes(f.readInt());
f.seek(asetOffset); // Skip to the offset specified by NroHeader.size
byte[] buffer = new byte[4];
f.read(buffer);
if(!(new String(buffer).equals("ASET")))
return;
f.skipBytes(0x4);
long iconOffset = Long.reverseBytes(f.readLong());
long iconSize = Long.reverseBytes(f.readLong());
if(iconOffset == 0 || iconSize == 0)
return;
f.seek(asetOffset + iconOffset);
byte[] iconData = new byte[(int)iconSize];
f.read(iconData);
new FileOutputStream(context.getFilesDir() + "/tmp.jpg").write(iconData);
Picasso.with(context).load(context.getFilesDir() + "/tmp.jpg").into(target);
}
catch(IOException e) {
Log.e("app_process64", "Error while loading ASET: " + e.getMessage());
return;
}
}
} }

View File

@ -0,0 +1,5 @@
<vector android:alpha="0.6" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z"/>
</vector>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp">
<ImageView
android:id="@+id/icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="false"
android:layout_centerVertical="true"
android:contentDescription="@string/icon"
android:src="@drawable/ic_missing_icon" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="60dp"
android:layout_marginTop="5dp"
android:textAppearance="?android:attr/textAppearanceListItem" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/text_title"
android:layout_alignStart="@id/text_title"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light" />
</RelativeLayout>

View File

@ -4,7 +4,9 @@
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<!-- Main --> <!-- Main -->
<string name="request_string">The following permission</string> <string name="refresh_string">The list of ROMs has been refreshed.</string>
<string name="launch_string">Launching</string>
<string name="icon">Icon</string>
<!-- Settings --> <!-- Settings -->
<string name="search">Search</string> <string name="search">Search</string>
<string name="search_location">Search Location</string> <string name="search_location">Search Location</string>

View File

@ -1,6 +1,6 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Switch Red Theme --> <!-- Switch Red Theme -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>