mirror of
https://github.com/slynn1324/tinypin.git
synced 2026-01-23 02:25:08 +00:00
added ios share integration and rudimentary upload
This commit is contained in:
parent
acc3f0b732
commit
27e48a1822
10 changed files with 450 additions and 59 deletions
265
server.js
265
server.js
|
|
@ -1,3 +1,5 @@
|
|||
( async () => {
|
||||
|
||||
const yargs = require('yargs');
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
|
|
@ -57,6 +59,9 @@ const DB_PATH = path.resolve(argv['db-path']);
|
|||
const IMAGE_PATH = path.resolve(argv['image-path']);
|
||||
const PORT = argv.port;
|
||||
|
||||
const THUMBNAIL_IMAGE_SIZE = 400;
|
||||
const ADDITIONAL_IMAGE_SIZES = [800,1280,1920,2560];
|
||||
|
||||
console.log('tinypin starting...');
|
||||
console.log('');
|
||||
console.log(`version: ${VERSION}`);
|
||||
|
|
@ -74,7 +79,7 @@ console.log('');
|
|||
|
||||
|
||||
const db = betterSqlite3(DB_PATH);
|
||||
initDb();
|
||||
await initDb();
|
||||
|
||||
const COOKIE_KEY = Buffer.from(db.prepare("SELECT value FROM properties WHERE key = ?").get('cookieKey').value, 'hex');
|
||||
|
||||
|
|
@ -82,8 +87,10 @@ const COOKIE_KEY = Buffer.from(db.prepare("SELECT value FROM properties WHERE ke
|
|||
const app = express();
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.use(bodyParser.raw({type: 'image/jpeg', limit: '25mb'}));
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.set('json spaces', 2);
|
||||
app.use(cookieParser());
|
||||
|
||||
|
|
@ -121,9 +128,39 @@ function decryptCookie(ciphertext){
|
|||
// handle auth
|
||||
app.use ( async (req, res, next) => {
|
||||
|
||||
// disable security
|
||||
// req.user = {
|
||||
// id: 1,
|
||||
// name: 'a'
|
||||
// };
|
||||
// next();
|
||||
// return;
|
||||
|
||||
|
||||
|
||||
if ( req.originalUrl.startsWith("/up/") ){
|
||||
console.log("got up!");
|
||||
console.log("content type = " + req.headers['content-type']);
|
||||
console.log(typeof(req.body));
|
||||
|
||||
await fs.writeFile('up.jpg', req.body);
|
||||
|
||||
res.send(OK);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( req.originalUrl.startsWith("/images/") ){
|
||||
req.user = {
|
||||
id: 1,
|
||||
name: "a"
|
||||
};
|
||||
next();
|
||||
return;
|
||||
}
|
||||
// skip auth for pub resources
|
||||
// handle login and register paths
|
||||
if ( req.originalUrl.startsWith("/pub/")){
|
||||
if ( req.originalUrl.startsWith("/pub/") ){
|
||||
next();
|
||||
return;
|
||||
} if ( req.method == "GET" && req.originalUrl == "/login" ){
|
||||
|
|
@ -224,11 +261,21 @@ app.use( (req, res, next) => {
|
|||
res.setHeader('Cache-control', `private, max-age=2592000000`); // 30 days
|
||||
res.sendFile(filepath);
|
||||
|
||||
} else if ( req.method == "GET" && req.originalUrl.startsWith("/dl/") ){
|
||||
|
||||
let path = req.originalUrl.replace("/dl/", "/images/");
|
||||
|
||||
let filepath = IMAGE_PATH + "/" + req.user.id + "/" + path;
|
||||
res.setHeader("Content-Disposition", 'attachment; filename="image.jpg');
|
||||
res.sendFile(filepath);
|
||||
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
app.use(express.static('static'));
|
||||
|
||||
//emulate slow down
|
||||
|
|
@ -332,8 +379,15 @@ app.delete("/api/boards/:boardId", async (req, res) => {
|
|||
|
||||
let pins = db.prepare("SELECT id FROM pins WHERE userId = @userId and boardId = @boardId").all({userId:req.user.id, boardId:req.params.boardId});
|
||||
for ( let i = 0; i < pins.length; ++i ){
|
||||
await fs.unlink(getThumbnailImagePath(req.user.id, pins[i].id).file);
|
||||
await fs.unlink(getOriginalImagePath(req.user.id, pins[i].id).file);
|
||||
|
||||
await fs.unlink(getImagePath(req.user.id, pins[i].id, 'o').file);
|
||||
|
||||
await fs.unlink(getImagePath(req.user.id, pins[i].id, THUMBNAIL_IMAGE_SIZE).file);
|
||||
|
||||
for ( let s = 0; s < ADDITIONAL_IMAGE_SIZES.length; ++s ){
|
||||
await fs.unlink(getImagePath(req.user.id, pins[i].id, ADDITIONAL_IMAGE_SIZES[s]).file);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let result = db.prepare("DELETE FROM pins WHERE userId = @userId and boardId = @boardId").run({userId:req.user.id, boardId:req.params.boardId});
|
||||
|
|
@ -369,6 +423,7 @@ app.get("/api/pins/:pinId", (req, res) => {
|
|||
app.post("/api/pins", async (req, res) => {
|
||||
try {
|
||||
|
||||
// download the image first to make sure we can get it
|
||||
let image = await downloadImage(req.body.imageUrl);
|
||||
|
||||
let result = db.prepare(`INSERT INTO PINS (
|
||||
|
|
@ -411,15 +466,7 @@ app.post("/api/pins", async (req, res) => {
|
|||
|
||||
let id = result.lastInsertRowid;
|
||||
|
||||
// write the images to disk
|
||||
let originalImagePath = getOriginalImagePath(req.user.id, id);
|
||||
let thumbnailImagePath = getThumbnailImagePath(req.user.id, id);
|
||||
await fs.mkdir(originalImagePath.dir, {recursive: true});
|
||||
await fs.mkdir(thumbnailImagePath.dir, {recursive: true});
|
||||
await fs.writeFile(originalImagePath.file, image.original.buffer);
|
||||
console.log(`Saved original to: ${originalImagePath.file}`);
|
||||
await fs.writeFile(thumbnailImagePath.file, image.thumbnail.buffer);
|
||||
console.log(`Saved thumbnail to: ${thumbnailImagePath.file}`);
|
||||
await saveImage(req.user.id, id, image);
|
||||
|
||||
// return the newly created row
|
||||
let pin = db.prepare("SELECT * FROM pins WHERE userId = @userId and id = @pinId").get({userId: req.user.id, pinId: id});
|
||||
|
|
@ -468,8 +515,12 @@ app.delete("/api/pins/:pinId", async (req, res) => {
|
|||
let result = db.prepare('DELETE FROM pins WHERE userId = @userId and id = @pinId').run({userId: req.user.id, pinId:req.params.pinId});
|
||||
|
||||
if ( result.changes == 1 ){
|
||||
await fs.unlink(getThumbnailImagePath(req.user.id, req.params.pinId).file);
|
||||
await fs.unlink(getOriginalImagePath(req.user.id, req.params.pinId).file);
|
||||
await fs.unlink(getImagePath(req.user.id, req.params.pinId, 'o').file);
|
||||
await fs.unlink(getImagePath(req.user.id, req.params.pinId, THUMBNAIL_IMAGE_SIZE).file);
|
||||
|
||||
for ( let s = 0; s < ADDITIONAL_IMAGE_SIZES.length; ++s ){
|
||||
await fs.unlink(getImagePath(req.user.id, req.params.pinId, ADDITIONAL_IMAGE_SIZES[s]).file);
|
||||
}
|
||||
|
||||
console.log(`deleted pin#${req.params.pinId}`);
|
||||
res.send(OK);
|
||||
|
|
@ -482,6 +533,77 @@ app.delete("/api/pins/:pinId", async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
app.post("/up/", async (req, res) => {
|
||||
console.log("got up!");
|
||||
console.log("content type = " + req.headers['content-type']);
|
||||
let boardName = req.headers['board-name'].trim();
|
||||
console.log("board name = " + req.headers['board-name']);
|
||||
console.log(typeof(req.body));
|
||||
|
||||
let result = db.prepare("SELECT id FROM boards WHERE name = @name and userId = @userId").get({name: boardName, userId: req.user.id});
|
||||
|
||||
let boardId = null;
|
||||
|
||||
if ( result ){
|
||||
boardId = result.id;
|
||||
} else {
|
||||
result = db.prepare("INSERT INTO boards (name, userId, hidden, createDate) VALUES (@name, @userId, @hidden, @createDate)").run({name: boardName, userId: req.user.id, hidden: null, createDate: new Date().toISOString()});
|
||||
boardId = result.lastInsertRowid;
|
||||
}
|
||||
|
||||
console.log("boardId=" + boardId);
|
||||
|
||||
let image = await processImage(req.body);
|
||||
|
||||
result = db.prepare(`INSERT INTO PINS (
|
||||
boardId,
|
||||
imageUrl,
|
||||
siteUrl,
|
||||
description,
|
||||
sortOrder,
|
||||
originalHeight,
|
||||
originalWidth,
|
||||
thumbnailHeight,
|
||||
thumbnailWidth,
|
||||
userId,
|
||||
createDate
|
||||
) VALUES (
|
||||
@boardId,
|
||||
@imageUrl,
|
||||
@siteUrl,
|
||||
@description,
|
||||
@sortOrder,
|
||||
@originalHeight,
|
||||
@originalWidth,
|
||||
@thumbnailHeight,
|
||||
@thumbnailWidth,
|
||||
@userId,
|
||||
@createDate)
|
||||
`).run({
|
||||
boardId: boardId,
|
||||
imageUrl: null,
|
||||
siteUrl: null,
|
||||
description: null,
|
||||
sortOrder: null,
|
||||
originalHeight: image.original.height,
|
||||
originalWidth: image.original.width,
|
||||
thumbnailHeight: image.thumbnail.height,
|
||||
thumbnailWidth: image.thumbnail.width,
|
||||
userId: req.user.id,
|
||||
createDate: new Date().toISOString()
|
||||
});
|
||||
|
||||
let id = result.lastInsertRowid;
|
||||
|
||||
await saveImage(req.user.id, id, image);
|
||||
|
||||
// return the newly created row
|
||||
let pin = db.prepare("SELECT * FROM pins WHERE userId = @userId and id = @pinId").get({userId: req.user.id, pinId: id});
|
||||
res.send(pin);
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
|
||||
|
||||
// start listening
|
||||
|
|
@ -490,7 +612,7 @@ app.listen(PORT, () => {
|
|||
console.log('');
|
||||
});
|
||||
|
||||
function initDb(){
|
||||
async function initDb(){
|
||||
|
||||
console.log("initializing database...");
|
||||
|
||||
|
|
@ -600,10 +722,67 @@ function initDb(){
|
|||
})();
|
||||
}
|
||||
|
||||
if ( schemaVersion < 3 ){
|
||||
// image file re-organization, additional sizes
|
||||
console.log(" running migration v3");
|
||||
|
||||
let pins = db.prepare(`SELECT * FROM pins`).all();
|
||||
|
||||
if ( require("fs").existsSync(`${IMAGE_PATH}`) ){
|
||||
let userdirs = await fs.readdir(IMAGE_PATH);
|
||||
console.log(" migrating images");
|
||||
for ( let i = 0; i < userdirs.length; ++i ){
|
||||
if ( require("fs").existsSync(`${IMAGE_PATH}/${userdirs[i]}/images/originals`) ){
|
||||
await fs.rename(`${IMAGE_PATH}/${userdirs[i]}/images/originals`, `${IMAGE_PATH}/${userdirs[i]}/images/o`);
|
||||
}
|
||||
if ( require("fs").existsSync(`${IMAGE_PATH}/${userdirs[i]}/images/thumbnails`) ){
|
||||
await fs.rename(`${IMAGE_PATH}/${userdirs[i]}/images/thumbnails`, `${IMAGE_PATH}/${userdirs[i]}/images/400`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( pins.length > 0 ){
|
||||
console.log(" generating additional image sizes...");
|
||||
}
|
||||
|
||||
for ( let i = 0; i < pins.length; ++i ){
|
||||
let pin = pins[i];
|
||||
let originalImagePath = getImagePath(pin.userId, pin.id, 'o');
|
||||
|
||||
for ( let i = 0; i < ADDITIONAL_IMAGE_SIZES.length; ++i ){
|
||||
let size = ADDITIONAL_IMAGE_SIZES[i];
|
||||
|
||||
let img = await sharp(originalImagePath.file);
|
||||
let resizedImg = await img.resize({width: size, height: size, fit: 'inside'})
|
||||
let buf = await resizedImg.toBuffer();
|
||||
|
||||
let imgPath = getImagePath(pin.userId, pin.id, size);
|
||||
await fs.mkdir(imgPath.dir, {recursive:true});
|
||||
await fs.writeFile(imgPath.file, buf);
|
||||
console.log(` saved additional size ${size} for pin#${pin.id} to: ${imgPath.file}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( pins.length > 0 ){
|
||||
console.log(" finished generating addditional image sizes");
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO migrations (id, createDate) VALUES ( @id, @createDate )
|
||||
`).run({id:3, createDate: new Date().toISOString()});
|
||||
|
||||
schemaVersion = 3;
|
||||
}
|
||||
|
||||
console.log(`database ready - schema version v${schemaVersion}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the image, converts it to JPG, and creates the thumbnail size so that standard dimensions can be taken
|
||||
* @param {string} imageUrl
|
||||
*/
|
||||
async function downloadImage(imageUrl){
|
||||
|
||||
let res = await fetch(imageUrl);
|
||||
|
|
@ -614,11 +793,15 @@ async function downloadImage(imageUrl){
|
|||
|
||||
let buffer = await res.buffer();
|
||||
|
||||
let original = sharp(buffer);
|
||||
let originalMetadata = await original.metadata();
|
||||
let originalBuffer = await original.toFormat("jpg").toBuffer();
|
||||
return await processImage(buffer);
|
||||
}
|
||||
|
||||
let thumbnail = await original.resize({ width: 400, height: 400, fit: 'inside' });
|
||||
async function processImage(buffer){
|
||||
let original = sharp(buffer);
|
||||
let originalBuffer = await original.toFormat("jpg").toBuffer();
|
||||
let originalMetadata = await original.metadata();
|
||||
|
||||
let thumbnail = await original.resize({ width: THUMBNAIL_IMAGE_SIZE, height: THUMBNAIL_IMAGE_SIZE, fit: 'inside' });
|
||||
let thumbnailBuffer = await thumbnail.toBuffer();
|
||||
let thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
||||
|
||||
|
|
@ -636,17 +819,43 @@ async function downloadImage(imageUrl){
|
|||
}
|
||||
}
|
||||
|
||||
// takes the response from downloadImage, creates ADDITIONAL_IMAGE_SIZE and writes all files to disk
|
||||
async function saveImage(userId, pinId, image){
|
||||
|
||||
|
||||
let originalImagePath = getImagePath(userId, pinId, 'o');
|
||||
await fs.mkdir(originalImagePath.dir, {recursive: true});
|
||||
await fs.writeFile(originalImagePath.file, image.original.buffer);
|
||||
console.log(`saved original to: ${originalImagePath.file}`);
|
||||
|
||||
let thumbnailImagePath = getImagePath(userId, pinId, '400');
|
||||
await fs.mkdir(thumbnailImagePath.dir, {recursive: true});
|
||||
await fs.writeFile(thumbnailImagePath.file, image.thumbnail.buffer);
|
||||
console.log(`saved thumbnail to: ${thumbnailImagePath.file}`);
|
||||
|
||||
// this will enlarge images if necessary, as the lanczos3 resize algorithm will create better looking enlargements than the browser will
|
||||
for ( let i = 0; i < ADDITIONAL_IMAGE_SIZES.length; ++i ){
|
||||
let size = ADDITIONAL_IMAGE_SIZES[i];
|
||||
|
||||
let img = await sharp(image.original.buffer);
|
||||
let resizedImg = await img.resize({width: size, height: size, fit: 'inside'})
|
||||
let buf = await resizedImg.toBuffer();
|
||||
|
||||
let imgPath = getImagePath(userId, pinId, size);
|
||||
await fs.mkdir(imgPath.dir, {recursive:true});
|
||||
await fs.writeFile(imgPath.file, buf);
|
||||
console.log(`saved additional size ${size} to: ${imgPath.file}`);
|
||||
}
|
||||
|
||||
function getOriginalImagePath(userId, pinId){
|
||||
let paddedId = pinId.toString().padStart(12, '0');
|
||||
let dir = `${IMAGE_PATH}/${userId}/images/originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return {dir: dir, file: file};
|
||||
}
|
||||
|
||||
function getThumbnailImagePath(userId, pinId){
|
||||
|
||||
function getImagePath(userId, pinId, size){
|
||||
let paddedId = pinId.toString().padStart(12, '0');
|
||||
let dir = `${IMAGE_PATH}/${userId}/images/thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let dir = `${IMAGE_PATH}/${userId}/images/${size}/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return {dir: dir, file: file};
|
||||
return { dir: dir, file: file};
|
||||
}
|
||||
|
||||
|
||||
})();
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
<link rel="stylesheet" href="client.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="ilen"></div>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="reef.min.js"></script>
|
||||
|
|
@ -28,6 +30,9 @@
|
|||
<script src="reef-databind.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
document.getElementById("ilen").innerHTML = window.location.hash.length;
|
||||
|
||||
Reef.debug(true);
|
||||
|
||||
app.addSetter("render", (data) => {
|
||||
|
|
@ -163,6 +168,7 @@ app.addSetter('addPinModal.save', async (data) => {
|
|||
|
||||
if ( res.status == 200 ){
|
||||
window.localStorage.addPinLastBoardId = boardId;
|
||||
data.done = true;
|
||||
window.close();
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +193,8 @@ const store = new Reef.Store({
|
|||
description: "",
|
||||
saveInProgress: false
|
||||
},
|
||||
initialized: false
|
||||
initialized: false,
|
||||
done: false
|
||||
},
|
||||
getters: app.getGetters(),
|
||||
setters: app.getSetters()
|
||||
|
|
@ -205,6 +212,7 @@ const appComponent = new Reef("#app", {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
//
|
||||
let imagePlaceholder = 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22300%22%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%22300%22%20height%3D%22300%22%20style%3D%22fill%3A%23dedede%3B%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20font-size%3D%2218%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20font-family%3D%22monospace%2C%20sans-serif%22%20fill%3D%22%23555555%22%3Eimage%3C%2Ftext%3E%3C%2Fsvg%3E';
|
||||
|
||||
let options = "";
|
||||
|
|
@ -225,12 +233,25 @@ const appComponent = new Reef("#app", {
|
|||
}
|
||||
|
||||
return /*html*/`
|
||||
<div id="doneModal">
|
||||
<div class="modal ${data.done ? 'is-active' : ''}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="box has-text-centered" style="max-width: 90%; margin-left: auto; margin-right: auto;">
|
||||
<!-- https://thenounproject.com/search/?q=done&i=587164 -->
|
||||
<img style="width: 80px; height: 80px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjNGU4ODJiIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAwIDEwMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PGc+PHBhdGggZmlsbD0iIzRlODgyYiIgZD0iTTY5LjQzNyw0My4zMDNsLTI0LjEyLDI1Ljc1M2MtMC44MTcsMC44NzEtMS44NTEsMS4zMDctMi45OTUsMS4zMDdzLTIuMjMyLTAuNDM2LTIuOTk1LTEuMzA3ICAgIEwyNy4yNDEsNTYuMTUyYy0xLjUyNC0xLjYzMy0xLjQ3LTQuMTkyLDAuMjE4LTUuNzcxYzEuNjMzLTEuNTI1LDQuMTkyLTEuNDE2LDUuNzcxLDAuMjE4bDkuMDkzLDkuNjkxbDIxLjE4LTIyLjU5NSAgICBjMS41MjQtMS42MzMsNC4xMzgtMS43NDIsNS43NzEtMC4xNjNDNzAuOTA3LDM5LjA1Niw3MS4wMTYsNDEuNjE1LDY5LjQzNyw0My4zMDN6Ij48L3BhdGg+PHBhdGggZmlsbD0iIzRlODgyYiIgZD0iTTUuMDgyLDUwQzUuMDgyLDI1LjIyNywyNS4xNzIsNS4xMzYsNTAsNS4wODJDNzQuNzczLDUuMTM2LDk0LjkxOCwyNS4yMjcsOTQuOTE4LDUwICAgIGMwLDI0LjgyOC0yMC4xNDUsNDQuOTE4LTQ0LjkxOCw0NC45MThDMjUuMTcyLDk0LjkxOCw1LjA4Miw3NC44MjgsNS4wODIsNTB6IE0yMy45NzUsNzYuMDI1ICAgIEMzMC42NzIsODIuNjY4LDM5LjgxOSw4Ni43NTEsNTAsODYuNzUxYzEwLjEyNywwLDE5LjMyOC00LjA4MywyNS45NzEtMTAuNzI2YzYuNjQyLTYuNjk3LDEwLjc4LTE1Ljg0NCwxMC43OC0yNi4wMjUgICAgYzAtMTAuMTI3LTQuMTM4LTE5LjI3NC0xMC43OC0yNS45NzFDNjkuMzI4LDE3LjM4Nyw2MC4xMjcsMTMuMjQ5LDUwLDEzLjI0OWMtMTAuMTgxLDAtMTkuMzI4LDQuMTM4LTI2LjAyNSwxMC43OCAgICBDMTcuMzMyLDMwLjcyNiwxMy4yNDksMzkuODczLDEzLjI0OSw1MEMxMy4yNDksNjAuMTgxLDE3LjMzMiw2OS4zMjgsMjMuOTc1LDc2LjAyNXoiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPg==" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="addPinModal">
|
||||
<div class="modal is-active">
|
||||
<div class="modal ${!data.done ? 'is-active' : ''}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Add Pin ${data.addPinModal.boardId}</p>
|
||||
<p class="modal-card-title">Add Pin ${window.location.hash.length}</p>
|
||||
<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@
|
|||
object-fit: contain;
|
||||
}
|
||||
|
||||
.supports-touch .lg-prev, .supports-touch .lg-next{
|
||||
.is-touch .lg-prev, .is-touch .lg-next{
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -358,6 +358,44 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
#lg-openOriginal {
|
||||
/* https://thenounproject.com/search/?q=download&i=2120379 */
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjRkZGRkZGIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PHBhdGggZD0iTTI1LDI0SDVjLTAuNiwwLTEsMC40LTEsMXMwLjQsMSwxLDFoMjBjMC42LDAsMS0wLjQsMS0xUzI1LjYsMjQsMjUsMjR6Ij48L3BhdGg+PHBhdGggZD0iTTE0LjMsMjEuOWMwLjIsMC4yLDAuNSwwLjMsMC43LDAuM3MwLjUtMC4xLDAuNy0wLjNsNy4xLTcuMWMwLjQtMC40LDAuNC0xLDAtMS40cy0xLTAuNC0xLjQsMEwxNiwxOC44VjVjMC0wLjYtMC40LTEtMS0xICAgcy0xLDAuNC0xLDF2MTMuOGwtNS40LTUuNGMtMC40LTAuNC0xLTAuNC0xLjQsMHMtMC40LDEsMCwxLjRMMTQuMywyMS45eiI+PC9wYXRoPjwvZz48L3N2Zz4=");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px 20px;
|
||||
background-position: 16px 14px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
#lg-openOriginal:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#lg-iosShare {
|
||||
/* https://thenounproject.com/search/?q=share&i=1058858 */
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjRkZGRkZGIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT40LjE8L3RpdGxlPjxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPjxnIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI0ZGRkZGRiI+PGc+PHBhdGggZD0iTTI2LDcuODM3NTM4ODcgTDI2LDE1IEwzOC4wMDk4MjE0LDE1IEM0MC4yMTM1MzYyLDE1IDQyLDE2Ljc4OTAyNzUgNDIsMTguOTg4MTIwNiBMNDIsNDMuMDExODc5NCBDNDIsNDUuMjE0NDU3NiA0MC4yMTQ3NTQ0LDQ3IDM4LjAwOTgyMTQsNDcgTDkuOTkwMTc4NTksNDcgQzcuNzg2NDYzOCw0NyA2LDQ1LjIxMDk3MjUgNiw0My4wMTE4Nzk0IEw2LDE4Ljk4ODEyMDYgQzYsMTYuNzg1NTQyNCA3Ljc4NTI0NTYsMTUgOS45OTAxNzg1OSwxNSBMMjIsMTUgTDIyLDcuODMxMDU5MSBMMTkuNzQxMDc3NSwxMC4wODk5ODE2IEMxOC45NjcyMzE4LDEwLjg2MzgyNzMgMTcuNzEyMTg2MiwxMC44NjM0MzM2IDE2LjkyNTY5MjMsMTAuMDc2OTM5NyBDMTYuMTQ0NjQzNyw5LjI5NTg5MTA5IDE2LjE0OTI1NCw4LjAyNDk1MDg5IDE2LjkxMjY1MDQsNy4yNjE1NTQ0NyBMMjIuNTk1NTg4NSwxLjU3ODYxNjM4IEMyMi45ODAzNzI2LDEuMTkzODMyMjEgMjMuNDg0MTMwNSwxLjAwMDQ3NDA0IDIzLjk5MDEwMSwxIEwyNC4wMDUzNjYyLDEuMDA1OTcwNzYgQzI0LjUxMzI5MjgsMS4wMDcwNTQzMiAyNS4wMTgzNTI3LDEuMTk5MDM3MzQgMjUuMzk3OTMxOCwxLjU3ODYxNjM4IEwzMS4wODA4Njk5LDcuMjYxNTU0NDcgQzMxLjg1NDcxNTYsOC4wMzU0MDAyIDMxLjg1NDMyMTgsOS4yOTA0NDU3NiAzMS4wNjc4Mjc5LDEwLjA3NjkzOTcgQzMwLjI4Njc3OTMsMTAuODU3OTg4MyAyOS4wMTU4MzkyLDEwLjg1MzM3OCAyOC4yNTI0NDI3LDEwLjA4OTk4MTYgTDI2LDcuODM3NTM4ODcgWiBNMjIsMTkgTDEwLDE5IEwxMCw0MyBMMzgsNDMgTDM4LDE5IEwyNiwxOSBMMjYsMjcuMDAyOTk1MyBDMjYsMjguMTA1OTEwNiAyNS4xMTIyNzA0LDI5IDI0LDI5IEMyMi44OTU0MzA1LDI5IDIyLDI4LjEwNTAyMTEgMjIsMjcuMDAyOTk1MyBMMjIsMTkgWiBNMTYsMTUgTDE2LDE5IEwzMiwxOSBMMzIsMTUgTDE2LDE1IFogTTIyLDE1IEwyNiwxNSBMMjYsMTkgTDIyLDE5IEwyMiwxNSBaIj48L3BhdGg+PC9nPjwvZz48L2c+PC9zdmc+");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px 20px;
|
||||
background-position: 16px 13px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s linear;
|
||||
display: none;
|
||||
}
|
||||
#lg-iosShare:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.is-ios #lg-openOriginal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.is-ios #lg-iosShare {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.lg-control-hide {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,10 +44,19 @@ app.addSetter("brickwall.deletePin", async (data) => {
|
|||
store.do('loader.hide');
|
||||
});
|
||||
|
||||
var supportsTouch = 'ontouchstart' in window;
|
||||
if ( supportsTouch ){
|
||||
document.body.classList.add("supports-touch");
|
||||
function openOriginal(el){
|
||||
|
||||
let data = store.data;
|
||||
let index = getLightGalleryIndex();
|
||||
let pin = data.board.pins[index];
|
||||
// alert(pin.id);
|
||||
|
||||
let path = getImagePath(pin.id, 'o');
|
||||
// alert(path);
|
||||
window.location = "https://sktp.quikstorm.net/" + path;
|
||||
|
||||
}
|
||||
|
||||
let lightgalleryElement = document.getElementById("lightgallery");
|
||||
let lightgalleryOpen = false;
|
||||
|
||||
|
|
@ -57,13 +66,49 @@ function openLightGallery(pinId){
|
|||
|
||||
let elements = [];
|
||||
let index = 0;
|
||||
|
||||
let maxWidth = window.innerWidth * window.devicePixelRatio;
|
||||
let maxHeight = window.innerHeight * window.devicePixelRatio;
|
||||
|
||||
for ( let i = 0; i < data.board.pins.length; ++i ){
|
||||
elements.push({
|
||||
src: getOriginalImagePath(data.board.pins[i].id),
|
||||
subHtml: data.board.pins[i].description,
|
||||
siteUrl: data.board.pins[i].siteUrl
|
||||
});
|
||||
|
||||
let pin = data.board.pins[i];
|
||||
|
||||
let item = {};
|
||||
item.subHtml = pin.description;
|
||||
item.siteUrl = pin.siteUrl;
|
||||
|
||||
const THUMBNAIL_IMAGE_SIZE = 400;
|
||||
const IMAGE_SIZES = [400,800,1280,1920,2560]; //TODO: make this dynamic to share the server-side setting
|
||||
// couldn't get srcset and sizes to work right with a vertical bounding box, so we'll just push the right image for the screen size. This won't refresh on resize until closing & re-opening the lightgallery.
|
||||
let maxSize = maxWidth;
|
||||
let isPortrait = "n";
|
||||
// portrait
|
||||
if ( pin.originalHeight > pin.originalWidth ){
|
||||
maxSize = maxHeight;
|
||||
isPortrait = "y";
|
||||
}
|
||||
|
||||
maxSize = maxSize * 0.74; // take an image 74% smaller than the physical pixel count, we have 10% borders + prefer the smaller size file if it will be enlarged < 10% by the browser (74% just bumps ipad pro portrait into the 2048 size)
|
||||
|
||||
let bestSize = -1;
|
||||
|
||||
for ( let s = 0; s < IMAGE_SIZES.length; ++s ){
|
||||
if ( maxSize <= IMAGE_SIZES[s] ){
|
||||
bestSize = IMAGE_SIZES[s];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( bestSize < 0 ){
|
||||
bestSize = 'o';
|
||||
}
|
||||
|
||||
item.src = getImagePath(pin.id, bestSize);
|
||||
item.originalUrl = getImagePath(pin.id, 'o');
|
||||
|
||||
elements.push(item);
|
||||
|
||||
if ( data.board.pins[i].id == pinId ){
|
||||
index = i;
|
||||
}
|
||||
|
|
@ -83,6 +128,11 @@ function openLightGallery(pinId){
|
|||
backdropDuration: 0 // disable animate in
|
||||
};
|
||||
|
||||
// disable automatically hiding controls on touch devices, they can tap to hide.
|
||||
if ( window.isTouch ){
|
||||
options.hideBarsDelay = 0;
|
||||
}
|
||||
|
||||
lightGallery(lightgalleryElement, options );
|
||||
lightgalleryOpen = true;
|
||||
}
|
||||
|
|
@ -170,7 +220,7 @@ app.addComponent('brickwall', (store) => { return new Reef('#brickwall', {
|
|||
|
||||
let boardImage = null;
|
||||
if ( board.titlePinId > 0 ){
|
||||
boardImage = `<img class="thumb" src="${getThumbnailImagePath(board.titlePinId)}" />`;
|
||||
boardImage = `<img class="thumb" src="${getImagePath(board.titlePinId, 400)}" />`;
|
||||
} else {
|
||||
boardImage = `<div class="board-brick-missing-thumbnail"><img class="thumb" src="${missingThumbnailSrc}" /></div>`;
|
||||
}
|
||||
|
|
@ -196,7 +246,7 @@ app.addComponent('brickwall', (store) => { return new Reef('#brickwall', {
|
|||
return { height: pin.thumbnailHeight, template: /*html*/`
|
||||
<div class="brick" >
|
||||
<a data-pinid="${pin.id}" onclick="openLightGallery(${pin.id})" >
|
||||
<img class="thumb" src="${getThumbnailImagePath(pin.id)}" width="${pin.thumbnailWidth}" height="${pin.thumbnailHeight}"/>
|
||||
<img class="thumb" src="${getImagePath(pin.id, 400)}" width="${pin.thumbnailWidth}" height="${pin.thumbnailHeight}"/>
|
||||
</a>
|
||||
</div>
|
||||
`};
|
||||
|
|
@ -236,7 +286,7 @@ app.addComponent('brickwall', (store) => { return new Reef('#brickwall', {
|
|||
for ( let c = 1; c < columns.length; ++c ){
|
||||
if ( columns[c].height < shortestHeight ){
|
||||
shortestIndex = c;
|
||||
shortestHeight = c.height;
|
||||
shortestHeight = columns[c].height;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ app.addComponent('editPinModal', (store) => { return new Reef("#editPinModal", {
|
|||
<div class="add-pin-flex">
|
||||
|
||||
<div class="add-pin-flex-left">
|
||||
<img src="${data.editPinModal.pin ? getThumbnailImagePath(data.editPinModal.pin.id) : ''}" />
|
||||
<img src="${data.editPinModal.pin ? getImagePath(data.editPinModal.pin.id, 400) : ''}" />
|
||||
</div>
|
||||
|
||||
<div class="add-pin-flex-right">
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
<head>
|
||||
<title>tinypin</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="tinypin">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="pub/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="pub/icons/favicon-32x32.png">
|
||||
|
|
@ -19,20 +21,23 @@
|
|||
<link rel="stylesheet" href="pub/bulma-custom.css" />
|
||||
<link rel="stylesheet" href="lightgallery/css/lightgallery.min.css" />
|
||||
<link rel="stylesheet" href="client.css" />
|
||||
<script src="lightgallery/js/lightgallery-custom.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="lightgallery"></div>
|
||||
|
||||
|
||||
<script src="utils.js"></script>
|
||||
|
||||
<script src="lightgallery/js/lightgallery-custom.js"></script>
|
||||
|
||||
<script src="reef.min.js"></script>
|
||||
|
||||
<script src="reef-bootstrap.js"></script>
|
||||
<script src="reef-databind.js"></script>
|
||||
|
||||
<script src="utils.js"></script>
|
||||
|
||||
<script src="components/navbar.js"></script>
|
||||
<script src="components/brickwall.js"></script>
|
||||
<!-- <script src="components/pinzoom.js"></script> -->
|
||||
|
|
|
|||
|
|
@ -594,7 +594,7 @@
|
|||
|
||||
|
||||
// tinypin -- add controls
|
||||
this.outer.querySelector('.lg-toolbar').insertAdjacentHTML('beforeend', '<a id="lg-deletePin" class="lg-icon" data-onclick="brickwall.deletePin"></a><a id="lg-siteUrl" class="lg-icon" target="_blank"></a><a id="lg-edit" class="lg-icon" data-onclick="brickwall.editPin"></a>');
|
||||
this.outer.querySelector('.lg-toolbar').insertAdjacentHTML('beforeend', '<a id="lg-deletePin" class="lg-icon" data-onclick="brickwall.deletePin"></a><a id="lg-siteUrl" class="lg-icon" target="_blank"></a><a id="lg-edit" class="lg-icon" data-onclick="brickwall.editPin"></a><a id="lg-iosShare" class="lg-icon"></a><a id="lg-openOriginal" class="lg-icon" download></a>');
|
||||
|
||||
if (this.s.download) {
|
||||
this.outer.querySelector('.lg-toolbar').insertAdjacentHTML('beforeend', '<a id="lg-download" aria-label="Download" target="_blank" download class="lg-download lg-icon"></a>');
|
||||
|
|
@ -1036,7 +1036,15 @@
|
|||
} else {
|
||||
siteUrlEl.classList.add("lg-control-hide");
|
||||
}
|
||||
|
||||
let openOriginalEl = document.getElementById("lg-openOriginal");
|
||||
openOriginalEl.setAttribute("href", _this.s.dynamicEl[index].originalUrl);
|
||||
|
||||
let iosShareEl = document.getElementById("lg-iosShare");
|
||||
iosShareEl.setAttribute("href", "shortcuts://run-shortcut?name=Open%20In&input=" + encodeURIComponent("https://sktp.quikstorm.net/" + _this.s.dynamicEl[index].originalUrl));
|
||||
|
||||
// end tinypin
|
||||
|
||||
_lgUtils2.default.trigger(_this.el, 'onBeforeSlide', {
|
||||
prevIndex: _prevIndex,
|
||||
index: index,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3,17 +3,18 @@ function getOriginalImagePath(pinId){
|
|||
return "";
|
||||
}
|
||||
let paddedId = pinId.toString().padStart(12, '0');
|
||||
let dir = `images/originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let dir = `images/o/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return file;
|
||||
}
|
||||
|
||||
function getThumbnailImagePath(pinId){
|
||||
function getImagePath(pinId, size){
|
||||
if ( !pinId ){
|
||||
return "";
|
||||
}
|
||||
|
||||
let paddedId = pinId.toString().padStart(12, '0');
|
||||
let dir = `images/thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let dir = `images/${size}/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return file;
|
||||
}
|
||||
|
|
@ -59,3 +60,27 @@ function getPinIndexById(id){
|
|||
function getPinById(id){
|
||||
return store.data.board.pins[getPinIndexById(id)];
|
||||
}
|
||||
|
||||
// feature detection
|
||||
if ( 'ontouchstart' in window ){
|
||||
window.isTouch = true;
|
||||
document.body.classList.add("is-touch");
|
||||
}
|
||||
|
||||
if ([
|
||||
'iPad Simulator',
|
||||
'iPhone Simulator',
|
||||
'iPod Simulator',
|
||||
'iPad',
|
||||
'iPhone',
|
||||
'iPod'
|
||||
].includes(navigator.platform)
|
||||
// iPad on iOS 13 detection
|
||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document) ){
|
||||
|
||||
|
||||
window.iOS = true;
|
||||
document.body.classList.add("is-ios");
|
||||
} else {
|
||||
window.iOS = false;
|
||||
}
|
||||
42
utils/regen-thumbs.js
Normal file
42
utils/regen-thumbs.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
const fs = require("fs").promises;
|
||||
const sharp = require("sharp");
|
||||
const betterSqlite3 = require('better-sqlite3');
|
||||
|
||||
|
||||
const DB_PATH = "tinypin.db";
|
||||
const IMAGE_PATH = "images";
|
||||
|
||||
const db = betterSqlite3(DB_PATH);
|
||||
|
||||
( async () => {
|
||||
|
||||
|
||||
let pins = db.prepare("select * from pins").all();
|
||||
|
||||
for ( let i = 0; i < pins.length; ++i ){
|
||||
|
||||
let pin = pins[i];
|
||||
|
||||
let originalPath = getImagePath(pin.userId, pin.id, 'o');
|
||||
|
||||
let img = await sharp(originalPath.file);
|
||||
|
||||
let resizedImg = await img.resize({width: 400, height: 400, fit: 'inside'})
|
||||
let buf = await resizedImg.toBuffer();
|
||||
|
||||
let thumbPath = getImagePath(pin.userId, pin.id, 't');
|
||||
await fs.mkdir(thumbPath.dir, {recursive: true});
|
||||
await fs.writeFile(thumbPath.file, buf);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function getImagePath(userId, pinId, size){
|
||||
let paddedId = pinId.toString().padStart(12, '0');
|
||||
let dir = `${IMAGE_PATH}/${userId}/images/${size}/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
|
||||
let file = `${dir}/${paddedId}.jpg`;
|
||||
return { dir: dir, file: file};
|
||||
}
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue