Redesign 1 bcrypt #226
33
README.md
|
@ -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!
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -8,5 +8,8 @@
|
|||
},
|
||||
"Logging": {
|
||||
"SlackWebHook": "SLACK_WEB_HOOK"
|
||||
},
|
||||
"Session": {
|
||||
"SessionKey": "SESSION_KEY"
|
||||
}
|
||||
}
|
|
@ -16,5 +16,8 @@
|
|||
"SlackWebHook": null,
|
||||
"SlackErrorChannel": null,
|
||||
"SlackInfoChannel": null
|
||||
},
|
||||
"Session": {
|
||||
"SessionKey": null
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
@ -39,17 +38,18 @@ module.exports = {
|
|||
nsfw : publishParams.metadata.nsfw,
|
||||
};
|
||||
const claimRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description : publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
contentType : fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
certificateId: user.channelClaimId,
|
||||
amount : publishParams.bid,
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
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,
|
||||
amount : publishParams.bid,
|
||||
};
|
||||
const upsertCriteria = {
|
||||
name : publishParams.name,
|
||||
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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.data.error) {
|
||||
return error.response.data.error.message;
|
||||
} 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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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" >
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
};
|
||||
|
|
22
migrations/AddChannelNameToClaim.js
Normal 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]);
|
||||
},
|
||||
};
|
|
@ -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]);
|
||||
},
|
||||
};
|
|
@ -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]);
|
||||
},
|
||||
};
|
46
migrations/UpdateUserPasswords5.js
Normal 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
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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('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('...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('...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 => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
578
public/assets/css/general.css
Normal 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%;
|
||||
}
|
|
@ -1,59 +1,96 @@
|
|||
@media (max-width: 1250px) {
|
||||
.wrapper {
|
||||
margin-left: 10%;
|
||||
width:80%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1050px) {
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.wrapper {
|
||||
margin-left: 10%;
|
||||
width:80%;
|
||||
.nav-bar--center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
BIN
public/assets/font/Lekton/Lekton-Bold.ttf
Normal file
BIN
public/assets/font/Lekton/Lekton-Italic.ttf
Normal file
BIN
public/assets/font/Lekton/Lekton-Regular.ttf
Normal file
93
public/assets/font/Lekton/OFL.txt
Normal 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.
|
15
public/assets/icon/Fill 5114 + Fill 5115 + Fill 5116.svg
Normal 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 |
16
public/assets/icon/Shape.svg
Normal 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 |
22
public/assets/icon/upload.svg
Normal 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 |
BIN
public/assets/img/Speech_Logo_Main@OG-02.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
public/assets/img/black_video_play.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 58 KiB |
BIN
public/assets/img/down_triangle.png
Normal file
After Width: | Height: | Size: 184 B |
BIN
public/assets/img/logo.gif
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
public/assets/img/upload_arrow.png
Normal file
After Width: | Height: | Size: 195 B |
BIN
public/assets/img/video_thumb_default.png
Normal file
After Width: | Height: | Size: 58 KiB |
1
public/assets/js/constants.js
Normal file
|
@ -0,0 +1 @@
|
|||
const EMAIL_FORMAT = 'ERROR_EMAIL_FORMAT';
|
56
public/assets/js/createChannelFunctions.js
Normal 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!');
|
||||
}
|
||||
})
|
||||
}
|
56
public/assets/js/dropzoneFunctions.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
@ -62,22 +60,99 @@ function toggleSection(event){
|
|||
}
|
||||
}
|
||||
|
||||
function createProgressBar(element, size){
|
||||
var x = 1;
|
||||
function createProgressBar(element, size){
|
||||
var x = 0;
|
||||
var adder = 1;
|
||||
function addOne(){
|
||||
var bars = '<p>|';
|
||||
for (var i = 0; i < x; i++){ bars += ' | '; }
|
||||
bars += '</p>';
|
||||
element.innerHTML = bars;
|
||||
if (x === size){
|
||||
adder = -1;
|
||||
} else if ( x === 0){
|
||||
adder = 1;
|
||||
}
|
||||
x += adder;
|
||||
// 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 === -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
|
||||
|
@ -111,4 +186,28 @@ function ChannelPasswordError(message) {
|
|||
this.stack = (new Error()).stack;
|
||||
}
|
||||
ChannelPasswordError.prototype = Object.create(Error.prototype);
|
||||
ChannelPasswordError.prototype.constructor = ChannelPasswordError;
|
||||
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');
|
||||
}
|
||||
|
|
79
public/assets/js/loginFunctions.js
Normal 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');
|
||||
}
|
||||
})
|
||||
}
|
15
public/assets/js/navBarFunctions.js
Normal 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}`;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// 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');
|
||||
function previewAndStageFile(selectedFile){
|
||||
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 !== '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,27 +53,46 @@ 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(() => {
|
||||
uploader.submitFiles(stagedFiles);
|
||||
uploader.submitFiles(stagedFiles);
|
||||
})
|
||||
.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;
|
||||
})
|
||||
|
|
23
public/assets/js/showFunctions.js
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,47 +1,56 @@
|
|||
|
||||
|
||||
// validation function which checks the proposed file's type, size, and name
|
||||
function validateFile(file) {
|
||||
if (!file) {
|
||||
throw new Error('no file provided');
|
||||
}
|
||||
if (/'/.test(file.name)) {
|
||||
throw new Error('apostrophes are not allowed in the file name');
|
||||
}
|
||||
// validate size and type
|
||||
switch (file.type) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
case 'image/gif':
|
||||
if (file.size > 50000000){
|
||||
throw new Error('Sorry, images are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'video/mp4':
|
||||
if (file.size > 50000000){
|
||||
throw new Error('Sorry, videos are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
|
||||
}
|
||||
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
|
||||
switch (file.type) {
|
||||
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){
|
||||
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
|
||||
if (name.length < 1) {
|
||||
throw new NameError("You must enter a name for your url");
|
||||
}
|
||||
// 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.');
|
||||
}
|
||||
// ensure a name was entered
|
||||
if (name.length < 1) {
|
||||
throw new NameError("You must enter a name for your url");
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
function validateChannelName (name) {
|
||||
name = name.substring(name.indexOf('@') + 1);
|
||||
name = name.substring(name.indexOf('@') + 1);
|
||||
// ensure a name was entered
|
||||
if (name.length < 1) {
|
||||
throw new ChannelNameError("You must enter a name for your channel");
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,9 +69,9 @@ function validatePassword (password) {
|
|||
}
|
||||
|
||||
function cleanseClaimName(name) {
|
||||
name = name.replace(/\s+/g, '-'); // replace spaces with dashes
|
||||
name = name.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
|
||||
return name;
|
||||
name = name.replace(/\s+/g, '-'); // replace spaces with dashes
|
||||
name = name.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
|
||||
return name;
|
||||
}
|
||||
|
||||
// validation functions to check claim & channel name eligibility as the inputs change
|
||||
|
@ -99,14 +108,13 @@ 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) {
|
||||
if (result === true) {
|
||||
hideError(errorDisplayElement);
|
||||
showSuccess(successDisplayElement)
|
||||
} else {
|
||||
} else {
|
||||
hideSuccess(successDisplayElement);
|
||||
showError(errorDisplayElement, errorMessage);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideSuccess(successDisplayElement);
|
||||
|
@ -119,58 +127,68 @@ 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/');
|
||||
const successDisplayElement = document.getElementById('input-success-claim-name');
|
||||
const errorDisplayElement = document.getElementById('input-error-claim-name');
|
||||
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
|
||||
if (!stagedFiles) {
|
||||
return reject(new FileError("Please select a file"));
|
||||
} else if (stagedFiles.length > 1) {
|
||||
return reject(new FileError("Only one file is allowed at a time"));
|
||||
}
|
||||
// 2. validate the file's name, type, and size
|
||||
try {
|
||||
validateFile(stagedFiles[0]);
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
// 3. validate that a channel was chosen
|
||||
if (channelName === 'new' || channelName === 'login') {
|
||||
return reject(new ChannelNameError("Please select a valid channel"));
|
||||
return new Promise(function (resolve, reject) {
|
||||
// 1. make sure 1 file was staged
|
||||
if (!stagedFiles) {
|
||||
reject(new FileError("Please select a file"));
|
||||
return;
|
||||
} else if (stagedFiles.length > 1) {
|
||||
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) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
// 3. validate that a channel was chosen
|
||||
if (channelName === 'new' || channelName === 'login') {
|
||||
reject(new ChannelNameError("Please log in to a channel"));
|
||||
return;
|
||||
};
|
||||
// 4. validate the claim name
|
||||
try {
|
||||
validateClaimName(claimName);
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
// if all validation passes, check availability of the name
|
||||
isNameAvailable(claimName, '/api/isClaimAvailable/')
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
// 4. validate the claim name
|
||||
try {
|
||||
validateClaimName(claimName);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
// 1. validate name
|
||||
try {
|
||||
validateChannelName(channelName);
|
||||
} catch (error) {
|
||||
|
@ -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');
|
||||
resolve();
|
||||
.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();
|
||||
});
|
||||
}
|
7
public/assets/vendors/imagesloaded/imagesloaded.pkgd.min.js
vendored
Normal file
9
public/assets/vendors/masonry/masonry.pkgd.min.js
vendored
Normal file
0
public/robots.txt
Normal 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);
|
||||
})
|
||||
// create publish parameters object
|
||||
.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 => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
53
speech.js
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
<div class="sidebar">
|
||||
{{> contribute}}
|
||||
</div>
|
||||
{{> footer}}
|
||||
</div>
|
||||
<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><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>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
{{#each this.claims}}
|
||||
{{> contentListItem}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{> footer}}
|
||||
<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}}
|
||||
{{> gridItem}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<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>
|
|
@ -1,7 +1,4 @@
|
|||
<div class="wrapper">
|
||||
{{> topBar}}
|
||||
<div>
|
||||
<h3>404: Not Found</h3>
|
||||
<p>That page does not exist. Return <a href="/">home</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3>404: Not Found</h3>
|
||||
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>
|
||||
</div>
|
||||
|
|
|
@ -1,69 +1,154 @@
|
|||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
|
||||
<div class="row">
|
||||
<div class="column column--2"></div>
|
||||
<div class="column column--8">
|
||||
{{> topBar}}
|
||||
{{> publishForm}}
|
||||
{{> learnMore}}
|
||||
{{> footer}}
|
||||
<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>
|
||||
|
||||
|
||||
<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;
|
||||
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;
|
||||
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
|
||||
});
|
||||
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('File is ' + percent.toFixed(2) + '% loaded to the server');
|
||||
updatePublishStatus('<p>File is loading to server</p>')
|
||||
updateUploadPercent(`<p class="blue">${percent.toFixed(2)}%</p>`)
|
||||
});
|
||||
/* socket.io message listeners */
|
||||
socket.on('publish-status', function(msg){
|
||||
updatePublishStatus(msg);
|
||||
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){
|
||||
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>';
|
||||
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){
|
||||
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;
|
||||
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;
|
||||
}
|
||||
if (thumbnail && (thumbnail.trim !== '')){
|
||||
event.file.meta.thumbnail = thumbnail;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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>
|
||||
{{{ 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>
|
|
@ -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>
|
||||
{{{ 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>
|
24
views/layouts/showlite.handlebars
Normal 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>
|
||||
|
|
@ -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>
|
||||
{{>channelLoginForm}}
|
||||
<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>
|
||||
<h2>Create New</h2>
|
||||
<div class="row row--wide">
|
||||
<div class="column column--6">
|
||||
<p>Create a brand new channel:</p>
|
||||
</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}}
|
||||
<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>
|
|
@ -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>
|
|
@ -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>
|
||||
<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
|
@ -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>
|
|
@ -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>
|
|
@ -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}}">
|
||||
{{!--fallback--}}
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
{{else}}
|
||||
<video class="show-asset" autoplay controls>
|
||||
<source src="/media/{{fileInfo.fileName}}">
|
||||
{{!--fallback--}}
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
{{/ifConditional}}
|
||||
{{else}}
|
||||
<img class="show-asset" src="/media/{{fileInfo.fileName}}" />
|
||||
{{/ifConditional}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||
{{#ifConditional fileInfo.fileExt '===' 'gifv'}}
|
||||
<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 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}}
|
||||
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
|
||||
<img class="image-show" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" />
|
||||
</a>
|
||||
{{/ifConditional}}
|
|
@ -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>
|
||||
<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 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>
|
||||
</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 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='<video width="100%" controls src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/></video>'/>
|
||||
{{else}}
|
||||
<input type="text" id="embed-text" class="link" readonly onclick="select()" spellcheck="false" value='<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>
|
||||
{{!--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}}
|
||||
{{/if}}
|
||||
|
||||
{{#if fileInfo.description}}
|
||||
<div class="row row--padded row--wide row--no-top">
|
||||
<span class="text">{{fileInfo.description}}</span>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>Description</h2>
|
||||
<p>{{fileInfo.description}}</p>
|
||||
{{/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>
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<video width="100%" controls poster="{{fileInfo.thumbnail}}" src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/></video>'/>
|
||||
{{else}}
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<img src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>'/>
|
||||
{{/ifConditional}}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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}}
|
||||
</div>
|
||||
</div>
|
||||
</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}}
|
||||
{{fileInfo.fileType}}
|
||||
{{else}}
|
||||
unknown
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
|
@ -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>
|
|
@ -1,28 +1,28 @@
|
|||
|
||||
<form id="publish-channel-form">
|
||||
|
||||
<div class="column column--3">
|
||||
<label class="label" for="new-channel-name">Name:</label>
|
||||
<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--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="exampleChannelName" value="" oninput="checkChannelName(event.target.value)">
|
||||
<span id="input-success-channel-name" class="info-message--success"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column column--9">
|
||||
<div id="input-error-channel-name" class="info-message info-message--failure"></div>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<div class="column column--3">
|
||||
<label class="label" for="new-channel-password">Password:</label>
|
||||
</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>
|
||||
|
||||
|
@ -34,45 +34,4 @@
|
|||
|
||||
<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>
|
||||
</div>
|
|
@ -1,45 +1,26 @@
|
|||
|
||||
<form id="channel-login-form">
|
||||
|
||||
<div class="column column--3">
|
||||
<label class="label" for="login-channel-name">Name:</label>
|
||||
<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" id="channel-login-name-input" class="input-text" placeholder="" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column column--9">
|
||||
<div id="login-error-display-element" class="info-message info-message--failure"></div>
|
||||
<div class="input-text--primary">
|
||||
<span>@</span>
|
||||
<input type="text" name="login-channel-name" id="login-channel-name" class="input-text" placeholder="" value="">
|
||||
<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 class="row row--wide">
|
||||
<button class="button--primary" onclick="loginToChannel(event)">Authenticate</button>
|
||||
</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>
|
||||
</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>
|
|
@ -1,15 +1,15 @@
|
|||
<div class='content-list-card'>
|
||||
<a href="{{this.showUrlLong}}"><span class='content-list-card-link'></span></a>
|
||||
{{#ifConditional this.contentType '===' 'video/mp4'}}
|
||||
<img class="content-list-asset" src="{{this.thumbnail}}"/>
|
||||
{{else}}
|
||||
<img class="content-list-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>
|
||||
<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-item-asset" src="{{this.thumbnail}}"/>
|
||||
{{else}}
|
||||
<img class="content-list-item-asset" src="{{this.directUrlLong}}" />
|
||||
{{/ifConditional}}
|
||||
</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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"><img src="https://spee.ch/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg"/></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"><img src="https://spee.ch/doitlive.png"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<footer class="row">
|
||||
<p> thanks for visiting spee.ch </p>
|
||||
</footer>
|
12
views/partials/gridItem.handlebars
Normal 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>
|
|
@ -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>
|
51
views/partials/navBar.handlebars
Normal 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<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>
|
|
@ -1,66 +1,106 @@
|
|||
<div class="row">
|
||||
<div class="column column--3">
|
||||
<label class="label" for="channel-name-select">Channel:</label>
|
||||
<!-- 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 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 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--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="new" >New</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<option value="login">Existing</option>
|
||||
<option value="new" >New</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<!-- create a channel -->
|
||||
<div id="channel-create-details" class="row row--padded row--short row--wide" hidden="true">
|
||||
{{> channelCreationForm}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="channel-login-details" class="row" hidden="true">
|
||||
{{> channelLoginForm}}
|
||||
</div>
|
||||
|
||||
<div id="channel-create-details" class="row" hidden="true">
|
||||
{{> channelCreationForm}}
|
||||
</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
|
||||
// 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>
|
|
@ -1,51 +1,50 @@
|
|||
<div id="details-detail" hidden="true">
|
||||
<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-title" class="label">Title: </label>
|
||||
</div>
|
||||
<div class="column column--9">
|
||||
<input type="text" id="publish-title" class="input-text input-text--primary">
|
||||
<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-description" class="label">Description: </label>
|
||||
</div>
|
||||
<div class="column column--9">
|
||||
<textarea rows="2" id="publish-description" class="input-textarea"></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>
|
||||
|
||||
|
||||
|
||||
|
|
10
views/partials/publishForm-Submit.handlebars
Normal 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>
|
56
views/partials/publishForm-Thumbnail.handlebars
Normal 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>
|
|
@ -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--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 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>
|
||||
</div>
|
|
@ -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>
|
3
views/partials/releaseBanner.handlebars
Normal 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<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>
|
|
@ -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>
|
|
@ -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
|
@ -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>
|
|
@ -1,24 +1,17 @@
|
|||
<div class="wrapper">
|
||||
{{> topBar}}
|
||||
<div class="main">
|
||||
{{> asset}}
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
{{> 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 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><div class="column column--5 column--sml-10 align-content-top">
|
||||
<!-- details -->
|
||||
<div class="row row--padded">
|
||||
{{> assetInfo}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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}}">
|
||||
{{!--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}}">
|
||||
{{!--fallback--}}
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
{{/ifConditional}}
|
||||
{{else}}
|
||||
<img class="show-asset-lite" src="/media/{{fileInfo.fileName}}" alt="{{fileInfo.fileName}}"/>
|
||||
{{/ifConditional}}
|
||||
</a>
|
||||
</div>
|
||||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||
{{#ifConditional fileInfo.fileExt '===' '.gifv'}}
|
||||
<video class="show-asset-light" 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-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<h</a>
|
||||
{{else}}
|
||||
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}">
|
||||
<img class="show-asset-lite" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" alt="{{fileInfo.fileName}}"/>
|
||||
</a>
|
||||
{{/ifConditional}}
|
|
@ -1,38 +1,32 @@
|
|||
<div class="wrapper">
|
||||
<div class="top-bar">
|
||||
{{> topBar}}
|
||||
</div>
|
||||
<div>
|
||||
<h3>Site Statistics</h3>
|
||||
<p>Serve: {{ totals.totalServe }}</p>
|
||||
<p>Publish: {{ totals.totalPublish }}</p>
|
||||
<p>Show: {{ totals.totalShow }}</p>
|
||||
<p>Percent Success: {{ percentSuccess}}%</p>
|
||||
<table>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Site Statistics</h3>
|
||||
<p>Serve: {{ totals.totalServe }}</p>
|
||||
<p>Publish: {{ totals.totalPublish }}</p>
|
||||
<p>Show: {{ totals.totalShow }}</p>
|
||||
<p>Percent Success: {{ percentSuccess}}%</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>action</th>
|
||||
<th >url</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="">{{ this.count }}</td>
|
||||
<td class="">{{ this.success }}</td>
|
||||
<td class="">{{ this.failure }}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="">{{ totals.totalCount }}</td>
|
||||
<td class="">{{ totals.totalSuccess }}</td>
|
||||
<td class="">{{ totals.totalFailure }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -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>
|