Redesign 1 bcrypt #226

Merged
bones7242 merged 156 commits from redesign-1-bcrypt into master 2017-10-30 15:55:14 +01:00
98 changed files with 2827 additions and 1885 deletions

View file

@ -28,28 +28,25 @@ spee.ch is a single-serving site that reads and publishes images and videos to a
#### GET
* /api/resolve/:name
* a successfull request returns the resolve results for the claim at that name in JSON format
* example: `curl https://spee.ch/api/resolve/doitlive`
* /api/claim_list/:name
* a successfull request returns a list of claims at that claim name in JSON format
* /api/isClaimAvailable/:name
* a successfull request returns a boolean: `true` if the name is still available, `false` if the name has already been published to by spee.ch.
* example: `curl https://spee.ch/api/claim_list/doitlive`
* /api/isClaimAvailable/:name (returns `true`/`false` for whether a name is available through spee.ch)
* example: `curl https://spee.ch/api/isClaimAvailable/doitlive`
#### POST
* /api/publish
* request parameters:
* body (form-data):
* name: string (optional)
* defaults to the file's name, sans extension
* names can only contain the following characters: `A-Z`, `a-z`, `_`, or `-`
* license: string (optional)
* defaults to "No License Provided"
* only "Public Domain" or "Creative Commons" licenses are allowed
* nsfw: string, number, or boolean (optional)
* defaults `true`
* nsfw can be a string ("on"/"off"), number (0 or 1), or boolean (`true`/`false`)
* files:
* the `files` object submitted must use "speech" or "null" as the key for the file's value object
* a successfull request will return the transaction details resulting from your published claim in JSON format
* example: `curl -X POST -F 'name=MyPictureName' -F 'nsfw=false' -F 'file=@/path/to/my/picture.jpeg' https://spee.ch/api/publish`
* Parameters:
* name (string)
* nsfw (boolean)
* file (.mp4, .jpeg, .jpg, .gif, or .png)
* license (string, optional)
* title (string, optional)
* description (string, optional)
* thumbnail (string, optional) (for .mp4 uploads only)
* channelName(string, optional)
* channelPassword (string, optional)
## bugs
If you find a bug or experience a problem, please report your issue here on github and find us in the lbry slack!

View file

