Skip to content
This repository has been archived by the owner on Jan 17, 2023. It is now read-only.

Commit

Permalink
Fix #5249, create export page
Browse files Browse the repository at this point in the history
This creates a page at /export that's just a simplified version of My Shots to use with Save As

Previous commits make it easier to include less other files to keep the number of extraneous files saved to a minimum.
  • Loading branch information
ianb committed Jan 15, 2019
1 parent bd566fd commit 6c84920
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 7 deletions.
6 changes: 6 additions & 0 deletions locales/en-US/server.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,12 @@ shotDeleteCancel = Cancel
shotDeleteConfirm = Delete
.title = Delete
## Export page

exportTitle = Export
# Note: "File" should match the name of the File Menu, and "Save Page As" should match that menu item. The file will be saved to "exportTitle_files" (it will be "_files" for all locales)
exportInstructions = To export: use File > Save Page As... and you will find your screenshots in Export_files/
## Metrics page
## All metrics strings are optional for translation

Expand Down
11 changes: 11 additions & 0 deletions server/src/pages/export/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const React = require("react");
const { Localized } = require("fluent-react/compat");
const { Footer } = require("../../footer-view.js");

exports.MyShotsFooter = class MyShotsFooter extends Footer {
updateLinks() {
this.links.push(<li key="removedata">
<Localized id="footerLinkRemoveAllData"><a href="/leave-screenshots">Remove All Data</a></Localized>
</li>);
}
};
31 changes: 31 additions & 0 deletions server/src/pages/export/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
exports.createModel = function(req) {
// FIXME: update title for export:
const title = req.getText("exportTitle");
const serverModel = {
title,
};
serverModel.shotsPerPage = req.shotsPerPage;
serverModel.pageNumber = req.pageNumber;
serverModel.totalShots = req.totalShots;
serverModel.shots = req.shots;
serverModel.isRtl = req.isRtl;
let shots = req.shots;
if (shots && shots.length) {
shots = shots.map(
shot => ({
id: shot.id,
json: shot.asRecallJson(),
expireTime: shot.expireTime,
isSynced: shot.isSynced,
}));
}
const jsonModel = Object.assign(
{},
serverModel,
{
shots,
downloadUrls: serverModel.downloadUrls,
}
);
return Promise.resolve({serverModel, jsonModel});
};
7 changes: 7 additions & 0 deletions server/src/pages/export/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { Page } = require("../../reactruntime");

exports.page = new Page({
dir: __dirname,
viewModule: require("./view.js"),
noBrowserJavascript: true,
});
36 changes: 36 additions & 0 deletions server/src/pages/export/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const express = require("express");
const reactrender = require("../../reactrender");
const { Shot } = require("../../servershot");
const mozlog = require("../../logging").mozlog("shotindex");

const SHOTS_PER_PAGE = 100;

const app = express();

exports.app = app;

app.get("/", function(req, res) {
if (!(req.deviceId || req.accountId)) {
res.redirect("/");
return;
}
const pageNumber = req.query.p || 1;
const getShotsPage = Shot.getShotsForDevice(req.backend, req.deviceId, req.accountId, null, pageNumber, SHOTS_PER_PAGE);
getShotsPage.then(_render)
.catch((err) => {
res.type("txt").status(500).send(req.getText("shotIndexPageErrorRendering", {error: err}));
mozlog.error("error-rendering", {msg: "Error rendering page", error: err, stack: err.stack});
});

function _render(shotsPage) {
if (shotsPage) {
["shots", "totalShots", "pageNumber", "shotsPerPage"].forEach(x => {
if (shotsPage[x] !== undefined) {
req[x] = shotsPage[x];
}
});
}
const page = require("./page").page;
reactrender.render(req, res, page);
}
});
262 changes: 262 additions & 0 deletions server/src/pages/export/view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/* globals controller */
const reactruntime = require("../../reactruntime");
const { MyShotsFooter } = require("./footer");
const React = require("react");
const PropTypes = require("prop-types");
const Masonry = require("react-masonry-component");
const { Localized } = require("fluent-react/compat");
const { isValidClipImageUrl } = require("../../../shared/shot");
const { getThumbnailDimensions } = require("../../../shared/thumbnailGenerator");
const { Header } = require("../../header.js");

class Head extends React.Component {

render() {
return (
<reactruntime.HeadTemplate noBrowserJavascript={true} {...this.props}>
<link rel="stylesheet" href={ this.props.staticLink("/static/css/shot-index.css") } />
</reactruntime.HeadTemplate>
);
}

}

Head.propTypes = {
staticLink: PropTypes.func,
};

function urlWithPage(page) {
return `/export?p=${encodeURIComponent(page)}`;
}

class Body extends React.Component {

render() {
return (
<reactruntime.BodyTemplate {...this.props}>
<div className="column-space full-height" id="shot-index-page">
<Header hasLogo={true} isOwner={true} />
<div id="shot-index" className="flex-1">
<Localized id="exportInstructions">
<p id="exportInstructions">
To export: use File &gt; Save Page As... and you will find your screenshots in Export_files/
</p>
</Localized>
{ this.renderShots() }
</div>
{ this.renderPageNavigation() }
{ this.renderErrorMessages() }
<MyShotsFooter {...this.props} />
</div>
</reactruntime.BodyTemplate>
);
}

renderShots() {
const children = [];
if (this.props.shots && this.props.shots.length) {
for (const shot of this.props.shots) {
children.push(<Card shot={shot} clipUrl={shot.urlDisplay} staticLink={this.props.staticLink} key={shot.id} />);
}
}

if (children.length === 0) {
children.push(this.renderNoShots());
} else {
const masonryOptions = {
originLeft: !this.props.isRtl,
};
return (
<div className="masonry-wrapper">
<Masonry options={masonryOptions}>
{children}
</Masonry>
</div>
);
}
return children;
}

renderPageNavigation() {
if (!this.props.totalShots || parseInt(this.props.totalShots, 10) === 0) {
return null;
}

const totalPages = Math.ceil(this.props.totalShots / this.props.shotsPerPage) || 1;
const hasPrev = this.props.pageNumber > 1;
const prevPageNumber = this.props.pageNumber - 1;
const prevClasses = ["shots-page-nav"].concat(!hasPrev && "disabled").join(" ");
const hasNext = this.props.pageNumber < totalPages;
const nextPageNumber = this.props.pageNumber - 0 + 1;
const nextClasses = ["shots-page-nav"].concat(!hasNext && "disabled").join(" ");
const hidden = totalPages < 2;
let arrowheadPrev = "←";
let arrowheadNext = "→";
if (this.props.isRtl) {
[arrowheadNext, arrowheadPrev] = [arrowheadPrev, arrowheadNext];
}

return (
<div id="shot-index-page-navigation" hidden={hidden} >
<span className={prevClasses}>
<Localized id="shotIndexPagePreviousPage" attrs={{title: true}}>
{
hasPrev || true
? <a href={ urlWithPage(prevPageNumber)}
title="previous page"
>{ arrowheadPrev }</a>
: arrowheadPrev
}
</Localized>
</span>
<bdo id="shots-page-number" dir="ltr">{this.props.pageNumber} / {totalPages}</bdo>
<span className={nextClasses}>
<Localized id="shotIndexPageNextPage" attrs={{title: true}}>
{
hasNext
? <a href={ urlWithPage(nextPageNumber) }
title="next page"
>{ arrowheadNext }</a>
: arrowheadNext
}
</Localized>
</span>
</div>
);
}

renderErrorMessages() {
return (
<div>
<Localized id="shotIndexAlertErrorFavoriteShot">
<div id="shotIndexAlertErrorFavoriteShot" hidden></div>
</Localized>
<Localized id="shotIndexPageErrorDeletingShot">
<div id="shotIndexPageErrorDeletingShot" hidden></div>
</Localized>
<Localized id="shotIndexPageConfirmShotDelete">
<div id="shotIndexPageConfirmShotDelete" hidden></div>
</Localized>
</div>
);
}

renderNoShots() {
return (
<div className="no-shots" key="no-shots-found">
<Localized id="gNoShots" attrs={{alt: true}}>
<img src={ this.props.staticLink("/static/img/image-noshots_screenshots.svg") } alt="no Shots found" width="432" height="432"/>
</Localized>
<Localized id="shotIndexPageNoShotsMessage">
<p>No saved shots.</p>
</Localized>
<Localized id="shotIndexPageNoShotsInvitation">
<p>Go on, create some.</p>
</Localized>
</div>
);
}

}

Body.propTypes = {
abTests: PropTypes.object,
pageNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
shots: PropTypes.array,
shotsPerPage: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
staticLink: PropTypes.func,
totalShots: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
isRtl: PropTypes.bool,
};

class Card extends React.Component {

render() {
const defaultImageUrl = this.props.staticLink("img/question-mark.svg");
const shot = this.props.shot;
const clip = shot.clipNames().length ? shot.getClip(shot.clipNames()[0]) : null;
if (!clip || !clip.image || !clip.image.url) {
// Some corrupted shot, we'll have to ignore it
return null;
}
let imageUrl = clip.image.url;

// fallback to the question mark if the imageUrl is invalid
if (!isValidClipImageUrl(imageUrl)) {
imageUrl = defaultImageUrl;
}
const filename = shot.filename.replace(/\s+/g, "_");
const fullImgUrl = `${imageUrl}/${encodeURIComponent(filename)}`;

return (
<div className={`shot ${this.getClipType(clip._image.dimensions)}`}
key={shot.id}>
<a href={fullImgUrl}>
<div className="shot-image-container">
<img src={fullImgUrl} />
</div>
<div className="shot-info">
<div className="title-container">
<h4>{this.displayTitle(shot.title)}</h4>
</div>
<div className="link-container">
<div className="shot-url">
{shot.urlDisplay}
</div>
</div>
</div>
</a>
</div>
);
}

getClipType(dimensions) {
// "portrait": 210 x 280, image scaled on X
// "landscape": 210 x 140, image scaled on Y
// "square": 210 x 210, image scaled on X or Y

const containerWidth = 210;
const landscapeHeight = 140;
const portraitHeight = 280;
const landscapeAspectRatio = containerWidth / landscapeHeight;
const portraitAspectRatio = containerWidth / portraitHeight;

const thumbnailDimensions = getThumbnailDimensions(dimensions.x, dimensions.y);
const thumbnailWidth = thumbnailDimensions.width;
const thumbnailHeight = thumbnailDimensions.height;

const thumbnailAspectRatio = thumbnailWidth / thumbnailHeight;

if (thumbnailAspectRatio <= portraitAspectRatio) {
return "portrait";
}
if (thumbnailAspectRatio >= landscapeAspectRatio) {
return "landscape";
}
if (thumbnailHeight > thumbnailWidth) {
return "square-x";
}
return "square-y";
}

displayTitle(title) {
// FIXME: this won't work for rtl languages. use CSS ellipsis instead? (#3116)
if (title.length > 140) {
return (title.substring(0, 140) + "...");
}
return title;
}

}

Card.propTypes = {
abTests: PropTypes.object,
hasFxa: PropTypes.bool,
isExtInstalled: PropTypes.bool,
isOwner: PropTypes.bool,
shot: PropTypes.object,
staticLink: PropTypes.func,
};

exports.HeadFactory = React.createFactory(Head);
exports.BodyFactory = React.createFactory(Body);
9 changes: 7 additions & 2 deletions server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ app.post("/api/set-expiration", function(req, res) {
});
});

app.get("/images/:imageid", function(req, res) {
function serveImage(req, res) {
const embedded = req.query.embedded;
const download = req.query.download;
const sig = req.query.sig;
Expand Down Expand Up @@ -997,7 +997,10 @@ app.get("/images/:imageid", function(req, res) {
}).catch((e) => {
errorResponse(res, "Error getting image from database:", e);
});
});
}

app.get("/images/:imageid", serveImage);
app.get("/images/:imageid/:filename", serveImage);

app.get("/__version__", function(req, res) {
dbschema.getCurrentDbPatchLevel().then(level => {
Expand Down Expand Up @@ -1270,6 +1273,8 @@ if (!config.disableMetrics) {

app.use("/shots", require("./pages/shotindex/server").app);

app.use("/export", require("./pages/export/server").app);

app.use("/leave-screenshots", require("./pages/leave-screenshots/server").app);

app.use("/creating", require("./pages/creating/server").app);
Expand Down
Loading

0 comments on commit 6c84920

Please sign in to comment.