mirror of
https://github.com/flutter/holobooth.git
synced 2025-05-17 21:36:00 +08:00
feat: share link page (#304)
* initial commit * improving holocard and footer * feat: better handling mobile * fix: lint * video pop up player * fix : test * not found page * feat: improving video player UI * fixing some footer links and player UI * fix rewrites * backdrop and some final fixes * fixing share tests
This commit is contained in:
@ -13,7 +13,7 @@
|
||||
"__/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{ "source": "/share/**", "function": "shareImage" },
|
||||
{ "source": "/share/**", "function": "shareVideo" },
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
@ -63,7 +63,7 @@
|
||||
"__/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{ "source": "/share/**", "function": "shareImage" },
|
||||
{ "source": "/share/**", "function": "shareVideo" },
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
|
@ -3,6 +3,7 @@ export const UPLOAD_PATH = 'uploads';
|
||||
export const SHARE_PATH = 'share';
|
||||
export const ALLOWED_HOSTS = [
|
||||
'http://localhost:5001',
|
||||
'http://localhost:5000',
|
||||
'https://io-photobooth-dev.web.app',
|
||||
'https://io-photo-booth.web.app',
|
||||
'https://photobooth.flutter.dev',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as admin from 'firebase-admin';
|
||||
admin.initializeApp();
|
||||
export { shareImage } from './share';
|
||||
export { shareVideo } from './share';
|
||||
export { convert } from './convert';
|
||||
|
@ -24,7 +24,7 @@ describe('Share API', () => {
|
||||
path: '',
|
||||
protocol: 'http',
|
||||
get(_: string) {
|
||||
return 'http://localhost:5001';
|
||||
return 'localhost:5001';
|
||||
},
|
||||
} as functions.https.Request;
|
||||
|
||||
@ -48,7 +48,7 @@ describe('Share API', () => {
|
||||
|
||||
test('Valid file name returns 200 and html', async () => {
|
||||
const req = Object.assign({}, baseReq, {
|
||||
path: 'test.png',
|
||||
path: 'test.mp4',
|
||||
});
|
||||
const res = await getShareResponse(req);
|
||||
expect(res.status).toEqual(200);
|
||||
|
@ -5,37 +5,50 @@ import * as querystring from 'querystring';
|
||||
import mustache from 'mustache';
|
||||
|
||||
import { UPLOAD_PATH, ALLOWED_HOSTS } from '../config';
|
||||
import footerTmpl from './templates/footer';
|
||||
import notFoundTmpl from './templates/notfound';
|
||||
import shareTmpl from './templates/share';
|
||||
import footerTmpl from './templates/footer';
|
||||
import stylesTmpl from './templates/styles';
|
||||
import scriptsTmpl from './templates/scripts';
|
||||
import gaTmpl from './templates/ga';
|
||||
|
||||
|
||||
const VALID_IMAGE_EXT = [ '.png', '.jpeg', '.jpg' ];
|
||||
const VALID_VIDEO_EXT = [ '.mp4' ];
|
||||
|
||||
const BaseHTMLContext: Record<string, string | Record<string, string>> = {
|
||||
appUrl: '',
|
||||
shareUrl: '',
|
||||
shareImageUrl: '',
|
||||
shareVideoUrl: '',
|
||||
assetUrls: {
|
||||
favicon: bucketPathForFile('public/favicon.png'),
|
||||
bg: bucketPathForFile('public/background.jpg'),
|
||||
bgMobile: bucketPathForFile('public/background-mobile.jpg'),
|
||||
notFoundPhoto: bucketPathForFile('public/404-photo.png'),
|
||||
fixedPhotosLeft: bucketPathForFile('public/table-photos-left.png'),
|
||||
fixedPhotosRight: bucketPathForFile('public/table-photos-right.png'),
|
||||
bg: bucketPathForFile('public/background.png'),
|
||||
playArrowIcon: bucketPathForFile('public/play-arrow.png'),
|
||||
shareIcon: bucketPathForFile('public/share.png'),
|
||||
flutterForwardLogo: bucketPathForFile('public/flutter-forward-logo.png'),
|
||||
videoFrame: bucketPathForFile('public/video-frame.png'),
|
||||
flutterIcon: bucketPathForFile('public/flutter-icon.png'),
|
||||
firebaseIcon: bucketPathForFile('public/firebase-icon.png'),
|
||||
tensorflowIcon: bucketPathForFile('public/tensorflow-icon.png'),
|
||||
holocard: bucketPathForFile('public/holocard.png'),
|
||||
notFoundBg: bucketPathForFile('public/not-found-bg.png'),
|
||||
notFoundMobileBg: bucketPathForFile('public/not-found-mobile-bg.png'),
|
||||
playerPlay: bucketPathForFile('public/player-play.png'),
|
||||
playerVolume: bucketPathForFile('public/player-volume.png'),
|
||||
playerFullscreen: bucketPathForFile('public/player-fullscreen.png'),
|
||||
close: bucketPathForFile('public/close.png'),
|
||||
},
|
||||
meta: {
|
||||
title: 'Google I/O Photo Booth',
|
||||
desc: (
|
||||
'Take a photo in the I/O Photo Booth with your favorite Google Developer Mascots! ' +
|
||||
'Built with Flutter & Firebase for Google I/O 2021.'
|
||||
),
|
||||
title: 'Flutter Forward Holobooth',
|
||||
desc: [
|
||||
'Jump into a new reality with the Flutter Holobooth! ',
|
||||
'Bring Dash and Sparky to life in this open source demo ',
|
||||
'built with Flutter, Firebase, Media Pipe & Tensorflow',
|
||||
].join(''),
|
||||
},
|
||||
footer: footerTmpl,
|
||||
ga: gaTmpl,
|
||||
styles: '',
|
||||
scripts: '',
|
||||
footer: '',
|
||||
};
|
||||
|
||||
|
||||
@ -62,35 +75,37 @@ function renderTemplate(
|
||||
tmpl: string, context: Record<string, string | Record<string, string>>
|
||||
): string {
|
||||
context.styles = mustache.render(stylesTmpl, context);
|
||||
context.scripts = mustache.render(scriptsTmpl, context);
|
||||
context.footer = mustache.render(footerTmpl, context);
|
||||
return mustache.render(tmpl, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the 404 html page
|
||||
* @param {string} imageFileName - filename of storage image
|
||||
* @param {string} videoFileName - filename of storage video
|
||||
* @param {string} baseUrl - http base fully qualified URL
|
||||
* @return {string} HTML string
|
||||
*/
|
||||
function renderNotFoundPage(imageFileName: string, baseUrl: string): string {
|
||||
function renderNotFoundPage(videoFileName: string, baseUrl: string): string {
|
||||
const context = Object.assign({}, BaseHTMLContext, {
|
||||
appUrl: baseUrl,
|
||||
shareUrl: `${baseUrl}/share/${imageFileName}`,
|
||||
shareImageUrl: bucketPathForFile(`${UPLOAD_PATH}/${imageFileName}`),
|
||||
shareUrl: `${baseUrl}/share/${videoFileName}`,
|
||||
shareVideoUrl: bucketPathForFile(`${UPLOAD_PATH}/${videoFileName}`),
|
||||
});
|
||||
return renderTemplate(notFoundTmpl, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate and return the share page HTML for given path
|
||||
* @param {string} imageFileName - filename of storage image
|
||||
* @param {string} videoFileName - filename of storage video
|
||||
* @param {string} baseUrl - http base fully qualified URL
|
||||
* @return {string} HTML string
|
||||
*/
|
||||
function renderSharePage(imageFileName: string, baseUrl: string): string {
|
||||
function renderSharePage(videoFileName: string, baseUrl: string): string {
|
||||
const context = Object.assign({}, BaseHTMLContext, {
|
||||
appUrl: baseUrl,
|
||||
shareUrl: `${baseUrl}/share/${imageFileName}`,
|
||||
shareImageUrl: bucketPathForFile(`${UPLOAD_PATH}/${imageFileName}`),
|
||||
shareUrl: `${baseUrl}/share/${videoFileName}`,
|
||||
shareVideoUrl: bucketPathForFile(`${UPLOAD_PATH}/${videoFileName}`),
|
||||
});
|
||||
return renderTemplate(shareTmpl, context);
|
||||
}
|
||||
@ -106,33 +121,33 @@ export async function getShareResponse(
|
||||
try {
|
||||
const host = req.get('host') ?? '';
|
||||
const baseUrl = `${req.protocol}://${host}`;
|
||||
const { ext, base: imageFileName } = path.parse(req.path);
|
||||
const { ext, base: videoFileName } = path.parse(req.path);
|
||||
|
||||
if (!ALLOWED_HOSTS.includes(host) || !VALID_IMAGE_EXT.includes(ext)) {
|
||||
functions.logger.log('Bad host or image ext', { host, baseUrl, ext, imageFileName });
|
||||
if (!ALLOWED_HOSTS.includes(baseUrl) || !VALID_VIDEO_EXT.includes(ext)) {
|
||||
functions.logger.log('Bad host or video ext', { host, baseUrl, ext, videoFileName });
|
||||
return {
|
||||
status: 404,
|
||||
send: renderNotFoundPage(imageFileName, baseUrl),
|
||||
send: renderNotFoundPage(videoFileName, baseUrl),
|
||||
};
|
||||
}
|
||||
|
||||
const imageBlobPath = `${UPLOAD_PATH}/${imageFileName}`;
|
||||
const imageExists = await admin.storage().bucket().file(imageBlobPath).exists();
|
||||
const videoBlobPath = `${UPLOAD_PATH}/${videoFileName}`;
|
||||
const videoExists = await admin.storage().bucket().file(videoBlobPath).exists();
|
||||
|
||||
if (Array.isArray(imageExists) && imageExists[0]) {
|
||||
if (Array.isArray(videoExists) && videoExists[0]) {
|
||||
return {
|
||||
status: 200,
|
||||
send: renderSharePage(imageFileName, baseUrl),
|
||||
send: renderSharePage(videoFileName, baseUrl),
|
||||
};
|
||||
}
|
||||
|
||||
functions.logger.log('Image does not exist', { imageBlobPath });
|
||||
functions.logger.log('Video does not exist', { videoBlobPath });
|
||||
|
||||
// NOTE 200 status so that default share meta tags work,
|
||||
// where twitter does not show meta tags on a 404 status
|
||||
return {
|
||||
status: 200,
|
||||
send: renderNotFoundPage(imageFileName, baseUrl),
|
||||
send: renderNotFoundPage(videoFileName, baseUrl),
|
||||
};
|
||||
} catch (error) {
|
||||
functions.logger.error(error);
|
||||
@ -147,7 +162,7 @@ export async function getShareResponse(
|
||||
/**
|
||||
* Public sharing function
|
||||
*/
|
||||
export const shareImage = functions.https.onRequest(async (req, res) => {
|
||||
export const shareVideo = functions.https.onRequest(async (req, res) => {
|
||||
const { status, send } = await getShareResponse(req);
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
res.status(status).send(send);
|
||||
|
@ -1,19 +1,33 @@
|
||||
export default `
|
||||
<footer>
|
||||
<div class="left">
|
||||
<span>Made with
|
||||
<a href="https://flutter.dev">Flutter</a> &
|
||||
<a href="https://firebase.google.com">Firebase</a>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ul>
|
||||
<li><a href="https://events.google.com/io/">Google I/O</a></li>
|
||||
<li><a href="https://flutter.dev/docs/codelabs">Codelab</a></li>
|
||||
<li><a href="https://medium.com/flutter/how-its-made-i-o-photo-booth-3b8355d35883">How It's Made</a></li>
|
||||
<li><a href="https://policies.google.com/terms">Terms of Service</a></li>
|
||||
<li><a href="https://policies.google.com/privacy">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
<footer>
|
||||
<div class="left">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://flutter.dev/">
|
||||
<img class="flutter-icon" src={{assetUrls.flutterIcon}} />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://firebase.google.com/">
|
||||
<img class="firebase-icon" src={{assetUrls.firebaseIcon}} />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.tensorflow.org/">
|
||||
<img class="tensorflow-icon" src={{assetUrls.tensorflowIcon}} />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ul>
|
||||
<li><a href="https://flutter.dev/">Flutter</a></li>
|
||||
<li><a href="https://firebase.google.com/">Firebase</a></li>
|
||||
<li><a href="https://www.tensorflow.org/">TensorFlow</a></li>
|
||||
<li><a href="https://mediapipe.dev/">MediaPipe</a></li>
|
||||
<li><a href="https://policies.google.com/terms">Terms of Service</a></li>
|
||||
<li><a href="https://policies.google.com/privacy">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
`;
|
||||
|
@ -27,19 +27,12 @@ export default `
|
||||
|
||||
<style>{{{styles}}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="backdrop"></div>
|
||||
<img src="{{{assetUrls.fixedPhotosLeft}}}" class="fixed-photos left">
|
||||
<img src="{{{assetUrls.fixedPhotosRight}}}" class="fixed-photos right">
|
||||
<main>
|
||||
<div class="share-image no-shadow">
|
||||
<img src="{{{assetUrls.notFoundPhoto}}}">
|
||||
</div>
|
||||
<div class="text">
|
||||
<h1>Taken with I/O Photo Booth</h1>
|
||||
<h2>Oops! This photo is gone, but that doesn't mean the fun has to end.</h2>
|
||||
<a class="share-btn" href="/">Take your own</a>
|
||||
</div>
|
||||
<body class="not-found-page">
|
||||
<div class="not-found-backdrop"></div>
|
||||
<main class="not-found-panel">
|
||||
<h1>This page can’t be found</h1>
|
||||
<h2>Try going back or relaunch the app.</h2>
|
||||
<a class="btn elevated-btn relaunch-btn" href="/">Relaunch HoloBooth</a>
|
||||
</main>
|
||||
{{{footer}}}
|
||||
</body>
|
||||
|
49
functions/src/share/templates/scripts.ts
Normal file
49
functions/src/share/templates/scripts.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export default `
|
||||
(function() {
|
||||
const frame = document.querySelector('.card-frame');
|
||||
const playButton = document.querySelector('.play-button');
|
||||
const closeButton = document.querySelector('.close-button');
|
||||
const videoPopUp = document.querySelector('.video-pop-up');
|
||||
const backdrop = document.querySelector('.video-pop-up-backdrop');
|
||||
const videoPlayer = document.querySelector('.video-pop-up video');
|
||||
const timeCounter = document.querySelector('.time-counter');
|
||||
const progressBar = document.querySelector('.video-progress-bar > div');
|
||||
const fullscreenButton = document.querySelector('.fullscreen-button');
|
||||
|
||||
frame.addEventListener('click', function() {
|
||||
backdrop.style.display = 'block';
|
||||
videoPopUp.classList.remove('fullscreen');
|
||||
videoPopUp.style.display = 'flex';
|
||||
videoPlayer.play();
|
||||
});
|
||||
|
||||
playButton.addEventListener('click', function() {
|
||||
videoPlayer.play();
|
||||
});
|
||||
|
||||
closeButton.addEventListener('click', function() {
|
||||
backdrop.style.display = 'none';
|
||||
videoPopUp.style.display = 'none';
|
||||
});
|
||||
|
||||
videoPlayer.addEventListener('loadeddata', function() {
|
||||
timeCounter.innerText = ['0:00 / 0:0', videoPlayer.duration].join('');
|
||||
});
|
||||
|
||||
videoPlayer.addEventListener('timeupdate', function(event) {
|
||||
timeCounter.innerText = [
|
||||
'0:0',
|
||||
Math.round(videoPlayer.currentTime),
|
||||
' / 0:0',
|
||||
videoPlayer.duration,
|
||||
].join('');
|
||||
|
||||
const progress = videoPlayer.currentTime / videoPlayer.duration * 100;
|
||||
progressBar.style.width = progress + '%';
|
||||
});
|
||||
|
||||
fullscreenButton.addEventListener('click', function() {
|
||||
videoPopUp.classList.toggle('fullscreen');
|
||||
});
|
||||
})();
|
||||
`;
|
@ -24,25 +24,82 @@ export default `
|
||||
<meta name="twitter:site" content="@flutterdev">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Google+Sans:400,500" rel="stylesheet">
|
||||
|
||||
|
||||
<style>{{{styles}}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="backdrop"></div>
|
||||
<img src="{{{assetUrls.fixedPhotosLeft}}}" class="fixed-photos left">
|
||||
<img src="{{{assetUrls.fixedPhotosRight}}}" class="fixed-photos right">
|
||||
<main>
|
||||
<div class="share-image">
|
||||
<img src="{{{shareImageUrl}}}">
|
||||
<div class="share-video">
|
||||
<img class="holocard" src={{assetUrls.holocard}} />
|
||||
<div class="card-frame">
|
||||
<div class="video-clip">
|
||||
<video src="{{{shareVideoUrl}}}"></video>
|
||||
</div>
|
||||
<img src={{assetUrls.videoFrame}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text">
|
||||
<h1>Taken with I/O Photo Booth</h1>
|
||||
<h2>Join the fun! Grab a photo with your favorite Google mascot
|
||||
at the I/O Photo Booth.</h2>
|
||||
<a class="share-btn" href="/">Get started</a>
|
||||
<div class="info">
|
||||
<div class="info-content">
|
||||
<img class="flutter-forward-logo" src={{assetUrls.flutterForwardLogo}} />
|
||||
<h1>Check it out my Flutter holocard!</h1>
|
||||
<h2>
|
||||
This video has been created with Flutter web app.
|
||||
Create your unique video in a few steps:
|
||||
</h2>
|
||||
<a class="btn elevated-btn try-now-btn" href="http://holobooth.flutter.dev/">
|
||||
<img src={{assetUrls.playArrowIcon}} />
|
||||
Try now
|
||||
</a>
|
||||
<p class="disclaimer">
|
||||
Your photo will be available at that URL for 30 days and then automatically deleted.
|
||||
To request early deletion of your photo, please contact flutter-photo-booth@google.com and
|
||||
be sure to include your unique URL in your request.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{{{footer}}}
|
||||
<div class="video-pop-up-backdrop">
|
||||
</div>
|
||||
<div class="video-pop-up">
|
||||
<div class="video-pop-up-screen">
|
||||
<video src="{{{shareVideoUrl}}}"></video>
|
||||
<a href="#" class="close-button">
|
||||
<img src={{assetUrls.close}} />
|
||||
</a>
|
||||
</div>
|
||||
<div class="video-progress-bar">
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="video-pop-up-controls">
|
||||
<div class="left-side-controls">
|
||||
<a href="#" class="player-btn play-button">
|
||||
<img src={{assetUrls.playerPlay}} />
|
||||
</a>
|
||||
<span class="time-counter">
|
||||
0:02 / 0:05
|
||||
</span>
|
||||
</div>
|
||||
<div class="right-side-controls">
|
||||
<a href="https://flutter.dev/" class="player-btn">
|
||||
<img class="flutter-icon" src={{assetUrls.flutterIcon}} />
|
||||
</a>
|
||||
<a href="https://firebase.google.com/" class="player-btn">
|
||||
<img class="firebase-icon" src={{assetUrls.firebaseIcon}} />
|
||||
</a>
|
||||
<a href="https://www.tensorflow.org/" class="player-btn">
|
||||
<img class="tensorflow-icon" src={{assetUrls.tensorflowIcon}} />
|
||||
</a>
|
||||
<a href="#" class="player-btn fullscreen-button">
|
||||
<img class="tensorflow-icon" src={{assetUrls.playerFullscreen}} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
{{{scripts}}}
|
||||
</script>
|
||||
</html>
|
||||
`;
|
||||
|
@ -25,221 +25,482 @@ body {
|
||||
z-index: -1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-image: url("{{{assetUrls.bgMobile}}}");
|
||||
background-image: url("{{{assetUrls.bg}}}");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.fixed-photos {
|
||||
position: fixed;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
z-index: 1;
|
||||
display: none;
|
||||
width: 780px;
|
||||
main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 95%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto 45px;
|
||||
text-align: center;
|
||||
flex: 1 0 auto;
|
||||
z-index: 10;
|
||||
.share-video {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.share-image {
|
||||
margin: 2rem auto;
|
||||
width: 90%;
|
||||
transform: rotate(-5deg);
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.share-image img {
|
||||
width: 100%;
|
||||
box-shadow: -3px 9px 7px 1px rgba(0, 0, 0, 0.3);
|
||||
.info-content {
|
||||
width: 524px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.share-image.no-shadow img {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: white;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
.flutter-forward-logo {
|
||||
margin-top: 104px;
|
||||
width: 330.13px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.2;
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
width: 80%;
|
||||
margin: 0 auto 15px;
|
||||
font-family: 'Google Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 64px;
|
||||
line-height: 80px;
|
||||
background: linear-gradient(90deg, #F8BBD0 0%, #9E81EF 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
h2 {
|
||||
line-height: 1.3;
|
||||
font-family: 'Google Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
font-weight: 100;
|
||||
width: 85%;
|
||||
margin: 0 auto 35px;
|
||||
line-height: 24px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
padding: 16px 0;
|
||||
background-color: #428eff;
|
||||
color: white;
|
||||
.btn {
|
||||
font-family: 'Google Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
padding: 12px 40px 12px 20px;
|
||||
width: 167px;
|
||||
height: 52px;
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
width: 208px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.elevated-btn {
|
||||
background: linear-gradient(91.87deg, #4100E0 0.1%, #F8BBD0 120.1%);
|
||||
}
|
||||
|
||||
.try-now-btn {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.outline-btn {
|
||||
border: 1px solid #676AB6;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 48px;
|
||||
font-family: 'Google Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #C0C0C0;
|
||||
}
|
||||
|
||||
.card-frame {
|
||||
width: 368px;
|
||||
height: 467px;
|
||||
position: absolute;
|
||||
bottom: 206px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-frame img {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.card-frame .video-clip {
|
||||
height: 318px;
|
||||
width: 286px;
|
||||
top: 48px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translate(-50%, 0);
|
||||
clip-path: inset(0);
|
||||
}
|
||||
|
||||
.card-frame .video-clip video {
|
||||
height: 318px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.holocard {
|
||||
width: 708px;
|
||||
height: 716px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 100;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 5% 1.5rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
padding-right: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
footer ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 2;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
footer .left {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 17px;
|
||||
footer ul li {
|
||||
list-style: none;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
footer ul li img.flutter-icon {
|
||||
width: 19px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
footer ul li img.firebase-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
footer ul li img.tensorflow-icon {
|
||||
width: 22px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
footer .left a {
|
||||
text-decoration: underline;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
footer li {
|
||||
display: inline;
|
||||
margin-left: 1rem;
|
||||
footer .right a {
|
||||
font-family: 'Google Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
margin-right: 24px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 375px) {
|
||||
h1 { width: 67%; }
|
||||
h2 { width: 75%; }
|
||||
.video-pop-up-backdrop {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(28, 32, 64, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.backdrop {
|
||||
background-image: url("{{{assetUrls.bg}}}");
|
||||
}
|
||||
.video-pop-up {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 80%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
background: rgba(2, 3, 32, 0.95);
|
||||
backdrop-filter: blur(7.5px);
|
||||
border-radius: 38px;
|
||||
border: 1px solid #9E81EF;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.fixed-photos {
|
||||
.video-pop-up.fullscreen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.video-pop-up-screen {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.video-pop-up-screen video {
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-pop-up .close-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 27.5px;
|
||||
background: rgba(4, 5, 34, 0.56);
|
||||
top: 8px;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.video-pop-up .close-button img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.video-pop-up .video-progress-bar {
|
||||
background: #1E1E1E;
|
||||
border-radius: 10px;
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.video-pop-up .video-progress-bar > div {
|
||||
background: #27F5DD;
|
||||
border-radius: 10px;
|
||||
height: 8px;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.video-pop-up .player-btn {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.video-pop-up-controls {
|
||||
padding: 12px 0px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-pop-up-controls > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-pop-up-controls .left-side-controls {
|
||||
}
|
||||
|
||||
.video-pop-up .play-button {
|
||||
margin-right: 22px;
|
||||
}
|
||||
|
||||
.video-pop-up .play-button img {
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.video-pop-up .time-counter {
|
||||
font-family: 'Google Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.right-side-controls .flutter-icon {
|
||||
width: 18px;
|
||||
height: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.right-side-controls .firebase-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.right-side-controls .tensorflow-icon {
|
||||
width: 20px;
|
||||
height: 22px;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
.right-side-controls .fullscreen-button img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.not-found-backdrop {
|
||||
z-index: -1;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-image: url("{{{assetUrls.notFoundBg}}}");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.not-found-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.not-found-panel h1 {
|
||||
font-size: 54px;
|
||||
line-height: 64px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.not-found-panel h2 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.relaunch-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px 48px;
|
||||
gap: 8px;
|
||||
|
||||
width: 286px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fixed-photos.left {
|
||||
top: -151px;
|
||||
left: -550px;
|
||||
body.not-found-page {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fixed-photos.right {
|
||||
top: -110px;
|
||||
right: -550px;
|
||||
.not-found-backdrop {
|
||||
background-image: url("{{{assetUrls.notFoundMobileBg}}}");
|
||||
}
|
||||
|
||||
.share-image {
|
||||
margin: 4.25rem auto 3rem;
|
||||
position: relative;
|
||||
left: -22px;
|
||||
width: calc(100vh * 0.7);
|
||||
max-width: 740px;
|
||||
.not-found-panel {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
width: 100%;
|
||||
.not-found-panel h1 {
|
||||
font-size: 34px;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 44px;
|
||||
.not-found-panel h2 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 21px;
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
font-size: 22px;
|
||||
.share-video {
|
||||
margin-top: 32px;
|
||||
height: 464px;
|
||||
}
|
||||
|
||||
.card-frame {
|
||||
width: 218px;
|
||||
height: 217px;
|
||||
position: absolute;
|
||||
bottom: 256px;
|
||||
}
|
||||
|
||||
.card-frame .video-clip {
|
||||
height: 218px;
|
||||
width: 186px;
|
||||
top: 14px;
|
||||
}
|
||||
|
||||
.card-frame .video-clip video {
|
||||
height: 318px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.holocard {
|
||||
width: 608px;
|
||||
height: 616px;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 0px 24px;
|
||||
}
|
||||
|
||||
.info .info-content {
|
||||
width: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info .flutter-forward-logo {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.info h1 {
|
||||
font-size: 34px;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info h2 {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.try-now-btn {
|
||||
margin-right: 0px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-bottom: 86px;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
padding-left: 2%;
|
||||
padding-right: 2%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
footer .left {
|
||||
font-size: 18px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.fixed-photos.left {
|
||||
left: -400px;
|
||||
}
|
||||
|
||||
.fixed-photos.right {
|
||||
right: -400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {}
|
||||
|
||||
@media (min-width: 1644px) {
|
||||
h1 { font-size: 56px; }
|
||||
h2 { font-size: 24px; }
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
.fixed-photos.left {
|
||||
left: -150px;
|
||||
}
|
||||
|
||||
.fixed-photos.right {
|
||||
right: -150px;
|
||||
}
|
||||
|
||||
.share-image {
|
||||
margin-top: 9.5rem;
|
||||
margin-bottom: 4rem;
|
||||
footer .right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
Reference in New Issue
Block a user