refactor lbrytv web server
12
.babelrc
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["@babel/react", "@babel/flow"],
|
|
||||||
"plugins": [
|
|
||||||
"import-glob",
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
"@babel/plugin-syntax-dynamic-import",
|
|
||||||
"react-hot-loader/babel",
|
|
||||||
["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }],
|
|
||||||
"@babel/plugin-transform-flow-strip-types",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
]
|
|
||||||
}
|
|
26
.flowconfig
|
@ -12,19 +12,19 @@ node_modules/lbryinc/flow-typed/
|
||||||
[options]
|
[options]
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
||||||
module.name_mapper='^constants\(.*\)$' -> '<PROJECT_ROOT>/src/ui/constants\1'
|
module.name_mapper='^constants\(.*\)$' -> '<PROJECT_ROOT>/ui/constants\1'
|
||||||
module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/src/ui/util\1'
|
module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/ui/util\1'
|
||||||
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/ui/redux\1'
|
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/ui/redux\1'
|
||||||
module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/ui/types\1'
|
module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/ui/types\1'
|
||||||
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/ui/component\1'
|
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/ui/component\1'
|
||||||
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/ui/page\1'
|
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/ui/page\1'
|
||||||
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/ui/lbry\1'
|
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/ui/lbry\1'
|
||||||
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/ui/modal\1'
|
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/ui/modal\1'
|
||||||
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/src/ui/app\1'
|
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/ui/app\1'
|
||||||
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/src/ui/native\1'
|
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/ui/native\1'
|
||||||
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/src/ui/analytics\1'
|
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/ui/analytics\1'
|
||||||
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/src/ui/i18n\1'
|
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1'
|
||||||
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/src/ui/effects\1'
|
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1'
|
||||||
module.name_mapper='^config\(.*\)$' -> '<PROJECT_ROOT>/config\1'
|
module.name_mapper='^config\(.*\)$' -> '<PROJECT_ROOT>/config\1'
|
||||||
|
|
||||||
|
|
||||||
|
|
2
.gitignore
vendored
|
@ -13,3 +13,5 @@ package-lock.json
|
||||||
.transifexrc
|
.transifexrc
|
||||||
.idea/
|
.idea/
|
||||||
/build/daemon*
|
/build/daemon*
|
||||||
|
/lbrytv/dist/
|
||||||
|
/lbrytv/node_modules
|
||||||
|
|
17
babel.config.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = api => {
|
||||||
|
api.cache(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
presets: ['@babel/env', '@babel/react', '@babel/flow'],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
'import-glob',
|
||||||
|
'@babel/plugin-transform-runtime',
|
||||||
|
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
|
||||||
|
'@babel/plugin-transform-flow-strip-types',
|
||||||
|
'@babel/plugin-proposal-class-properties',
|
||||||
|
'react-hot-loader/babel',
|
||||||
|
],
|
||||||
|
ignore: [/node_modules/],
|
||||||
|
};
|
||||||
|
};
|
15
lbrytv/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const config = require('../config');
|
||||||
|
const path = require('path');
|
||||||
|
const Koa = require('koa');
|
||||||
|
const serve = require('koa-static');
|
||||||
|
const logger = require('koa-logger');
|
||||||
|
const router = require('./src/routes');
|
||||||
|
|
||||||
|
const app = new Koa();
|
||||||
|
const DIST_ROOT = path.resolve(__dirname, 'dist');
|
||||||
|
|
||||||
|
app.use(logger());
|
||||||
|
app.use(serve(DIST_ROOT)); // Check if the request url matches any assets inside of /dist
|
||||||
|
|
||||||
|
app.use(router.routes());
|
||||||
|
app.listen(config.WEB_SERVER_PORT, () => `Server up at localhost:${config.WEB_SERVER_PORT}`);
|
29
lbrytv/middleware/redirect.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const config = require('../../config');
|
||||||
|
const redirectHosts = ['open.lbry.com'];
|
||||||
|
|
||||||
|
async function redirectMiddleware(ctx, next) {
|
||||||
|
const requestHost = ctx.host;
|
||||||
|
const path = ctx.path;
|
||||||
|
const url = ctx.url;
|
||||||
|
|
||||||
|
if (urlPath.endsWith('/') && urlPath.length > 1) {
|
||||||
|
ctx.redirect(url.replace(/\/$/, ''));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.startsWith('/$/') && path.match(/^([^@/:]+)\/([^:/]+)$/)) {
|
||||||
|
ctx.redirect(url.replace(/^([^@/:]+)\/([^:/]+)(:(\/.*))/, '$1:$2')); // test against path, but use ctx.url to retain parameters
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectHosts.includes(requestHost)) {
|
||||||
|
const redirectUrl = config.DOMAIN + path;
|
||||||
|
ctx.redirect(redirectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No redirects needed
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = redirectMiddleware;
|
56
lbrytv/package.json
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "lbry.tv",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "A web based browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||||
|
"keywords": [
|
||||||
|
"lbry"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://lbry.com/",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/lbryio/lbry-desktop/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lbryio/lbry-desktop"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "LBRY Inc.",
|
||||||
|
"email": "hello@lbry.com"
|
||||||
|
},
|
||||||
|
"main": "./index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --progess --config webpack.config.js",
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot --progress --config webpack.config.js",
|
||||||
|
"dev:server": "nodemon index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@koa/router": "^8.0.2",
|
||||||
|
"cross-env": "^6.0.3",
|
||||||
|
"koa": "^2.11.0",
|
||||||
|
"koa-logger": "^3.2.1",
|
||||||
|
"koa-send": "^5.0.0",
|
||||||
|
"koa-static": "^5.0.0",
|
||||||
|
"lbry-redux": "lbryio/lbry-redux#ba429043e6fa8144a62bcf4d45ed8d183ea5aff9",
|
||||||
|
"mysql": "^2.17.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.3.0",
|
||||||
|
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.4.3",
|
||||||
|
"@babel/polyfill": "^7.2.5",
|
||||||
|
"@babel/preset-env": "^7.7.1",
|
||||||
|
"@babel/preset-flow": "^7.0.0",
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"@babel/register": "^7.0.0",
|
||||||
|
"cache-loader": "^4.1.0",
|
||||||
|
"nodemon": "^1.19.4",
|
||||||
|
"speed-measure-webpack-plugin": "^1.3.1",
|
||||||
|
"webpack": "^4.41.2",
|
||||||
|
"webpack-bundle-analyzer": "^3.6.0",
|
||||||
|
"webpack-dev-server": "^3.9.0",
|
||||||
|
"webpack-merge": "^4.2.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import { Lbry } from 'lbry-redux';
|
import { Lbry } from 'lbry-redux';
|
||||||
import apiPublishCallViaWeb from './publish';
|
import apiPublishCallViaWeb from './publish';
|
||||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
||||||
|
|
||||||
const PROXY_PATH = 'api/v1/proxy';
|
const PROXY_PATH = 'api/v1/proxy';
|
||||||
export const SDK_API_URL = `${process.env.SDK_API_URL}/${PROXY_PATH}` || `https://api.lbry.tv/${PROXY_PATH}`;
|
export const SDK_API_URL = `${process.env.SDK_API_URL}/${PROXY_PATH}` || `https://api.lbry.tv/${PROXY_PATH}`;
|
||||||
|
|
||||||
Lbry.setDaemonConnectionString(SDK_API_URL);
|
Lbry.setDaemonConnectionString(SDK_API_URL);
|
||||||
|
|
||||||
Lbry.setOverride(
|
Lbry.setOverride(
|
|
@ -5,7 +5,7 @@
|
||||||
- 'file' binary
|
- 'file' binary
|
||||||
- 'json_payload' collection of publish params to be passed to the server's sdk.
|
- 'json_payload' collection of publish params to be passed to the server's sdk.
|
||||||
*/
|
*/
|
||||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
||||||
import { doUpdateUploadProgress } from 'lbryinc';
|
import { doUpdateUploadProgress } from 'lbryinc';
|
||||||
import { apiCall } from 'lbry-redux';
|
import { apiCall } from 'lbry-redux';
|
||||||
|
|
53
lbrytv/src/chainquery.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
const mysql = require('mysql');
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
connectionLimit: 100,
|
||||||
|
host: 'chainquery.lbry.com',
|
||||||
|
user: 'lbrytv',
|
||||||
|
password: process.env.CHAINQUERY_MYSQL_PASSWORD,
|
||||||
|
database: 'chainquery',
|
||||||
|
});
|
||||||
|
|
||||||
|
function queryPool(sql, params) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
pool.query(sql, params, (error, rows) => {
|
||||||
|
if (error) {
|
||||||
|
throw Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getClaim = async function getClaim(claimName, claimId, channelName, channelClaimId) {
|
||||||
|
let params = [claimName];
|
||||||
|
|
||||||
|
let sql =
|
||||||
|
'SELECT channel_claim.name as channel, claim.claim_id, claim.name, claim.description, claim.language, claim.thumbnail_url, claim.title, claim.source_media_type, claim.frame_width, claim.frame_height ' +
|
||||||
|
'FROM claim ' +
|
||||||
|
'LEFT JOIN claim channel_claim on claim.publisher_id = channel_claim.claim_id ' +
|
||||||
|
'WHERE claim.name = ?';
|
||||||
|
|
||||||
|
if (claimId) {
|
||||||
|
sql += ' AND claim.claim_id LIKE ?';
|
||||||
|
params.push(claimId + '%');
|
||||||
|
} else {
|
||||||
|
sql += ' AND claim.bid_state = "controlling"';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimName[0] !== '@' && channelName) {
|
||||||
|
sql += ' AND channel_claim.name = ?';
|
||||||
|
params.push('@' + channelName);
|
||||||
|
if (channelClaimId) {
|
||||||
|
sql += ' AND channel_claim.claim_id LIKE ?';
|
||||||
|
params.push(channelClaimId + '%');
|
||||||
|
} else {
|
||||||
|
sql += ' AND channel_claim.bid_state = "controlling"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' LIMIT 1';
|
||||||
|
|
||||||
|
return queryPool(sql, params);
|
||||||
|
};
|
104
lbrytv/src/html.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
const { DOMAIN } = require('../../config.js');
|
||||||
|
const { generateStreamUrl } = require('../../ui/util/lbrytv');
|
||||||
|
const { getClaim } = require('./chainquery');
|
||||||
|
const { parseURI } = require('lbry-redux');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let html = fs.readFileSync(path.join(__dirname, '/../dist/index.html'), 'utf8');
|
||||||
|
|
||||||
|
const defaultHead =
|
||||||
|
'<title>lbry.tv</title>\n' +
|
||||||
|
`<meta property="og:url" content="${DOMAIN}" />\n` +
|
||||||
|
'<meta property="og:title" content="lbry.tv" />\n' +
|
||||||
|
'<meta property="og:site_name" content="lbry.tv | Content Freedom"/>\n' +
|
||||||
|
'<meta property="og:description" content="Meet LBRY, an open, free, and community-controlled content wonderland." />\n' +
|
||||||
|
`<meta property="og:image" content="${DOMAIN}/og.png" />\n` +
|
||||||
|
'<meta name="twitter:card" content="summary_large_image"/>\n' +
|
||||||
|
`<meta name="twitter:image" content="${DOMAIN}/og.png"/>\n` +
|
||||||
|
'<meta property="fb:app_id" content="1673146449633983" />';
|
||||||
|
|
||||||
|
function insertToHead(fullHtml, htmlToInsert = defaultHead) {
|
||||||
|
return fullHtml.replace(/<!-- VARIABLE_HEAD_BEGIN -->.*<!-- VARIABLE_HEAD_END -->/s, htmlToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateDescription(description) {
|
||||||
|
return description.length > 200 ? description.substr(0, 200) + '...' : description;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtmlProperty(property) {
|
||||||
|
return property
|
||||||
|
? String(property)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOgMetadata(uri, claim) {
|
||||||
|
const { isChannel } = parseURI(uri);
|
||||||
|
const title = escapeHtmlProperty(claim.title ? claim.title : claimName);
|
||||||
|
const claimDescription =
|
||||||
|
claim.description && claim.description.length > 0
|
||||||
|
? escapeHtmlProperty(truncateDescription(claim.description))
|
||||||
|
: `Watch ${title} on LBRY.tv`;
|
||||||
|
const claimLanguage = escapeHtmlProperty(claim.language) || 'en_US';
|
||||||
|
const claimThumbnail = escapeHtmlProperty(claim.thumbnail_url) || `${DOMAIN}/og.png`;
|
||||||
|
const claimTitle = claim.channel && !isChannel ? `${title} from ${claim.channel} on LBRY.tv` : `${title} on LBRY.tv`;
|
||||||
|
|
||||||
|
let head = '';
|
||||||
|
|
||||||
|
head += '<meta charset="utf8"/>';
|
||||||
|
head += `<title>${claimTitle}</title>`;
|
||||||
|
head += `<meta name="description" content="${claimDescription}"/>`;
|
||||||
|
if (claim.tags) {
|
||||||
|
head += `<meta name="keywords" content="${claim.tags.toString()}"/>`;
|
||||||
|
}
|
||||||
|
head += `<meta name="twitter:card" content="summary_large_image"/>`;
|
||||||
|
head += `<meta name="twitter:image" content="${claimThumbnail}"/>`;
|
||||||
|
head += `<meta property="og:description" content="${claimDescription}"/>`;
|
||||||
|
head += `<meta property="og:image" content="${claimThumbnail}"/>`;
|
||||||
|
head += `<meta property="og:locale" content="${claimLanguage}"/>`;
|
||||||
|
head += `<meta property="og:site_name" content="LBRY.tv"/>`;
|
||||||
|
head += `<meta property="og:type" content="website"/>`;
|
||||||
|
head += `<meta property="og:title" content="${claimTitle}"/>`;
|
||||||
|
// below should be canonical_url, but not provided by chainquery yet
|
||||||
|
head += `<meta property="og:url" content="${DOMAIN}/${claim.name}:${claim.claim_id}"/>`;
|
||||||
|
|
||||||
|
if (claim.source_media_type && claim.source_media_type.startsWith('video/')) {
|
||||||
|
const videoUrl = generateStreamUrl(claim.name, claim.claim_id);
|
||||||
|
head += `<meta property="og:video" content="${videoUrl}" />`;
|
||||||
|
head += `<meta property="og:video:secure_url" content="${videoUrl}" />`;
|
||||||
|
head += `<meta property="og:video:type" content="${claim.source_media_type}" />`;
|
||||||
|
if (claim.frame_width && claim.frame_height) {
|
||||||
|
head += `<meta property="og:video:width" content="${claim.frame_width}"/>`;
|
||||||
|
head += `<meta property="og:video:height" content="${claim.frame_height}"/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getHtml = async function getHtml(ctx) {
|
||||||
|
const path = ctx.path;
|
||||||
|
|
||||||
|
if (path.length === 0 || path[1] === '$') {
|
||||||
|
return insertToHead(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimUri = path.slice(1).replace(/:/g, '#');
|
||||||
|
const { isChannel, streamName, channelName, channelClaimId, streamClaimId } = parseURI(claimUri);
|
||||||
|
const claimName = isChannel ? '@' + channelName : streamName;
|
||||||
|
const claimId = isChannel ? channelClaimId : streamClaimId;
|
||||||
|
|
||||||
|
const rows = await getClaim(claimName, claimId, channelName, channelClaimId);
|
||||||
|
if (!rows || !rows.length) {
|
||||||
|
return insertToHead(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
const claim = rows[0];
|
||||||
|
const ogMetadata = buildOgMetadata(claimUri, claim);
|
||||||
|
return insertToHead(html, ogMetadata);
|
||||||
|
};
|
18
lbrytv/src/routes.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const { getHtml } = require('./html');
|
||||||
|
const { generateStreamUrl } = require('../../ui/util/lbrytv');
|
||||||
|
const Router = require('@koa/router');
|
||||||
|
const send = require('koa-send');
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.get(`/embed/:claimName/:claimId`, async ctx => {
|
||||||
|
const { claimName, claimId } = ctx.params;
|
||||||
|
const streamUrl = generateStreamUrl(claimName, claimName);
|
||||||
|
ctx.redirect = streamUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('*', async ctx => {
|
||||||
|
const html = await getHtml(ctx);
|
||||||
|
ctx.body = html;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
17
lbrytv/stubs/fs.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
function logWarning(method) {
|
||||||
|
if (NODE_ENV !== 'production') {
|
||||||
|
console.error(`Called fs.${method} on lbry.tv. This should be removed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
readFileSync: () => {
|
||||||
|
logWarning('readFileSync');
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
accessFileSync: () => {
|
||||||
|
logWarning('accessFileSync');
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
constants: {},
|
||||||
|
};
|
|
@ -1,22 +1,22 @@
|
||||||
const { WEBPACK_WEB_PORT, LBRY_TV_API } = require('./config.js');
|
const { WEBPACK_WEB_PORT, LBRY_TV_API } = require('../config.js');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const baseConfig = require('./webpack.base.config.js');
|
const baseConfig = require('../webpack.base.config.js');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
const { DefinePlugin, ProvidePlugin } = require('webpack');
|
const { DefinePlugin, ProvidePlugin } = require('webpack');
|
||||||
|
|
||||||
const STATIC_ROOT = path.resolve(__dirname, 'static/');
|
const STATIC_ROOT = path.resolve(__dirname, '../static/');
|
||||||
const DIST_ROOT = path.resolve(__dirname, 'dist/');
|
const DIST_ROOT = path.resolve(__dirname, 'dist/');
|
||||||
const WEB_PLATFORM_ROOT = path.resolve(__dirname, 'src/platforms/web/');
|
const WEB_PLATFORM_ROOT = __dirname;
|
||||||
|
|
||||||
const webConfig = {
|
const webConfig = {
|
||||||
target: 'web',
|
target: 'web',
|
||||||
entry: {
|
entry: {
|
||||||
ui: './src/ui/index.jsx',
|
ui: '../ui/index.jsx',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
path: __dirname + '/dist/web',
|
path: __dirname + '/dist/',
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
},
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
|
@ -25,10 +25,17 @@ const webConfig = {
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(jsx?$|s?css$)/,
|
loader: 'babel-loader',
|
||||||
use: [
|
test: /\.jsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
options: {
|
||||||
|
rootMode: 'upward',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
loader: 'preprocess-loader',
|
loader: 'preprocess-loader',
|
||||||
|
test: /\.jsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
options: {
|
options: {
|
||||||
TARGET: 'web',
|
TARGET: 'web',
|
||||||
ppOptions: {
|
ppOptions: {
|
||||||
|
@ -38,33 +45,25 @@ const webConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
resolve: {
|
||||||
modules: [path.resolve(__dirname, 'src/platforms/')],
|
|
||||||
|
|
||||||
alias: {
|
alias: {
|
||||||
electron: path.resolve(__dirname, 'src/platforms/web/stubs'),
|
electron: `${WEB_PLATFORM_ROOT}/stubs/electron.js`,
|
||||||
fs: path.resolve(__dirname, 'src/platforms/web/fs'),
|
fs: `${WEB_PLATFORM_ROOT}/stubs/fs.js`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{
|
{
|
||||||
from: `${STATIC_ROOT}/index-web.html`,
|
from: `${STATIC_ROOT}/index-web.html`,
|
||||||
to: `${DIST_ROOT}/web/index.html`,
|
to: `${DIST_ROOT}/index.html`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: `${STATIC_ROOT}/img/favicon.ico`,
|
from: `${STATIC_ROOT}/img/favicon.ico`,
|
||||||
to: `${DIST_ROOT}/web/favicon.ico`,
|
to: `${DIST_ROOT}/favicon.ico`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: `${STATIC_ROOT}/img/og.png`,
|
from: `${STATIC_ROOT}/img/og.png`,
|
||||||
to: `${DIST_ROOT}/web/og.png`,
|
to: `${DIST_ROOT}/og.png`,
|
||||||
},
|
|
||||||
{
|
|
||||||
from: `${WEB_PLATFORM_ROOT}/server.js`,
|
|
||||||
to: `${DIST_ROOT}/web/server.js`,
|
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
5689
lbrytv/yarn.lock
Normal file
26
package.json
|
@ -21,12 +21,12 @@
|
||||||
"main": "./dist/electron/main.js",
|
"main": "./dist/electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile:electron": "webpack --progress --config webpack.electron.config.js",
|
"compile:electron": "webpack --progress --config webpack.electron.config.js",
|
||||||
"compile:web": "webpack --config webpack.web.config.js",
|
"compile:web": "cd ./lbrytv && webpack --config webpack.config.js",
|
||||||
"compile": "cross-env NODE_ENV=production yarn compile:electron && cross-env NODE_ENV=production yarn compile:web",
|
"compile": "cross-env NODE_ENV=production yarn compile:electron && cross-env NODE_ENV=production yarn compile:web",
|
||||||
"dev": "yarn dev:electron",
|
"dev": "yarn dev:electron",
|
||||||
"dev:electron": "cross-env NODE_ENV=development node ./src/platforms/electron/devServer.js",
|
"dev:electron": "cross-env NODE_ENV=development node ./src/platforms/electron/devServer.js",
|
||||||
"dev:web": "cross-env NODE_ENV=development webpack-dev-server --open --hot --progress --config webpack.web.config.js",
|
"dev:web": "cd ./lbrytv && yarn dev",
|
||||||
"dev:web-server": "cross-env NODE_ENV=development yarn compile:web && concurrently \"cross-env NODE_ENV=development yarn compile:web --watch\" \"nodemon dist/web/server.js\"",
|
"dev:web-server": "cross-env NODE_ENV=development yarn compile:web && concurrently \"cross-env NODE_ENV=development yarn compile:web --watch\" \"cd ./lbrytv && yarn dev:server\"",
|
||||||
"dev:internal-apis": "LBRY_API_URL='http://localhost:8080' yarn dev:electron",
|
"dev:internal-apis": "LBRY_API_URL='http://localhost:8080' yarn dev:electron",
|
||||||
"run:web": "cross-env NODE_ENV=production yarn compile:web && node ./dist/web/server.js",
|
"run:web": "cross-env NODE_ENV=production yarn compile:web && node ./dist/web/server.js",
|
||||||
"pack": "electron-builder --dir",
|
"pack": "electron-builder --dir",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
"flow-defs": "flow-typed install",
|
"flow-defs": "flow-typed install",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"preinstall": "yarn cache clean lbry-redux && yarn cache clean lbryinc",
|
"preinstall": "yarn cache clean lbry-redux && yarn cache clean lbryinc",
|
||||||
"postinstall": "if-env NODE_ENV=production && yarn postinstall:warning || if-env APP_ENV=web && echo 'Done installing deps' || yarn postinstall:electron",
|
"postinstall": "cd ./lbrytv && yarn && cd .. && if-env NODE_ENV=production && yarn postinstall:warning || if-env APP_ENV=web && echo 'Done installing deps' || yarn postinstall:electron",
|
||||||
"postinstall:electron": "electron-builder install-app-deps && node ./build/downloadDaemon.js",
|
"postinstall:electron": "electron-builder install-app-deps && node ./build/downloadDaemon.js",
|
||||||
"postinstall:warning": "echo '\n\nWARNING\n\nNot all node modules were installed because NODE_ENV is set to \"production\".\nThis should only be set after installing dependencies with \"yarn\". The app will not work.\n\n'"
|
"postinstall:warning": "echo '\n\nWARNING\n\nNot all node modules were installed because NODE_ENV is set to \"production\".\nThis should only be set after installing dependencies with \"yarn\". The app will not work.\n\n'"
|
||||||
},
|
},
|
||||||
|
@ -47,12 +47,8 @@
|
||||||
"electron-log": "^2.2.12",
|
"electron-log": "^2.2.12",
|
||||||
"electron-notarize": "^0.1.1",
|
"electron-notarize": "^0.1.1",
|
||||||
"electron-updater": "^4.1.2",
|
"electron-updater": "^4.1.2",
|
||||||
"express": "^4.16.4",
|
|
||||||
"if-env": "^1.0.4",
|
"if-env": "^1.0.4",
|
||||||
"imagesloaded": "^4.1.4",
|
"keytar": "^4.4.1"
|
||||||
"keytar": "^4.4.1",
|
|
||||||
"mysql": "^2.17.1",
|
|
||||||
"nodemon": "^1.19.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
|
@ -62,6 +58,7 @@
|
||||||
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.3",
|
"@babel/plugin-transform-runtime": "^7.4.3",
|
||||||
"@babel/polyfill": "^7.2.5",
|
"@babel/polyfill": "^7.2.5",
|
||||||
|
"@babel/preset-env": "^7.7.1",
|
||||||
"@babel/preset-flow": "^7.0.0",
|
"@babel/preset-flow": "^7.0.0",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@babel/register": "^7.0.0",
|
"@babel/register": "^7.0.0",
|
||||||
|
@ -80,8 +77,6 @@
|
||||||
"babel-plugin-import-glob": "^2.0.0",
|
"babel-plugin-import-glob": "^2.0.0",
|
||||||
"babel-plugin-transform-imports": "^1.5.1",
|
"babel-plugin-transform-imports": "^1.5.1",
|
||||||
"bluebird": "^3.5.1",
|
"bluebird": "^3.5.1",
|
||||||
"butterchurn": "^2.6.7",
|
|
||||||
"butterchurn-presets": "^2.4.7",
|
|
||||||
"chalk": "^2.4.2",
|
"chalk": "^2.4.2",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"codemirror": "^5.39.2",
|
"codemirror": "^5.39.2",
|
||||||
|
@ -102,8 +97,6 @@
|
||||||
"electron-builder": "^21.2.0",
|
"electron-builder": "^21.2.0",
|
||||||
"electron-devtools-installer": "^2.2.4",
|
"electron-devtools-installer": "^2.2.4",
|
||||||
"electron-is-dev": "^0.3.0",
|
"electron-is-dev": "^0.3.0",
|
||||||
"electron-publisher-s3": "^20.8.1",
|
|
||||||
"electron-webpack": "^2.6.2",
|
|
||||||
"electron-window-state": "^4.1.1",
|
"electron-window-state": "^4.1.1",
|
||||||
"eslint": "^5.15.2",
|
"eslint": "^5.15.2",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"eslint-config-prettier": "^2.9.0",
|
||||||
|
@ -118,13 +111,14 @@
|
||||||
"eslint-plugin-promise": "^4.0.1",
|
"eslint-plugin-promise": "^4.0.1",
|
||||||
"eslint-plugin-react": "^7.7.0",
|
"eslint-plugin-react": "^7.7.0",
|
||||||
"eslint-plugin-react-hooks": "^1.6.0",
|
"eslint-plugin-react-hooks": "^1.6.0",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"file-loader": "^4.2.0",
|
||||||
"flow-bin": "^0.97.0",
|
"flow-bin": "^0.97.0",
|
||||||
"flow-typed": "^2.3.0",
|
"flow-typed": "^2.3.0",
|
||||||
"formik": "^0.10.4",
|
"formik": "^0.10.4",
|
||||||
"hast-util-sanitize": "^1.1.2",
|
"hast-util-sanitize": "^1.1.2",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
|
"imagesloaded": "^4.1.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#4491b975cc3e23bf3733272b7c6079a28c1036b3",
|
"lbry-redux": "lbryio/lbry-redux#4491b975cc3e23bf3733272b7c6079a28c1036b3",
|
||||||
|
@ -132,7 +126,6 @@
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.14",
|
||||||
"make-runnable": "^1.3.6",
|
|
||||||
"mammoth": "^1.4.6",
|
"mammoth": "^1.4.6",
|
||||||
"moment": "^2.22.0",
|
"moment": "^2.22.0",
|
||||||
"node-abi": "^2.5.1",
|
"node-abi": "^2.5.1",
|
||||||
|
@ -189,9 +182,10 @@
|
||||||
"wavesurfer.js": "^2.2.1",
|
"wavesurfer.js": "^2.2.1",
|
||||||
"webpack": "^4.28.4",
|
"webpack": "^4.28.4",
|
||||||
"webpack-bundle-analyzer": "^3.1.0",
|
"webpack-bundle-analyzer": "^3.1.0",
|
||||||
|
"webpack-cli": "^3.3.10",
|
||||||
"webpack-config-utils": "^2.3.1",
|
"webpack-config-utils": "^2.3.1",
|
||||||
"webpack-dev-middleware": "^3.6.0",
|
"webpack-dev-middleware": "^3.6.0",
|
||||||
"webpack-dev-server": "^3.1.14",
|
"webpack-dev-server": "^3.9.0",
|
||||||
"webpack-hot-middleware": "^2.24.3",
|
"webpack-hot-middleware": "^2.24.3",
|
||||||
"webpack-merge": "^4.2.1",
|
"webpack-merge": "^4.2.1",
|
||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
["@babel/preset-env", { "useBuiltIns": "entry" }],
|
|
||||||
"@babel/react",
|
|
||||||
"@babel/flow"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"babel-plugin-transform-imports",
|
|
||||||
["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }],
|
|
||||||
"@babel/plugin-transform-flow-strip-types",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
"babel-plugin-add-module-exports"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export const clipboard = () => {
|
|
||||||
throw new Error('Fix me!');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ipcRenderer = () => {
|
|
||||||
throw new Error('Fix me!');
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
export default {
|
|
||||||
readFileSync: () => undefined,
|
|
||||||
accessFileSync: () => undefined,
|
|
||||||
constants: {},
|
|
||||||
};
|
|
|
@ -1,154 +0,0 @@
|
||||||
const { parseURI } = require('lbry-redux');
|
|
||||||
const { generateStreamUrl } = require('../../src/ui/util/lbrytv');
|
|
||||||
const { WEB_SERVER_PORT, DOMAIN } = require('../../config');
|
|
||||||
const { readFileSync } = require('fs');
|
|
||||||
const express = require('express');
|
|
||||||
const path = require('path');
|
|
||||||
const app = express();
|
|
||||||
const mysql = require('mysql');
|
|
||||||
|
|
||||||
const pool = mysql.createPool({
|
|
||||||
connectionLimit: 100,
|
|
||||||
host: 'chainquery.lbry.com',
|
|
||||||
user: 'lbrytv',
|
|
||||||
password: process.env.CHAINQUERY_MYSQL_PASSWORD,
|
|
||||||
database: 'chainquery',
|
|
||||||
});
|
|
||||||
|
|
||||||
const getClaim = (claimName, claimId, channelName, channelClaimId, callback) => {
|
|
||||||
let params = [claimName];
|
|
||||||
|
|
||||||
let sql =
|
|
||||||
'SELECT channel_claim.name as channel, claim.claim_id, claim.name, claim.description, claim.language, claim.thumbnail_url, claim.title, claim.source_media_type, claim.frame_width, claim.frame_height ' +
|
|
||||||
'FROM claim ' +
|
|
||||||
'LEFT JOIN claim channel_claim on claim.publisher_id = channel_claim.claim_id ' +
|
|
||||||
'WHERE claim.name = ?';
|
|
||||||
|
|
||||||
if (claimId) {
|
|
||||||
sql += ' AND claim.claim_id LIKE ?';
|
|
||||||
params.push(claimId + '%');
|
|
||||||
} else {
|
|
||||||
sql += ' AND claim.bid_state = "controlling"';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (claimName[0] !== '@' && channelName) {
|
|
||||||
sql += ' AND channel_claim.name = ?';
|
|
||||||
params.push('@' + channelName);
|
|
||||||
if (channelClaimId) {
|
|
||||||
sql += ' AND channel_claim.claim_id LIKE ?';
|
|
||||||
params.push(channelClaimId + '%');
|
|
||||||
} else {
|
|
||||||
sql += ' AND channel_claim.bid_state = "controlling"';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += ' LIMIT 1';
|
|
||||||
|
|
||||||
pool.query(sql, params, callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use(express.static(__dirname));
|
|
||||||
|
|
||||||
function truncateDescription(description) {
|
|
||||||
return description.length > 200 ? description.substr(0, 200) + '...' : description;
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertToHead(fullHtml, htmlToInsert) {
|
|
||||||
return fullHtml.replace(/<!-- VARIABLE_HEAD_BEGIN -->.*<!-- VARIABLE_HEAD_END -->/s, htmlToInsert);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtmlProperty(property) {
|
|
||||||
return property
|
|
||||||
? String(property)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultHead =
|
|
||||||
'<title>lbry.tv</title>\n' +
|
|
||||||
`<meta property="og:url" content="${DOMAIN}" />\n` +
|
|
||||||
'<meta property="og:title" content="lbry.tv" />\n' +
|
|
||||||
'<meta property="og:site_name" content="lbry.tv | Content Freedom"/>\n' +
|
|
||||||
'<meta property="og:description" content="Meet LBRY, an open, free, and community-controlled content wonderland." />\n' +
|
|
||||||
`<meta property="og:image" content="${DOMAIN}/og.png" />\n` +
|
|
||||||
'<meta name="twitter:card" content="summary_large_image"/>\n' +
|
|
||||||
`<meta name="twitter:image" content="${DOMAIN}/og.png"/>\n` +
|
|
||||||
'<meta property="fb:app_id" content="1673146449633983" />';
|
|
||||||
|
|
||||||
app.get('*', async (req, res) => {
|
|
||||||
let html = readFileSync(path.join(__dirname, '/index.html'), 'utf8');
|
|
||||||
const urlPath = req.path.substr(1); // trim leading slash
|
|
||||||
|
|
||||||
if (!urlPath.startsWith('$/') && urlPath.match(/^([^@/:]+)\/([^:/]+)$/)) {
|
|
||||||
return res.redirect(301, req.url.replace(/^([^@/:]+)\/([^:/]+)(:(\/.*))/, '$1:$2')); // test against urlPath, but use req.url to retain parameters
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlPath.endsWith('/') && urlPath.length > 1) {
|
|
||||||
return res.redirect(301, req.url.replace(/\/$/, ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlPath.length > 0 && urlPath[0] !== '$') {
|
|
||||||
const { isChannel, streamName, channelName, channelClaimId, streamClaimId } = parseURI(urlPath.replace(/:/g, '#'));
|
|
||||||
const claimName = isChannel ? '@' + channelName : streamName;
|
|
||||||
const claimId = isChannel ? channelClaimId : streamClaimId;
|
|
||||||
|
|
||||||
getClaim(claimName, claimId, channelName, channelClaimId, (err, rows) => {
|
|
||||||
if (!err && rows && rows.length > 0) {
|
|
||||||
const claim = rows[0];
|
|
||||||
const title = escapeHtmlProperty(claim.title ? claim.title : claimName);
|
|
||||||
const claimDescription =
|
|
||||||
claim.description && claim.description.length > 0
|
|
||||||
? escapeHtmlProperty(truncateDescription(claim.description))
|
|
||||||
: `Watch ${title} on LBRY.tv`;
|
|
||||||
const claimLanguage = escapeHtmlProperty(claim.language) || 'en_US';
|
|
||||||
const claimThumbnail = escapeHtmlProperty(claim.thumbnail_url) || `${DOMAIN}/og.png`;
|
|
||||||
const claimTitle =
|
|
||||||
claim.channel && !isChannel ? `${title} from ${claim.channel} on LBRY.tv` : `${title} on LBRY.tv`;
|
|
||||||
|
|
||||||
let head = '';
|
|
||||||
|
|
||||||
head += '<meta charset="utf8"/>';
|
|
||||||
head += `<title>${claimTitle}</title>`;
|
|
||||||
head += `<meta name="description" content="${claimDescription}"/>`;
|
|
||||||
if (claim.tags) {
|
|
||||||
head += `<meta name="keywords" content="${claim.tags.toString()}"/>`;
|
|
||||||
}
|
|
||||||
head += `<meta name="twitter:card" content="summary_large_image"/>`;
|
|
||||||
head += `<meta name="twitter:image" content="${claimThumbnail}"/>`;
|
|
||||||
head += `<meta property="og:description" content="${claimDescription}"/>`;
|
|
||||||
head += `<meta property="og:image" content="${claimThumbnail}"/>`;
|
|
||||||
head += `<meta property="og:locale" content="${claimLanguage}"/>`;
|
|
||||||
head += `<meta property="og:site_name" content="LBRY.tv"/>`;
|
|
||||||
head += `<meta property="og:type" content="website"/>`;
|
|
||||||
head += `<meta property="og:title" content="${claimTitle}"/>`;
|
|
||||||
// below should be canonical_url, but not provided by chainquery yet
|
|
||||||
head += `<meta property="og:url" content="${DOMAIN}/${claim.name}:${claim.claim_id}"/>`;
|
|
||||||
|
|
||||||
if (claim.source_media_type && claim.source_media_type.startsWith('video/')) {
|
|
||||||
const videoUrl = generateStreamUrl(claim.name, claim.claim_id);
|
|
||||||
head += `<meta property="og:video" content="${videoUrl}" />`;
|
|
||||||
head += `<meta property="og:video:secure_url" content="${videoUrl}" />`;
|
|
||||||
head += `<meta property="og:video:type" content="${claim.source_media_type}" />`;
|
|
||||||
if (claim.frame_width && claim.frame_height) {
|
|
||||||
head += `<meta property="og:video:width" content="${claim.frame_width}"/>`;
|
|
||||||
head += `<meta property="og:video:height" content="${claim.frame_height}"/>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html = insertToHead(html, head);
|
|
||||||
} else {
|
|
||||||
html = insertToHead(html, defaultHead);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send(html);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.send(insertToHead(html, defaultHead));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(WEB_SERVER_PORT, () => console.log(`UI server listening at http://localhost:${WEB_SERVER_PORT}`)); // eslint-disable-line
|
|
|
@ -1,17 +0,0 @@
|
||||||
import React, { Suspense } from 'react';
|
|
||||||
|
|
||||||
const MarkdownPreviewInternal = React.lazy(() =>
|
|
||||||
import(/* webpackChunkName: "markdownPreview" */
|
|
||||||
/* webpackPrefetch: true */
|
|
||||||
'./markdown-preview-internal')
|
|
||||||
);
|
|
||||||
|
|
||||||
const MarkdownPreview = props => {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<div className="markdown-preview" />}>
|
|
||||||
<MarkdownPreviewInternal {...props} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarkdownPreview;
|
|
|
@ -1,106 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
|
|
||||||
import SettingsPage from 'page/settings';
|
|
||||||
import HelpPage from 'page/help';
|
|
||||||
import ReportPage from 'page/report';
|
|
||||||
import AccountPage from 'page/account';
|
|
||||||
import ShowPage from 'page/show';
|
|
||||||
import PublishPage from 'page/publish';
|
|
||||||
import DiscoverPage from 'page/discover';
|
|
||||||
import RewardsPage from 'page/rewards';
|
|
||||||
import FileListDownloaded from 'page/fileListDownloaded';
|
|
||||||
import FileListPublished from 'page/fileListPublished';
|
|
||||||
import TransactionHistoryPage from 'page/transactionHistory';
|
|
||||||
import InvitePage from 'page/invite';
|
|
||||||
import SearchPage from 'page/search';
|
|
||||||
import LibraryPage from 'page/library';
|
|
||||||
import WalletPage from 'page/wallet';
|
|
||||||
import WalletSendPage from 'page/walletSend';
|
|
||||||
import WalletReceivePage from 'page/walletReceive';
|
|
||||||
import TagsPage from 'page/tags';
|
|
||||||
import FollowingPage from 'page/following';
|
|
||||||
import ListBlockedPage from 'page/listBlocked';
|
|
||||||
import FourOhFourPage from 'page/fourOhFour';
|
|
||||||
import SignInPage from 'page/signIn';
|
|
||||||
import ChannelsPage from 'page/channels';
|
|
||||||
|
|
||||||
// Tell the browser we are handling scroll restoration
|
|
||||||
if ('scrollRestoration' in history) {
|
|
||||||
history.scrollRestoration = 'manual';
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrivateRouteProps = {
|
|
||||||
component: any,
|
|
||||||
isAuthenticated: boolean,
|
|
||||||
location: { pathname: string },
|
|
||||||
};
|
|
||||||
|
|
||||||
function PrivateRoute(props: PrivateRouteProps) {
|
|
||||||
const { component: Component, isAuthenticated, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
{...rest}
|
|
||||||
render={props =>
|
|
||||||
isAuthenticated || !IS_WEB ? (
|
|
||||||
<Component {...props} />
|
|
||||||
) : (
|
|
||||||
<Redirect to={`/$/${PAGES.AUTH}?redirect=${props.location.pathname}`} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
currentScroll: number,
|
|
||||||
location: { pathname: string, search: string },
|
|
||||||
isAuthenticated: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
function AppRouter(props: Props) {
|
|
||||||
const {
|
|
||||||
currentScroll,
|
|
||||||
location: { pathname },
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo(0, currentScroll);
|
|
||||||
}, [currentScroll, pathname]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path="/" exact component={DiscoverPage} />
|
|
||||||
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
|
|
||||||
<Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} />
|
|
||||||
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
|
|
||||||
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
|
|
||||||
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
|
|
||||||
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.DOWNLOADED}`} component={FileListDownloaded} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.REPORT}`} component={ReportPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} component={RewardsPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS}`} component={SettingsPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.TRANSACTIONS}`} component={TransactionHistoryPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.ACCOUNT}`} component={AccountPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.FOLLOWING}`} component={FollowingPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET_SEND}`} exact component={WalletSendPage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET_RECEIVE}`} exact component={WalletReceivePage} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
|
||||||
|
|
||||||
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
|
||||||
<Route path="/:claimName" exact component={ShowPage} />
|
|
||||||
<Route path="/:claimName/:streamName" exact component={ShowPage} />
|
|
||||||
<Route path="/*" component={FourOhFourPage} />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(AppRouter);
|
|
Before Width: | Height: | Size: 265 KiB |
Before Width: | Height: | Size: 278 KiB |
|
@ -1,130 +0,0 @@
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/400.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/400.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/400i.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/400i.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/500.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/500.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/500i.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/500i.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/600.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/600.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/600i.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/600i.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/700.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/700.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/700i.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/700i.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 800;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/800.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/800.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 800;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/800i.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/800i.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/900.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/900.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 900;
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/900i.woff2') format('woff2'),
|
|
||||||
url('../../../static/font/inter/900i.woff') format('woff');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Single variable font.
|
|
||||||
Note that you may want to do something like this to make sure you are serving
|
|
||||||
constant fonts to older browsers:
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: "Inter UI", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (font-variation-settings: normal) {
|
|
||||||
html {
|
|
||||||
font-family: "Inter UI var", sans-serif;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter UI Variable';
|
|
||||||
font-weight: 400 900; // safe weight range
|
|
||||||
src: url('../../../static/font/inter/variable.woff2') format('woff2-variations'),
|
|
||||||
url('../../../static/font/inter/variable.woff2') format('woff2');
|
|
||||||
}
|
|
BIN
static/font/inter/300.woff
Normal file
BIN
static/font/inter/300.woff2
Normal file
BIN
static/font/inter/300i.woff
Normal file
BIN
static/font/inter/300i.woff2
Normal file
|
@ -1,129 +0,0 @@
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-Regular.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-Regular.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-Italic.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-Italic.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-Medium.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-Medium.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-MediumItalic.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-MediumItalic.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-SemiBold.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-SemiBold.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-SemiBoldItalic.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-SemiBoldItalic.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-Bold.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-Bold.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-BoldItalic.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-BoldItalic.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 800;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-ExtraBold.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-ExtraBold.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 800;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-ExtraBoldItalic.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-ExtraBoldItalic.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-Black.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-Black.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI";
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 900;
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI-BlackItalic.woff2") format("woff2"),
|
|
||||||
url("Inter-UI-BlackItalic.woff") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Single variable font.
|
|
||||||
Note that you may want to do something like this to make sure you"re serving
|
|
||||||
constant fonts to older browsers:
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: "Inter UI", sans-serif;
|
|
||||||
}
|
|
||||||
@supports (font-variation-settings: normal) {
|
|
||||||
html {
|
|
||||||
font-family: "Inter UI var", sans-serif;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter UI var";
|
|
||||||
font-weight: 400 900; /* safe weight range */
|
|
||||||
src: url("Inter-UI.var.woff2") format("woff2-variations"),
|
|
||||||
url("Inter-UI.var.woff2") format("woff2");
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square70x70logo src="/mstile-70x70.png"/>
|
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
|
||||||
<square310x310logo src="/mstile-310x310.png"/>
|
|
||||||
<wide310x150logo src="/mstile-310x150.png"/>
|
|
||||||
<TileColor>#da532c</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 1.9 KiB |
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"name": "My app",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "\/android-chrome-36x36.png",
|
|
||||||
"sizes": "36x36",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "0.75"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/android-chrome-48x48.png",
|
|
||||||
"sizes": "48x48",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/android-chrome-72x72.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "1.5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/android-chrome-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "2.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/android-chrome-144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "3.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "\/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "4.0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 168 KiB |
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 142 KiB |
BIN
static/img/tray/mac/trayTemplate@2x copy.png
Normal file
After Width: | Height: | Size: 3.3 KiB |