From 36df731b22b88a3339be7f0c6d57adc1e02083da Mon Sep 17 00:00:00 2001 From: VicVsl Date: Sat, 19 Oct 2024 23:53:56 +0200 Subject: [PATCH] Add support for the new memory api --- client/components/memoire/memoireV2.tsx | 36 +++ client/components/navbar/navbar.tsx | 8 + client/models/memoryV2.ts | 32 ++ client/package-lock.json | 2 +- client/package.json | 4 +- .../pages/allMemories/allMemories.module.scss | 41 +++ client/pages/allMemories/index.tsx | 295 ++++++++++++++++++ client/pages/api/memoriesV1.ts | 33 ++ client/pages/api/memoriesV2.ts | 33 ++ 9 files changed, 481 insertions(+), 3 deletions(-) create mode 100644 client/components/memoire/memoireV2.tsx create mode 100644 client/models/memoryV2.ts create mode 100644 client/pages/allMemories/allMemories.module.scss create mode 100644 client/pages/allMemories/index.tsx create mode 100644 client/pages/api/memoriesV1.ts create mode 100644 client/pages/api/memoriesV2.ts diff --git a/client/components/memoire/memoireV2.tsx b/client/components/memoire/memoireV2.tsx new file mode 100644 index 0000000..ed8d3ca --- /dev/null +++ b/client/components/memoire/memoireV2.tsx @@ -0,0 +1,36 @@ +import MemoryV2 from "@/models/memoryV2" +import s from "./memoire.module.scss" +import Draggable from "react-draggable" +import { useState } from "react"; + + +export default function Memoire({ memory }: { memory: MemoryV2 }) { + + let [swap, setSwap] = useState(false); + + let date = new Date(memory.date); + let formatOptions: any = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; + + let [location] = useState(""); + + return ( +
+
+
+ {date.toLocaleDateString(undefined, formatOptions)} +
+
+ {location} +
+
+
+ +
setSwap(!swap)}> + + setSwap(!swap)} onMouseDown={(e) => { e.stopPropagation() }} /> + +
+
+
+ ) +} \ No newline at end of file diff --git a/client/components/navbar/navbar.tsx b/client/components/navbar/navbar.tsx index 1c2c023..efd5f14 100644 --- a/client/components/navbar/navbar.tsx +++ b/client/components/navbar/navbar.tsx @@ -32,6 +32,8 @@ export default function Navbar() { return "profile"; } else if (router.pathname == "/memories") { return "memories"; + } else if (router.pathname == "/allMemories") { + return "allMemories"; } else if (router.pathname == "/realmojis") { return "realmojis"; } else if (router.pathname == "/") { @@ -79,6 +81,9 @@ export default function Navbar() { + + + @@ -93,6 +98,9 @@ export default function Navbar() { router.pathname == "/feed" ? <> + + + diff --git a/client/models/memoryV2.ts b/client/models/memoryV2.ts new file mode 100644 index 0000000..2a8b795 --- /dev/null +++ b/client/models/memoryV2.ts @@ -0,0 +1,32 @@ +class MemoryV2 { + id: string; + primary: string; + secondary: string; + date: string; + time: string; + + constructor( + id: string, primary: string, secondary: string, date: string, time: string + ) { + this.id = id; + this.primary = primary; + this.secondary = secondary; + this.date = date; + this.time = time; + } + + + static create(raw: any) { + let id = raw.id; + let primary = raw.primary.url; + let secondary = raw.secondary.url; + + let takenAt = raw.takenAt.split("T"); + let date = takenAt[0]; + let time = takenAt[1].split(".")[0]; + + return new MemoryV2(id, primary, secondary, date, time); + } +} + +export default MemoryV2; \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 21cfb26..2822743 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -44,7 +44,7 @@ "typescript": "5.1.3" }, "devDependencies": { - "@types/file-saver": "^2.0.5" + "@types/file-saver": "^2.0.7" } }, "node_modules/@aws-crypto/crc32": { diff --git a/client/package.json b/client/package.json index 01afbe9..e7963e7 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.1.0", + "version": "0.1.1", "private": true, "scripts": { "build": "next build", @@ -45,6 +45,6 @@ "typescript": "5.1.3" }, "devDependencies": { - "@types/file-saver": "^2.0.5" + "@types/file-saver": "^2.0.7" } } diff --git a/client/pages/allMemories/allMemories.module.scss b/client/pages/allMemories/allMemories.module.scss new file mode 100644 index 0000000..87b257a --- /dev/null +++ b/client/pages/allMemories/allMemories.module.scss @@ -0,0 +1,41 @@ +.mem { + width: 100%; +} + +.allMemories { + display: flex; + overflow-x: scroll !important; + + + @media screen and (max-width: 600px) { + flex-direction: column; + align-items: center; + width: 100%; + + } +} + +.download { + padding-top: 40px; + button { + cursor: pointer; + font-size: 16px; + font-weight: 500; + height: 34px; + width: 200px; + border-radius: 8px; + background-color: white; + border: none; + margin-top: 20px; + } + + .canvas { + display: none; + } + + .error { + color: red; + } +} + + diff --git a/client/pages/allMemories/index.tsx b/client/pages/allMemories/index.tsx new file mode 100644 index 0000000..0a46a56 --- /dev/null +++ b/client/pages/allMemories/index.tsx @@ -0,0 +1,295 @@ + +import React, { useState } from 'react' +import { useEffect } from 'react' +import axios from 'axios' +import useCheck from '@/utils/check'; +import s from './allMemories.module.scss' +import l from '@/styles/loader.module.scss'; +import MemoireV2 from '@/components/memoire/memoireV2'; +import JSZip from 'jszip'; +import MemoryV2 from '../../models/memoryV2'; +import FileSaver from 'file-saver'; + +// Made memories global for downloading (kinda ugly) +let memories: MemoryV2[] = []; + +export default function MemoriesV2() { + + useCheck(); + + let [_memories, setMemories] = useState([]); + let [loading, setLoading] = useState(true); + + useEffect(() => { + + let token = localStorage.getItem("token"); + let body = JSON.stringify({ "token": token }); + let options1 = { + url: "/api/memoriesV1", + method: "POST", + headers: { 'Content-Type': 'application/json' }, + data: body, + } + + axios.request(options1).then( + async (response) => { + let data = response.data.data; + + function createMemory(data: any) { + let memory = MemoryV2.create(data); + memories.push(memory); + return memory; + } + + let counter = 0; + for (let i = 0; i < data.length; i++) { + try { + axios.request({ + url: "/api/memoriesV2?momentId=" + data[i].momentId, + method: "POST", + headers: { 'Content-Type': 'application/json' }, + data: body, + }).then( + async (res) => { + const posts: any[] = res.data.posts + posts.forEach((post: any) => { + createMemory(post) + }) + + counter++ + if (counter >= data.length) { + memories.sort((a, b) => { return a.date > b.date ? -1 : 1 }); + setMemories(memories); + setLoading(false); + } + } + ).catch((error) => { console.log(error); }) + } catch (error) { + console.log("COULDN'T MAKE MEMORY WITH DATA: ", data[i]) + console.log(error); + } + } + } + ).catch((error) => { console.log(error); }) + }, []); + + + + return ( +
+
+ { + loading ?
: + _memories.map((memory, index) => { + return ( + + ) + }) + } +
+ + +
+ + +
+

+
+ +
+

+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + + ) + +} + +async function downloadMemories() { + + // Note: this is JS code not TS which is why it's throwing an error but runs fine + + // @ts-ignore: Object is possibly 'null'. + let separateImages = document.getElementById("separate").checked; + // @ts-ignore: Object is possibly 'null'. + let mergedImage = document.getElementById("merged").checked; + // @ts-ignore: Object is possibly 'null'. + let status = document.getElementById("downloadStatus"); + // @ts-ignore: Object is possibly 'null'. + let error = document.getElementById("error"); + // @ts-ignore: Object is possibly 'null'. + let downloadButton = document.getElementById("download"); + + // Reset text + // @ts-ignore: Object is possibly 'null'. + status.textContent = ""; + // @ts-ignore: Object is possibly 'null'. + error.textContent = ""; + + // Don't do anything if no boxes are checked + if (!(separateImages || mergedImage)) { + + // @ts-ignore: Object is possibly 'null'. + status.textContent = "No export option selected."; + return; + } + + + // Disable download button + // @ts-ignore: Object is possibly 'null'. + downloadButton.disabled = true; + + const batchSize = 100; + const batches: MemoryV2[][] = []; + for (let i = 0; i < memories.length; i += batchSize) { + batches.push(memories.slice(i, i + batchSize)); + } + + let superZip = new JSZip(); + let superCounter = 0; + for (let j = 0; j < batches.length; j++) { + let batch = batches[j]; + let zip = new JSZip(); + console.log("Batch ", j); + + // Loop through each memory + let counter = 0; + for (let i = 0; i < batch.length; i++) { + let memory = batch[i]; + + // Update memory status + // @ts-ignore: Object is possibly 'null'. + status.textContent = `Zipping, ${(((j * batchSize + i + 1) / (memories.length)) * 100).toFixed(1)}% (Memory ${j * batchSize + i + 1}/${(memories.length)})` + + + // Date strings for folder/file names + let memoryDate = `${memory.date.replaceAll("-", "")}-${memory.time.replaceAll(":", "")}` + + // An error can happen here, InvalidStateException + // Caused by the primary/secondary image fetch being corrupt, + // but only happens rarely on specific memories + try { + // REPLACE WITH PROPER PROXY SETUP! + // Fetch image data + let primary = await fetch("https://toofake-cors-proxy-4fefd1186131.herokuapp.com/" + memory.primary) + .then((result) => result.blob()) + + let secondary = await fetch("https://toofake-cors-proxy-4fefd1186131.herokuapp.com/" + memory.secondary) + .then((result) => result.blob()) + + + // Create zip w/ image, adapted from https://stackoverflow.com/a/49836948/21809626 + // Zip (primary + secondary separate) + if (separateImages) { + zip.file(`${memoryDate} - primary.png`, primary) + zip.file(`${memoryDate} - secondary.png`, secondary) + } + + // Merging images for combined view + // (Must have canvas declaration here to be accessed by toBlob()) + if (mergedImage) { + var canvas = document.getElementById("myCanvas") as HTMLCanvasElement; + + let primaryImage = await createImageBitmap(primary); + let secondaryImage = await createImageBitmap(secondary); + + canvas.width = primaryImage.width; + canvas.height = primaryImage.height; + + var ctx = canvas.getContext("2d"); + + // Check if ctx is null for dealing with TS error (not necessary) + // Bereal-style combined image + // NOTE: secondary image is bugged for custom-uploaded images through the site, + // that aren't phone-sized + if (ctx) { + ctx.drawImage(primaryImage, 0, 0) + + // Rounded secondary image, adapted from https://stackoverflow.com/a/19593950/21809626 + + // Values relative to image size + let width = secondaryImage.width * 0.3; + let height = secondaryImage.height * 0.3; + let x = primaryImage.width * 0.03; + let y = primaryImage.height * 0.03; + let radius = 70; + + + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + + ctx.closePath(); + + ctx.lineWidth = 20; + ctx.stroke(); + ctx.clip() + + ctx.drawImage(secondaryImage, x, y, width, height) + } + + canvas.toBlob(async blob => { + if (blob) { + zip.file(`${memoryDate}.png`, blob) + console.log(`Zipped ${j}.${i}`) + } + + counter++ + if (counter >= batch.length) { + zip.generateAsync({ type: 'blob' }).then(function (content: any) { + console.log(`Generated zip ${j}`) + superZip.file(`batch-${j}.zip`, content) + + superCounter++ + if (superCounter >= 3) { + superZip.generateAsync({ type: 'blob' }).then(function (x: any) { + console.log(`Super zipping`) + FileSaver.saveAs(x, `bereal-export-${new Date().toLocaleString("en-us", { + year: "2-digit", month: "2-digit", day: "2-digit" + }).replace(/\//g, '-')}.zip`); + + // Reset status + // @ts-ignore: Object is possibly 'null'. + status.textContent = "Zip will download shortly..."; + + // Enable download button + // @ts-ignore: Object is possibly 'null'. + downloadButton.disabled = false; + }) + } + }) + } + }) + } + } catch (e) { + // @ts-ignore: Object is possibly 'null'. + error.textContent = "Errors found, check console." + console.log(`ERROR: Memory #${i} on ${memoryDate} could not be zipped:\n${e}`); + } + } + } +} diff --git a/client/pages/api/memoriesV1.ts b/client/pages/api/memoriesV1.ts new file mode 100644 index 0000000..13f90eb --- /dev/null +++ b/client/pages/api/memoriesV1.ts @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import axios from 'axios'; +import { getAuthHeaders } from '@/utils/authHeaders'; +import { PROXY } from '@/utils/constants'; + +export const config = { + api: { + responseLimit: false, + }, + maxDuration: 300, +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + + let authorization_token = req.body.token; + console.log("me"); + console.log(authorization_token); + + return axios.request({ + url: `${PROXY}https://mobile.bereal.com/api` + "/feeds/memories-v1", + method: "GET", + headers: getAuthHeaders(req.body.token), + }).then( + (response) => { + res.status(200).json(response.data); + } + ).catch( + (error) => { + console.log(error); + res.status(400).json({ status: "error" }); + } + ) +} \ No newline at end of file diff --git a/client/pages/api/memoriesV2.ts b/client/pages/api/memoriesV2.ts new file mode 100644 index 0000000..112a030 --- /dev/null +++ b/client/pages/api/memoriesV2.ts @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import axios from 'axios'; +import { getAuthHeaders } from '@/utils/authHeaders'; +import { PROXY } from '@/utils/constants'; + +export const config = { + api: { + responseLimit: false, + }, + maxDuration: 300, +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + return axios.request({ + url: `${PROXY}https://mobile.bereal.com/api` + "/feeds/memories-v2/" + req.query.momentId, + method: "GET", + headers: getAuthHeaders(req.body.token), + }).then( + (response) => { + res.status(200).json(response.data); + } + ).catch( + (error) => { + console.log(error); + res.status(400).json({ status: "error" }); + } + ) + } catch (e) { + console.log(`-----------retrying ${req.query.momentId}-----------`); + handler(req, res); + } +} \ No newline at end of file