--- a/addon-sdk/source/test/jetpack-package.ini
+++ b/addon-sdk/source/test/jetpack-package.ini
@@ -1,16 +1,16 @@
[DEFAULT]
support-files =
buffers/**
commonjs-test-adapter/**
event/**
fixtures/**
loader/**
- libs/**
+ lib/**
modules/**
private-browsing/**
sidebar/**
tabs/**
traits/**
windows/**
zip/**
fixtures.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1547,17 +1547,17 @@ pref("browser.newtabpage.enabled", true)
// number of rows of newtab grid
pref("browser.newtabpage.rows", 3);
// number of columns of newtab grid
pref("browser.newtabpage.columns", 5);
// directory tiles download URL
-pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/v2/links/fetch");
+pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/v2/links/fetch/%LOCALE%");
// endpoint to send newtab click and view pings
pref("browser.newtabpage.directory.ping", "https://tiles.services.mozilla.com/v2/links/");
// Enable the DOM fullscreen API.
pref("full-screen-api.enabled", true);
// True if the fullscreen API requires approval upon a domain entering fullscreen.
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -18,16 +18,19 @@ body,
font-family: Open Sans,sans-serif;
}
.standalone-header {
border-radius: 4px;
background: #fff;
border: 1px solid #E7E7E7;
box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.03);
+ background-image: url("../shared/img/beta-ribbon.svg#beta-ribbon");
+ background-size: 5rem 5rem;
+ background-repeat: no-repeat;
}
.header-box {
padding: 1rem 5rem;
margin-top: 2rem;
}
/*
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -278,17 +278,21 @@ loop.webapp = (function($, _, OT, mozL10
this.setState({callState: "ringing"});
},
_cancelOutgoingCall: function() {
this.props.websocket.cancel();
},
render: function() {
- var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
+ var callStateStringEntityName = "call_progress_" + this.state.callState + "_description";
+ var callState = mozL10n.get(callStateStringEntityName);
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get(callStateStringEntityName)});
return (
React.DOM.div({className: "container"},
React.DOM.div({className: "container-box"},
React.DOM.header({className: "pending-header header-box"},
ConversationBranding(null)
),
React.DOM.div({id: "cameraPreview"}),
@@ -514,16 +518,19 @@ loop.webapp = (function($, _, OT, mozL10
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
render: function() {
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get("status_conversation_ended")});
return (
React.DOM.div({className: "ended-conversation"},
sharedViews.FeedbackView({
feedbackApiClient: this.props.feedbackApiClient,
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}
),
sharedViews.ConversationView({
initiate: false,
@@ -534,26 +541,30 @@ loop.webapp = (function($, _, OT, mozL10
)
)
);
}
});
var StartConversationView = React.createClass({displayName: 'StartConversationView',
render: function() {
+ document.title = mozL10n.get("clientShortname2");
return this.transferPropsTo(
InitiateConversationView({
title: mozL10n.get("initiate_call_button_label2"),
callButtonLabel: mozL10n.get("initiate_audio_video_call_button2")})
);
}
});
var FailedConversationView = React.createClass({displayName: 'FailedConversationView',
render: function() {
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get("status_error")});
return this.transferPropsTo(
InitiateConversationView({
title: mozL10n.get("call_failed_title"),
callButtonLabel: mozL10n.get("retry_call_button")})
);
}
});
@@ -631,16 +642,19 @@ loop.webapp = (function($, _, OT, mozL10
client: this.props.client}
)
);
}
case "pending": {
return PendingConversationView({websocket: this._websocket});
}
case "connected": {
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get("status_in_conversation")});
return (
sharedViews.ConversationView({
initiate: true,
sdk: this.props.sdk,
model: this.props.conversation,
video: {enabled: this.props.conversation.hasVideoStream("outgoing")}}
)
);
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -278,17 +278,21 @@ loop.webapp = (function($, _, OT, mozL10
this.setState({callState: "ringing"});
},
_cancelOutgoingCall: function() {
this.props.websocket.cancel();
},
render: function() {
- var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
+ var callStateStringEntityName = "call_progress_" + this.state.callState + "_description";
+ var callState = mozL10n.get(callStateStringEntityName);
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get(callStateStringEntityName)});
return (
<div className="container">
<div className="container-box">
<header className="pending-header header-box">
<ConversationBranding />
</header>
<div id="cameraPreview" />
@@ -514,16 +518,19 @@ loop.webapp = (function($, _, OT, mozL10
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
render: function() {
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get("status_conversation_ended")});
return (
<div className="ended-conversation">
<sharedViews.FeedbackView
feedbackApiClient={this.props.feedbackApiClient}
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived}
/>
<sharedViews.ConversationView
initiate={false}
@@ -534,26 +541,30 @@ loop.webapp = (function($, _, OT, mozL10
/>
</div>
);
}
});
var StartConversationView = React.createClass({
render: function() {
+ document.title = mozL10n.get("clientShortname2");
return this.transferPropsTo(
<InitiateConversationView
title={mozL10n.get("initiate_call_button_label2")}
callButtonLabel={mozL10n.get("initiate_audio_video_call_button2")} />
);
}
});
var FailedConversationView = React.createClass({
render: function() {
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get("status_error")});
return this.transferPropsTo(
<InitiateConversationView
title={mozL10n.get("call_failed_title")}
callButtonLabel={mozL10n.get("retry_call_button")} />
);
}
});
@@ -631,16 +642,19 @@ loop.webapp = (function($, _, OT, mozL10
client={this.props.client}
/>
);
}
case "pending": {
return <PendingConversationView websocket={this._websocket} />;
}
case "connected": {
+ document.title = mozL10n.get("standalone_title_with_status",
+ {clientShortname: mozL10n.get("clientShortname2"),
+ currentStatus: mozL10n.get("status_in_conversation")});
return (
<sharedViews.ConversationView
initiate={true}
sdk={this.props.sdk}
model={this.props.conversation}
video={{enabled: this.props.conversation.hasVideoStream("outgoing")}}
/>
);
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties
+++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties
@@ -114,8 +114,17 @@ rooms_room_full_label=There are already
rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
rooms_room_joined_label=Someone has joined the conversation!
rooms_room_join_label=Join the conversation
brand_website=https://www.mozilla.org/firefox/
privacy_website=https://www.mozilla.org/privacy/
legal_website=/legal/terms/
+
+## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be
+## replaced by the brand name and {{currentStatus}} will be replaced
+## by the current call status (Connecting, Ringing, etc.)
+standalone_title_with_status={{clientShortname}} — {{currentStatus}}
+status_in_conversation=In conversation
+status_conversation_ended=Conversation ended
+status_error=Something went wrong
+
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -178,16 +178,19 @@ let DirectoryLinksProvider = {
_removePrefsObserver: function DirectoryLinksProvider_removeObserver() {
for (let pref in this._observedPrefs) {
let prefName = this._observedPrefs[pref];
Services.prefs.removeObserver(prefName, this);
}
},
_fetchAndCacheLinks: function DirectoryLinksProvider_fetchAndCacheLinks(uri) {
+ // Replace with the same display locale used for selecting links data
+ uri = uri.replace("%LOCALE%", this.locale);
+
let deferred = Promise.defer();
let xmlHttp = new XMLHttpRequest();
let self = this;
xmlHttp.onload = function(aResponse) {
let json = this.responseText;
if (this.status && this.status != 200) {
json = "{}";
@@ -201,24 +204,22 @@ let DirectoryLinksProvider = {
});
};
xmlHttp.onerror = function(e) {
deferred.reject("Fetching " + uri + " results in error code: " + e.target.status);
};
try {
- xmlHttp.open('POST', uri);
+ xmlHttp.open("GET", uri);
// Override the type so XHR doesn't complain about not well-formed XML
xmlHttp.overrideMimeType(DIRECTORY_LINKS_TYPE);
// Set the appropriate request type for servers that require correct types
xmlHttp.setRequestHeader("Content-Type", DIRECTORY_LINKS_TYPE);
- xmlHttp.send(JSON.stringify({
- locale: this.locale,
- }));
+ xmlHttp.send();
} catch (e) {
deferred.reject("Error fetching " + uri);
Cu.reportError(e);
}
return deferred.promise;
},
/**
--- a/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
+++ b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
@@ -48,32 +48,29 @@ const kPingUrl = kBaseUrl + kPingPath;
Services.prefs.setCharPref(kLocalePref, "en-US");
Services.prefs.setCharPref(kSourceUrlPref, kTestURL);
Services.prefs.setCharPref(kPingUrlPref, kPingUrl);
Services.prefs.setBoolPref(kNewtabEnhancedPref, true);
const kHttpHandlerData = {};
kHttpHandlerData[kExamplePath] = {"en-US": [{"url":"http://example.com","title":"RemoteSource"}]};
-const expectedBodyObject = {locale: DirectoryLinksProvider.locale};
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
+let gLastRequestPath;
function getHttpHandler(path) {
let code = 200;
let body = JSON.stringify(kHttpHandlerData[path]);
if (path == kFailPath) {
code = 204;
}
return function(aRequest, aResponse) {
- let bodyStream = new BinaryInputStream(aRequest.bodyInputStream);
- let bodyObject = JSON.parse(NetUtil.readInputStreamToString(bodyStream, bodyStream.available()));
- isIdentical(bodyObject, expectedBodyObject);
-
+ gLastRequestPath = aRequest.path;
aResponse.setStatusLine(null, code);
aResponse.setHeader("Content-Type", "application/json");
aResponse.write(body);
};
}
function isIdentical(actual, expected) {
if (expected == null) {
@@ -126,17 +123,19 @@ function promiseDirectoryDownloadOnPrefC
let oldValue = Services.prefs.getCharPref(pref);
if (oldValue != newValue) {
// if the preference value is already equal to newValue
// the pref service will not call our observer and we
// deadlock. Hence only setup observer if values differ
let observer = new LinksChangeObserver();
DirectoryLinksProvider.addObserver(observer);
Services.prefs.setCharPref(pref, newValue);
- return observer.deferred.promise;
+ return observer.deferred.promise.then(() => {
+ DirectoryLinksProvider.removeObserver(observer);
+ });
}
return Promise.resolve();
}
function promiseSetupDirectoryLinksProvider(options = {}) {
return Task.spawn(function() {
let linksURL = options.linksURL || kTestURL;
yield DirectoryLinksProvider.init();
@@ -291,17 +290,18 @@ add_task(function test_fetchAndCacheLink
let data = yield readJsonFile();
isIdentical(data, kURLData);
});
add_task(function test_fetchAndCacheLinks_remote() {
yield DirectoryLinksProvider.init();
yield cleanJsonFile();
// this must trigger directory links json download and save it to cache file
- yield DirectoryLinksProvider._fetchAndCacheLinks(kExampleURL);
+ yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL + "%LOCALE%");
+ do_check_eq(gLastRequestPath, kExamplePath + "en-US");
let data = yield readJsonFile();
isIdentical(data, kHttpHandlerData[kExamplePath]);
});
add_task(function test_fetchAndCacheLinks_malformedURI() {
yield DirectoryLinksProvider.init();
yield cleanJsonFile();
let someJunk = "some junk";
@@ -331,17 +331,18 @@ add_task(function test_fetchAndCacheLink
// File should be empty.
let data = yield readJsonFile();
isIdentical(data, "");
});
add_task(function test_fetchAndCacheLinks_non200Status() {
yield DirectoryLinksProvider.init();
yield cleanJsonFile();
- yield DirectoryLinksProvider._fetchAndCacheLinks(kFailURL);
+ yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kFailURL);
+ do_check_eq(gLastRequestPath, kFailPath);
let data = yield readJsonFile();
isIdentical(data, {});
});
// To test onManyLinksChanged observer, trigger a fetch
add_task(function test_DirectoryLinksProvider__linkObservers() {
yield DirectoryLinksProvider.init();
@@ -503,16 +504,17 @@ add_task(function test_DirectoryLinksPro
yield cleanJsonFile();
// ensure that provider does not think it needs to download
do_check_false(DirectoryLinksProvider._needsDownload);
// change the source URL, which should force directory download
yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL);
// then wait for testObserver to fire and test that json is downloaded
yield testObserver.deferred.promise;
+ do_check_eq(gLastRequestPath, kExamplePath);
let data = yield readJsonFile();
isIdentical(data, kHttpHandlerData[kExamplePath]);
yield promiseCleanDirectoryLinksProvider();
});
add_task(function test_DirectoryLinksProvider_fetchDirectoryOnShow() {
yield promiseSetupDirectoryLinksProvider();
--- a/mobile/android/base/home/ReadingListRow.java
+++ b/mobile/android/base/home/ReadingListRow.java
@@ -3,40 +3,85 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.home.TwoLinePageRow;
import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ReadingListRow extends LinearLayout {
+
+ private final Resources resources;
-public class ReadingListRow extends TwoLinePageRow {
+ private final TextView title;
+ private final TextView excerpt;
+ private final TextView readTime;
+
+ // Average reading speed in words per minute.
+ private static final int AVERAGE_READING_SPEED = 250;
+
+ // Length of average word.
+ private static final float AVERAGE_WORD_LENGTH = 5.1f;
+
public ReadingListRow(Context context) {
this(context, null);
}
public ReadingListRow(Context context, AttributeSet attrs) {
super(context, attrs);
+
+ LayoutInflater.from(context).inflate(R.layout.reading_list_row_view, this);
+
+ setOrientation(LinearLayout.VERTICAL);
+
+ resources = context.getResources();
+
+ title = (TextView) findViewById(R.id.title);
+ excerpt = (TextView) findViewById(R.id.excerpt);
+ readTime = (TextView) findViewById(R.id.read_time);
}
- @Override
- protected void updateDisplayedUrl() {
- String pageUrl = getUrl();
+ public void updateFromCursor(Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
- boolean isPrivate = Tabs.getInstance().getSelectedTab().isPrivate();
- Tab tab = Tabs.getInstance().getFirstReaderTabForUrl(pageUrl, isPrivate);
+ final int titleIndex = cursor.getColumnIndexOrThrow(ReadingListItems.TITLE);
+ title.setText(cursor.getString(titleIndex));
- if (tab != null) {
- setUrl(R.string.switch_to_tab);
- setSwitchToTabIcon(R.drawable.ic_url_bar_tab);
+ final int excerptIndex = cursor.getColumnIndexOrThrow(ReadingListItems.EXCERPT);
+ excerpt.setText(cursor.getString(excerptIndex));
+
+ final int lengthIndex = cursor.getColumnIndexOrThrow(ReadingListItems.LENGTH);
+ final int minutes = getEstimatedReadTime(cursor.getInt(lengthIndex));
+ if (minutes <= 60) {
+ readTime.setText(resources.getString(R.string.reading_list_time_minutes, minutes));
} else {
- setUrl(pageUrl);
- setSwitchToTabIcon(NO_ICON);
+ readTime.setText(resources.getString(R.string.reading_list_time_over_an_hour));
}
}
+ /**
+ * Calculates the estimated time to read an article based on its length.
+ *
+ * @param length of the article (in characters)
+ * @return estimated time to read the article (in minutes)
+ */
+ private static int getEstimatedReadTime(int length) {
+ final int minutes = (int) Math.ceil((length / AVERAGE_WORD_LENGTH) / AVERAGE_READING_SPEED);
+
+ // Minimum of one minute.
+ return Math.max(minutes, 1);
+ }
}
--- a/mobile/android/base/home/RemoteTabsPanel.java
+++ b/mobile/android/base/home/RemoteTabsPanel.java
@@ -1,17 +1,16 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import java.util.EnumMap;
-import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.AccountLoader;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
@@ -47,20 +46,24 @@ public class RemoteTabsPanel extends Hom
// The current fragment being shown to reflect the system account state. We
// don't want to detach and re-attach panels unnecessarily, because that
// causes flickering.
private Fragment mCurrentFragment;
// A lazily-populated cache of fragments corresponding to the possible
// system account states. We don't want to re-create panels unnecessarily,
- // because that can cause flickering. Be aware that null is a valid key; it
- // corresponds to "no Account, neither Firefox nor Legacy Sync."
+ // because that can cause flickering. `null` is not a valid key.
private final Map<Action, Fragment> mFragmentCache = new EnumMap<>(Action.class);
+ // The fragment that corresponds to the null action -- "no Account,
+ // neither Firefox nor Legacy Sync."
+ // Lazily populated.
+ private Fragment mFallbackFragment;
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.home_remote_tabs_panel, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
@@ -170,22 +173,30 @@ public class RemoteTabsPanel extends Hom
* A null Account means there is no Account (Sync or Firefox) on the device.
*
* @param account
* Android Account (Sync or Firefox); may be null.
*/
private Fragment getFragmentNeeded(Account account) {
final Action actionNeeded = getActionNeeded(account);
- // We use containsKey rather than get because null is a valid key.
- if (!mFragmentCache.containsKey(actionNeeded)) {
- final Fragment fragment = makeFragmentForAction(actionNeeded);
+ if (actionNeeded == null) {
+ if (mFallbackFragment == null) {
+ mFallbackFragment = makeFragmentForAction(null);
+ }
+ return mFallbackFragment;
+ }
+
+ Fragment fragment = mFragmentCache.get(actionNeeded);
+ if (fragment == null) {
+ fragment = makeFragmentForAction(actionNeeded);
mFragmentCache.put(actionNeeded, fragment);
}
- return mFragmentCache.get(actionNeeded);
+
+ return fragment;
}
/**
* Update the UI to reflect the given <code>Account</code> and its state.
* <p>
* A null Account means there is no Account (Sync or Firefox) on the device.
*
* @param account
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -359,16 +359,22 @@ size. -->
<!ENTITY site_settings_cancel "Cancel">
<!ENTITY site_settings_clear "Clear">
<!ENTITY site_settings_no_settings "There are no settings to clear.">
<!ENTITY reading_list_added "Page added to your Reading List">
<!ENTITY reading_list_failed "Failed to add page to your Reading List">
<!ENTITY reading_list_duplicate "Page already in your Reading List">
+<!-- Localization note (reading_list_time_minutes) : This string is used in the "Reading List"
+ panel on the home page to give the user an estimate of how many minutes it will take to
+ read an article. The word "minute" should be abbreviated if possible. -->
+<!ENTITY reading_list_time_minutes "&formatD;min">
+<!ENTITY reading_list_time_over_an_hour "Over an hour">
+
<!-- Localization note : These strings are used as alternate text for accessibility.
They are not visible in the UI. -->
<!ENTITY page_action_dropmarker_description "Additional Actions">
<!ENTITY masterpassword_create_title "Create Master Password">
<!ENTITY masterpassword_remove_title "Remove Master Password">
<!ENTITY masterpassword_password "Password">
<!ENTITY masterpassword_confirm "Confirm password">
--- a/mobile/android/base/resources/layout/home_reading_list_panel.xml
+++ b/mobile/android/base/resources/layout/home_reading_list_panel.xml
@@ -11,10 +11,10 @@
<ViewStub android:id="@+id/home_empty_view_stub"
android:layout="@layout/home_empty_reading_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.mozilla.gecko.home.HomeListView android:id="@+id/list"
style="@style/Widget.ReadingListView"
android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
+ android:layout_height="match_parent"/>
</LinearLayout>
--- a/mobile/android/base/resources/layout/reading_list_item_row.xml
+++ b/mobile/android/base/resources/layout/reading_list_item_row.xml
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<org.mozilla.gecko.home.ReadingListRow xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.BookmarkItemView"
android:layout_width="match_parent"
- android:layout_height="@dimen/page_row_height"
- android:minHeight="@dimen/page_row_height"/>
+ android:layout_height="wrap_content"
+ android:padding="10dp"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/reading_list_row_view.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ style="@style/Widget.ReadingListRow.Title" />
+
+ <TextView
+ android:id="@+id/read_time"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ style="@style/Widget.ReadingListRow.ReadTime" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/excerpt"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/Widget.ReadingListRow.Description" />
+
+</merge>
--- a/mobile/android/base/resources/values-v16/styles.xml
+++ b/mobile/android/base/resources/values-v16/styles.xml
@@ -12,16 +12,22 @@
<style name="TextAppearance.Widget.Home.ItemTitle" parent="TextAppearance.Medium">
<item name="android:fontFamily">sans-serif-light</item>
</style>
<style name="TextAppearance.Widget.Home.PageTitle" parent="TextAppearance.Medium">
<item name="android:fontFamily">sans-serif-light</item>
</style>
+ <style name="Widget.ReadingListRow.ReadTime">
+ <item name="android:textStyle">italic</item>
+ <item name="android:textColor">#FF9400</item>
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ </style>
+
<style name="OnboardStartTextAppearance.Subtext">
<item name="android:textSize">18sp</item>
<item name="android:fontFamily">sans-serif-light</item>
</style>
<style name="TextAppearance.UrlBar.Title" parent="TextAppearance.Small">
<item name="android:textSize">15sp</item>
<item name="android:fontFamily">sans-serif-light</item>
</style>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -122,16 +122,35 @@
<style name="Widget.TwoLinePageRow.Url">
<item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
<item name="android:includeFontPadding">false</item>
<item name="android:singleLine">true</item>
<item name="android:ellipsize">middle</item>
</style>
+ <style name="Widget.ReadingListRow" />
+
+ <style name="Widget.ReadingListRow.Title">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="Widget.ReadingListRow.Description">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
+ <item name="android:maxLines">4</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="Widget.ReadingListRow.ReadTime">
+ <item name="android:textStyle">italic</item>
+ <item name="android:textColor">@color/text_color_highlight</item>
+ </style>
+
<style name="Widget.BookmarkFolderView" parent="Widget.TwoLinePageRow.Title">
<item name="android:singleLine">true</item>
<item name="android:ellipsize">none</item>
<item name="android:paddingLeft">10dip</item>
<item name="android:drawablePadding">10dip</item>
<item name="android:drawableLeft">@drawable/bookmark_folder</item>
</style>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -293,16 +293,18 @@
<string name="site_settings_title">&site_settings_title3;</string>
<string name="site_settings_cancel">&site_settings_cancel;</string>
<string name="site_settings_clear">&site_settings_clear;</string>
<string name="site_settings_no_settings">&site_settings_no_settings;</string>
<string name="reading_list_added">&reading_list_added;</string>
<string name="reading_list_failed">&reading_list_failed;</string>
<string name="reading_list_duplicate">&reading_list_duplicate;</string>
+ <string name="reading_list_time_minutes">&reading_list_time_minutes;</string>
+ <string name="reading_list_time_over_an_hour">&reading_list_time_over_an_hour;</string>
<string name="page_action_dropmarker_description">&page_action_dropmarker_description;</string>
<string name="contextmenu_open_new_tab">&contextmenu_open_new_tab;</string>
<string name="contextmenu_open_private_tab">&contextmenu_open_private_tab;</string>
<string name="contextmenu_remove">&contextmenu_remove;</string>
<string name="contextmenu_add_to_launcher">&contextmenu_add_to_launcher;</string>
<string name="contextmenu_share">&contextmenu_share;</string>
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -532,17 +532,17 @@ var Addons = {
onUninstalled: function(aAddon) {
let list = document.getElementById("addons-list");
let element = this._getElementForAddon(aAddon.id);
list.removeChild(element);
// Go back if we're in the detail view of the add-on that was uninstalled.
let detailItem = document.querySelector("#addons-details > .addon-item");
- if (detailItem.addon == aAddon) {
+ if (detailItem.addon.id == aAddon.id) {
history.back();
}
},
onInstallFailed: function(aInstall) {
},
onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {