refactor lbrytv web server

This commit is contained in:
Sean Yesmunt 2019-11-07 14:39:22 -05:00
parent eedaf56ee0
commit 6ad31a3ce9
598 changed files with 6919 additions and 2317 deletions

View file

@ -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",
]
}

View file

@ -12,19 +12,19 @@ node_modules/lbryinc/flow-typed/
[options]
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
module.name_mapper='^constants\(.*\)$' -> '<PROJECT_ROOT>/src/ui/constants\1'
module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/src/ui/util\1'
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/ui/redux\1'
module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/ui/types\1'
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/ui/component\1'
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/ui/page\1'
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/ui/lbry\1'
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/ui/modal\1'
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/src/ui/app\1'
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/src/ui/native\1'
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/src/ui/analytics\1'
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/src/ui/i18n\1'
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/src/ui/effects\1'
module.name_mapper='^constants\(.*\)$' -> '<PROJECT_ROOT>/ui/constants\1'
module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/ui/util\1'
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/ui/redux\1'
module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/ui/types\1'
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/ui/component\1'
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/ui/page\1'
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/ui/lbry\1'
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/ui/modal\1'
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/ui/app\1'
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/ui/native\1'
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/ui/analytics\1'
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1'
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1'
module.name_mapper='^config\(.*\)$' -> '<PROJECT_ROOT>/config\1'

2
.gitignore vendored
View file

@ -13,3 +13,5 @@ package-lock.json
.transifexrc
.idea/
/build/daemon*
/lbrytv/dist/
/lbrytv/node_modules

17
babel.config.js Normal file
View 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
View 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}`);

View 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
View 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"
}
}

View file

@ -1,9 +1,10 @@
import { Lbry } from 'lbry-redux';
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';
export const SDK_API_URL = `${process.env.SDK_API_URL}/${PROXY_PATH}` || `https://api.lbry.tv/${PROXY_PATH}`;
Lbry.setDaemonConnectionString(SDK_API_URL);
Lbry.setOverride(

View file

@ -5,7 +5,7 @@
- 'file' binary
- '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 { apiCall } from 'lbry-redux';

53
lbrytv/src/chainquery.js Normal file
View 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
View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
: '';
}
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
View 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
View 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: {},
};

View file

@ -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 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 { 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 WEB_PLATFORM_ROOT = path.resolve(__dirname, 'src/platforms/web/');
const WEB_PLATFORM_ROOT = __dirname;
const webConfig = {
target: 'web',
entry: {
ui: './src/ui/index.jsx',
ui: '../ui/index.jsx',
},
output: {
filename: '[name].js',
path: __dirname + '/dist/web',
path: __dirname + '/dist/',
publicPath: '/',
},
devServer: {
@ -25,46 +25,45 @@ const webConfig = {
module: {
rules: [
{
test: /\.(jsx?$|s?css$)/,
use: [
{
loader: 'preprocess-loader',
options: {
TARGET: 'web',
ppOptions: {
type: 'js',
},
},
loader: 'babel-loader',
test: /\.jsx?$/,
exclude: /node_modules/,
options: {
rootMode: 'upward',
},
},
{
loader: 'preprocess-loader',
test: /\.jsx?$/,
exclude: /node_modules/,
options: {
TARGET: 'web',
ppOptions: {
type: 'js',
},
],
},
},
],
},
resolve: {
modules: [path.resolve(__dirname, 'src/platforms/')],
alias: {
electron: path.resolve(__dirname, 'src/platforms/web/stubs'),
fs: path.resolve(__dirname, 'src/platforms/web/fs'),
electron: `${WEB_PLATFORM_ROOT}/stubs/electron.js`,
fs: `${WEB_PLATFORM_ROOT}/stubs/fs.js`,
},
},
plugins: [
new CopyWebpackPlugin([
{
from: `${STATIC_ROOT}/index-web.html`,
to: `${DIST_ROOT}/web/index.html`,
to: `${DIST_ROOT}/index.html`,
},
{
from: `${STATIC_ROOT}/img/favicon.ico`,
to: `${DIST_ROOT}/web/favicon.ico`,
to: `${DIST_ROOT}/favicon.ico`,
},
{
from: `${STATIC_ROOT}/img/og.png`,
to: `${DIST_ROOT}/web/og.png`,
},
{
from: `${WEB_PLATFORM_ROOT}/server.js`,
to: `${DIST_ROOT}/web/server.js`,
to: `${DIST_ROOT}/og.png`,
},
]),
new DefinePlugin({

5689
lbrytv/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -21,12 +21,12 @@
"main": "./dist/electron/main.js",
"scripts": {
"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",
"dev": "yarn dev:electron",
"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-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": "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\" \"cd ./lbrytv && yarn dev:server\"",
"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",
"pack": "electron-builder --dir",
@ -38,7 +38,7 @@
"flow-defs": "flow-typed install",
"precommit": "lint-staged",
"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: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-notarize": "^0.1.1",
"electron-updater": "^4.1.2",
"express": "^4.16.4",
"if-env": "^1.0.4",
"imagesloaded": "^4.1.4",
"keytar": "^4.4.1",
"mysql": "^2.17.1",
"nodemon": "^1.19.1"
"keytar": "^4.4.1"
},
"devDependencies": {
"@babel/core": "^7.0.0",
@ -62,6 +58,7 @@
"@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",
@ -80,8 +77,6 @@
"babel-plugin-import-glob": "^2.0.0",
"babel-plugin-transform-imports": "^1.5.1",
"bluebird": "^3.5.1",
"butterchurn": "^2.6.7",
"butterchurn-presets": "^2.4.7",
"chalk": "^2.4.2",
"classnames": "^2.2.5",
"codemirror": "^5.39.2",
@ -102,8 +97,6 @@
"electron-builder": "^21.2.0",
"electron-devtools-installer": "^2.2.4",
"electron-is-dev": "^0.3.0",
"electron-publisher-s3": "^20.8.1",
"electron-webpack": "^2.6.2",
"electron-window-state": "^4.1.1",
"eslint": "^5.15.2",
"eslint-config-prettier": "^2.9.0",
@ -118,13 +111,14 @@
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.7.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-typed": "^2.3.0",
"formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2",
"history": "^4.9.0",
"husky": "^0.14.3",
"imagesloaded": "^4.1.4",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#4491b975cc3e23bf3733272b7c6079a28c1036b3",
@ -132,7 +126,6 @@
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",
"lodash-es": "^4.17.14",
"make-runnable": "^1.3.6",
"mammoth": "^1.4.6",
"moment": "^2.22.0",
"node-abi": "^2.5.1",
@ -189,9 +182,10 @@
"wavesurfer.js": "^2.2.1",
"webpack": "^4.28.4",
"webpack-bundle-analyzer": "^3.1.0",
"webpack-cli": "^3.3.10",
"webpack-config-utils": "^2.3.1",
"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-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2",

View file

@ -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"
]
}

View file

@ -1,7 +0,0 @@
export const clipboard = () => {
throw new Error('Fix me!');
};
export const ipcRenderer = () => {
throw new Error('Fix me!');
};

View file

@ -1,5 +0,0 @@
export default {
readFileSync: () => undefined,
accessFileSync: () => undefined,
constants: {},
};

View file

@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
: '';
}
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

View file

@ -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;

View file

@ -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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

View file

@ -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

Binary file not shown.

BIN
static/font/inter/300.woff2 Normal file

Binary file not shown.

BIN
static/font/inter/300i.woff Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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");
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -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"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Some files were not shown because too many files have changed in this diff Show more