@ -2,21 +2,23 @@ const db = require('../models');
const logger = require('winston');
module.exports = {
authenticateApiPublish (username, password) {
authenticateChannelCredentials (channelName, userPassword) {
return new Promise((resolve, reject) => {
if (username === 'none') {
if (!channelName) {
resolve(true);
return;
}
const userName = channelName.substring(1);
logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`);
db.User
.findOne({where: {userName: username}})
.findOne({where: { userName }})
.then(user => {
if (!user) {
logger.debug('no user found');
resolve(false);
return;
}
if (!user.validPassword(password, user.password)) {
if (!user.validPassword(userPassword, user.password)) {
logger.debug('incorrect password');
resolve(false);
return;

View file

@ -8,5 +8,8 @@
},
"Logging": {
"SlackWebHook": "SLACK_WEB_HOOK"
},
"Session": {
"SessionKey": "SESSION_KEY"
}
}

View file

@ -16,5 +16,8 @@
"SlackWebHook": null,
"SlackErrorChannel": null,
"SlackInfoChannel": null
},
"Session": {
"SessionKey": null
}
}

View file

@ -7,24 +7,23 @@ module.exports = {
publish (publishParams, fileName, fileType) {
return new Promise((resolve, reject) => {
let publishResults = {};
// 1. make sure the name is available
publishHelpers.checkClaimNameAvailability(publishParams.name)
// 2. publish the file
.then(result => {
if (result === true) {
return lbryApi.publishClaim(publishParams);
} else {
return new Error('That name is already in use by spee.ch.');
}
})
// 3. upsert File record (update is in case the claim has been published before by this daemon)
// 1. publish the file
return lbryApi.publishClaim(publishParams)
// 2. upsert File record (update is in case the claim has been published before by this daemon)
.then(tx => {
logger.info(`Successfully published ${fileName}`, tx);
publishResults = tx;
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
return db.Channel.findOne({where: {channelName: publishParams.channel_name}}); // note: should this be db.User ??
})
.then(user => {
if (user) { logger.debug('successfully found user in User table') } else { logger.error('user for publish not found in User table') };
.then(channel => {
let certificateId;
if (channel) {
certificateId = channel.channelClaimId;
logger.debug('successfully found channel in Channel table');
} else {
certificateId = null;
logger.debug('channel for publish not found in Channel table');
};
const fileRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
@ -44,11 +43,12 @@ module.exports = {
title : publishParams.metadata.title,
description: publishParams.metadata.description,
address : publishParams.claim_address,
thumbnail : publishParams.metadata.thumbnail,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
contentType: fileType,
nsfw : publishParams.metadata.nsfw,
certificateId: user.channelClaimId,
certificateId,
amount : publishParams.bid,
};
const upsertCriteria = {
@ -67,6 +67,7 @@ module.exports = {
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
})
.catch(error => {
logger.error('publishController.publish, error', error);
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
reject(error);
});

View file

@ -7,7 +7,9 @@ const { postToStats, sendGoogleAnalytics } = require('../controllers/statsContro
const SERVE = 'SERVE';
const SHOW = 'SHOW';
const SHOWLITE = 'SHOWLITE';
const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/content-freedom-large.png';
const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/video_thumb_default.png';
const NO_CHANNEL = 'NO_CHANNEL';
const NO_CLAIM = 'NO_CLAIM';
function checkForLocalAssetByClaimId (claimId, name) {
return new Promise((resolve, reject) => {
@ -56,7 +58,8 @@ function getAssetByLongClaimId (fullClaimId, name) {
// if a result was found, return early with the result
if (dataValues) {
logger.debug('found a local file for this name and claimId');
return resolve(dataValues);
resolve(dataValues);
return;
}
logger.debug('no local file found for this name and claimId');
// 2. if no local claim, resolve and get the claim
@ -78,7 +81,7 @@ function getAssetByLongClaimId (fullClaimId, name) {
// insert a record in the File table & Update Claim table
return db.File.create(fileRecord);
})
.then(fileRecordResults => {
.then(() => {
logger.debug('File record successfully updated');
resolve(fileRecord);
})
@ -105,15 +108,17 @@ function chooseThumbnail (claimInfo, defaultThumbnail) {
module.exports = {
getAssetByClaim (claimName, claimId) {
logger.debug('getting asset by claim');
logger.debug(`getAssetByClaim(${claimName}, ${claimId})`);
return new Promise((resolve, reject) => {
// 1. get the long claim id
db
.getLongClaimId(claimName, claimId)
// 2. get the claim Id
.then(longClaimId => {
logger.debug('long claim id = ', longClaimId);
resolve(getAssetByLongClaimId(longClaimId, claimName));
db.getLongClaimId(claimName, claimId) // 1. get the long claim id
.then(result => { // 2. get the asset using the long claim id
logger.debug('getLongClaimId result:', result);
if (result === NO_CLAIM) {
logger.debug('resolving NO_CLAIM');
resolve(NO_CLAIM);
return;
}
resolve(getAssetByLongClaimId(result, claimName));
})
.catch(error => {
reject(error);
@ -123,17 +128,21 @@ module.exports = {
getAssetByChannel (channelName, channelId, claimName) {
logger.debug('getting asset by channel');
return new Promise((resolve, reject) => {
// 1. get the long channel id
db
.getLongChannelId(channelName, channelId)
// 2. get the claim Id
.then(longChannelId => {
return db.getClaimIdByLongChannelId(longChannelId, claimName);
db.getLongChannelId(channelName, channelId) // 1. get the long channel id
.then(result => { // 2. get the long claim Id
if (result === NO_CHANNEL) {
resolve(NO_CHANNEL);
return;
}
return db.getClaimIdByLongChannelId(result, claimName);
})
// 3. get the asset by this claim id and name
.then(claimId => {
logger.debug('asset claim id = ', claimId);
resolve(getAssetByLongClaimId(claimId, claimName));
.then(result => { // 3. get the asset using the long claim id
logger.debug('asset claim id =', result);
if (result === NO_CHANNEL || result === NO_CLAIM) {
resolve(result);
return;
}
resolve(getAssetByLongClaimId(result, claimName));
})
.catch(error => {
reject(error);
@ -144,35 +153,41 @@ module.exports = {
return new Promise((resolve, reject) => {
let longChannelId;
let shortChannelId;
// 1. get the long channel Id
db
.getLongChannelId(channelName, channelId)
// 2. get all claims for that channel
.then(result => {
db.getLongChannelId(channelName, channelId) // 1. get the long channel Id
.then(result => { // 2. get all claims for that channel
if (result === NO_CHANNEL) {
return NO_CHANNEL;
}
longChannelId = result;
return db.getShortChannelIdFromLongChannelId(longChannelId, channelName);
})
// 3. get all Claim records for this channel
.then(result => {
.then(result => { // 3. get all Claim records for this channel
if (result === NO_CHANNEL) {
return NO_CHANNEL;
}
shortChannelId = result;
return db.getAllChannelClaims(longChannelId);
})
// 4. add extra data not available from Claim table
.then(allChannelClaims => {
if (allChannelClaims) {
allChannelClaims.forEach(element => {
.then(result => { // 4. add extra data not available from Claim table
if (result === NO_CHANNEL) {
resolve(NO_CHANNEL);
return;
}
if (result) {
result.forEach(element => {
const fileExtenstion = element.contentType.substring(element.contentType.lastIndexOf('/') + 1);
element['showUrlLong'] = `/${channelName}:${longChannelId}/${element.name}`;
element['directUrlLong'] = `/${channelName}:${longChannelId}/${element.name}.${fileExtenstion}`;
element['showUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}`;
element['directUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}.${fileExtenstion}`;
element['thumbnail'] = chooseThumbnail(element, DEFAULT_THUMBNAIL);
});
}
return resolve({
resolve({
channelName,
longChannelId,
shortChannelId,
claims: allChannelClaims,
claims: result,
});
})
.catch(error => {
@ -206,9 +221,12 @@ module.exports = {
return db.resolveClaim(fileInfo.name, fileInfo.claimId);
})
.then(resolveResult => {
logger.debug('resolve result >>', resolveResult);
fileInfo['thumbnail'] = chooseThumbnail(resolveResult, DEFAULT_THUMBNAIL);
fileInfo['title'] = resolveResult.title;
fileInfo['description'] = resolveResult.description;
if (resolveResult.certificateId) { fileInfo['certificateId'] = resolveResult.certificateId };
if (resolveResult.channelName) { fileInfo['channelName'] = resolveResult.channelName };
showFile(fileInfo, res);
return fileInfo;
})

View file

@ -69,79 +69,6 @@ module.exports = {
}
});
},
getStatsSummary (startDate) {
logger.debug('retrieving request records');
return new Promise((resolve, reject) => {
// get the raw Requests data
db.Request
.findAll({
where: {
createdAt: {
gt: startDate,
},
},
})
.then(data => {
let resultHashTable = {};
let totalServe = 0;
let totalPublish = 0;
let totalShow = 0;
let totalCount = 0;
let totalSuccess = 0;
let totalFailure = 0;
let percentSuccess;
// summarise the data
for (let i = 0; i < data.length; i++) {
let key = data[i].action + data[i].url;
totalCount += 1;
switch (data[i].action) {
case 'SERVE':
totalServe += 1;
break;
case 'PUBLISH':
totalPublish += 1;
break;
case 'SHOW':
totalShow += 1;
break;
default: break;
}
if (resultHashTable[key]) {
resultHashTable[key]['count'] += 1;
if (data[i].result === 'success') {
resultHashTable[key]['success'] += 1;
totalSuccess += 1;
} else {
resultHashTable[key]['failure'] += 1;
totalFailure += 1;
}
} else {
resultHashTable[key] = {
action : data[i].action,
url : data[i].url,
count : 1,
success: 0,
failure: 0,
};
if (data[i].result === 'success') {
resultHashTable[key]['success'] += 1;
totalSuccess += 1;
} else {
resultHashTable[key]['failure'] += 1;
totalFailure += 1;
}
}
}
percentSuccess = Math.round(totalSuccess / totalCount * 100);
// return results
resolve({ records: resultHashTable, totals: { totalServe, totalPublish, totalShow, totalCount, totalSuccess, totalFailure }, percentSuccess });
})
.catch(error => {
logger.error('sequelize error >>', error);
reject(error);
});
});
},
getTrendingClaims (startDate) {
logger.debug('retrieving trending requests');
return new Promise((resolve, reject) => {
@ -155,7 +82,7 @@ module.exports = {
element['directUrlLong'] = `/${element.claimId}/${element.name}.${fileExtenstion}`;
element['directUrlShort'] = `/${element.claimId}/${element.name}.${fileExtenstion}`;
element['contentType'] = element.fileType;
element['thumbnail'] = 'https://spee.ch/assets/img/content-freedom-large.png';
element['thumbnail'] = 'https://spee.ch/assets/img/video_thumb_default.png';
});
}
resolve(results);

44
helpers/authHelpers.js Normal file
View file

@ -0,0 +1,44 @@
const db = require('../models'); // require our models for syncing
const logger = require('winston');
module.exports = {
populateLocalsDotUser (req, res, next) {
if (req.user) {
res.locals.user = {
id : req.user.id,
userName : req.user.userName,
channelName : req.user.channelName,
channelClaimId: req.user.channelClaimId,
shortChannelId: req.user.shortChannelId,
};
}
next();
},
serializeSpeechUser (user, done) {
done(null, user.id);
},
deserializeSpeechUser (id, done) {
let userInfo = {};
db.User.findOne({ where: { id } })
.then(user => {
userInfo['id'] = user.id;
userInfo['userName'] = user.userName;
return user.getChannel();
})
.then(channel => {
userInfo['channelName'] = channel.channelName;
userInfo['channelClaimId'] = channel.channelClaimId;
return db.getShortChannelIdFromLongChannelId(channel.channelClaimId, channel.channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
// return done(null, userInfo);
done(null, userInfo);
return null;
})
.catch(error => {
logger.error(error);
done(error, null);
});
},
};

View file

@ -1,20 +1,9 @@
const logger = require('winston');
const { postToStats } = require('../controllers/statsController.js');
function useObjectPropertiesIfNoKeys (err) {
if (Object.keys(err).length === 0) {
let newErrorObject = {};
Object.getOwnPropertyNames(err).forEach((key) => {
newErrorObject[key] = err[key];
});
return newErrorObject;
}
return err;
}
module.exports = {
handleRequestError (action, originalUrl, ip, error, res) {
logger.error('Request Error:', useObjectPropertiesIfNoKeys(error));
logger.error(`Request Error: ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));
postToStats(action, originalUrl, ip, null, null, error);
if (error.response) {
res.status(error.response.status).send(error.response.data.error.message);
@ -27,13 +16,31 @@ module.exports = {
}
},
handlePublishError (error) {
logger.error('Publish Error:', useObjectPropertiesIfNoKeys(error));
logger.error('Publish Error:', module.exports.useObjectPropertiesIfNoKeys(error));
if (error.code === 'ECONNREFUSED') {
return 'Connection refused. The daemon may not be running.';
} else if (error.response) {
if (error.response.data) {
if (error.response.data.message) {
return error.response.data.message;
} else if (error.response.data.error) {
return error.response.data.error.message;
}
return error.response.data;
}
return error.response;
} else {
return error;
}
},
useObjectPropertiesIfNoKeys (err) {
if (Object.keys(err).length === 0) {
let newErrorObject = {};
Object.getOwnPropertyNames(err).forEach((key) => {
newErrorObject[key] = err[key];
});
return newErrorObject;
}
return err;
},
};

View file

@ -51,7 +51,7 @@ module.exports = {
}
},
addTwitterCard (mimeType, source, embedUrl, directFileUrl) {
let basicTwitterTags = `<meta name="twitter:site" content="@speechch" >`;
let basicTwitterTags = `<meta name="twitter:site" content="@spee_ch" >`;
if (mimeType === 'video/mp4') {
return new Handlebars.SafeString(
`${basicTwitterTags} <meta name="twitter:card" content="player" >

View file

@ -2,14 +2,14 @@ const axios = require('axios');
const logger = require('winston');
function handleResponse ({ data }, resolve, reject) {
logger.debug('handling lbry api response');
logger.debug('handling lbry api response...');
if (data.result) {
// check for an error
if (data.result.error) {
reject(data.result.error);
return;
};
logger.debug('data.result', data.result);
// logger.debug('data.result', data.result);
resolve(data.result);
return;
}
@ -118,9 +118,11 @@ module.exports = {
},
})
.then(response => {
logger.verbose('createChannel response:', response);
handleResponse(response, resolve, reject);
})
.catch(error => {
logger.error('createChannel error:', error);
reject(error);
});
});

View file

@ -1,77 +1,129 @@
const logger = require('winston');
const config = require('config');
const fs = require('fs');
const db = require('../models');
const config = require('config');
module.exports = {
validateFile (file, name, license, nsfw) {
validateApiPublishRequest (body, files) {
if (!body) {
throw new Error('no body found in request');
}
if (!body.name) {
throw new Error('no name field found in request');
}
if (!body.nsfw) {
throw new Error('no nsfw field found in request');
}
if (!files) {
throw new Error('no files found in request');
}
if (!files.file) {
throw new Error('no file with key of [file] found in request');
}
},
validatePublishSubmission (file, claimName, nsfw) {
try {
module.exports.validateFile(file);
module.exports.validateClaimName(claimName);
module.exports.validateNSFW(nsfw);
} catch (error) {
throw error;
}
},
validateFile (file) {
if (!file) {
throw new Error('No file was submitted or the key used was incorrect. Files posted through this route must use a key of "speech" or null');
logger.debug('publish > file validation > no file found');
throw new Error('no file provided');
}
// check the file name
if (/'/.test(file.name)) {
logger.debug('publish > file validation > file name had apostrophe in it');
throw new Error('apostrophes are not allowed in the file name');
}
// check file type and size
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000) {
logger.debug('publish > file validation > .jpeg/.jpg/.png was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000) {
throw new Error('Your image exceeds the 50 megabyte limit.');
logger.debug('publish > file validation > .gif was too big');
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000) {
throw new Error('Your video exceeds the 50 megabyte limit.');
logger.debug('publish > file validation > .mp4 was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
throw new Error('The ' + file.Type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
logger.debug('publish > file validation > unrecognized file type');
throw new Error('The ' + file.type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
}
// validate claim name
const invalidCharacters = /[^A-Za-z0-9,-]/.exec(name);
return file;
},
validateClaimName (claimName) {
const invalidCharacters = /[^A-Za-z0-9,-]/.exec(claimName);
if (invalidCharacters) {
throw new Error('The url name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
throw new Error('The claim name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
}
// validate license
},
validateLicense (license) {
if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1)) {
throw new Error('Only posts with a "Public Domain" license, or one of the Creative Commons licenses are eligible for publishing through spee.ch');
throw new Error('Only posts with a "Public Domain" or "Creative Commons" license are eligible for publishing through spee.ch');
}
},
cleanseNSFW (nsfw) {
switch (nsfw) {
case true:
case false:
case 'true':
case 'false':
case 'on':
case 'true':
case 1:
case '1':
return true;
case false:
case 'false':
case 'off':
case 0:
case '0':
case 1:
case '1':
break;
return false;
default:
throw new Error('NSFW value was not accepted. NSFW must be set to either true, false, "on", or "off"');
return null;
}
},
createPublishParams (name, filePath, title, description, license, nsfw, channel) {
logger.debug(`Creating Publish Parameters for "${name}"`);
const claimAddress = config.get('WalletConfig.LbryClaimAddress');
const defaultChannel = config.get('WalletConfig.DefaultChannel');
// filter nsfw and ensure it is a boolean
if (nsfw === false) {
nsfw = false;
} else if (typeof nsfw === 'string') {
if (nsfw.toLowerCase === 'false' || nsfw.toLowerCase === 'off' || nsfw === '0') {
nsfw = false;
cleanseChannelName (channelName) {
if (channelName) {
if (channelName.indexOf('@') !== 0) {
channelName = `@${channelName}`;
}
} else if (nsfw === 0) {
nsfw = false;
} else {
nsfw = true;
}
// provide defaults for title & description
if (title === null || title === '') {
return channelName;
},
validateNSFW (nsfw) {
if (nsfw === true || nsfw === false) {
return;
}
throw new Error('NSFW must be set to either true or false');
},
createPublishParams (filePath, name, title, description, license, nsfw, thumbnail, channelName) {
logger.debug(`Creating Publish Parameters`);
// provide defaults for title
if (title === null || title.trim() === '') {
title = name;
}
// provide default for description
if (description === null || description.trim() === '') {
description = `${name} published via spee.ch`;
description = '';
}
// provide default for license
if (license === null || license.trim() === '') {
license = ' '; // default to empty string
}
// create the publish params
const publishParams = {
@ -86,21 +138,24 @@ module.exports = {
license,
nsfw,
},
claim_address: claimAddress,
claim_address: config.get('WalletConfig.LbryClaimAddress'),
};
// add channel if applicable
if (channel !== 'none') {
publishParams['channel_name'] = channel;
} else {
publishParams['channel_name'] = defaultChannel;
// add thumbnail to channel if video
if (thumbnail !== null) {
publishParams['metadata']['thumbnail'] = thumbnail;
}
// add channel to params, if applicable
if (channelName) {
publishParams['channel_name'] = channelName;
}
logger.debug('publishParams:', publishParams);
return publishParams;
},
deleteTemporaryFile (filePath) {
fs.unlink(filePath, err => {
if (err) throw err;
if (err) {
logger.error(`error deleting temporary file ${filePath}`);
throw err;
}
logger.debug(`successfully deleted ${filePath}`);
});
},

View file

@ -4,8 +4,8 @@ function createOpenGraphInfo ({ fileType, claimId, name, fileName, fileExt }) {
return {
embedUrl : `https://spee.ch/embed/${claimId}/${name}`,
showUrl : `https://spee.ch/${claimId}/${name}`,
source : `https://spee.ch/${claimId}/${name}${fileExt}`,
directFileUrl: `https://spee.ch/media/${fileName}`,
source : `https://spee.ch/${claimId}/${name}.${fileExt}`,
directFileUrl: `https://spee.ch/${claimId}/${name}.${fileExt}`,
};
}
@ -40,6 +40,6 @@ module.exports = {
},
showFileLite (fileInfo, res) {
const openGraphInfo = createOpenGraphInfo(fileInfo);
res.status(200).render('showLite', { layout: 'show', fileInfo, openGraphInfo });
res.status(200).render('showLite', { layout: 'showlite', fileInfo, openGraphInfo });
},
};

View file

@ -0,0 +1,22 @@
module.exports = {
up: (queryInterface, Sequelize) => {
// logic for transforming into the new state
const p1 = queryInterface.addColumn(
'Claim',
'channelName',
{
type : Sequelize.STRING,
allowNull: true,
}
);
return Promise.all([p1]);
},
down: (queryInterface, Sequelize) => {
// logic for reverting the changes
const p1 = queryInterface.removeColumn(
'Claim',
'channelName'
);
return Promise.all([p1]);
},
};

View file

@ -1,79 +0,0 @@
module.exports = {
up: (queryInterface, Sequelize) => {
// logic for transforming into the new state
const p1 = queryInterface.removeColumn(
'Certificate',
'UserId'
);
const p2 = queryInterface.addColumn(
'Certificate',
'ChannelId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
const p3 = queryInterface.addConstraint(
'Certificate',
['ChannelId'],
{
type : 'FOREIGN KEY',
name : 'Certificate_ibfk_1',
references: {
table: 'Channel',
field: 'id',
},
onUpdate: 'cascade',
onDelete: 'cascade',
}
);
const p4 = queryInterface.changeColumn(
'Claim',
'FileId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
const p5 = queryInterface.addConstraint(
'Claim',
['FileId'],
{
type : 'FOREIGN KEY',
name : 'Claim_ibfk_1',
references: {
table: 'File',
field: 'id',
},
onUpdate: 'cascade',
onDelete: 'cascade',
}
);
const p6 = queryInterface.removeColumn(
'File',
'UserId'
);
return Promise.all([p1, p2, p3, p4, p5, p6]);
},
down: (queryInterface, Sequelize) => {
// logic for reverting the changes
const p1 = queryInterface.addColumn(
'Certificate',
'UserId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
const p2 = queryInterface.addColumn(
'File',
'UserId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
return Promise.all([p1, p2]);
},
};

View file

@ -1,46 +0,0 @@
module.exports = {
up: (queryInterface, Sequelize) => {
// logic for transforming into the new state
const p1 = queryInterface.addColumn(
'User',
'userName',
{
type : Sequelize.STRING,
allowNull: true,
}
);
const p2 = queryInterface.removeColumn(
'User',
'channelName'
);
const p3 = queryInterface.removeColumn(
'User',
'channelClaimId'
);
return Promise.all([p1, p2, p3]);
},
down: (queryInterface, Sequelize) => {
// logic for reverting the changes
const p1 = queryInterface.removeColumn(
'User',
'userName'
);
const p2 = queryInterface.addColumn(
'User',
'channelName',
{
type : Sequelize.STRING,
allowNull: true,
}
);
const p3 = queryInterface.addColumn(
'User',
'channelClaimId',
{
type : Sequelize.STRING,
allowNull: true,
}
);
return Promise.all([p1, p2, p3]);
},
};

View file

@ -0,0 +1,46 @@
const db = require('../models');
const bcrypt = require('bcrypt');
const logger = require('winston');
module.exports = {
up: (queryInterface, Sequelize) => {
// get all the users
return db.User
.findAll()
.then((users) => {
// create an array of promises, with each promise bcrypting a password and updating the record
const promises = users.map((record) => {
// bcrypt
// generate a salt string to use for hashing
return new Promise((resolve, reject) => {
bcrypt.genSalt((saltError, salt) => {
if (saltError) {
logger.error('salt error', saltError);
reject(saltError);
return;
}
// generate a hashed version of the user's password
bcrypt.hash(record.password, salt, (hashError, hash) => {
// if there is an error with the hash generation return the error
if (hashError) {
logger.error('hash error', hashError);
reject(hashError);
return;
}
// replace the password string with the hash password value
resolve(queryInterface.sequelize.query(`UPDATE User SET User.password = "${hash}" WHERE User.id = ${record.id}`));
});
});
});
});
// return the array of promises
return Promise.all(promises);
})
.catch(error => {
logger.error('error prepping promises array', error);
});
},
down: (queryInterface, Sequelize) => {
// logic for reverting the changes
},
};

View file

@ -134,6 +134,11 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, ARRAY, DECIMAL, D
type : STRING,
default: null,
},
channelName: {
type : STRING,
allowNull: true,
default : null,
},
},
{
freezeTableName: true,

View file

@ -6,6 +6,9 @@ const config = require('config');
const db = {};
const logger = require('winston');
const NO_CHANNEL = 'NO_CHANNEL';
const NO_CLAIM = 'NO_CLAIM';
const database = config.get('Database.Database');
const username = config.get('Database.Username');
const password = config.get('Database.Password');
@ -52,7 +55,7 @@ function getLongClaimIdFromShortClaimId (name, shortId) {
.then(result => {
switch (result.length) {
case 0:
throw new Error('That is an invalid Short Claim Id');
return resolve(NO_CLAIM);
default: // note results must be sorted
return resolve(result[0].claimId);
}
@ -68,9 +71,10 @@ function getTopFreeClaimIdByClaimName (name) {
db
.sequelize.query(`SELECT claimId FROM Claim WHERE name = '${name}' ORDER BY effectiveAmount DESC, height ASC LIMIT 1`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
logger.debug('getTopFreeClaimIdByClaimName result:', result);
switch (result.length) {
case 0:
return resolve(null);
return resolve(NO_CLAIM);
default:
return resolve(result[0].claimId);
}
@ -88,7 +92,7 @@ function getLongChannelIdFromShortChannelId (channelName, channelId) {
.then(result => {
switch (result.length) {
case 0:
throw new Error('That is an invalid Short Channel Id');
return resolve(NO_CHANNEL);
default: // note results must be sorted
return resolve(result[0].claimId);
}
@ -100,13 +104,14 @@ function getLongChannelIdFromShortChannelId (channelName, channelId) {
}
function getLongChannelIdFromChannelName (channelName) {
logger.debug(`getLongChannelIdFromChannelName(${channelName})`);
return new Promise((resolve, reject) => {
db
.sequelize.query(`SELECT claimId, amount, height FROM Certificate WHERE name = '${channelName}' ORDER BY amount DESC, height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT })
.sequelize.query(`SELECT claimId, amount, height FROM Certificate WHERE name = '${channelName}' ORDER BY effectiveAmount DESC, height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
throw new Error('That is an invalid Channel Name');
return resolve(NO_CHANNEL);
default:
return resolve(result[0].claimId);
}
@ -230,7 +235,7 @@ db['getAllFreeClaims'] = (name) => {
db['resolveClaim'] = (name, claimId) => {
return new Promise((resolve, reject) => {
db
.sequelize.query(`SELECT name, claimId, outpoint, height, address, title, description, thumbnail FROM Claim WHERE name = '${name}' AND claimId = '${claimId}'`, { type: db.sequelize.QueryTypes.SELECT })
.sequelize.query(`SELECT name, claimId, outpoint, height, address, title, description, thumbnail, certificateId, channelName FROM Claim WHERE name = '${name}' AND claimId = '${claimId}'`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
@ -255,7 +260,7 @@ db['getClaimIdByLongChannelId'] = (channelId, claimName) => {
.then(result => {
switch (result.length) {
case 0:
throw new Error('There is no such claim for that channel');
return resolve(NO_CLAIM);
default:
return resolve(result[0].claimId);
}
@ -270,7 +275,7 @@ db['getAllChannelClaims'] = (channelId) => {
return new Promise((resolve, reject) => {
logger.debug(`finding all claims in channel "${channelId}"`);
db
.sequelize.query(`SELECT name, claimId, outpoint, height, address, contentType, title, description, license, thumbnail FROM Claim WHERE certificateId = '${channelId}' ORDeR BY height DESC;`, { type: db.sequelize.QueryTypes.SELECT })
.sequelize.query(`SELECT name, claimId, outpoint, height, address, contentType, title, description, license, thumbnail FROM Claim WHERE certificateId = '${channelId}' ORDER BY height DESC;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
@ -286,22 +291,24 @@ db['getAllChannelClaims'] = (channelId) => {
};
db['getLongClaimId'] = (claimName, claimId) => {
if (claimId && (claimId.length === 40)) {
logger.debug(`getLongClaimId(${claimName}, ${claimId})`);
if (claimId && (claimId.length === 40)) { // if a full claim id is provided
return new Promise((resolve, reject) => resolve(claimId));
} else if (claimId && claimId.length < 40) {
return getLongClaimIdFromShortClaimId(claimName, claimId); // need to create this function
} else { // if no claim id provided
return getTopFreeClaimIdByClaimName(claimName);
return getLongClaimIdFromShortClaimId(claimName, claimId); // if a short claim id is provided
} else {
return getTopFreeClaimIdByClaimName(claimName); // if no claim id is provided
}
};
db['getLongChannelId'] = (channelName, channelId) => {
if (channelId && (channelId.length === 40)) { // full channel id
logger.debug(`getLongChannelId (${channelName}, ${channelId})`);
if (channelId && (channelId.length === 40)) { // if a full channel id is provided
return new Promise((resolve, reject) => resolve(channelId));
} else if (channelId && channelId.length < 40) { // short channel id
} else if (channelId && channelId.length < 40) { // if a short channel id is provided
return getLongChannelIdFromShortChannelId(channelName, channelId);
} else {
return getLongChannelIdFromChannelName(channelName);
return getLongChannelIdFromChannelName(channelName); // if no channel id provided
}
};

View file

@ -1,3 +1,7 @@
'use strict';
const bcrypt = require('bcrypt');
const logger = require('winston');
module.exports = (sequelize, { STRING }) => {
const User = sequelize.define(
'User',
@ -20,10 +24,37 @@ module.exports = (sequelize, { STRING }) => {
User.hasOne(db.Channel);
};
User.prototype.validPassword = (givenpassword, thispassword) => {
console.log(`${givenpassword} === ${thispassword}`);
return (givenpassword === thispassword);
User.prototype.comparePassword = function (password, callback) {
logger.debug(`User.prototype.comparePassword ${password} ${this.password}`);
bcrypt.compare(password, this.password, callback);
};
// pre-save hook method to hash the user's password before the user's info is saved to the db.
User.hook('beforeCreate', (user, options) => {
logger.debug('...beforeCreate hook...');
return new Promise((resolve, reject) => {
// generate a salt string to use for hashing
bcrypt.genSalt((saltError, salt) => {
if (saltError) {
logger.error('salt error', saltError);
reject(saltError);
return;
}
// generate a hashed version of the user's password
bcrypt.hash(user.password, salt, (hashError, hash) => {
// if there is an error with the hash generation return the error
if (hashError) {
logger.error('hash error', hashError);
reject(hashError);
return;
}
// replace the password string with the hash password value
user.password = hash;
resolve();
});
});
});
});
return User;
};

View file

@ -27,12 +27,15 @@
"homepage": "https://github.com/lbryio/spee.ch#readme",
"dependencies": {
"axios": "^0.16.1",
"bcrypt": "^1.0.3",
"body-parser": "^1.17.1",
"config": "^1.26.1",
"connect-multiparty": "^2.0.0",
"cookie-session": "^2.0.0-beta.3",
"express": "^4.15.2",
"express-handlebars": "^3.0.0",
"express-session": "^1.15.5",
"form-data": "^2.3.1",
"helmet": "^3.8.1",
"mysql2": "^1.3.5",
"nodemon": "^1.11.0",

View file

@ -1,3 +1,4 @@
const PassportLocalStrategy = require('passport-local').Strategy;
const db = require('../models');
const logger = require('winston');
@ -11,6 +12,7 @@ module.exports = new PassportLocalStrategy(
},
(req, username, password, done) => {
logger.debug(`verifying loggin attempt ${username} ${password}`);
let userInfo = {};
return db.User
.findOne({where: {userName: username}})
.then(user => {
@ -18,13 +20,35 @@ module.exports = new PassportLocalStrategy(
logger.debug('no user found');
return done(null, false, {message: 'Incorrect username or password.'});
}
if (!user.validPassword(password, user.password)) {
logger.debug('user found:', user.dataValues);
logger.debug('...comparing password...');
return user.comparePassword(password, (passwordErr, isMatch) => {
if (passwordErr) {
logger.error('passwordErr:', passwordErr);
return done(passwordErr);
}
if (!isMatch) {
logger.debug('incorrect password');
return done(null, false, {message: 'Incorrect username or password.'});
}
logger.debug('user found:', user.dataValues);
return user.getChannel().then(channel => {
return done(null, user);
logger.debug('...password was a match...');
userInfo['id'] = user.id;
userInfo['userName'] = user.userName;
// get the User's channel info
return user.getChannel()
.then(channel => {
userInfo['channelName'] = channel.channelName;
userInfo['channelClaimId'] = channel.channelClaimId;
return db.getShortChannelIdFromLongChannelId(channel.channelClaimId, channel.channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
return done(null, userInfo);
})
.catch(error => {
throw error;
});
});
})
.catch(error => {

View file

@ -11,8 +11,8 @@ module.exports = new PassportLocalStrategy(
passReqToCallback: true, // we want to be able to read the post body message parameters in the callback
},
(req, username, password, done) => {
logger.debug(`new channel signup request: ${username} ${password}`);
let user;
logger.verbose(`new channel signup request. user: ${username} pass: ${password} .`);
let userInfo = {};
// server-side validaton of inputs (username, password)
// create the channel and retrieve the metadata
@ -23,34 +23,42 @@ module.exports = new PassportLocalStrategy(
userName: username,
password: password,
};
logger.debug('userData >', userData);
logger.verbose('userData >', userData);
// create user record
const channelData = {
channelName : `@${username}`,
channelClaimId: tx.claim_id,
};
logger.debug('channelData >', channelData);
logger.verbose('channelData >', channelData);
// create certificate record
const certificateData = {
claimId: tx.claim_id,
name : `@${username}`,
// address,
};
logger.debug('certificateData >', certificateData);
logger.verbose('certificateData >', certificateData);
// save user and certificate to db
return Promise.all([db.User.create(userData), db.Channel.create(channelData), db.Certificate.create(certificateData)]);
})
.then(([newUser, newChannel, newCertificate]) => {
user = newUser;
logger.debug('user and certificate successfully created');
logger.verbose('user and certificate successfully created');
logger.debug('user result >', newUser.dataValues);
logger.debug('user result >', newChannel.dataValues);
logger.debug('certificate result >', newCertificate.dataValues);
userInfo['id'] = newUser.id;
userInfo['userName'] = newUser.userName;
logger.verbose('channel result >', newChannel.dataValues);
userInfo['channelName'] = newChannel.channelName;
userInfo['channelClaimId'] = newChannel.channelClaimId;
logger.verbose('certificate result >', newCertificate.dataValues);
// associate the instances
return Promise.all([newCertificate.setChannel(newChannel), newChannel.setUser(newUser)]);
}).then(() => {
logger.debug('user and certificate successfully associated');
return done(null, user);
})
.then(() => {
logger.verbose('user and certificate successfully associated');
return db.getShortChannelIdFromLongChannelId(userInfo.channelClaimId, userInfo.channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
return done(null, userInfo);
})
.catch(error => {
logger.error('signup error', error);

View file

@ -1,276 +0,0 @@
/* GENERAL */
/* TEXT */
body, button, input, textarea, label, select, option {
font-family: serif;
}
p {
padding-left: 0.3em;
}
.center-text {
text-align: center;
}
.url-text {
margin:0px;
padding:0px;
}
/* HEADERS */
h1 {
font-size: x-large;
}
h2 {
font-size: medium;
margin-top: 1em;
border-top: 1px #999 solid;
background-color: lightgray;
padding: 6px;
}
h3 {
color: black;;
}
.h3--secondary {
color: lightgray;
}
h4 {
padding: 3px;
}
/* CONTAINERS */
.wrapper {
margin-left: 20%;
width:60%;
}
.full {
clear: both;
}
.main {
float: left;
width: 65%;
}
.panel {
overflow: auto;
word-wrap: break-word;
}
.sidebar {
float: right;
width: 33%;
}
footer {
display: inline-block;
width: 100%;
margin-bottom: 2px;
padding-bottom: 2px;
border-bottom: 1px lightgrey solid;
margin-top: 2px;
padding-top: 2px;
border-top: 1px lightgrey solid;
text-align: center;
color: grey;
}
/* COLUMNS AND ROWS */
.col-left, .col-right {
overflow: auto;
margin: 0px;
width: 48%;
}
.col-left {
padding: 5px 10px 5px 0px;
float: left;
}
.col-right {
padding: 5px 0px 5px 10px;
float: right;
}
.row {
padding: 1em 2% 1em 2%;
margin: 0px;
}
.row--wide {
padding-right: 0px;
padding-left: 0px;
}
.row--thin {
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.top-bar {
margin: 2em 0px 2px 0px;
padding: 0px 0px 2px 0px;
border-bottom: 1px lightgrey solid;
overflow: auto;
text-align: right;
vertical-align: text-bottom;
}
.column {
display: inline-block;
padding: 0px;
margin: 0px;
}
.column--1 {
width: 8%;
}
.column--2 {
width: 16%;
}
.column--3 {
width: 24%;
}
.column--4 {
width: 32%;
}
.column--5 {
width: 40%;
}
.column--6 {
width: 48%;
}
.column--7 {
width: 56%;
}
.column--8 {
width: 64%;
}
.column--9 {
width: 72%;
}
.column--10 {
width: 80%;
}
.column--11 {
width: 88%;
}
.column--12 {
width: 96%;
}
/* LINKS */
a, a:visited {
color: blue;
text-decoration: none;
}
/* ERROR MESSAGES */
.info-message {
font-weight: bold;
}
.info-message--success {
color: green;
}
.info-message--failure {
color: red;
}
/* INPUT FIELDS */
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset;
}
.label, .input-text, .select, .textarea {
font-size: medium;
padding: 0.3em;
outline: none;
border: 0px;
background-color: white;
}
.input-text--primary, .select--primary, .textarea--primary {
border-bottom: 1px solid blue;
}
.input-text--primary:focus, .select--primary:focus, .textarea--primary:focus {
border-bottom: 1px solid grey;
}
.input-checkbox, .input-textarea {
border: 1px solid grey;
}
/* BUTTONS */
button {
border: 1px solid black;
padding: 0.5em;
margin: 0.5em 0.3em 0.5em 0.3em;
color: black;
background-color: white;
}
button:hover {
border: 1px solid blue;
color: white;
background-color: blue;
}
button:active{
border: 1px solid blue;
color: white;
background-color: white;
}
/* TABLES */
table {
width: 100%;
text-align: left;
}
/* other */
.stop-float {
clear: both;
}
.toggle-link {
float: right;
}
.wrap-words {
word-wrap: break-word;
}

View file

@ -1,185 +0,0 @@
/* top bar */
#logo, #title {
float: left;
}
#logo {
height: 1.5em;
}
#title {
margin: 2px 5px 2px 5px;
}
.top-bar-left {
float: left;
}
.top-bar-tagline {
font-style: italic;
color: grey;
}
.top-bar-right {
margin-left: 0.5em;
}
/* publish */
#drop-zone {
border: 1px dashed lightgrey;
padding: 1em;
height: 13em;
background: #F5F0EF;
}
#asset-preview-holder {
width: 100%;
margin-bottom: 1em;
}
/* show routes */
.show-asset {
width: 100%;
margin-bottom: 1em;
margin-top: 1em;
}
.show-asset-lite {
margin: 0px;
}
.panel.links {
font-size: small;
}
input.link {
width: 80%;
}
button.copy-button {
padding: 4px;
float: right;
}
.share-option {
margin-bottom: 1em;
}
.metadata-table {
font-size: small;
border-collapse: collapse;
margin-bottom: 1em;
}
.metadata-row {
border-bottom: 1px solid lightgrey;
margin: 2px;
}
.left-column {
width: 30%;
font-weight: bold;
vertical-align: top;
}
/* trending claims */
.grid-item {
width: 23%;
margin: 0px 1% 20px 1%;
}
/* learn more */
.learn-more {
text-align: center;
border-top: 1px solid lightgrey;
}
/* examples */
.example {
clear: both;
width: 100%;
margin-bottom: 15px;
overflow: auto;
}
.example-image, .example-code {
float: left;
margin: 2%;
}
.example-image {
width: 21%;
}
.example-code {
float: right;
padding: 4%;
width: 62%;
background-color: lightgrey;
font-family: monospace;
color: #666;
word-wrap: break-word;
}
/* contribute */
#github-logo {
float: right;
height: 1em;
}
/* content lists */
.content-list-card {
margin-top: 2px;
padding-top: 2px;
border-top: 1px lightgrey solid;
overflow: auto;
position: relative;
}
.content-list-card-link {
position:absolute;
width:100%;
height:100%;
top:0;
left: 0;
z-index: 1;
}
.content-list-asset {
width: 20%;
float: left;
margin: 5px 30px 5px 0px;
}
.content-list-title {
color: black;
font-weight: bold;
}
.content-list-details {
word-wrap: break-word;
}
.content-list-details > ul {
position: relative;
z-index: 2;
list-style: none;
list-style-type: none;
}
.content-list-card:hover {
background-color: #F5F0EF;
}
/* statistics */
.totals-row {
border-top: 1px solid grey;
border-bottom: 1px solid grey;
font-weight: bold;
}
.stats-table-url {
word-wrap: break-word;
}

View file

@ -0,0 +1,578 @@
@font-face {
font-family: 'Lekton';
src: url('../font/Lekton/Lekton-Regular.ttf');
}
@font-face {
font-family: 'Lekton';
src: url('../font/Lekton/Lekton-Bold.ttf');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Lekton';
src: url('../font/Lekton/Lekton-Italic.ttf');
font-weight: normal;
font-style: italic;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body, .flex-container {
display: -webkit-flex;
display: flex;
}
body, .flex-container--column {
-webkit-flex-direction: column;
flex-direction: column;
}
.flex-container--row {
-webkit-flex-direction: row;
flex-direction: row;
justify-content: space-between;
}
.flex-container--wrap {
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
}
.flex-container--align-center {
align-items: center;
}
.flex-container--justify-center {
justify-content: center;
}
.flex-container--justify-space-between {
justify-content: space-between;
}
.flex-container--left-bottom {
justify-content: flex-start;
align-items: baseline;
}
.hidden {
display: none;
}
/* TEXT */
body, button, input, textarea, label, select, option {
font-family: 'Lekton', monospace;
font-size: large;
}
h3, p {
font-size: x-large;
}
.text--large {
font-size: 2rem;
}
.pull-quote {
font-size: 3rem;
margin-top: 1rem;
}
.fine-print {
font-size: small;
}
.blue {
color: #4156C5;
}
.blue--underlined {
color: #4156C5;
text-decoration: underline;
}
/* TOOL TIPS */
/* Tooltip container */
.tooltip {
position: relative;
}
/* Tooltip text */
.tooltip > .tooltip-text {
visibility: hidden;
width: 15em;
background-color: #9b9b9b;
color: #fff;
text-align: center;
padding: 0.5em;
/* Position the tooltip text */
position: absolute;
z-index: 1;
bottom: 110%;
left: 50%;
margin-left: -8em; /* Use half of the width (120/2 = 60), to center the tooltip */
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover > .tooltip-text {
visibility: visible;
}
/* arrow at bottom of tooltip text */
.tooltip > .tooltip-text::after {
content: " ";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #9b9b9b transparent transparent transparent;
}
/* LINKS */
a, a:visited {
text-decoration: none;
}
.link--primary, .link--primary:visited {
color: #4156C5;
}
.link--nav {
color: black;
border-bottom: 2px solid white;
}
.link--nav:hover {
color: #4156C5;
}
.link--nav-active {
color: #4156C5;
border-bottom: 2px solid #4156C5;
}
/* COLUMNS AND ROWS */
.row {
clear: both;
margin: 0px;
}
.row--padded {
padding: 3rem;
}
.row--margined {
margin: 3rem;
}
.row--wide {
padding-right: 0px;
padding-left: 0px;
}
.row--short {
padding-top: 0px;
padding-bottom: 0px;
}
.row--tall {
flex: 1 0 auto;
}
.row--no-top {
padding-top: 0px;
}
.row--no-bottom {
padding-bottom: 0px;
}
.row--no-right {
padding-right: 0px;
}
.column {
display: inline-block;
padding: 0px;
margin: 0px;
}
.column--1 {
width: 10%;
}
.column--2 {
width: 20%;
}
.column--3 {
width: 30%;
}
.column--4 {
width: 40%;
}
.column--5 {
width: 50%;
}
.column--6 {
width: 60%;
}
.column--7 {
width: 70%;
}
.column--8 {
width: 80%;
}
.column--9 {
width: 90%;
}
.column--10 {
width: 100%;
}
/* ALIGNMENT */
.align-content-left {
text-align: left;
}
.align-content-center {
text-align: center;
}
.align-content-right {
text-align: right;
}
.align-content-top {
vertical-align: top;
}
.align-content-right {
vertical-align: bottom;
}
/* ERROR MESSAGES */
.info-message--success, .info-message--failure {
font-size: medium;
margin: 0px;
padding: 0.3em;
}
.info-message--success {
color: green;
}
.info-message--failure {
color: red;
}
.info-message-placeholder {
}
/* INPUT FIELDS */
/* blocks */
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset;
}
.label, .input-text, .select, .textarea, .text--large {
margin: 0px;
padding: 0.3em;
outline: none;
border: 0px;
background-color: white;
display: inline-block;
}
.input-disabled {
border: 1px solid black;
padding: 0.5em;
margin: 0px;
color: black;
background-color: white;
}
option {
font-family: monospace;
}
.input-checkbox {
border: 1px solid black;
background: white;
}
.input-file {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.input-radio, .label--pointer {
cursor: pointer;
}
#claim-name-input {
}
#input-success-claim-name {
}
.span--relative {
position: relative;
}
.span--absolute {
position: absolute;
bottom: 0px;
right: 0px;
}
/* modifiers */
.select--arrow {
-moz-appearance:none;
-webkit-appearance: none;
background: url('../icon/Shape.svg') no-repeat right;
cursor: pointer;
padding-right: 1.5em;
}
.input-text--primary, .select--primary {
border-bottom: 1px solid #9b9b9b;
}
.input-text--primary:focus, .select--primary:focus {
border-bottom: 1px solid #9b9b9b;
}
.textarea--primary {
border-bottom: 1px solid #9b9b9b;
}
.textarea--primary:focus {
border-bottom: 1px solid #9b9b9b;
}
.input-text--full-width, .textarea--full-width {
width: calc(100% - 0.6em);
}
.input-disabled--full-width {
width: calc(100% - 1em - 2px);
}
.url-text--primary, .url-text--secondary {
margin:0px;
padding:0px;
}
.url-text--primary {
color: black;
}
.url-text--secondary {
color: #9b9b9b;
}
/* BUTTONS */
button {
cursor: pointer;
}
.button--primary {
border: 1px solid black;
padding: 0.5em;
margin: 0.5em 0.3em 0.5em 0.3em;
color: black;
background-color: white;
}
.button--primary:hover {
border: 1px solid #4156C5;
color: white;
background-color: #4156C5;
}
.button--primary:active{
border: 1px solid #4156C5;
color: white;
background-color: white;
}
.button--large{
margin: 0px;
width: calc(100% - 2px);
padding: 2rem;
font-size: x-large;
}
.button--cancel{
border: 0px;
background-color: white;
color: #9b9b9b;
}
/* TABLES */
table {
width: 100%;
text-align: left;
}
/* NAV BAR */
.nav-bar {
border-bottom: 0.5px solid #cacaca;
}
.nav-bar--left {
align-self: center;
}
.nav-bar-tagline {
font-size: small;
}
.nav-bar-link {
padding: 1.5rem;
display: inline-block;
}
/* PUBLISH FORM */
.dropzone {
border: 2px dashed #9b9b9b;
text-align: center;
position: relative;
}
.dropzone:hover, .dropzone--drag-over {
border: 2px dashed #4156C5;
cursor: pointer;
}
#primary-dropzone-instructions, #dropbzone-dragover {
z-index: -1;
}
.position-absolute {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
}
#asset-preview-holder {
position: relative;
}
#asset-preview {
display: block;
padding: 0.5rem;
width: calc(100% - 1rem);
}
/* Show page */
.video-show, .gifv-show, .image-show {
display: block;
width: 100%;
}
#video-player {
background-color: black;
cursor: pointer;
}
.show-asset-light {
max-width: 50%;
}
/* item lists */
.content-list-item-asset {
width: 90%;
}
/* progress bar */
.progress-bar--inactive {
color: lightgrey;
}
.progress-bar--active {
color: #4156C5;
}
/* other */
.wrap-words {
word-wrap: break-word;
}
#new-release-banner {
font-size: small;
background-color: #4156C5;
color: white;
text-align: center;
}
/* ---- grid items ---- */
.grid-item {
width: calc(33% - 2rem);
padding: 0px;
margin: 1rem;
float: left;
border: 0.5px solid white;
}
.grid-item-image {
width: 100%;
}
.grid-item-details {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
cursor: pointer;
}
.grid-item-details-text {
font-size: medium;
margin: 0px;
text-align: center;
padding: 1em 0px 1em 0px;
width: 100%;
}

View file

@ -1,59 +1,96 @@
@media (max-width: 1250px) {
.wrapper {
margin-left: 10%;
width:80%;
}
@media (max-width: 1050px) {
.nav-bar--center {
display: none;
}
@media (max-width: 1000px) {
.wrapper {
margin-left: 10%;
width:80%;
}
.main {
float: none;
width: 100%;
margin-right: 0px;
padding-right: 0px;
border-right: 0px;
margin-bottom: 5px;
}
.sidebar {
border-top: 1px solid lightgray;
float: none;
.column--med-10 {
width: 100%;
}
}
@media (max-width: 750px ) {
.col-left, .col-right {
float: none;
margin: 0px;
padding: 0px;
body, button, input, textarea, label, select, option, p, h3 {
font-size: medium;
}
.pull-quote {
font-size: 1.5rem;
}
.column--sml-10 {
width: 100%;
}
.col-right {
padding-top: 20px;
.nav-bar-logo {
height: 1rem;
}
.all-claims-asset {
width:30%;
.link--nav, .link--nav-active {
padding: 1rem 0.5rem 1rem 0.5rem;
}
.all-claims-details {
.select--arrow {
padding-right: 1.5em;
}
.show-asset-light {
max-width: 100%;
}
}
@media (max-width: 500px) {
.nav-bar-logo {
height: 1rem;
}
.row--padded {
padding: 1rem;
}
.row--short {
padding-top: 0px;
padding-bottom: 0px;
}
.row--margined {
margin: 1rem;
}
body, button, input, textarea, label, select, option, p, h3, .fine-print {
font-size: small;
}
.show-asset-lite {
width: 100%;
.pull-quote, .text--large, .button--large {
font-size: medium;
}
.top-bar-tagline {
clear: both;
text-align: left;
width: 100%;
.grid-item {
width: calc(100% - 2em);
float: none;
padding: 1em;
margin: 0px;
}
.info-message--success, .info-message--failure {
font-size: small;
}
}
@media (max-width: 360px) {
body, button, input, textarea, label, select, option, p, h3, .fine-print {
font-size: x-small;
}
.pull-quote, .text--large, .button--large {
font-size: small;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,93 @@
Copyright (c) 2008-2010, Isia Urbino (http://www.isiaurbino.net)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="23px" height="22px" viewBox="0 0 23 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47 (45396) - http://www.bohemiancoding.com/sketch -->
<title>Fill 5114 + Fill 5115 + Fill 5116</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Explore" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-59.000000, -113.000000)" id="Fill-5114-+-Fill-5115-+-Fill-5116" fill="#9B9B9B">
<g transform="translate(59.000000, 113.000000)">
<path d="M14.1990756,0.96478816 C9.91555556,0.96478816 6.43018667,4.37190137 6.43018667,8.56379458 C6.43018667,12.7564878 9.91555556,16.163601 14.1990756,16.163601 C18.4834133,16.163601 21.9679644,12.7564878 21.9679644,8.56379458 C21.9679644,4.37190137 18.4834133,0.96478816 14.1990756,0.96478816 L14.1990756,0.96478816 Z M14.1990756,16.9635806 C9.46414222,16.9635806 5.61240889,13.1964766 5.61240889,8.56379458 C5.61240889,3.93191257 9.46414222,0.164008559 14.1990756,0.164008559 C18.9331911,0.164008559 22.7857422,3.93191257 22.7857422,8.56379458 C22.7857422,13.1964766 18.9331911,16.9635806 14.1990756,16.9635806 L14.1990756,16.9635806 Z" id="Fill-5114"></path>
<path d="M0.910186667,21.9642532 C0.805511111,21.9642532 0.700835556,21.9242542 0.621511111,21.8442563 C0.461226667,21.6922601 0.461226667,21.4354667 0.621511111,21.2834706 L8.12789333,13.9404576 C8.28817778,13.7804617 8.54659556,13.7804617 8.70606222,13.9404576 C8.86634667,14.0916538 8.86634667,14.3476472 8.70606222,14.4996434 L1.19968,21.8442563 C1.11953778,21.9242542 1.01486222,21.9642532 0.910186667,21.9642532" id="Fill-5115"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="12px" height="5px" viewBox="0 0 12 5" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47 (45396) - http://www.bohemiancoding.com/sketch -->
<title>Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Nav-(Upload)" transform="translate(-993.000000, -29.000000)" stroke-width="0.8" stroke="#2F2F2F">
<g id="Group-13">
<g id="chevron-down" transform="translate(994.000000, 29.000000)">
<polyline id="Shape" points="0 0 5 5 10 0"></polyline>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 858 B

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="20px" height="22px" viewBox="0 0 20 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47 (45396) - http://www.bohemiancoding.com/sketch -->
<title>upload</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Nav-(Profile)" transform="translate(-620.000000, -20.000000)" stroke="#000000">
<g id="Group-13">
<g id="Group-12">
<g id="Group-8" transform="translate(621.000000, 21.000000)">
<g id="upload">
<path d="M0,15 L0,18 C0,19.1045695 0.8954305,20 2,20 L16,20 C17.1045695,20 18,19.1045695 18,18 L18,15" id="Shape"></path>
<polyline id="Shape" points="13 4 9 0 5 4"></polyline>
<path d="M9,0 L9,14" id="Shape"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

BIN
public/assets/img/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1 @@
const EMAIL_FORMAT = 'ERROR_EMAIL_FORMAT';

View file

@ -0,0 +1,56 @@
function showChannelCreateInProgressDisplay () {
const publishChannelForm = document.getElementById('publish-channel-form');
publishChannelForm.hidden = true;
const inProgress = document.getElementById('channel-publish-in-progress');
inProgress.hidden = false;
createProgressBar(document.getElementById('create-channel-progress-bar'), 12);
}
function showChannelCreateDoneDisplay() {
const inProgress = document.getElementById('channel-publish-in-progress');
inProgress.hidden=true;
const done = document.getElementById('channel-publish-done');
done.hidden = false;
}
function showChannelCreationError(msg) {
const inProgress = document.getElementById('channel-publish-in-progress');
inProgress.innerText = msg;
}
function publishNewChannel (event) {
const userName = document.getElementById('new-channel-name').value;
const password = document.getElementById('new-channel-password').value;
// prevent default so this script can handle submission
event.preventDefault();
// validate submission
validateNewChannelSubmission(userName, password)
.then(() => {
showChannelCreateInProgressDisplay();
return sendAuthRequest(userName, password, '/signup') // post the request
})
.then(result => {
showChannelCreateDoneDisplay();
// refresh window logged in as the channel
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId); // set cookies
})
.then(() => {
if (window.location.pathname === '/') {
// remove old channel and replace with new one & select it
replaceChannelOptionInPublishChannelSelect();
// remove old channel and replace with new one & select it
replaceChannelOptionInNavBarChannelSelect();
} else {
window.location = '/';
}
})
.catch(error => {
if (error.name === 'ChannelNameError' || error.name === 'ChannelPasswordError'){
const channelNameErrorDisplayElement = document.getElementById('input-error-channel-name');
showError(channelNameErrorDisplayElement, error.message);
} else {
console.log('signup failure:', error);
showChannelCreationError('Unfortunately, Spee.ch encountered an error while creating your channel. Please let us know in slack!');
}
})
}

View file

@ -0,0 +1,56 @@
function triggerFileChooser(fileInputId, event) {
document.getElementById(fileInputId).click();
}
function drop_handler(event) {
event.preventDefault();
// if dropped items aren't files, reject them
var dt = event.dataTransfer;
if (dt.items) {
if (dt.items[0].kind == 'file') {
var droppedFile = dt.items[0].getAsFile();
previewAndStageFile(droppedFile);
}
}
}
function dragover_handler(event) {
event.preventDefault();
}
function dragend_handler(event) {
var dt = event.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
event.dataTransfer.clearData();
}
}
function dragenter_handler(event) {
var thisDropzone = document.getElementById(event.target.id);
thisDropzone.setAttribute('class', 'dropzone dropzone--drag-over row row--margined row--padded row--tall flex-container flex-container--column flex-container--justify-center');
thisDropzone.firstElementChild.setAttribute('class', 'hidden');
thisDropzone.lastElementChild.setAttribute('class', '');
}
function dragexit_handler(event) {
var thisDropzone = document.getElementById(event.target.id);
thisDropzone.setAttribute('class', 'dropzone row row--tall row--margined row--padded flex-container flex-container--column flex-container--justify-center');
thisDropzone.firstElementChild.setAttribute('class', '');
thisDropzone.lastElementChild.setAttribute('class', 'hidden');
}
function preview_onmouseenter_handler () {
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'flex-container flex-container--column flex-container--justify-center position-absolute');
document.getElementById('asset-preview').style.opacity = 0.2;
}
function preview_onmouseleave_handler () {
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'hidden');
document.getElementById('asset-preview').style.opacity = 1;
}

View file

@ -1,15 +1,14 @@
function getRequest (url) {
console.log('making GET request to', url)
return new Promise((resolve, reject) => {
let xhttp = new XMLHttpRequest();
xhttp.open('GET', url, true);
xhttp.responseType = 'json';
xhttp.onreadystatechange = () => {
if (xhttp.readyState == 4 ) {
console.log(xhttp);
if ( xhttp.status == 200) {
console.log('response:', xhttp.response);
resolve(xhttp.response);
} else if (xhttp.status == 401) {
reject('Wrong username or password');
} else {
reject('request failed with status:' + xhttp.status);
};
@ -20,7 +19,6 @@ function getRequest (url) {
}
function postRequest (url, params) {
console.log('making POST request to', url)
return new Promise((resolve, reject) => {
let xhttp = new XMLHttpRequest();
xhttp.open('POST', url, true);
@ -28,10 +26,10 @@ function postRequest (url, params) {
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhttp.onreadystatechange = () => {
if (xhttp.readyState == 4 ) {
console.log(xhttp);
if ( xhttp.status == 200) {
console.log('response:', xhttp.response);
resolve(xhttp.response);
} else if (xhttp.status == 401) {
reject( new AuthenticationError('Wrong username or password'));
} else {
reject('request failed with status:' + xhttp.status);
};
@ -63,21 +61,98 @@ function toggleSection(event){
}
function createProgressBar(element, size){
var x = 1;
var x = 0;
var adder = 1;
function addOne(){
var bars = '<p>|';
for (var i = 0; i < x; i++){ bars += ' | '; }
bars += '</p>';
element.innerHTML = bars;
// create the bar holder & place it
var barHolder = document.createElement('p');
for (var i = 0; i < size; i++) {
const bar = document.createElement('span');
bar.innerText = '| ';
bar.setAttribute('class', 'progress-bar progress-bar--inactive');
barHolder.appendChild(bar);
}
element.appendChild(barHolder);
// get the bars
const bars = document.getElementsByClassName('progress-bar');
// function to update the bars' classes
function updateOneBar(){
// update the appropriate bar
if (x > -1 && x < size){
if (adder === 1){
bars[x].setAttribute('class', 'progress-bar progress-bar--active');
} else {
bars[x].setAttribute('class', 'progress-bar progress-bar--inactive');
}
}
// set x
if (x === size){
adder = -1;
} else if ( x === 0){
} else if ( x === -1){
adder = 1;
}
// update the adder
x += adder;
};
setInterval(addOne, 300);
// start updater
setInterval(updateOneBar, 300);
}
function setCookie(key, value) {
document.cookie = `${key}=${value}`;
}
function getCookie(cname) {
const name = cname + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function checkCookie() {
const channelName = getCookie("channel_name");
if (channelName != "") {
console.log(`cookie found for ${channelName}`);
} else {
console.log('no channel_name cookie found');
}
}
function clearCookie(name) {
document.cookie = `${name}=; expires=Thu, 01-Jan-1970 00:00:01 GMT;`;
}
function setUserCookies(channelName, channelClaimId, shortChannelId) {
setCookie('channel_name', channelName)
setCookie('channel_claim_id', channelClaimId);
setCookie('short_channel_id', shortChannelId);
}
function clearUserCookies() {
clearCookie('channel_name')
clearCookie('channel_claim_id');
clearCookie('short_channel_id');
}
function copyToClipboard(event){
var elementToCopy = event.target.dataset.elementtocopy;
var element = document.getElementById(elementToCopy);
var errorElement = 'input-error-copy-text' + elementToCopy;
element.select();
try {
document.execCommand('copy');
} catch (err) {
showError(errorElement, 'Oops, unable to copy');
}
}
// Create new error objects, that prototypically inherit from the Error constructor
@ -112,3 +187,27 @@ function ChannelPasswordError(message) {
}
ChannelPasswordError.prototype = Object.create(Error.prototype);
ChannelPasswordError.prototype.constructor = ChannelPasswordError;
function AuthenticationError(message) {
this.name = 'AuthenticationError';
this.message = message || 'Default Message';
this.stack = (new Error()).stack;
}
AuthenticationError.prototype = Object.create(Error.prototype);
AuthenticationError.prototype.constructor = AuthenticationError;
function showAssetDetails(event) {
var thisAssetHolder = document.getElementById(event.target.id);
var thisAssetImage = thisAssetHolder.firstElementChild;
var thisAssetDetails = thisAssetHolder.lastElementChild;
thisAssetImage.style.opacity = 0.2;
thisAssetDetails.setAttribute('class', 'grid-item-details flex-container flex-container--column flex-container--justify-center');
}
function hideAssetDetails(event) {
var thisAssetHolder = document.getElementById(event.target.id);
var thisAssetImage = thisAssetHolder.firstElementChild;
var thisAssetDetails = thisAssetHolder.lastElementChild;
thisAssetImage.style.opacity = 1;
thisAssetDetails.setAttribute('class', 'hidden');
}

View file

@ -0,0 +1,79 @@
function replaceChannelOptionInPublishChannelSelect() {
// remove the old channel option
const oldChannel = document.getElementById('publish-channel-select-channel-option')
if (oldChannel){
oldChannel.parentNode.removeChild(oldChannel);
}
// get channel details from cookies
const loggedInChannel = getCookie('channel_name');
// create new channel option
const newChannelOption = document.createElement('option');
newChannelOption.setAttribute('value', loggedInChannel);
newChannelOption.setAttribute('id', 'publish-channel-select-channel-option');
newChannelOption.setAttribute('selected', '');
newChannelOption.innerText = loggedInChannel;
// add the new option
const channelSelect = document.getElementById('channel-name-select');
channelSelect.insertBefore(newChannelOption, channelSelect.firstChild);
// carry out channel selection
toggleSelectedChannel(loggedInChannel);
}
function replaceChannelOptionInNavBarChannelSelect () {
// remove the old channel option
const oldChannel = document.getElementById('nav-bar-channel-select-channel-option');
if (oldChannel){
oldChannel.parentNode.removeChild(oldChannel);
}
// get channel details from cookies
const loggedInChannel = getCookie('channel_name');
// create new channel option & select it
const newChannelOption = document.createElement('option');
newChannelOption.setAttribute('value', loggedInChannel);
newChannelOption.setAttribute('id', 'nav-bar-channel-select-channel-option');
newChannelOption.setAttribute('selected', '');
newChannelOption.innerText = loggedInChannel;
// add the new option
const channelSelect = document.getElementById('nav-bar-channel-select');
channelSelect.style.display = 'inline-block';
channelSelect.insertBefore(newChannelOption, channelSelect.firstChild);
// hide login
const navBarLoginLink = document.getElementById('nav-bar-login-link');
navBarLoginLink.style.display = 'none';
}
function loginToChannel (event) {
const userName = document.getElementById('channel-login-name-input').value;
const password = document.getElementById('channel-login-password-input').value;
// prevent default
event.preventDefault()
validateNewChannelLogin(userName, password)
.then(() => {
// send request
return sendAuthRequest(userName, password, '/login')
})
.then(result => {
// update session cookie with new channel name and id's
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId); // replace the current cookies
})
.then(() => {
// update channel selection
if (window.location.pathname === '/') {
// remove old channel and replace with new one & select it
replaceChannelOptionInPublishChannelSelect();
// remove old channel and replace with new one & select it
replaceChannelOptionInNavBarChannelSelect();
} else {
window.location = '/';
}
})
.catch(error => {
const loginErrorDisplayElement = document.getElementById('login-error-display-element');
if (error.name){
showError(loginErrorDisplayElement, error.message);
} else {
showError(loginErrorDisplayElement, 'There was an error logging into your channel');
}
})
}

View file

@ -0,0 +1,15 @@
function toggleNavBarSelection (value) {
const selectedOption = value;
if (selectedOption === 'LOGOUT') {
// remove session cookies
clearUserCookies();
// send logout request to server
window.location.href = '/logout';
} else if (selectedOption === 'VIEW') {
// get channel info
const channelName = getCookie('channel_name');
const channelClaimId = getCookie('channel_claim_id');
// redirect to channel page
window.location.href = `/${channelName}:${channelClaimId}`;
}
}

View file

@ -1,62 +1,48 @@
/* drop zone functions */
function drop_handler(ev) {
ev.preventDefault();
// if dropped items aren't files, reject them
var dt = ev.dataTransfer;
if (dt.items) {
if (dt.items[0].kind == 'file') {
var droppedFile = dt.items[0].getAsFile();
previewAndStageFile(droppedFile);
}
}
}
function dragover_handler(ev) {
ev.preventDefault();
}
function dragend_handler(ev) {
var dt = ev.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
ev.dataTransfer.clearData();
}
}
/* publish functions */
// update the publish status
function updatePublishStatus(msg){
document.getElementById('publish-status').innerHTML = msg;
function cancelPublish () {
window.location.href = '/';
}
// When a file is selected for publish, validate that file and
// stage it so it will be ready when the publish button is clicked.
function previewAndStageFile(selectedFile){
var previewHolder = document.getElementById('asset-preview-holder');
var dropzone = document.getElementById('drop-zone');
var previewReader = new FileReader();
var nameInput = document.getElementById('claim-name-input');
const publishForm = document.getElementById('publish-form');
const assetPreview = document.getElementById('asset-preview-target');
const primaryDropzone = document.getElementById('primary-dropzone');
const previewReader = new FileReader();
const nameInput = document.getElementById('claim-name-input');
const fileSelectionInputError = document.getElementById('input-error-file-selection');
const thumbnailSelectionTool = document.getElementById('publish-thumbnail');
const thumbnailSelectionInput = document.getElementById('claim-thumbnail-input');
// validate the file's name, type, and size
try {
validateFile(selectedFile);
} catch (error) {
showError('input-error-file-selection', error.message);
showError(fileSelectionInputError, error.message);
return;
}
// set the image preview, if an image was provided
if (selectedFile.type !== 'video/mp4') {
if (selectedFile.type === 'image/gif') {
assetPreview.innerHTML = `<p>loading preview...</p>`
}
previewReader.readAsDataURL(selectedFile);
previewReader.onloadend = function () {
dropzone.style.display = 'none';
previewHolder.style.display = 'block';
previewHolder.innerHTML = '<img width="100%" src="' + previewReader.result + '" alt="image preview"/>';
assetPreview.innerHTML = '<img id="asset-preview" src="' + previewReader.result + '" alt="image preview"/>';
};
// clear & hide the thumbnail selection input
thumbnailSelectionInput.value = '';
thumbnailSelectionTool.hidden = true;
} else {
assetPreview.innerHTML = `<img id="asset-preview" src="/assets/img/black_video_play.jpg"/>`;
// clear & show the thumbnail selection input
thumbnailSelectionInput.value = '';
thumbnailSelectionTool.hidden = false;
}
// hide the drop zone
primaryDropzone.setAttribute('class', 'hidden');
publishForm.setAttribute('class', 'row')
// set the name input value to the image name if none is set yet
if (nameInput.value === "") {
var filename = selectedFile.name.substring(0, selectedFile.name.indexOf('.'))
@ -67,12 +53,31 @@ function previewAndStageFile(selectedFile){
stagedFiles = [selectedFile];
}
// Validate the publish submission and then trigger publishing.
function publishSelectedImage(event) {
var claimName = document.getElementById('claim-name-input').value;
var channelName = document.getElementById('channel-name-select').value;
// Validate the publish submission and then trigger upload
function publishStagedFile(event) {
// prevent default so this script can handle submission
event.preventDefault();
// declare variables
const claimName = document.getElementById('claim-name-input').value;
let channelName = document.getElementById('channel-name-select').value;
const fileSelectionInputError = document.getElementById('input-error-file-selection');
const claimNameError = document.getElementById('input-error-claim-name');
const channelSelectError = document.getElementById('input-error-channel-select');
const publishSubmitError = document.getElementById('input-error-publish-submit');
let anonymousOrInChannel;
// replace channelName with 'anonymous' if appropriate
const radios = document.getElementsByName('anonymous-or-channel');
for (let i = 0; i < radios.length; i++) {
if (radios[i].checked) {
// do whatever you want with the checked radio
anonymousOrInChannel = radios[i].value;
// only one radio can be logically checked, don't check the rest
break;
}
}
if (anonymousOrInChannel === 'anonymous') {
channelName = null;
};
// validate, submit, and handle response
validateFilePublishSubmission(stagedFiles, claimName, channelName)
.then(() => {
@ -80,14 +85,14 @@ function publishSelectedImage(event) {
})
.catch(error => {
if (error.name === 'FileError') {
showError(document.getElementById('input-error-file-selection'), error.message);
showError(fileSelectionInputError, error.message);
} else if (error.name === 'NameError') {
showError(document.getElementById('input-error-claim-name'), error.message);
showError(claimNameError, error.message);
} else if (error.name === 'ChannelNameError'){
console.log(error);
showError(document.getElementById('input-error-channel-select'), error.message);
showError(channelSelectError, error.message);
} else {
showError(document.getElementById('input-error-publish-submit'), error.message);
showError(publishSubmitError, error.message);
}
return;
})

View file

@ -0,0 +1,23 @@
function playOrPause(video){
if (video.paused == true) {
video.play();
}
else{
video.pause();
}
}
// if a video player is present, set the listeners
const video = document.getElementById('video-player');
if (video) {
// add event listener for click
video.addEventListener('click', ()=> {
playOrPause(video);
});
// add event listener for space bar
document.body.onkeyup = (event) => {
if (event.keyCode == 32) {
playOrPause(video);
}
};
}

View file

@ -1,11 +1,11 @@
// validation function which checks the proposed file's type, size, and name
function validateFile(file) {
if (!file) {
console.log('no file found');
throw new Error('no file provided');
}
if (/'/.test(file.name)) {
console.log('file name had apostrophe in it');
throw new Error('apostrophes are not allowed in the file name');
}
// validate size and type
@ -13,20 +13,29 @@ function validateFile(file) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000){
console.log('file was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000){
throw new Error('Sorry, images are limited to 50 megabytes.');
console.log('file was too big');
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000){
console.log('file was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
console.log('file type is not supported');
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
}
}
// validation function that checks to make sure the claim name is valid
function validateClaimName (name) {
// ensure a name was entered
@ -36,7 +45,7 @@ function validateClaimName (name) {
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(name);
if (invalidCharacters) {
throw new NameError('"' + invalidCharacters + '" characters are not allowed in the url.');
throw new NameError('"' + invalidCharacters + '" characters are not allowed');
}
}
@ -49,7 +58,7 @@ function validateChannelName (name) {
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-,@]/g.exec(name);
if (invalidCharacters) {
throw new ChannelNameError('"' + invalidCharacters + '" characters are not allowed in the channel name.');
throw new ChannelNameError('"' + invalidCharacters + '" characters are not allowed');
}
}
@ -99,7 +108,6 @@ function checkAvailability(name, successDisplayElement, errorDisplayElement, val
// check to make sure it is available
isNameAvailable(name, apiUrl)
.then(result => {
console.log('result:', result)
if (result === true) {
hideError(errorDisplayElement);
showSuccess(successDisplayElement)
@ -121,45 +129,54 @@ function checkAvailability(name, successDisplayElement, errorDisplayElement, val
function checkClaimName(name){
const successDisplayElement = document.getElementById('input-success-claim-name');
const errorDisplayElement = document.getElementById('input-error-claim-name');
checkAvailability(name, successDisplayElement, errorDisplayElement, validateClaimName, isNameAvailable, 'Sorry, that url ending has been taken by another user', '/api/isClaimAvailable/');
checkAvailability(name, successDisplayElement, errorDisplayElement, validateClaimName, isNameAvailable, 'Sorry, that ending is already taken', '/api/isClaimAvailable/');
}
function checkChannelName(name){
const successDisplayElement = document.getElementById('input-success-channel-name');
const errorDisplayElement = document.getElementById('input-error-channel-name');
name = `@${name}`;
checkAvailability(name, successDisplayElement, errorDisplayElement, validateChannelName, isNameAvailable, 'Sorry, that Channel has been taken by another user', '/api/isChannelAvailable/');
checkAvailability(name, successDisplayElement, errorDisplayElement, validateChannelName, isNameAvailable, 'Sorry, that name is already taken', '/api/isChannelAvailable/');
}
// validation function which checks all aspects of the publish submission
function validateFilePublishSubmission(stagedFiles, claimName, channelName){
return new Promise(function (resolve, reject) {
// 1. make sure only 1 file was selected
// 1. make sure 1 file was staged
if (!stagedFiles) {
return reject(new FileError("Please select a file"));
reject(new FileError("Please select a file"));
return;
} else if (stagedFiles.length > 1) {
return reject(new FileError("Only one file is allowed at a time"));
reject(new FileError("Only one file is allowed at a time"));
return;
}
// 2. validate the file's name, type, and size
try {
validateFile(stagedFiles[0]);
} catch (error) {
return reject(error);
reject(error);
return;
}
// 3. validate that a channel was chosen
if (channelName === 'new' || channelName === 'login') {
return reject(new ChannelNameError("Please select a valid channel"));
reject(new ChannelNameError("Please log in to a channel"));
return;
};
// 4. validate the claim name
try {
validateClaimName(claimName);
} catch (error) {
return reject(error);
reject(error);
return;
}
// if all validation passes, check availability of the name
isNameAvailable(claimName, '/api/isClaimAvailable/')
.then(() => {
// if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?)
return isNameAvailable(claimName, '/api/isClaimAvailable/')
.then(result => {
if (result) {
resolve();
} else {
reject(new NameError('Sorry, that ending is already taken'));
}
})
.catch(error => {
reject(error);
@ -167,8 +184,9 @@ function validateFilePublishSubmission(stagedFiles, claimName, channelName){
});
}
// validation function which checks all aspects of the publish submission
function validateNewChannelSubmission(channelName, password){
// validation function which checks all aspects of a new channel submission
function validateNewChannelSubmission(userName, password){
const channelName = `@${userName}`;
return new Promise(function (resolve, reject) {
// 1. validate name
try {
@ -184,13 +202,35 @@ function validateNewChannelSubmission(channelName, password){
}
// 3. if all validation passes, check availability of the name
isNameAvailable(channelName, '/api/isChannelAvailable/') // validate the availability
.then(() => {
console.log('channel is avaliable');
.then(result => {
if (result) {
resolve();
} else {
reject(new ChannelNameError('Sorry, that name is already taken'));
}
})
.catch( error => {
console.log('error: channel is not avaliable');
console.log('error evaluating channel name availability', error);
reject(error);
});
});
}
// validation function which checks all aspects of a new channel login
function validateNewChannelLogin(userName, password){
const channelName = `@${userName}`;
return new Promise(function (resolve, reject) {
// 1. validate name
try {
validateChannelName(channelName);
} catch (error) {
return reject(error);
}
// 2. validate password
try {
validatePassword(password);
} catch (error) {
return reject(error);
}
resolve();
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

0
public/robots.txt Normal file
View file

View file

@ -4,10 +4,10 @@ const multipartMiddleware = multipart();
const db = require('../models');
const { publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri } = require('../helpers/lbryApi.js');
const { createPublishParams, validateFile, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const { createPublishParams, validateApiPublishRequest, validatePublishSubmission, cleanseNSFW, cleanseChannelName, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
const { authenticateApiPublish } = require('../auth/authentication.js');
const { authenticateChannelCredentials } = require('../auth/authentication.js');
module.exports = (app) => {
// route to run a claim_list request on the daemon
@ -25,7 +25,7 @@ module.exports = (app) => {
});
});
// route to check whether spee.ch has published to a claim
app.get('/api/isClaimAvailable/:name', ({ ip, originalUrl, params }, res) => {
app.get('/api/isClaimAvailable/:name', ({ params }, res) => {
// send response
checkClaimNameAvailability(params.name)
.then(result => {
@ -71,52 +71,79 @@ module.exports = (app) => {
});
});
// route to run a publish request on the daemon
app.post('/api/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl }, res) => {
// google analytics
sendGoogleAnalytics('PUBLISH', headers, ip, originalUrl);
// validate that a file was provided
const file = files.speech || files.null;
const name = body.name || file.name.substring(0, file.name.indexOf('.'));
const title = body.title || null;
const description = body.description || null;
const license = body.license || 'No License Provided';
const nsfw = body.nsfw || null;
const channelName = body.channelName || 'none';
const channelPassword = body.channelPassword || null;
logger.debug(`name: ${name}, license: ${license}, nsfw: ${nsfw}`);
app.post('/api/publish', multipartMiddleware, (req, res) => {
logger.debug('req:', req);
// validate that mandatory parts of the request are present
const body = req.body;
const files = req.files;
try {
validateFile(file, name, license, nsfw);
validateApiPublishRequest(body, files);
} catch (error) {
postToStats('publish', originalUrl, ip, null, null, error.message);
logger.debug('rejected >>', error.message);
res.status(400).send(error.message);
logger.debug('publish request rejected, insufficient request parameters');
res.status(400).json({success: false, message: error.message});
return;
}
// validate file, name, license, and nsfw
const file = files.file;
const fileName = file.name;
const filePath = file.path;
const fileType = file.type;
// channel authorization
authenticateApiPublish(channelName, channelPassword)
const name = body.name;
let nsfw = body.nsfw;
nsfw = cleanseNSFW(nsfw); // cleanse nsfw
try {
validatePublishSubmission(file, name, nsfw);
} catch (error) {
logger.debug('publish request rejected');
res.status(400).json({success: false, message: error.message});
return;
}
logger.debug(`name: ${name}, nsfw: ${nsfw}`);
// optional inputs
const license = body.license || null;
const title = body.title || null;
const description = body.description || null;
const thumbnail = body.thumbnail || null;
let channelName = body.channelName || null;
channelName = cleanseChannelName(channelName);
const channelPassword = body.channelPassword || null;
logger.debug(`license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}"`);
// check channel authorization
authenticateChannelCredentials(channelName, channelPassword)
.then(result => {
if (!result) {
res.status(401).send('Authentication failed, you do not have access to that channel');
throw new Error('authentication failed');
throw new Error('Authentication failed, you do not have access to that channel');
}
return createPublishParams(name, filePath, title, description, license, nsfw, channelName);
// make sure the claim name is available
return checkClaimNameAvailability(name);
})
.then(result => {
if (!result) {
throw new Error('That name is already in use by spee.ch.');
}
// create publish parameters object
return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName);
})
.then(publishParams => {
logger.debug('publishParams:', publishParams);
// publish the asset
return publish(publishParams, fileName, fileType);
})
// publish the asset
.then(result => {
postToStats('publish', originalUrl, ip, null, null, 'success');
res.status(200).json(result);
res.status(200).json({
success: true,
message: {
url : `spee.ch/${result.claim_id}/${name}`,
lbryTx: result,
},
});
})
.catch(error => {
logger.error('publish api error', error);
res.status(400).json({success: false, message: error.message});
});
});
// route to get a short claim id from long claim Id
app.get('/api/shortClaimId/:longId/:name', ({ originalUrl, ip, params }, res) => {
// serve content
@ -134,7 +161,7 @@ module.exports = (app) => {
// serve content
db.getShortChannelIdFromLongChannelId(params.longId, params.name)
.then(shortId => {
console.log('sending back short channel id', shortId);
logger.debug('sending back short channel id', shortId);
res.status(200).json(shortId);
})
.catch(error => {

View file

@ -4,12 +4,23 @@ const passport = require('passport');
module.exports = (app) => {
// route for sign up
app.post('/signup', passport.authenticate('local-signup'), (req, res) => {
logger.debug('successful signup');
res.status(200).json(true);
logger.verbose(`successful signup for ${req.user.channelName}`);
res.status(200).json({
success : true,
channelName : req.user.channelName,
channelClaimId: req.user.channelClaimId,
shortChannelId: req.user.shortChannelId,
});
});
// route for log in
app.post('/login', passport.authenticate('local-login'), (req, res) => {
logger.debug('req.user:', req.user);
logger.debug('successful login');
res.status(200).json(true);
res.status(200).json({
success : true,
channelName : req.user.channelName,
channelClaimId: req.user.channelClaimId,
shortChannelId: req.user.shortChannelId,
});
});
};

View file

@ -1,5 +1,5 @@
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats, getStatsSummary, getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js');
const { getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js');
module.exports = (app) => {
// route to log out
@ -31,12 +31,12 @@ module.exports = (app) => {
getTrendingClaims(dateTime)
.then(result => {
// logger.debug(result);
res.status(200).render('trending', {
res.status(200).render('popular', {
trendingAssets: result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
errorHandlers.handleRequestError(null, null, null, error, res);
});
});
// route to display a list of the trending images
@ -49,32 +49,13 @@ module.exports = (app) => {
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
});
});
// route to show statistics for spee.ch
app.get('/stats', ({ ip, originalUrl, user }, res) => {
// get and render the content
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1);
getStatsSummary(startDate)
.then(result => {
postToStats('show', originalUrl, ip, null, null, 'success');
res.status(200).render('statistics', {
user,
result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
errorHandlers.handleRequestError(null, null, null, error, res);
});
});
// route to send embedable video player (for twitter)
app.get('/embed/:claimId/:name', ({ params }, res) => {
const claimId = params.claimId;
const name = params.name;
console.log('claimId ==', claimId);
console.log('name ==', name);
// get and render the content
res.status(200).render('embed', { layout: 'embed', claimId, name });
});

View file

@ -7,7 +7,11 @@ const SHOW = 'SHOW';
const SHOWLITE = 'SHOWLITE';
const CHANNEL = 'CHANNEL';
const CLAIM = 'CLAIM';
const CHANNELID_INDICATOR = ':';
const CLAIM_ID_CHAR = ':';
const CHANNEL_CHAR = '@';
const CLAIMS_PER_PAGE = 10;
const NO_CHANNEL = 'NO_CHANNEL';
const NO_CLAIM = 'NO_CLAIM';
function isValidClaimId (claimId) {
return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));
@ -32,12 +36,58 @@ function getAsset (claimType, channelName, channelId, name, claimId) {
}
}
function getPage (query) {
if (query.p) {
return parseInt(query.p);
}
return 1;
}
function extractPageFromClaims (claims, pageNumber) {
logger.debug('claims is array?', Array.isArray(claims));
logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber));
const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE;
const claimEndIndex = claimStartIndex + 10;
const pageOfClaims = claims.slice(claimStartIndex, claimEndIndex);
logger.debug('page of claims:', pageOfClaims);
return pageOfClaims;
}
function determineTotalPages (totalClaims) {
if (totalClaims === 0) {
return 0;
}
if (totalClaims < CLAIMS_PER_PAGE) {
return 1;
}
const fullPages = Math.floor(totalClaims / CLAIMS_PER_PAGE);
const remainder = totalClaims % CLAIMS_PER_PAGE;
if (remainder === 0) {
return fullPages;
}
return fullPages + 1;
}
function determinePreviousPage (currentPage) {
if (currentPage === 1) {
return null;
}
return currentPage - 1;
}
function determineNextPage (totalPages, currentPage) {
if (currentPage === totalPages) {
return null;
}
return currentPage + 1;
}
module.exports = (app) => {
// route to serve a specific asset
app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => {
let identifier = params.identifier;
let name = params.name;
let claimType;
let claimOrChannel;
let channelName = null;
let claimId = null;
let channelId = null;
@ -74,8 +124,8 @@ module.exports = (app) => {
// parse identifier for whether it is a channel, short url, or claim_id
if (identifier.charAt(0) === '@') {
channelName = identifier;
claimType = CHANNEL;
const channelIdIndex = channelName.indexOf(CHANNELID_INDICATOR);
claimOrChannel = CHANNEL;
const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR);
if (channelIdIndex !== -1) {
channelId = channelName.substring(channelIdIndex + 1);
channelName = channelName.substring(0, channelIdIndex);
@ -84,17 +134,21 @@ module.exports = (app) => {
} else {
claimId = identifier;
logger.debug('claim id =', claimId);
claimType = CLAIM;
claimOrChannel = CLAIM;
}
// 1. retrieve the asset and information
getAsset(claimType, channelName, channelId, name, claimId)
getAsset(claimOrChannel, channelName, channelId, name, claimId)
// 2. serve or show
.then(fileInfo => {
if (!fileInfo) {
res.status(200).render('noClaims');
} else {
return serveOrShowAsset(fileInfo, fileExtension, method, headers, originalUrl, ip, res);
.then(result => {
logger.debug('getAsset result:', result);
if (result === NO_CLAIM) {
res.status(200).render('noClaim');
return;
} else if (result === NO_CHANNEL) {
res.status(200).render('noChannel');
return;
}
return serveOrShowAsset(result, fileExtension, method, headers, originalUrl, ip, res);
})
// 3. update the file
.then(fileInfoForUpdate => {
@ -105,16 +159,18 @@ module.exports = (app) => {
});
});
// route to serve the winning asset at a claim
app.get('/:name', ({ headers, ip, originalUrl, params }, res) => {
app.get('/:name', ({ headers, ip, originalUrl, params, query }, res) => {
// parse name param
let name = params.name;
let method;
let fileExtension;
let channelName = null;
let channelId = null;
if (name.charAt(0) === '@') {
// (a) handle channel requests
if (name.charAt(0) === CHANNEL_CHAR) {
channelName = name;
const channelIdIndex = channelName.indexOf(CHANNELID_INDICATOR);
const paginationPage = getPage(query);
const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR);
if (channelIdIndex !== -1) {
channelId = channelName.substring(channelIdIndex + 1);
channelName = channelName.substring(0, channelIdIndex);
@ -125,16 +181,39 @@ module.exports = (app) => {
getChannelContents(channelName, channelId)
// 2. respond to the request
.then(result => {
logger.debug('result');
if (!result.claims) {
if (result === NO_CHANNEL) { // no channel found
res.status(200).render('noChannel');
} else {
res.status(200).render('channel', result);
} else if (!result.claims) { // channel found, but no claims
res.status(200).render('channel', {
channelName : result.channelName,
longChannelId : result.longChannelId,
shortChannelId: result.shortChannelId,
claims : [],
previousPage : 0,
currentPage : 0,
nextPage : 0,
totalPages : 0,
totalResults : 0,
});
} else { // channel found, with claims
const totalPages = determineTotalPages(result.claims.length);
res.status(200).render('channel', {
channelName : result.channelName,
longChannelId : result.longChannelId,
shortChannelId: result.shortChannelId,
claims : extractPageFromClaims(result.claims, paginationPage),
previousPage : determinePreviousPage(paginationPage),
currentPage : paginationPage,
nextPage : determineNextPage(totalPages, paginationPage),
totalPages : totalPages,
totalResults : result.claims.length,
});
}
})
.catch(error => {
handleRequestError('serve', originalUrl, ip, error, res);
});
// (b) handle stream requests
} else {
if (name.indexOf('.') !== -1) {
method = SERVE;
@ -155,11 +234,12 @@ module.exports = (app) => {
// 1. retrieve the asset and information
getAsset(CLAIM, null, null, name, null)
// 2. respond to the request
.then(fileInfo => {
if (!fileInfo) {
res.status(200).render('noClaims');
.then(result => {
logger.debug('getAsset result', result);
if (result === NO_CLAIM) {
res.status(200).render('noClaim');
} else {
return serveOrShowAsset(fileInfo, fileExtension, method, headers, originalUrl, ip, res);
return serveOrShowAsset(result, fileExtension, method, headers, originalUrl, ip, res);
}
})
// 3. update the database

View file

@ -1,7 +1,7 @@
const logger = require('winston');
const publishController = require('../controllers/publishController.js');
const publishHelpers = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { publish } = require('../controllers/publishController.js');
const { createPublishParams } = require('../helpers/publishHelpers.js');
const { useObjectPropertiesIfNoKeys } = require('../helpers/errorHandlers.js');
const { postToStats } = require('../controllers/statsController.js');
module.exports = (app, siofu, hostedContentPath) => {
@ -34,31 +34,38 @@ module.exports = (app, siofu, hostedContentPath) => {
uploader.on('saved', ({ file }) => {
if (file.success) {
logger.debug(`Client successfully uploaded ${file.name}`);
socket.emit('publish-status', 'File upload successfully completed. Your image is being published to LBRY (this might take a second)...');
/*
NOTE: need to validate that client has the credentials to the channel they chose
otherwise they could circumvent security client side.
*/
socket.emit('publish-update', 'File upload successfully completed. Your image is being published to LBRY (this might take a second)...');
// /*
// NOTE: need to validate that client has the credentials to the channel they chose
// otherwise they could circumvent security.
// */
let thumbnail;
if (file.meta.thumbnail) {
thumbnail = file.meta.thumbnail;
} else {
thumbnail = null;
}
let channelName;
if (file.meta.channel) {
channelName = file.meta.channel;
} else {
channelName = null;
}
// prepare the publish parameters
const publishParams = publishHelpers.createPublishParams(file.meta.name, file.pathName, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, file.meta.channel);
logger.debug(publishParams);
const publishParams = createPublishParams(file.pathName, file.meta.name, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, thumbnail, channelName);
logger.debug('publish parameters:', publishParams);
// publish the file
publishController.publish(publishParams, file.name, file.meta.type)
publish(publishParams, file.name, file.meta.type)
.then(result => {
postToStats('PUBLISH', '/', null, null, null, 'success');
socket.emit('publish-complete', { name: publishParams.name, result });
})
.catch(error => {
error = errorHandlers.handlePublishError(error);
postToStats('PUBLISH', '/', null, null, null, error);
socket.emit('publish-failure', error);
logger.error('Publish Error:', useObjectPropertiesIfNoKeys(error));
socket.emit('publish-failure', error.message);
});
} else {
logger.error(`An error occurred in uploading the client's file`);
socket.emit('publish-failure', 'File uploaded, but with errors');
postToStats('PUBLISH', '/', null, null, null, 'File uploaded, but with errors');
logger.error(`An error occurred in uploading the client's file`);
// to-do: remove the file, if not done automatically
}
});

View file

@ -5,6 +5,7 @@ const siofu = require('socketio-file-upload');
const expressHandlebars = require('express-handlebars');
const Handlebars = require('handlebars');
const handlebarsHelpers = require('./helpers/handlebarsHelpers.js');
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
const config = require('config');
const logger = require('winston');
const { getDownloadDirectory } = require('./helpers/lbryApi');
@ -13,7 +14,7 @@ const PORT = 3000; // set port
const app = express(); // create an Express application
const db = require('./models'); // require our models for syncing
const passport = require('passport');
const session = require('express-session');
const cookieSession = require('cookie-session');
// configure logging
const logLevel = config.get('Logging.LogLevel');
@ -34,40 +35,20 @@ app.use(bodyParser.urlencoded({ extended: true })); // 'body parser' for parsing
app.use(siofu.router); // 'socketio-file-upload' router for uploading with socket.io
app.use((req, res, next) => { // custom logging middleware to log all incoming http requests
logger.verbose(`Request on ${req.originalUrl} from ${req.ip}`);
logger.debug(req.body);
logger.debug('req.body:', req.body);
next();
});
// initialize passport
app.use(session({ secret: 'cats' }));
app.use(cookieSession({
name : 'session',
keys : [config.get('Session.SessionKey')],
maxAge: 24 * 60 * 60 * 1000, // 24 hours
}));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => { // this populates req.user
let userInfo = {};
db.User.findOne({ where: { id } })
.then(user => {
userInfo['id'] = user.id;
userInfo['userName'] = user.userName;
return user.getChannel();
})
.then(channel => {
userInfo['channelName'] = channel.channelName;
userInfo['channelClaimId'] = channel.channelClaimId;
return db.getShortChannelIdFromLongChannelId(channel.channelClaimId, channel.channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
done(null, userInfo);
return null;
})
.catch(error => {
logger.error('sequelize error', error);
done(error, null);
});
});
passport.serializeUser(serializeSpeechUser); // takes the user id from the db and serializes it
passport.deserializeUser(deserializeSpeechUser); // this deserializes id then populates req.user with info
const localSignupStrategy = require('./passport/local-signup.js');
const localLoginStrategy = require('./passport/local-login.js');
passport.use('local-signup', localSignupStrategy);
@ -83,19 +64,7 @@ app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
// middleware to pass user info back to client (for handlebars access), if user is logged in
app.use((req, res, next) => {
if (req.user) {
logger.verbose(req.user);
res.locals.user = {
id : req.user.id,
userName : req.user.userName,
channelName : req.user.channelName,
channelClaimId: req.user.channelClaimId,
shortChannelId: req.user.shortChannelId,
};
}
next();
});
app.use(populateLocalsDotUser);
// start the server
db.sequelize

View file

@ -1,17 +1,20 @@
<div class="wrapper">
{{> topBar}}
<div class="main">
<h2>About Spee.ch</h2>
<p>Spee.ch is a single-serving site that reads and publishes images to and from the <a class="white-text" href="https://lbry.io">LBRY</a> blockchain.</p>
<p>Spee.ch is an image hosting service, but with the added benefit that it stores your images on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.</p>
{{> examples}}
{{> documentation}}
{{> bugs}}
<div class="row row--padded">
<div class="column column--5 column--med-10 align-content-top">
<div class="column column--8 column--med-10">
<p class="pull-quote">Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
<p><a class="link--primary" target="_blank" href="https://twitter.com/spee_ch">TWITTER</a></p>
<p><a class="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch">GITHUB</a></p>
<p><a class="link--primary" target="_blank" href="https://discord.gg/YjYbwhS">DISCORD CHANNEL</a></p>
<p><a class="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch/blob/master/README.md">DOCUMENTATION</a></p>
</div>
<div class="sidebar">
{{> contribute}}
</div><div class="column column--5 column--med-10 align-content-top">
<div class="column column--8 column--med-10">
<p>Spee.ch is a media-hosting site that reads and publishes content from the <a class="link--primary" href="https://lbry.io">LBRY</a> blockchain.</p>
<p>Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.</p>
<h3>Contribute</h3>
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a class="link--primary" href="https://github.com/lbryio/spee.ch">github repo</a> and go to town!</p>
<p>If you want to improve spee.ch, join our <a class="link--primary" href="https://discord.gg/YjYbwhS">discord channel</a> or solve one of our <a class="link--primary" href="https://github.com/lbryio/spee.ch/issues">github issues</a>.</p>
</div>
{{> footer}}
</div>
<script src="/assets/js/generalFunctions.js"></script>
</div>

View file

@ -1,11 +1,47 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>{{this.channelName}}<span class="h3--secondary">:{{this.longChannelId}}</span></h3>
<p>Below is all the free content in this channel.</p>
<div class="row row--padded">
<div class="row">
<p>Below are the contents for {{this.channelName}}:{{this.longChannelId}}</p>
<div class="grid">
{{#each this.claims}}
{{> contentListItem}}
{{> gridItem}}
{{/each}}
</div>
{{> footer}}
<div class="row">
<div class="column column--3 align-content--left">
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p=1">First [1]</a>
</div><div class="column column--4 align-content-center">
{{#if this.previousPage}}
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.previousPage}}">Previous</a>
{{else}}
<a disabled>Previous</a>
{{/if}}
|
{{#if this.nextPage}}
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.nextPage}}">Next</a>
{{else}}
<a disabled>Next</a>
{{/if}}
</div><div class="column column--3 align-content-right">
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.totalPages}}">Last [{{this.totalPages}}]</a>
</div>
</div>
</div>
</div>
<script src="/assets/vendors/masonry/masonry.pkgd.min.js"></script>
<script src="/assets/vendors/imagesloaded/imagesloaded.pkgd.min.js"></script>
<script>
// init masonry with element
var grid = document.querySelector('.grid');
var msnry;
imagesLoaded( grid, function() {
msnry = new Masonry( grid, {
itemSelector: '.grid-item',
columnWidth: 3,
percentPosition: true
});
});
</script>

View file

@ -1,7 +1,4 @@
<div class="wrapper">
{{> topBar}}
<div>
<div class="row">
<h3>404: Not Found</h3>
<p>That page does not exist. Return <a href="/">home</a>.</p>
</div>
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>
</div>

View file

@ -1,69 +1,154 @@
<script src="/assets/js/generalFunctions.js"></script>
<div class="row row--tall flex-container flex-container--column">
<form>
<input class="input-file" type="file" id="siofu_input" name="siofu_input" accept="video/*,image/*" onchange="previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</form>
<div id="primary-dropzone" class="dropzone row row--margined row--padded row--tall flex-container flex-container--column flex-container--justify-center" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)" ondragenter="dragenter_handler(event)" ondragleave="dragexit_handler(event)" onclick="triggerFileChooser('siofu_input', event)">
<div id="primary-dropzone-instructions">
<p class="info-message-placeholder info-message--failure" id="input-error-file-selection" hidden="true"></p>
<p>Drag & drop image or video here to publish</p>
<p class="fine-print">OR</p>
<p class="blue--underlined">CHOOSE FILE</p>
</div>
<div id="dropbzone-dragover" class="hidden">
<p class="blue">Drop it.</p>
</div>
</div>
<div id="publish-form" class="hidden">
<div class="row row--padded row--no-bottom">
<div class="column column--10">
<!-- title input -->
<input type="text" id="publish-title" class="input-text text--large input-text--full-width" placeholder="Give your post a title...">
</div>
<div class="column column--5 column--sml-10" >
<!-- preview -->
<div class="row row--padded">
<div id="asset-preview-holder" class="dropzone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)" ondragenter="preview_onmouseenter_handler()" ondragleave="preview_onmouseleave_handler()" onmouseenter="preview_onmouseenter_handler()" onmouseleave="preview_onmouseleave_handler()" onclick="triggerFileChooser('siofu_input', event)">
<div id="asset-preview-dropzone-instructions" class="hidden">
<p>Drag & drop image or video here</p>
<p class="fine-print">OR</p>
<p class="blue--underlined">CHOOSE FILE</p>
</div>
<div id="asset-preview-target"></div>
</div>
</div>
</div><div class="column column--5 column--sml-10 align-content-top">
<div id="publish-active-area" class="row row--padded">
{{> publishForm-Channel}}
{{> publishForm-Url}}
{{> publishForm-Thumbnail}}
{{> publishForm-Details}}
{{> publishForm-Submit}}
</div>
</div>
</div>
</div>
<div id="publish-status" class="hidden">
<div class="row row--margined">
<div id="publish-update" class="row align-content-center"></div>
<div id="publish-progress-bar" class="row align-content-center"></div>
<div id="upload-percent" class="row align-content-center"></div>
</div>
</div>
</div>
<div class="row">
<div class="column column--2"></div>
<div class="column column--8">
{{> topBar}}
{{> publishForm}}
{{> learnMore}}
{{> footer}}
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/siofu/client.js"></script>
<script src="/assets/js/validationFunctions.js"></script>
<script src="/assets/js/publishFileFunctions.js"></script>
<script typ="text/javascript">
// define variables
var socket = io();
var uploader = new SocketIOFileUpload(socket);
var stagedFiles = null;
checkCookie();
const socket = io();
const uploader = new SocketIOFileUpload(socket);
let stagedFiles = null;
const publishFormWrapper = document.getElementById('publish-form');
const publishStatus = document.getElementById('publish-status');
const publishUpdate = document.getElementById('publish-update');
const publishProgressBar = document.getElementById('publish-progress-bar');
const uploadPercent = document.getElementById('upload-percent');
/* socketio-file-upload listeners */
uploader.addEventListener('start', function(event){
var name = document.getElementById('claim-name-input').value;
var title = document.getElementById('publish-title').value;
var description = document.getElementById('publish-description').value;
var license = document.getElementById('publish-license').value;
var nsfw = document.getElementById('publish-nsfw').checked;
var channel = document.getElementById('channel-name-select').value;
console.log('starting upload');
addInputValuesToFileMetaData(event)
// hide the publish tool
hidePublishTools();
// show the progress status and animation
showPublishStatus();
showPublishProgressBar();
});
uploader.addEventListener('progress', function(event){
var percent = event.bytesLoaded / event.file.size * 100;
updatePublishStatus('<p>File is loading to server</p>')
updateUploadPercent(`<p class="blue">${percent.toFixed(2)}%</p>`)
});
/* socket.io message listeners */
socket.on('publish-update', function(msg){
updatePublishStatus(`<p>${msg}</p>`);
updateUploadPercent(`<p>Curious what magic is happening here? <a class="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>`);
});
socket.on('publish-failure', function(msg){
updatePublishStatus('<p> --(✖╭╮✖)→ </p><p>' + JSON.stringify(msg) + '</p><strong>For help, post the above error text in the #speech channel on the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">lbry discord</a></strong>');
hidePublishProgressBar();
hideUploadPercent();
});
socket.on('publish-complete', function(msg){
const showUrl = msg.result.claim_id + "/" + msg.name;
// update status
updatePublishStatus('<p>Your publish is complete! You are being redirected to it now.</p>');
updateUploadPercent('<p><a class="link--primary" target="_blank" href="\' + showUrl + \'">If you do not get redirected, click here.</a></p>')
// redirect the user
window.location.href = showUrl;
});
function hidePublishTools() {
publishFormWrapper.setAttribute('class', 'hidden');
}
// publish status functions
function showPublishStatus() {
publishStatus.setAttribute('class', 'row row--tall flex-container flex-container--column flex-container--justify-center');
}
function updatePublishStatus(msg){
publishUpdate.innerHTML = msg;
}
// progress bar functions
function showPublishProgressBar(){
createProgressBar(publishProgressBar, 12);
}
function hidePublishProgressBar(){
publishProgressBar.hidden = true;
}
// upload percent functions
function updateUploadPercent(msg){
uploadPercent.innerHTML = msg;
}
function hideUploadPercent(){
uploadPercent.hidden = true;
}
function addInputValuesToFileMetaData(event) {
// get values from inputs
const name = document.getElementById('claim-name-input').value.trim();
const title = document.getElementById('publish-title').value.trim();
const description = document.getElementById('publish-description').value.trim();
const license = document.getElementById('publish-license').value.trim();
const nsfw = document.getElementById('publish-nsfw').checked;
const anonymous = document.getElementById('anonymous-select').checked;
const channel = document.getElementById('channel-name-select').value.trim();
const thumbnail = document.getElementById('claim-thumbnail-input').value.trim();
// set values on file meta data
event.file.meta.name = name;
event.file.meta.title = title;
event.file.meta.description = description;
event.file.meta.license = license;
event.file.meta.nsfw = nsfw;
event.file.meta.type = stagedFiles[0].type;
if (!anonymous) {
event.file.meta.channel = channel;
// re-set the html in the publish area
document.getElementById('publish-active-area').innerHTML = '<div id="publish-status"></div><div id="progress-bar"></div>';
// start a progress animation
createProgressBar(document.getElementById('progress-bar'), 12);
// google analytics
ga('send', {
hitType: 'event',
eventCategory: 'publish',
eventAction: name
});
});
uploader.addEventListener('progress', function(event){
var percent = event.bytesLoaded / event.file.size * 100;
updatePublishStatus('File is ' + percent.toFixed(2) + '% loaded to the server');
});
/* socket.io message listeners */
socket.on('publish-status', function(msg){
updatePublishStatus(msg);
});
socket.on('publish-failure', function(msg){
document.getElementById('publish-active-area').innerHTML = '<p> --(✖╭╮✖)→ </p><p>' + JSON.stringify(msg) + '</p><strong>For help, post the above error text in the #speech channel on the <a href="https://lbry.slack.com/" target="_blank">lbry slack</a></strong>';
});
socket.on('publish-complete', function(msg){
var publishResults;
var showUrl = msg.result.claim_id + "/" + msg.name;
// build new publish area
publishResults = '<p>Your publish is complete! You are being redirected to it now.</p>';
publishResults += '<p><a target="_blank" href="' + showUrl + '">If you do not get redirected, click here.</a></p>';
// update publish area
document.getElementById('publish-active-area').innerHTML = publishResults;
window.location.href = showUrl;
});
}
if (thumbnail && (thumbnail.trim !== '')){
event.file.meta.thumbnail = thumbnail;
}
}
</script>

View file

@ -6,21 +6,31 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/BEM.css" type="text/css">
<link rel="stylesheet" href="/assets/css/componentStyle.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@lbryio" />
<meta property="og:title" content="spee.ch">
<meta property="og:site_name" content="spee.ch">
<meta property="og:type" content="website">
<meta property="og:image" content="https://spee.ch/assets/img/content-freedom-64px.png">
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg">
<meta property="og:url" content="http://spee.ch/">
<meta property="og:description" content="Open-source, decentralized image and video hosting.">
<!--google font-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
<!-- google analytics -->
{{ googleAnalytics }}
</head>
<body>
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/validationFunctions.js"></script>
<script src="/assets/js/publishFileFunctions.js"></script>
<script src="/assets/js/authFunctions.js"></script>
<script src="/assets/js/loginFunctions.js"></script>
<script src="/assets/js/dropzoneFunctions.js"></script>
<script src="/assets/js/createChannelFunctions.js"></script>
<script src="/assets/js/navBarFunctions.js"></script>
{{> navBar}}
{{{ body }}}
</body>
</html>

View file

@ -6,22 +6,27 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/BEM.css" type="text/css">
<link rel="stylesheet" href="/assets/css/componentStyle.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
<meta property="fb:app_id" content="1371961932852223">
{{#unless fileInfo.nsfw}}
{{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}}
{{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}}
{{/unless}}
<!--google font-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
<!-- google analytics -->
{{ googleAnalytics }}
</head>
<body>
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/validationFunctions.js"></script>
<script src="/assets/js/authFunctions.js"></script>
<script src="/assets/js/loginFunctions.js"></script>
<script src="/assets/js/navBarFunctions.js"></script>
{{> navBar}}
{{{ body }}}
<script src="/assets/js/showFunctions.js"></script>
</body>
</html>
<script>
</script>

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
<meta property="fb:app_id" content="1371961932852223">
{{#unless fileInfo.nsfw}}
{{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}}
{{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}}
{{/unless}}
<!-- google analytics -->
{{ googleAnalytics }}
</head>
<body>
{{{ body }}}
<script src="/assets/js/showFunctions.js"></script>
</body>
</html>

View file

@ -1,23 +1,15 @@
<div class="wrapper">
{{> topBar}}
<h2>Log In</h2>
<div class="row row--wide">
<div class="column column--6">
<p>Log in to an existing channel:</p>
<div class="row row--padded">
<div class="column column--5 column--med-10 align-content-top">
<div class="column column--8 column--med-10">
<p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're <a class="link--primary" target="_blank" href="/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8">documenting important events</a>, or making a public repository for <a class="link--primary" target="_blank" href="/@catGifs">cat gifs</a> (password: '1234'), try creating a channel for it!</p>
</div>
</div><div class="column column--5 column--med-10 align-content-top">
<div class="column column--8 column--med-10">
<h3 class="h3--no-bottom">Log in to an existing channel:</h3>
{{>channelLoginForm}}
</div>
</div>
<h2>Create New</h2>
<div class="row row--wide">
<div class="column column--6">
<p>Create a brand new channel:</p>
<h3 class="h3--no-bottom">Create a brand new channel:</h3>
{{>channelCreationForm}}
</div>
</div>
{{> footer}}
</div>
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/validationFunctions.js"></script>
<script src="/assets/js/authFunctions.js"></script>

View file

@ -1,26 +0,0 @@
<div class="wrapper">
{{> topBar}}
<div>
<h2>New on Spee.ch</h2>
<p><i>The 25 most recent publishes on spee.ch</i></p>
{{#each newClaims}}
<div class="all-claims-item">
<a href="/{{this.claimId}}/{{this.name}}">
{{#ifConditional this.fileType '===' 'video/mp4'}}
<img class="all-claims-asset" src="/assets/img/content-freedom-large.png"/>
{{else}}
<img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.ext" />
{{/ifConditional}}
</a>
<div class="all-claims-details">
<ul style="list-style-type:none">
<li><bold>{{this.name}}</bold></li>
<li><i>Claim Id: {{this.claimId}}</i></li>
<li><a href="/{{this.claimId}}/{{this.name}}">Link</a></li>
</ul>
</div>
</div>
{{/each}}
</div>
{{> footer}}
</div>

View file

@ -1,8 +1,5 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>No Claims</h3>
<p>There are no free assets on this channel.</p>
<p><i>If you think this message is an error, contact us in the <a href="https://lbry.slack.com/" target="_blank">LBRY slack!</a></i></p>
</div>
<div class="row">
<h3>No Channel</h3>
<p>There are no published channels matching your url</p>
<p>If you think this message is an error, contact us in the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY Discord!</a></p>
</div>

5
views/noClaim.handlebars Normal file
View file

@ -0,0 +1,5 @@
<div class="row">
<h3>No Claims</h3>
<p>There are no free assets at that claim. You should publish one at <a class="link--primary" href="/">spee.ch</a>.</p>
<p>NOTE: it is possible your claim was published, but it is still being processed by the blockchain</p>
</div>

View file

@ -1,8 +0,0 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>No Claims</h3>
<p>There are no free assets at that claim. You should publish one at <a href="/">spee.ch</a>.</p>
<p>NOTE: it is possible your claim was published, but it is still being processed by the blockchain</p>
</div>
</div>

View file

@ -1,23 +1,29 @@
<div class="row">
<div id="asset-placeholder">
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
{{#ifConditional fileInfo.fileExt '===' 'gifv'}}
<video class="show-asset" autoplay loop muted>
<source src="/media/{{fileInfo.fileName}}">
<video class="gifv-show" autoplay loop muted>
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
{{else}}
<video class="show-asset" autoplay controls>
<source src="/media/{{fileInfo.fileName}}">
<video id="video-player" class="video-show video" controls poster="{{fileInfo.thumbnail}}">
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', resizeVideoPlayer)
window.addEventListener("resize", resizeVideoPlayer);
function resizeVideoPlayer() {
const div = document.getElementById('video-player');
const width = div.offsetWidth;
div.height = (width * 9 / 16);
}
</script>
{{/ifConditional}}
{{else}}
<img class="show-asset" src="/media/{{fileInfo.fileName}}" />
{{/ifConditional}}
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
<img class="image-show" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" />
</a>
</div>
</div>
{{/ifConditional}}

View file

@ -1,76 +1,104 @@
<div class="panel">
<h2>Title</h2>
<p>{{fileInfo.title}}</>
{{#if fileInfo.channelName}}
<div class="row row--padded row--wide row--no-top">
<div class="column column--2 column--med-10">
<span class="text">Channel:</span>
</div><div class="column column--8 column--med-10">
<span class="text"><a href="/{{fileInfo.channelName}}:{{fileInfo.certificateId}}">{{fileInfo.channelName}}</a></span>
</div>
<div class="panel links">
<h2>Links</h2>
{{!--short direct link to asset--}}
<div class="share-option">
<a href="/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">Permanent Short Link</a> (most convenient)
</div>
{{/if}}
{{#if fileInfo.description}}
<div class="row row--padded row--wide row--no-top">
<span class="text">{{fileInfo.description}}</span>
</div>
{{/if}}
<div class="row row--wide">
<div id="show-short-link">
<div class="column column--2 column--med-10">
<a class="link--primary" href="/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"><span class="text">Link:</span></a>
</div><div class="column column--8 column--med-10">
<div class="row row--short row--wide">
<div class="column column--7">
<div class="input-error" id="input-error-copy-short-link" hidden="true"></div>
<br/>
<input type="text" id="short-link" class="link" readonly spellcheck="false" value="https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" onclick="select()"/>
<button class="copy-button" data-elementtocopy="short-link" onclick="copyToClipboard(event)">copy</button>
<input type="text" id="short-link" class="input-disabled input-text--full-width" readonly spellcheck="false" value="https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" onclick="select()"/>
</div><div class="column column--1"></div><div class="column column--2">
<button class="button--primary" data-elementtocopy="short-link" onclick="copyToClipboard(event)">copy</button>
</div>
{{!-- link to show route for asset--}}
<div class="share-option">
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">Permanent Long Link</a> (fastest service)
<div class="input-error" id="input-error-copy-long-link" hidden="true"></div>
</br>
<input type="text" id="long-link" class="link" readonly onclick="select()" spellcheck="false" value="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>
<button class="copy-button" data-elementtocopy="long-link" onclick="copyToClipboard(event)">copy</button>
</div>
{{!-- html text for embedding asset--}}
<div class="share-option">
Embed HTML
</div>
</div>
<div id="show-embed-code">
<div class="column column--2 column--med-10">
<span class="text">Embed:</span>
</div><div class="column column--8 column--med-10">
<div class="row row--short row--wide">
<div class="column column--7">
<div class="input-error" id="input-error-copy-embed-text" hidden="true"></div>
<br/>
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
<input type="text" id="embed-text" class="link" readonly onclick="select()" spellcheck="false" value='&lt;video width="100%" controls src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>&lt;/video>'/>
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='&lt;video width="100%" controls poster="{{fileInfo.thumbnail}}" src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>&lt;/video>'/>
{{else}}
<input type="text" id="embed-text" class="link" readonly onclick="select()" spellcheck="false" value='&lt;img src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" />'/>
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='&lt;img src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>'/>
{{/ifConditional}}
<button class="copy-button" data-elementtocopy="embed-text" onclick="copyToClipboard(event)">copy</button>
</div><div class="column column--1"></div><div class="column column--2">
<button class="button--primary" data-elementtocopy="embed-text" onclick="copyToClipboard(event)">copy</button>
</div>
{{!--markdown text using asset--}}
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
{{else}}
<div class="share-option">
Markdown
<div class="input-error" id="input-error-copy-markdown-text" hidden="true"></div>
<br/>
<input type="text" id="markdown-text" class="link" readonly onclick="select()" spellcheck="false" value='![{{fileInfo.name}}](https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}})'/>
<button class="copy-button" data-elementtocopy="markdown-text" onclick="copyToClipboard(event)">copy</button>
</div>
{{/ifConditional}}
</div>
<div class="panel">
<h2>Description</h2>
<p>{{fileInfo.description}}</p>
</div>
<div class="panel">
<h2>Metadata</h2>
<table class="metadata-table" style="table-layout: fixed">
<tr class="metadata-row">
<td class="left-column">Name</td>
<td>{{fileInfo.name}}</td>
</tr>
<tr class="metadata-row">
<td class="left-column">Claim Id</td>
<td>{{fileInfo.claimId}}</td>
</tr>
<tr class="metadata-row">
<td class="left-column">File Name</td>
<td>{{fileInfo.fileName}}</td>
</tr>
<tr>
<td class="left-column">File Type</td>
<td>{{#if fileInfo.fileType}}
</div>
<div id="show-share-buttons">
<div class="row row--padded row--wide">
<div class="column column--2 column--med-10">
<span class="text">Share:</span>
</div><div class="column column--7 column--med-10">
<div class="row row--short row--wide flex-container flex-container--row flex-container--wrap">
<a class="link--primary" target="_blank" href="https://twitter.com/intent/tweet?text=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">twitter</a>
<a class="link--primary" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">facebook</a>
<a class="link--primary" target="_blank" href="http://tumblr.com/widgets/share/tool?canonicalUrl=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">tumblr</a>
<a class="link--primary" target="_blank" href="https://www.reddit.com/submit?url=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}&title={{fileInfo.name}}">reddit</a>
</div>
</div>
</div>
</div>
<div class="row row--wide">
<a class="text link--primary" id="show-details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="show-details">[more]</a>
</div>
<div id="show-details" class="row row--padded row--wide" hidden="true">
<div id="show-claim-name">
<div class="column column--2 column--med-10">
<span class="text">Name:</span>
</div><div class="column column--8 column--med-10">
{{fileInfo.name}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">Claim Id:</span>
</div><div class="column column--8 column--med-10">
{{fileInfo.claimId}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">File Name:</span>
</div><div class="column column--8 column--med-10">
{{fileInfo.fileName}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">File Type:</span>
</div><div class="column column--8 column--med-10">
{{#if fileInfo.fileType}}
{{fileInfo.fileType}}
{{else}}
unknown
{{/if}}
</td>
</tr>
</table>
</div>
</div>
</div>

View file

@ -1,4 +0,0 @@
<div>
<h2>Bugs</h2>
<p>Spee.ch is young and under continuous development so it will have bugs. Please leave an issue on our <a href="https://github.com/lbryio/spee.ch">github</a> if you experience a problem or have suggestions.</p>
</div>

View file

@ -1,28 +1,28 @@
<form id="publish-channel-form">
<div class="column column--3">
<p id="input-error-channel-name" class="info-message-placeholder info-message--failure"></p>
<div class="row row--wide row--short">
<div class="column column--3 column--sml-10">
<label class="label" for="new-channel-name">Name:</label>
</div>
<div class="column column--9">
<div id="input-error-channel-name" class="info-message info-message--failure"></div>
</div><div class="column column--6 column--sml-10">
<div class="input-text--primary">
<span>@</span>
<input type="text" name="new-channel-name" id="new-channel-name" class="input-text" placeholder="exampleChannel" value="" oninput="checkChannelName(event.target.value)">
<span id="input-success-channel-name" class="info-message info-message--success"></span>
<input type="text" name="new-channel-name" id="new-channel-name" class="input-text" placeholder="exampleChannelName" value="" oninput="checkChannelName(event.target.value)">
<span id="input-success-channel-name" class="info-message--success"></span>
</div>
</div>
<div class="column column--3">
</div>
<div class="row row--wide row--short">
<div class="column column--3 column--sml-10">
<label class="label" for="new-channel-password">Password:</label>
</div><div class="column column--6 column--sml-10">
<div class="input-text--primary">
<input type="password" name="new-channel-password" id="new-channel-password" class="input-text" placeholder="" value="" >
</div>
</div>
<div class="column column--9">
<div id="input-error-channel-password" class="info-message info-message--failure"></div>
<input type="password" name="new-channel-password" id="new-channel-password" placeholder="" value="" class="input-text input-text--primary">
</div>
<div class="row row--wide">
<button onclick="publishNewChannel(event)">Create Channel</button>
<button class="button--primary" onclick="publishNewChannel(event)">Create Channel</button>
</div>
</form>
@ -35,44 +35,3 @@
<div id="channel-publish-done" hidden="true">
<p>Your channel has been successfully created!</p>
</div>
<script type="text/javascript">
function publishNewChannel (event) {
const channelName = document.getElementById('new-channel-name').value;
const password = document.getElementById('new-channel-password').value;
const channelNameErrorDisplayElement = document.getElementById('input-error-channel-name');
const passwordErrorDisplayElement = document.getElementById('input-error-channel-password');
const chanelCreateForm = document.getElementById('publish-channel-form');
const inProgress = document.getElementById('channel-publish-in-progress');
const done = document.getElementById('channel-publish-done');
// prevent default so this script can handle submission
event.preventDefault();
// validate submission
validateNewChannelSubmission(channelName, password)
.then(() => {
console.log('in progress');
chanelCreateForm.hidden = true;
inProgress.hidden = false;
createProgressBar(document.getElementById('create-channel-progress-bar'), 12);
return sendAuthRequest(channelName, password, '/signup') // post the request
})
.then(() => {
console.log('success');
inProgress.hidden=true;
done.hidden = false;
// refresh window logged in as the channel
window.location.href = '/';
})
.catch(error => {
if (error.name === 'ChannelNameError'){
showError(channelNameErrorDisplayElement, error.message);
} else if (error.name === 'ChannelPasswordError'){
showError(passwordErrorDisplayElement, error.message);
} else {
console.log('failure:', error);
}
})
}
</script>

View file

@ -1,45 +1,26 @@
<form id="channel-login-form">
<div class="column column--3">
<label class="label" for="login-channel-name">Name:</label>
</div>
<div class="column column--9">
<div id="login-error-display-element" class="info-message info-message--failure"></div>
<p id="login-error-display-element" class="info-message-placeholder info-message--failure"></p>
<div class="row row--wide row--short">
<div class="column column--3 column--sml-10">
<label class="label" for="channel-login-name-input">Name:</label>
</div><div class="column column--6 column--sml-10">
<div class="input-text--primary">
<span>@</span>
<input type="text" name="login-channel-name" id="login-channel-name" class="input-text" placeholder="" value="">
<input type="text" id="channel-login-name-input" class="input-text" placeholder="" value="">
</div>
</div>
</div>
<div class="row row--wide row--short">
<div class="column column--3 column--sml-10">
<label class="label" for="channel-login-password-input" >Password:</label>
</div><div class="column column--6 column--sml-10">
<div class="input-text--primary">
<input type="password" id="channel-login-password-input" class="input-text" placeholder="" value="">
</div>
</div>
</div>
<div class="column column--3">
<label class="label" for="login-channel-password" >Password:</label>
</div>
<div class="column column--9">
<input type="password" name="login-channel-password" id="login-channel-password" class="input-text input-text--primary" placeholder="" value="">
</div>
</form>
<div class="row row--wide">
<button onclick="loginToChannel(event)">Login</button>
<button class="button--primary" onclick="loginToChannel(event)">Authenticate</button>
</div>
<script type="text/javascript">
function loginToChannel (event) {
const channelName = document.getElementById('login-channel-name').value;
const password = document.getElementById('login-channel-password').value;
const loginErrorDisplayElement = document.getElementById('login-error-display-element');
// prevent default
event.preventDefault()
// send request
sendAuthRequest(channelName, password, '/login')
.then(() => {
console.log('login success');
window.location.href = '/';
})
.catch(error => {
showError(loginErrorDisplayElement, error);
console.log('login failure:', error);
})
}
</script>
</form>

View file

@ -1,15 +1,15 @@
<div class='content-list-card'>
<a href="{{this.showUrlLong}}"><span class='content-list-card-link'></span></a>
<div class='row row--wide'>
<div class="column column--3 align-content-top">
<a href="{{this.showUrlLong}}">
{{#ifConditional this.contentType '===' 'video/mp4'}}
<img class="content-list-asset" src="{{this.thumbnail}}"/>
<img class="content-list-item-asset" src="{{this.thumbnail}}"/>
{{else}}
<img class="content-list-asset" src="{{this.directUrlLong}}" />
<img class="content-list-item-asset" src="{{this.directUrlLong}}" />
{{/ifConditional}}
<div class="content-list-details">
<ul>
<li class="content-list-title">{{this.title}}</li>
<li><a href="{{this.directUrlShort}}">spee.ch{{this.directUrlShort}}</a></li>
</ul>
</a>
</div><div class="column column--7 align-content-top">
<p>{{this.title}}</p>
<a class="link--primary" href="{{this.showUrlShort}}">spee.ch{{this.showUrlShort}}</a>
</div>
</div>

View file

@ -1,8 +0,0 @@
<div>
<h2>Contribute
<a href="https://github.com/lbryio/spee.ch" target="_blank"><img id="github-logo" src="/assets/img/GitHub-Mark-32px.png"/></a>
</h2>
<p><strong>Spee.ch is an open source project. Please contribute to the existing site, or fork it and make your own!</strong></p>
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a href="https://github.com/lbryio/spee.ch">github repo</a> and go to town!</p>
<p>If you want to improve spee.ch, join our <a href="https://lbry.slack.com">slack channel</a> or solve one of our <a href="https://github.com/lbryio/spee.ch/issues">github issues</a>.</p>
</div>

View file

@ -1,36 +0,0 @@
<div class="panel">
<h2>Documentation
<a class="toggle-link" id="documentation-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[ - ]" data-closedlabel="[ + ]" data-slaveelementid="documentation-detail">[ + ]</a>
</h2>
<div id="documentation-detail" hidden="true">
<code>https://spee.ch/</code>
<ul>
<li>Learn about Spee.ch and publish your own media</li>
</ul>
<code>https://spee.ch/:name.ext</code>
<ul>
<li >Serves the winning free, public claim at this name directly</li>
<li >E.g. <a href="/doitlive.png">spee.ch/doitlive.png</a></li>
</ul>
<code>https://spee.ch/:name</code>
<ul>
<li >Serves an HTML page which shows the winning claim at this name with additional details</li>
<li >E.g. <a href="/doitlive">spee.ch/doitlive</a></li>
</ul>
<code>https://spee.ch/:name/:claim_id.ext</code>
<ul>
<li >Serves a specific image or video file directly</li>
<li >E.g. <a href="/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg">spee.ch/doitlive/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0.jpg</a></li>
</ul>
<code>https://spee.ch/:name/:claim_id</code>
<ul>
<li >Serves an HTML page with this specific claim and additional details</li>
<li >E.g. <a href="/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive">spee.ch/doitlive/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0</a></li>
</ul>
<code>https://spee.ch/:name/all</code>
<ul>
<li >Displays a list of all files at a claim</li>
<li >E.g. <a href="/doitlive/all">spee.ch/doitlive/all</a></li>
</ul>
</div>
</div>

View file

@ -1,17 +0,0 @@
<div class="panel">
<h2>Examples
<a class="toggle-link" id="examples-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[ - ]" data-closedlabel="[ + ]" data-slaveelementid="examples-detail">[ + ]</a>
</h2>
<div id="examples-detail" hidden="true">
<div class="example">
<h4>Use spee.ch to embed a specific image:</h4>
<a href="/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg"><img class="example-image" src="/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg"/></a>
<div class="example-code">&lt;img src="https://spee.ch/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg"/&gt;</div>
</div>
<div class="example">
<h4>Use spee.ch to serve the top free image at a claim:</h4>
<a href="/doitlive.png"><img class="example-image" src="/doitlive.png"/></a>
<div class="example-code">&lt;img src="https://spee.ch/doitlive.png"/&gt;</div>
</div>
</div>
</div>

View file

@ -1,3 +0,0 @@
<footer class="row">
<p> thanks for visiting spee.ch </p>
</footer>

View file

@ -0,0 +1,12 @@
<div class="grid-item" id="grid-item-{{this.name}}-{{this.claimId}}" onmouseenter="showAssetDetails(event)" onmouseleave="hideAssetDetails(event)">
{{#ifConditional this.contentType '===' 'video/mp4'}}
<img class="grid-item-image" src="{{this.thumbnail}}"/>
{{else}}
<img class="grid-item-image" src="{{this.directUrlLong}}" />
{{/ifConditional}}
<div class="hidden" onclick="window.location='{{this.showUrlLong}}'">
<p class="grid-item-details-text">{{this.name}}</p>
</div>
</div>

View file

@ -1,3 +0,0 @@
<div class="row learn-more">
<p><i>Spee.ch is an open-source project. You should <a href="https://github.com/lbryio/spee.ch/issues">contribute</a> on github, or <a href="https://github.com/lbryio/spee.ch">fork it</a> and make your own!</i></p>
</div>

View file

@ -0,0 +1,51 @@
<div class="row row--wide nav-bar">
<div class="row row--padded row--short flex-container flex-container--row flex-container--align-center">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" height="24px" viewBox="0 0 80 31" enable-background="new 0 0 80 31" xml:space="preserve" class="nav-bar-logo">
<a href="/">
<title>Logo</title>
<desc>Spee.ch logo</desc>
<g id="About">
<g id="Publish-Form-V2-_x28_filled_x29_" transform="translate(-42.000000, -23.000000)">
<g id="Group-17" transform="translate(42.000000, 22.000000)">
<text transform="matrix(1 0 0 1 0 20)" font-size="25" font-family="Roboto">Spee&lt;h</text>
<g id="Group-16" transform="translate(0.000000, 30.000000)">
<path id="Line-8" fill="none" stroke="#09F911" stroke-width="1" stroke-linecap="square" d="M0.5,1.5h15"/>
<path id="Line-8-Copy" fill="none" stroke="#029D74" stroke-width="1" stroke-linecap="square" d="M16.5,1.5h15"/>
<path id="Line-8-Copy-2" fill="none" stroke="#E35BD8" stroke-width="1" stroke-linecap="square" d="M32.5,1.5h15"/>
<path id="Line-8-Copy-3" fill="none" stroke="#4156C5" stroke-width="1" stroke-linecap="square" d="M48.5,1.5h15"/>
<path id="Line-8-Copy-4" fill="none" stroke="#635688" stroke-width="1" stroke-linecap="square" d="M64.5,1.5h15"/>
</g>
</g>
</g>
</g>
</a>
</svg>
<div class="nav-bar--center">
<span class="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
</div>
<div class="nav-bar--right">
<a class="nav-bar-link link--nav" href="/">Upload</a>
<a class="nav-bar-link link--nav" href="/popular">Popular</a>
<a class="nav-bar-link link--nav" href="/about">About</a>
<select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event.target.selectedOptions[0].value)" {{#unless user}}style="display:none"{{/unless}}>
<option id="nav-bar-channel-select-channel-option">@{{user.userName}}</option>
<option value="VIEW">View</option>
<option value="LOGOUT">Logout</option>
</select>
<a id="nav-bar-login-link" class="nav-bar-link link--nav" href="/login" {{#if user}}style="display:none"{{/if}}>Channel</a>
</div>
</div>
</div>
<script type="text/javascript">
// highlight the link for the current page
const navBarLinks = document.getElementsByClassName('link--nav');
for (let i = 0; i < navBarLinks.length; i++){
const link = navBarLinks[i];
if (link.href == window.location.href) {
link.setAttribute('class', 'nav-bar-link link--nav-active');
} else if (`/${link.value}` === window.location.pathname) {
link.setAttribute('class', 'select select--arrow link--nav-active');
}
}
</script>

View file

@ -1,66 +1,106 @@
<div class="row">
<!-- select whether to publish anonymously or in a channel -->
<div class="row row--padded row--short row--wide">
<div class="column column--10">
<form>
<div class="column column--3 column--med-10">
<input type="radio" name="anonymous-or-channel" id="anonymous-select" class="input-radio" value="anonymous" {{#unless user}}checked {{/unless}} onchange="toggleChannel(event.target.value)"/>
<label class="label label--pointer" for="anonymous-select">Anonymous</label>
</div><div class="column column--7 column--med-10">
<input type="radio" name="anonymous-or-channel" id="in-a-channel-select" class="input-radio" value="in a channel" {{#if user}}checked {{/if}} onchange="toggleChannel(event.target.value)"/>
<label class="label label--pointer" for="in-a-channel-select">In a channel</label>
</div>
</form>
</div>
</div>
<div id="channel-select-options" {{#unless user}}hidden="true"{{/unless}}>
<div class="row row--padded row--no-top row--no-bottom row--wide">
<!--error display-->
<p id="input-error-channel-select" class="info-message-placeholder info-message--failure"></p>
<!--channel login/create select-->
<div class="column column--3">
<label class="label" for="channel-name-select">Channel:</label>
</div>
<div class="column column--9">
<div id="input-error-channel-select" class="info-message info-message--failure"></div>
<select type="text" id="channel-name-select" class="select select--primary" value="channel" onchange="toggleChannel(event)">
<optgroup>
</div><div class="column column--7">
<select type="text" id="channel-name-select" class="select select--arrow" onchange="toggleSelectedChannel(event.target.selectedOptions[0].value)">
{{#if user}}
<option value="{{user.channelName}}" >@{{user.userName}}</option>
<option value="{{user.channelName}}" id="publish-channel-select-channel-option">{{user.channelName}}</option>
{{/if}}
<option value="none" >None</option>
</optgroup>
<optgroup>
<option value="login">Login</option>
<option value="login">Existing</option>
<option value="new" >New</option>
</optgroup>
</select>
</div>
</div>
<div id="channel-login-details" class="row" hidden="true">
<!-- log into an existing channel -->
<div id="channel-login-details" class="row row--padded row--short row--wide" {{#if user}}hidden="true"{{/if}}>
{{> channelLoginForm}}
</div>
<div id="channel-create-details" class="row" hidden="true">
<!-- create a channel -->
<div id="channel-create-details" class="row row--padded row--short row--wide" hidden="true">
{{> channelCreationForm}}
</div>
</div>
<script src="/assets/js/authFunctions.js"></script>
<script type="text/javascript">
function toggleChannel (event) {
// show or hide the channel selection tools
function toggleChannel (selectedOption) {
const channelSelectOptions = document.getElementById('channel-select-options');
// show/hide the login and new channel forms
if (selectedOption === 'anonymous') {
channelSelectOptions.hidden = true;
channelSelectOptions.hidden = true;
// update url
updateUrl(selectedOption);
} else if (selectedOption === 'in a channel') {
channelSelectOptions.hidden = false;
// update url
let selectedChannel = document.getElementById('channel-name-select').selectedOptions[0].value
toggleSelectedChannel(selectedChannel);
} else {
console.log('selected option was not recognized');
}
}
// show or hide the channel create/login tool
function toggleSelectedChannel (selectedChannel) {
const createChannelTool = document.getElementById('channel-create-details');
const loginToChannelTool = document.getElementById('channel-login-details');
const selectedOption = event.target.selectedOptions[0].value;
const urlChannel = document.getElementById('url-channel');
console.log('toggle event triggered');
if (selectedOption === 'new') {
// show/hide the login and new channel forms
if (selectedChannel === 'new') {
createChannelTool.hidden = false;
loginToChannelTool.hidden = true;
// update URL
urlChannel.innerText = '';
} else if (selectedOption === 'login') {
// show/hide the login and new channel forms
} else if (selectedChannel === 'login') {
loginToChannelTool.hidden = false;
createChannelTool.hidden = true;
// update URL
urlChannel.innerText = '';
} else {
// hide the login and new channel forms
loginToChannelTool.hidden = true;
createChannelTool.hidden = true;
hideError(document.getElementById('input-error-channel-select'));
// update URL
if (selectedOption === 'none'){
console.log('selected option: none');
urlChannel.innerText = '';
} else {
console.log('selected option:', selectedOption);
// retrieve short url from db
urlChannel.innerText = `{{user.channelName}}:{{user.shortChannelId}}/`;
}
// update url
updateUrl(selectedChannel);
}
function updateUrl (selectedOption) {
const urlChannel = document.getElementById('url-channel');
const urlNoChannelPlaceholder = document.getElementById('url-no-channel-placeholder');
const urlChannelPlaceholder = document.getElementById('url-channel-placeholder');
if (selectedOption === 'new' || selectedOption === 'login' || selectedOption === ''){
urlChannel.hidden = true;
urlNoChannelPlaceholder.hidden = true;
urlChannelPlaceholder.hidden = false;
} else if (selectedOption === 'anonymous'){
urlChannel.hidden = true;
urlNoChannelPlaceholder.hidden = false;
urlChannelPlaceholder.hidden = true;
} else {
urlChannel.hidden = false;
// show channel and short id
const selectedChannel = getCookie('channel_name');
const shortChannelId = getCookie('short_channel_id');
urlChannel.innerText = `${selectedChannel}:${shortChannelId}`;
urlNoChannelPlaceholder.hidden = true;
urlChannelPlaceholder.hidden = true;
}
}
</script>

View file

@ -1,51 +1,50 @@
<div id="details-detail" hidden="true">
<div class="row row--thin">
<div class="column column--3">
<label for="publish-title" class="label">Title: </label>
</div>
<div class="column column--9">
<input type="text" id="publish-title" class="input-text input-text--primary">
<div class="row row--padded row--no-top row--no-bottom row--wide">
<div class="column column--10">
<a class="label link--primary" id="publish-details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="publish-details">[more]</a>
</div>
</div>
<div class="row row--thin">
<div class="column column--3">
<label for="publish-description" class="label">Description: </label>
</div>
<div class="column column--9">
<textarea rows="2" id="publish-description" class="input-textarea"></textarea>
<div id="publish-details" hidden="true" class="row">
<!-- description input -->
<div class="row row--no-top">
<div class="column column--3 column--sml-10 align-content-top">
<label for="publish-license" class="label">Description:</label>
</div><div class="column column--7 column--sml-10">
<textarea rows="1" id="publish-description" class="textarea textarea--primary textarea--full-width" placeholder="Optional description"></textarea>
</div>
</div>
<div class="row row--thin">
<div class="column column--3">
<label for="publish-license" class="label">License:* </label>
</div>
<div class="column column--9">
<div class="row row--no-top">
<div class="column column--3 column--sml-10">
<label for="publish-license" class="label">License:</label>
</div><div class="column column--7 column--sml-10">
<select type="text" id="publish-license" class="select select--primary">
<option value=" ">Unspecified</option>
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons">Creative Commons</option>
</select>
</div>
</div>
<div class="row row--thin">
<div class="column column--3">
<label for="publish-nsfw" class="label">NSFW*</label>
</div>
<div class="column column--9">
<div class="row row--no-top">
<div class="column column--3 column--sml-10">
<label for="publish-nsfw" class="label">Mature:</label>
</div><div class="column column--7 column--sml-10">
<input class="input-checkbox" type="checkbox" id="publish-nsfw">
</div>
</div>
</div>
<div class="row">
<div class="column column--12">
<a class="label" id="details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="details-detail">[more]</a>
</div>
</div>
<script type="text/javascript">
const textarea = document.getElementById('publish-description');
const limit = 200;
textarea.oninput = () => {
textarea.style.height = '';
textarea.style.height = Math.min(textarea.scrollHeight, limit) + 'px';
}
</script>

View file

@ -0,0 +1,10 @@
<div class="row row--padded row--wide">
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
<button id="publish-submit" class="button--primary button--large" onclick="publishStagedFile(event)">Upload</button>
</div>
<div class="row row--short align-content-center">
<button class="button--cancel" onclick="cancelPublish()">Cancel</button>
</div>
<div class="row row--short align-content-center">
<p class="fine-print">By clicking 'Upload', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a class="link--primary" target="_blank" href="https://lbry.io/learn">Read more.</a></p>
</div>

View file

@ -0,0 +1,56 @@
<div class="row row--padded row--wide row--no-top" id="publish-thumbnail" hidden="true">
<div class="column column--3 column--sml-10">
<label class="label">Thumbnail:</label>
</div><div class="column column--6 column--sml-10">
<div class="input-text--primary">
<input type="text" id="claim-thumbnail-input" class="input-text input-text--full-width" placeholder="https://spee.ch/xyz/example.jpg" value="" oninput="updateVideoThumb(event)">
</div>
</div>
</div>
<script type="text/javascript">
function urlIsAnImage(url) {
return(url.match(/\.(jpeg|jpg|gif|png)$/) != null);
}
function testImage(url, timeoutT) {
return new Promise(function (resolve, reject) {
var timeout = timeoutT || 5000;
var timer, img = new Image();
img.onerror = img.onabort = function () {
clearTimeout(timer);
reject("error");
};
img.onload = function () {
clearTimeout(timer);
resolve("success");
};
timer = setTimeout(function () {
// reset .src to invalid URL so it stops previous
// loading, but doesn't trigger new load
img.src = "//!!!!/test.jpg";
reject("timeout");
}, timeout);
img.src = url;
});
}
function updateVideoThumb(event){
var videoPreview = document.getElementById('asset-preview');
var imageUrl = event.target.value;
if (urlIsAnImage(imageUrl)){
testImage(imageUrl, 3000)
.then(function(result) {
if (result === 'success'){
videoPreview.src = imageUrl;
} else if (result === 'timeout') {
console.log('could not resolve the provided thumbnail image url');
}
})
.catch(function(error) {
console.log('encountered an error loading thumbnail image url.')
})
}
}
</script>

View file

@ -1,13 +1,16 @@
<div class="row">
<div class="column column--3">
<div class="row row--padded row--wide">
<!--error display-->
<p id="input-error-claim-name" class="info-message-placeholder info-message--failure" hidden="true"></p>
<!--url selection-->
<div class="column column--3 column--sml-10">
<label class="label">URL:</label>
</div>
<div class="column column--9">
<div id="input-error-claim-name" class="info-message info-message--failure" hidden="true"></div>
<div class="input-text--primary">
<span class="url-text">Spee.ch/</span><span id="url-channel" class="url-text">{{#if user}}{{user.channelName}}:{{user.shortChannelId}}/{{/if}}</span><input type="text" id="claim-name-input" class="input-text" placeholder="your-url-here" oninput="checkClaimName(event.target.value)">
<span id="input-success-claim-name" class="info-message info-message--success"></span>
</div><div class="column column--7 column--sml-10 input-text--primary span--relative">
<span class="url-text--secondary">spee.ch /</span>
<span id="url-channel" class="url-text--secondary" {{#if user}}{{else}}hidden="true"{{/if}}>{{user.channelName}}:{{user.shortChannelId}}</span>
<span id="url-no-channel-placeholder" class="url-text--secondary tooltip" {{#if user}}hidden="true"{{else}}{{/if}}>xyz<span class="tooltip-text">This will be a random id</span></span>
<span id="url-channel-placeholder" class="url-text--secondary tooltip" hidden="true">@channel<span class="tooltip-text">Select a channel above</span></span> /
<input type="text" id="claim-name-input" class="input-text" placeholder="your-url-here" oninput="checkClaimName(event.target.value)">
<span id="input-success-claim-name" class="info-message--success span--absolute"></span>
</div>
</div>
</div>

View file

@ -1,62 +0,0 @@
<div class="panel">
<h2>Publish</h2>
<div class="row">
<div class="col-left">
<div id="file-selection-area">
<div id="drop-zone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)">
<div class="row">
<p>Drag and drop your file here, or choose your file below.</p>
<div class="info-message info-message--failure" id="input-error-file-selection" hidden="true"></div>
</div>
<div class="row">
<input type="file" id="siofu_input" name="file" accept="video/*,image/*" onchange="previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</div>
</div>
<div id="asset-preview-holder"></div>
</div>
</div>
<div class="col-right">
<div id="publish-active-area">
{{> publishForm-Channel}}
{{> publishForm-Url}}
{{> publishForm-Details}}
<div class="row">
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
<button id="publish-submit" onclick="publishSelectedImage(event)">Publish</button>
<button onclick="resetPublishArea()">Reset</button>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" >
function resetPublishArea (){
// reset file selection area
document.getElementById('file-selection-area').innerHTML = `<div id="drop-zone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)">
<p>Drag and drop your file here, or choose your file below.</p>
<div class="info-message info-message--failure" id="input-error-file-selection" hidden="true"></div>
<input type="file" id="siofu_input" name="file" accept="video/*,image/*" onchange="previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</div>
<div id="asset-preview-holder"></div>`;
// reset inputs
document.getElementById('claim-name-input').value = '';
document.getElementById('publish-title').value = '';
document.getElementById('publish-description').value = '';
document.getElementById('publish-nsfw').checked = false;
// remove staged files
stagedFiles = null;
// clear any errors
document.getElementById('input-error-file-selection').innerHTML = '';
document.getElementById('input-error-claim-name').innerHTML = '';
document.getElementById('input-error-publish-submit').innerHTML = '';
document.getElementById('input-success-claim-name').hidden = true;
}
</script>

View file

@ -0,0 +1,3 @@
<div id="new-release-banner" class="row row--short row--wide">
Hi there! You've stumbled upon the new version of Spee&#60;h, launching soon! Send us your feedback in <a style="color:white; text-decoration: underline" target="_blank" href="https://discord.gg/YjYbwhS">our discord</a>
</div>

View file

@ -1,32 +0,0 @@
<div class="row top-bar">
<a href="https://en.wikipedia.org/wiki/Freedom_of_information" target="_blank"><img id="logo" src="/assets/img/content-freedom-64px.png"/></a>
<h1 id="title"><a href="/">Spee.ch</a></h1><span class="top-bar-left">(beta)</span>
<a href="/popular" class="top-bar-right">popular</a>
<a href="https://github.com/lbryio/spee.ch" target="_blank" class="top-bar-right">source</a>
<a href="/about" class="top-bar-right">help</a>
{{#if user}}
<select type="text" class="select" onchange="toggleLogin(event)">
<option value="none">@{{user.userName}}</option>
<option value="view">view</option>
<option value="logout">logout</option>
</select>
{{else}}
<a href="/login" class="top-bar-right">login</a>
{{/if}}
<div class="top-bar-tagline">Open-source, decentralized image and video hosting.</div>
</div>
<script type="text/javascript">
function toggleLogin (event) {
console.log(event);
const selectedOption = event.target.selectedOptions[0].value;
if (selectedOption === 'logout') {
console.log('login');
window.location.href = '/logout';
} else if (selectedOption === 'view') {
console.log('view channel');
window.location.href = '/{{user.channelName}}:{{user.channelClaimId}}';
}
}
</script>

View file

@ -1,5 +0,0 @@
<h2>What Is Spee.ch?</h2>
<h3>Spee.ch is for sharing</h3>
<p>Spee.ch is a platform by which you can publish images to the Lbry blockchain. Just upload an image, title it, and send it off into the lbry blockchain.</p>
<p>Spee.ch is also a platform to serve you those images. It's like have a personal chef that will serve you a meal anywhere in the world. All you have to do is ask for it, by using "spee.ch/" + the name of a claim.</p>
<p>If you want a specific image, just ask for it with the claim_id by using "spee.ch/" + the name of the claim + "/" + the claim id.</p>

23
views/popular.handlebars Normal file
View file

@ -0,0 +1,23 @@
<div class="row row--padded">
<div class="grid">
{{#each trendingAssets}}
{{> gridItem}}
{{/each}}
</div>
</div>
<script src="/assets/vendors/masonry/masonry.pkgd.min.js"></script>
<script src="/assets/vendors/imagesloaded/imagesloaded.pkgd.min.js"></script>
<script>
// init masonry with element
var grid = document.querySelector('.grid');
var msnry;
imagesLoaded( grid, function() {
msnry = new Masonry( grid, {
itemSelector: '.grid-item',
columnWidth: 3,
percentPosition: true
});
});
</script>

View file

@ -1,24 +1,17 @@
<div class="wrapper">
{{> topBar}}
<div class="main">
<div class="row row--tall row--padded">
<div class="column column--10">
<!-- title -->
<span class="text--large">{{fileInfo.title}}</span>
</div>
<div class="column column--5 column--sml-10 align-content-top">
<!-- asset -->
<div class="row row--padded">
{{> asset}}
</div>
<div class="sidebar">
</div><div class="column column--5 column--sml-10 align-content-top">
<!-- details -->
<div class="row row--padded">
{{> assetInfo}}
</div>
{{> footer}}
</div>
<script type ="text/javascript">
function copyToClipboard(event){
var elementToCopy = event.target.dataset.elementtocopy;
var element = document.getElementById(elementToCopy);
var errorElement = 'input-error-copy-text' + elementToCopy;
element.select();
try {
document.execCommand('copy');
} catch (err) {
showError(errorElement, 'Oops, unable to copy');
}
}
</script>
</div>

View file

@ -1,21 +1,21 @@
<div id="asset-placeholder">
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}">
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
{{#ifConditional fileInfo.fileExt '===' '.gifv'}}
<video class="show-asset-light" autoplay loop muted>
<source src="/media/{{fileInfo.fileName}}">
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
{{else}}
<video class="show-asset-light" autoplay controls>
<source src="/media/{{fileInfo.fileName}}">
<video class="show-asset-light" controls id="video-player">
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
{{/ifConditional}}
<br/>
<a class="link--primary fine-print" href="/{{fileInfo.claimId}}/{{fileInfo.name}}">hosted via spee&lt;h</a>
{{else}}
<img class="show-asset-lite" src="/media/{{fileInfo.fileName}}" alt="{{fileInfo.fileName}}"/>
{{/ifConditional}}
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}">
<img class="show-asset-lite" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" alt="{{fileInfo.fileName}}"/>
</a>
</div>
{{/ifConditional}}

View file

@ -1,7 +1,3 @@
<div class="wrapper">
<div class="top-bar">
{{> topBar}}
</div>
<div>
<h3>Site Statistics</h3>
<p>Serve: {{ totals.totalServe }}</p>
@ -12,27 +8,25 @@
<tr>
<th>action</th>
<th >url</th>
<th class="center-text">count</th>
<th class="center-text">success</th>
<th class="center-text">failure</th>
<th class="">count</th>
<th class="">success</th>
<th class="">failure</th>
</tr>
{{#each records}}
<tr>
<td>{{ this.action }}</td>
<td class="stats-table-url">{{ this.url }}</td>
<td class="center-text">{{ this.count }}</td>
<td class="center-text">{{ this.success }}</td>
<td class="center-text">{{ this.failure }}</td>
<td class="">{{ this.count }}</td>
<td class="">{{ this.success }}</td>
<td class="">{{ this.failure }}</td>
</tr>
{{/each}}
<tr>
<td></td>
<td></td>
<td class="totals-row center-text">{{ totals.totalCount }}</td>
<td class="totals-row center-text">{{ totals.totalSuccess }}</td>
<td class="totals-row center-text">{{ totals.totalFailure }}</td>
<td class="">{{ totals.totalCount }}</td>
<td class="">{{ totals.totalSuccess }}</td>
<td class="">{{ totals.totalFailure }}</td>
</tr>
</table>
</div>
</div>

View file

@ -1,11 +0,0 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>Popular</h3>
<p>Below are the 25 most popular items on spee.ch</p>
{{#each trendingAssets}}
{{> contentListItem}}
{{/each}}
</div>
{{> footer}}
</div>