mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Media detail page redone as a "slide-up" panel.
Loads and displays default or English description, and categories. No caching of this info yet. Scrollable pane is a ListView, with the title/desc/category label in a 'header' view along with a spacer view. The height of the spacer is set dynamically to the height of the total fragment minus 48dp, giving room for an initially-visible title section and a little spillover so you can see it's scrollable. Clicking on a category in the cats list opens the category page in an external web browser. In the future this should open the category within the app, but we don't have a per-cat view yet. Description and category list are not yet editable. GitHub: https://github.com/wikimedia/apps-android-commons/pull/41 Change-Id: I46d0a77481dbe64a268a72f3efe49ae72168541f
This commit is contained in:
parent
a1d435f86e
commit
4df8ec8fa9
13 changed files with 586 additions and 30 deletions
10
commons/res/drawable/media_info_shadow.xml
Normal file
10
commons/res/drawable/media_info_shadow.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:startColor="#00000000"
|
||||
android:endColor="#ff000000"
|
||||
android:angle="270"
|
||||
>
|
||||
</gradient>
|
||||
</shape>
|
||||
12
commons/res/layout/detail_category_item.xml
Normal file
12
commons/res/layout/detail_category_item.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:padding="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:id="@+id/mediaDetailCategoryItemText"
|
||||
android:textSize="18sp"
|
||||
android:background="#AA000000"
|
||||
/>
|
||||
48
commons/res/layout/detail_main_panel.xml
Normal file
48
commons/res/layout/detail_main_panel.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
|
||||
<!-- Placeholder. Height gets set at runtime based on container size; the initial value is a hack to keep
|
||||
the detail info offscreen until it's placed properly. May be a better way to do this. -->
|
||||
<org.wikimedia.commons.media.MediaDetailSpacer
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="1600dp"
|
||||
android:id="@+id/mediaDetailSpacer"/>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#AA000000"
|
||||
android:padding="8dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Title of the media"
|
||||
android:id="@+id/mediaDetailTitle"
|
||||
android:layout_gravity="left|start"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp" /> <!-- 18sp == MediumText -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Description of the media goes here. This can potentially be fairly long, and will need to wrap across multiple lines. We hope it looks nice though."
|
||||
android:id="@+id/mediaDetailDesc"
|
||||
android:layout_gravity="left|start"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/detail_panel_cats_label"
|
||||
android:textSize="18sp"
|
||||
android:layout_gravity="left|start"
|
||||
android:paddingTop="24dp" android:textColor="@android:color/white"/>
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/mediaDetailCategoryList"
|
||||
android:layout_gravity="left|start"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
@ -28,31 +28,14 @@
|
|||
android:scaleType="fitCenter"
|
||||
/>
|
||||
|
||||
<RelativeLayout
|
||||
<ListView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center|bottom"
|
||||
android:background="#AA000000"
|
||||
android:padding="8dp"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/mediaDetailTitle"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:singleLine="true"
|
||||
android:textColor="#FFFFFF"/>
|
||||
<!-- <TextView
|
||||
android:id="@+id/mediaDetailDescription"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/mediaDetailTitle"
|
||||
android:layout_alignParentBottom="true"
|
||||
style="?android:textAppearanceSmall"
|
||||
android:textColor="#FFFFFFFF"
|
||||
/> -->
|
||||
</RelativeLayout>
|
||||
android:id="@+id/mediaDetailListView"
|
||||
android:divider="#00A0A0A0"
|
||||
android:fillViewport="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:cacheColorHint="@android:color/transparent"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -89,4 +89,7 @@
|
|||
<string name="welcome_final_text">Message asking user if they understand what kinds of images to upload.</string>
|
||||
<string name="welcome_final_button_text">Button text for confirming the user understands what kinds of images to upload.
|
||||
{{Identical|Yes}}</string>
|
||||
<string name="detail_panel_cats_label">Label for categories list in media detail panel</string>
|
||||
<string name="detail_panel_cats_loading">Placeholder for categories list in media detail panel, while loading from network.</string>
|
||||
<string name="detail_panel_cats_none">Placeholder for categories list in media detail panel, if no categories found.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -102,4 +102,7 @@
|
|||
<string name="welcome_copyright_subtext">Avoid copyrighted materials you found from the Internet as well as images of posters, book covers, etc.</string>
|
||||
<string name="welcome_final_text">You think you got it?</string>
|
||||
<string name="welcome_final_button_text">Yes!</string>
|
||||
<string name="detail_panel_cats_label">Categories</string>
|
||||
<string name="detail_panel_cats_loading">Loading...</string>
|
||||
<string name="detail_panel_cats_none">None selected</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -210,5 +210,4 @@ public class CommonsApplication extends Application {
|
|||
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
|
||||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.wikimedia.commons;
|
|||
|
||||
import android.net.Uri;
|
||||
import android.os.*;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.*;
|
||||
|
|
@ -19,6 +20,8 @@ public class Media implements Parcelable {
|
|||
};
|
||||
|
||||
protected Media() {
|
||||
this.categories = new ArrayList<String>();
|
||||
this.descriptions = new HashMap<String, String>();
|
||||
}
|
||||
|
||||
private HashMap<String, Object> tags = new HashMap<String, Object>();
|
||||
|
|
@ -127,10 +130,11 @@ public class Media implements Parcelable {
|
|||
this.license = license;
|
||||
}
|
||||
|
||||
// Primary metadata fields
|
||||
protected Uri localUri;
|
||||
protected String imageUrl;
|
||||
protected String filename;
|
||||
protected String description;
|
||||
protected String description; // monolingual description on input...
|
||||
protected long dataLength;
|
||||
protected Date dateCreated;
|
||||
protected Date dateUploaded;
|
||||
|
|
@ -141,8 +145,45 @@ public class Media implements Parcelable {
|
|||
|
||||
protected String creator;
|
||||
|
||||
protected ArrayList<String> categories; // as loaded at runtime?
|
||||
protected Map<String, String> descriptions; // multilingual descriptions as loaded
|
||||
|
||||
public ArrayList<String> getCategories() {
|
||||
return (ArrayList<String>)categories.clone(); // feels dirty
|
||||
}
|
||||
|
||||
public void setCategories(List<String> categories) {
|
||||
this.categories.removeAll(this.categories);
|
||||
this.categories.addAll(categories);
|
||||
}
|
||||
|
||||
public void setDescriptions(Map<String,String> descriptions) {
|
||||
for (String key : this.descriptions.keySet()) {
|
||||
this.descriptions.remove(key);
|
||||
}
|
||||
for (String key : descriptions.keySet()) {
|
||||
this.descriptions.put(key, descriptions.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
public String getDescription(String preferredLanguage) {
|
||||
if (descriptions.containsKey(preferredLanguage)) {
|
||||
// See if the requested language is there.
|
||||
return descriptions.get(preferredLanguage);
|
||||
} else if (descriptions.containsKey("en")) {
|
||||
// Ah, English. Language of the world, until the Chinese crush us.
|
||||
return descriptions.get("en");
|
||||
} else if (descriptions.containsKey("default")) {
|
||||
// No languages marked...
|
||||
return descriptions.get("default");
|
||||
} else {
|
||||
// FIXME: return the first available non-English description?
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
|
||||
this();
|
||||
this.localUri = localUri;
|
||||
this.imageUrl = imageUrl;
|
||||
this.filename = filename;
|
||||
|
|
@ -170,6 +211,8 @@ public class Media implements Parcelable {
|
|||
parcel.writeInt(width);
|
||||
parcel.writeInt(height);
|
||||
parcel.writeString(license);
|
||||
parcel.writeStringList(categories);
|
||||
parcel.writeMap(descriptions);
|
||||
}
|
||||
|
||||
public Media(Parcel in) {
|
||||
|
|
@ -185,6 +228,8 @@ public class Media implements Parcelable {
|
|||
width = in.readInt();
|
||||
height = in.readInt();
|
||||
license = in.readString();
|
||||
in.readStringList(categories);
|
||||
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
package org.wikimedia.commons;
|
||||
|
||||
import android.util.Log;
|
||||
import org.mediawiki.api.ApiResult;
|
||||
import org.mediawiki.api.MWApi;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Fetch additional media data from the network that we don't store locally.
|
||||
*
|
||||
* This includes things like category lists and multilingual descriptions,
|
||||
* which are not intrinsic to the media and may change due to editing.
|
||||
*/
|
||||
public class MediaDataExtractor {
|
||||
private boolean fetched;
|
||||
private boolean processed;
|
||||
|
||||
private String filename;
|
||||
private ArrayList<String> categories;
|
||||
private Map<String, String> descriptions;
|
||||
private String author;
|
||||
private Date date;
|
||||
|
||||
/**
|
||||
* @param filename of the target media object, should include 'File:' prefix
|
||||
*/
|
||||
public MediaDataExtractor(String filename) {
|
||||
this.filename = filename;
|
||||
categories = new ArrayList<String>();
|
||||
descriptions = new HashMap<String, String>();
|
||||
fetched = false;
|
||||
processed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually fetch the data over the network.
|
||||
* todo: use local caching?
|
||||
*
|
||||
* Warning: synchronous i/o, call on a background thread
|
||||
*/
|
||||
public void fetch() throws IOException {
|
||||
if (fetched) {
|
||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
|
||||
}
|
||||
|
||||
MWApi api = CommonsApplication.createMWApi();
|
||||
ApiResult result = api.action("query")
|
||||
.param("prop", "revisions")
|
||||
.param("titles", filename)
|
||||
.param("rvprop", "content")
|
||||
.param("rvlimit", 1)
|
||||
.param("rvgeneratexml", 1)
|
||||
.get();
|
||||
|
||||
processResult(result);
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
private void processResult(ApiResult result) throws IOException {
|
||||
|
||||
String wikiSource = result.getString("/api/query/pages/page/revisions/rev");
|
||||
String parseTreeXmlSource = result.getString("/api/query/pages/page/revisions/rev/@parsetree");
|
||||
|
||||
// In-page category links are extracted from source, as XML doesn't cover [[links]]
|
||||
extractCategories(wikiSource);
|
||||
|
||||
// Description template info is extracted from preprocessor XML
|
||||
processWikiParseTree(parseTreeXmlSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* We could fetch all category links from API, but we actually only want the ones
|
||||
* directly in the page source so they're editable. In the future this may change.
|
||||
*
|
||||
* @param source wikitext source code
|
||||
*/
|
||||
private void extractCategories(String source) {
|
||||
Pattern regex = Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = regex.matcher(source);
|
||||
while (matcher.find()) {
|
||||
String cat = matcher.group(1).trim();
|
||||
categories.add(cat);
|
||||
}
|
||||
}
|
||||
|
||||
private void processWikiParseTree(String source) throws IOException {
|
||||
Document doc;
|
||||
try {
|
||||
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
doc = docBuilder.parse(new ByteArrayInputStream(source.getBytes("UTF-8")));
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IllegalStateException e) {
|
||||
throw new IOException(e);
|
||||
} catch (SAXException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
Node templateNode = findTemplate(doc.getDocumentElement(), "information");
|
||||
if (templateNode != null) {
|
||||
Node descriptionNode = findTemplateParameter(templateNode, "description");
|
||||
descriptions = getMultilingualText(descriptionNode);
|
||||
|
||||
Node authorNode = findTemplateParameter(templateNode, "author");
|
||||
author = Utils.getStringFromDOM(authorNode);
|
||||
}
|
||||
}
|
||||
|
||||
private Node findTemplate(Element parentNode, String title) throws IOException {
|
||||
String ucTitle= Utils.capitalize(title);
|
||||
NodeList nodes = parentNode.getChildNodes();
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("template")) {
|
||||
String foundTitle = getTemplateTitle(node);
|
||||
if (Utils.capitalize(foundTitle).equals(ucTitle)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getTemplateTitle(Node templateNode) throws IOException {
|
||||
NodeList nodes = templateNode.getChildNodes();
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("title")) {
|
||||
return node.getTextContent().trim();
|
||||
}
|
||||
}
|
||||
throw new IOException("Template has no title element.");
|
||||
}
|
||||
|
||||
private static abstract class TemplateChildNodeComparator {
|
||||
abstract public boolean match(Node node);
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
|
||||
final String theName = name;
|
||||
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
|
||||
@Override
|
||||
public boolean match(Node node) {
|
||||
return (Utils.capitalize(node.getTextContent().trim()).equals(Utils.capitalize(theName)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, int index) throws IOException {
|
||||
final String theIndex = "" + index;
|
||||
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
|
||||
@Override
|
||||
public boolean match(Node node) {
|
||||
Element el = (Element)node;
|
||||
if (el.getTextContent().trim().equals(theIndex)) {
|
||||
return true;
|
||||
} else if (el.getAttribute("index") != null && el.getAttribute("index").trim().equals(theIndex)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Node findTemplateParameter(Node templateNode, TemplateChildNodeComparator comparator) throws IOException {
|
||||
NodeList nodes = templateNode.getChildNodes();
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("part")) {
|
||||
NodeList childNodes = node.getChildNodes();
|
||||
for (int j = 0; j < childNodes.getLength(); j++) {
|
||||
Node childNode = childNodes.item(j);
|
||||
if (childNode.getNodeName().equals("name")) {
|
||||
if (comparator.match(childNode)) {
|
||||
// yay! Now fetch the value node.
|
||||
for (int k = j + 1; k < childNodes.getLength(); k++) {
|
||||
Node siblingNode = childNodes.item(k);
|
||||
if (siblingNode.getNodeName().equals("value")) {
|
||||
return siblingNode;
|
||||
}
|
||||
}
|
||||
throw new IOException("No value node found for matched template parameter.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IOException("No matching template parameter node found.");
|
||||
}
|
||||
|
||||
// Extract a dictionary of multilingual texts from a subset of the parse tree.
|
||||
// Texts are wrapped in things like {{en|foo} or {{en|1=foo bar}}.
|
||||
// Text outside those wrappers is stuffed into a 'default' faux language key if present.
|
||||
private Map<String, String> getMultilingualText(Node parentNode) throws IOException {
|
||||
Map<String, String> texts = new HashMap<String, String>();
|
||||
StringBuilder localText = new StringBuilder();
|
||||
|
||||
NodeList nodes = parentNode.getChildNodes();
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Node node = nodes.item(i);
|
||||
if (node.getNodeName().equals("template")) {
|
||||
// process a template node
|
||||
String title = getTemplateTitle(node);
|
||||
if (title.length() < 3) {
|
||||
// Hopefully a language code. Nasty hack!
|
||||
String lang = title;
|
||||
Node valueNode = findTemplateParameter(node, 1);
|
||||
String value = Utils.getStringFromDOM(valueNode); // hope there's no subtemplates or formatting for now
|
||||
texts.put(lang, value);
|
||||
}
|
||||
} else if (node.getNodeType() == Node.TEXT_NODE) {
|
||||
localText.append(node.getTextContent());
|
||||
}
|
||||
}
|
||||
|
||||
// Some descriptions don't list multilingual variants
|
||||
String defaultText = localText.toString().trim();
|
||||
if (defaultText.length() > 0) {
|
||||
texts.put("default", localText.toString());
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take our metadata and inject it into a live Media object.
|
||||
* Media object might contain stale or cached data, or emptiness.
|
||||
* @param media
|
||||
*/
|
||||
public void fill(Media media) {
|
||||
if (!fetched) {
|
||||
throw new IllegalStateException("Tried to call MediaDataExtractor.fill() before fetch().");
|
||||
}
|
||||
|
||||
media.setCategories(categories);
|
||||
media.setDescriptions(descriptions);
|
||||
|
||||
// add author, date, etc fields
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package org.wikimedia.commons;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.*;
|
||||
import com.nostra13.universalimageloader.core.*;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||
|
|
@ -174,4 +175,23 @@ public class Utils {
|
|||
throw new RuntimeException("Unrecognized license value");
|
||||
}
|
||||
|
||||
public static String implode(String glue, Iterable<String> pieces) {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
boolean first = true;
|
||||
for (String piece : pieces) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
buffer.append(glue);
|
||||
}
|
||||
buffer.append(pieces);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
public static Uri uriForWikiPage(String name) {
|
||||
String underscored = name.trim().replace(" ", "_");
|
||||
String uriStr = CommonsApplication.HOME_URL + urlEncode(underscored);
|
||||
return Uri.parse(uriStr);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ public class Contribution extends Media {
|
|||
}
|
||||
|
||||
public Contribution() {
|
||||
super();
|
||||
timestamp = new Date(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package org.wikimedia.commons.media;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.*;
|
||||
import android.net.Uri;
|
||||
import android.os.*;
|
||||
import android.text.*;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import com.actionbarsherlock.app.SherlockFragment;
|
||||
|
|
@ -14,8 +17,13 @@ import com.nostra13.universalimageloader.core.assist.ImageLoadingListener;
|
|||
|
||||
import com.android.volley.toolbox.*;
|
||||
|
||||
import org.mediawiki.api.ApiResult;
|
||||
import org.mediawiki.api.MWApi;
|
||||
import org.wikimedia.commons.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MediaDetailFragment extends SherlockFragment {
|
||||
|
||||
private boolean editable;
|
||||
|
|
@ -33,6 +41,8 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
Bundle state = new Bundle();
|
||||
state.putBoolean("editable", editable);
|
||||
state.putInt("index", index);
|
||||
state.putInt("listIndex", 0);
|
||||
state.putInt("listTop", 0);
|
||||
|
||||
mf.setArguments(state);
|
||||
|
||||
|
|
@ -40,9 +50,22 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
}
|
||||
|
||||
private ImageView image;
|
||||
private EditText title;
|
||||
//private EditText title;
|
||||
private ProgressBar loadingProgress;
|
||||
private ImageView loadingFailed;
|
||||
private MediaDetailSpacer spacer;
|
||||
private int initialListIndex = 0;
|
||||
private int initialListTop = 0;
|
||||
|
||||
private TextView title;
|
||||
private TextView desc;
|
||||
private ListView listView;
|
||||
private ArrayList<String> categoryNames;
|
||||
private boolean categoriesLoaded = false;
|
||||
private boolean categoriesPresent = false;
|
||||
private ArrayAdapter categoryAdapter;
|
||||
private ViewTreeObserver.OnGlobalLayoutListener observer; // for layout stuff, only used once!
|
||||
private AsyncTask<Void,Void,Boolean> detailFetchTask;
|
||||
|
||||
|
||||
@Override
|
||||
|
|
@ -50,6 +73,20 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("index", index);
|
||||
outState.putBoolean("editable", editable);
|
||||
|
||||
getScrollPosition();
|
||||
outState.putInt("listIndex", initialListIndex);
|
||||
outState.putInt("listTop", initialListTop);
|
||||
}
|
||||
|
||||
private void getScrollPosition() {
|
||||
int initialListIndex = listView.getFirstVisiblePosition();
|
||||
View firstVisibleItem = listView.getChildAt(initialListIndex);
|
||||
if (firstVisibleItem == null) {
|
||||
initialListTop = 0;
|
||||
} else {
|
||||
initialListTop = firstVisibleItem.getTop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -59,19 +96,46 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
if(savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable");
|
||||
index = savedInstanceState.getInt("index");
|
||||
initialListIndex = savedInstanceState.getInt("listIndex");
|
||||
initialListTop = savedInstanceState.getInt("listTop");
|
||||
} else {
|
||||
editable = getArguments().getBoolean("editable");
|
||||
index = getArguments().getInt("index");
|
||||
}
|
||||
final Media media = detailProvider.getMediaAtPosition(index);
|
||||
categoryNames = new ArrayList<String>();
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_loading));
|
||||
|
||||
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
|
||||
image = (ImageView) view.findViewById(R.id.mediaDetailImage);
|
||||
title = (EditText) view.findViewById(R.id.mediaDetailTitle);
|
||||
loadingProgress = (ProgressBar) view.findViewById(R.id.mediaDetailImageLoading);
|
||||
loadingFailed = (ImageView) view.findViewById(R.id.mediaDetailImageFailed);
|
||||
listView = (ListView) view.findViewById(R.id.mediaDetailListView);
|
||||
|
||||
// Detail consists of a list view with main pane in header view, plus category list.
|
||||
View detailView = getActivity().getLayoutInflater().inflate(R.layout.detail_main_panel, null, false);
|
||||
listView.addHeaderView(detailView, null, false);
|
||||
categoryAdapter = new ArrayAdapter(getActivity(), R.layout.detail_category_item, categoryNames);
|
||||
listView.setAdapter(categoryAdapter);
|
||||
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
if (categoriesLoaded && categoriesPresent) {
|
||||
String selectedCategoryTitle = "Category:" + categoryNames.get(position - 1);
|
||||
Intent viewIntent = new Intent();
|
||||
viewIntent.setAction(Intent.ACTION_VIEW);
|
||||
viewIntent.setData(Utils.uriForWikiPage(selectedCategoryTitle));
|
||||
startActivity(viewIntent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
spacer = (MediaDetailSpacer) detailView.findViewById(R.id.mediaDetailSpacer);
|
||||
title = (TextView) detailView.findViewById(R.id.mediaDetailTitle);
|
||||
desc = (TextView) detailView.findViewById(R.id.mediaDetailDesc);
|
||||
|
||||
// Enable or disable editing on the title
|
||||
/*
|
||||
title.setClickable(editable);
|
||||
title.setFocusable(editable);
|
||||
title.setCursorVisible(editable);
|
||||
|
|
@ -79,6 +143,8 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
if(!editable) {
|
||||
title.setBackgroundDrawable(null);
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
String actualUrl = TextUtils.isEmpty(media.getImageUrl()) ? media.getLocalUri().toString() : media.getThumbnailUrl(640);
|
||||
if(actualUrl.startsWith("http")) {
|
||||
|
|
@ -88,6 +154,56 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
mwImage.setMedia(media, loader);
|
||||
Log.d("Volley", actualUrl);
|
||||
// FIXME: For transparent images
|
||||
|
||||
// Load image metadata: desc, license, categories
|
||||
// FIXME: keep the spinner going while we load data
|
||||
// FIXME: cache this data
|
||||
detailFetchTask = new AsyncTask<Void, Void, Boolean>() {
|
||||
private MediaDataExtractor extractor;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
extractor = new MediaDataExtractor(media.getFilename());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
extractor.fetch();
|
||||
return Boolean.TRUE;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean success) {
|
||||
detailFetchTask = null;
|
||||
|
||||
if (success.booleanValue()) {
|
||||
extractor.fill(media);
|
||||
|
||||
// Fill some fields
|
||||
desc.setText(media.getDescription("en"));
|
||||
|
||||
categoryNames.removeAll(categoryNames);
|
||||
categoryNames.addAll(media.getCategories());
|
||||
|
||||
categoriesLoaded = true;
|
||||
categoriesPresent = (categoryNames.size() > 0);
|
||||
if (!categoriesPresent) {
|
||||
// Stick in a filler element.
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_none));
|
||||
}
|
||||
|
||||
categoryAdapter.notifyDataSetChanged();
|
||||
} else {
|
||||
Log.d("Commons", "Failed to load photo details.");
|
||||
}
|
||||
}
|
||||
};
|
||||
Utils.executeAsyncTask(detailFetchTask);
|
||||
} else {
|
||||
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, image, displayOptions, new ImageLoadingListener() {
|
||||
public void onLoadingStarted(String s, View view) {
|
||||
|
|
@ -113,8 +229,11 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
}
|
||||
});
|
||||
}
|
||||
title.setText(media.getDisplayTitle());
|
||||
|
||||
title.setText(media.getDisplayTitle());
|
||||
desc.setText("");
|
||||
|
||||
/*
|
||||
title.addTextChangedListener(new TextWatcher() {
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
||||
|
||||
|
|
@ -130,6 +249,35 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Layout observer to size the spacer item relative to the available space.
|
||||
// There may be a .... better way to do this.
|
||||
observer = new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
private int currentHeight = -1;
|
||||
|
||||
public void onGlobalLayout() {
|
||||
int viewHeight = view.getHeight();
|
||||
//int textHeight = title.getLineHeight();
|
||||
int paddingDp = 48;
|
||||
float paddingPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingDp, getResources().getDisplayMetrics());
|
||||
int newHeight = viewHeight - Math.round(paddingPx);
|
||||
|
||||
if (newHeight != currentHeight) {
|
||||
currentHeight = newHeight;
|
||||
ViewGroup.LayoutParams params = spacer.getLayoutParams();
|
||||
params.height = newHeight;
|
||||
spacer.setLayoutParams(params);
|
||||
|
||||
// hack hack to trigger relayout
|
||||
categoryAdapter.notifyDataSetChanged();
|
||||
|
||||
listView.setSelectionFromTop(initialListIndex, initialListTop);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
view.getViewTreeObserver().addOnGlobalLayoutListener(observer);
|
||||
return view;
|
||||
}
|
||||
|
||||
|
|
@ -139,4 +287,17 @@ public class MediaDetailFragment extends SherlockFragment {
|
|||
|
||||
displayOptions = Utils.getGenericDisplayOptions().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (detailFetchTask != null) {
|
||||
detailFetchTask.cancel(true);
|
||||
detailFetchTask = null;
|
||||
}
|
||||
if (observer != null) {
|
||||
getView().getViewTreeObserver().removeGlobalOnLayoutListener(observer); // old Android was on crack. CRACK IS WHACK
|
||||
observer = null;
|
||||
}
|
||||
super.onDestroyView();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package org.wikimedia.commons.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public class MediaDetailSpacer extends View {
|
||||
public MediaDetailSpacer(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MediaDetailSpacer(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MediaDetailSpacer(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue