Enter a website and enjoy your flōt!
- Use the options to control the opacity of the flōted content. When Flōt is not the active window, only
- the flōted website will be displayed!
+ Use the options to control the opacity of the flōted content.
+ When Flōt is not the active window, only the flōted website will
+ be displayed!
- Note: some websites can't be loaded with Flōt as the website itself
- must permit being displayed as embedded content. If a page fails to load, check if the site specifies an
- "embed" version of the address.
+ Note: some websites can't be
+ loaded with Flōt as the website itself must permit being
+ displayed as embedded content. If a page fails to load, check if
+ the site specifies an "embed" version of the address.
diff --git a/renderer/next.config.js b/renderer/next.config.js
index b40838f..84850f2 100644
--- a/renderer/next.config.js
+++ b/renderer/next.config.js
@@ -1,7 +1,7 @@
module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
- config.target = 'electron-renderer';
+ config.target = "electron-renderer";
config.node = {
__dirname: true,
};
diff --git a/renderer/pages/_app.tsx b/renderer/pages/_app.tsx
index c7de478..c69e31a 100644
--- a/renderer/pages/_app.tsx
+++ b/renderer/pages/_app.tsx
@@ -1,7 +1,7 @@
-import type { AppProps } from 'next/app';
-import React from 'react';
-import { useStore } from '../store';
-import '../styles/globals.css';
+import type { AppProps } from "next/app";
+import React from "react";
+import { useStore } from "../store";
+import "../styles/globals.css";
function MyApp({ Component, pageProps }: AppProps) {
React.useEffect(() => {
@@ -11,21 +11,24 @@ function MyApp({ Component, pageProps }: AppProps) {
const onMouseleave = () => useStore.setState({ windowHover: false });
const onMsg = (event: MessageEvent) => {
try {
- const { key = 'UNKNOWN', msg = 'UNKNOWN' } = event.data;
+ const { key = "UNKNOWN", msg = "UNKNOWN" } = event.data;
switch (key) {
- case 'location':
- if (typeof window !== 'undefined') localStorage.setItem('flot-last-url', msg);
+ case "location":
+ if (typeof window !== "undefined")
+ localStorage.setItem("flot-last-url", msg);
useStore.setState({ url: msg });
break;
- case 'active':
+ case "active":
useStore.setState({ childActive: msg });
break;
- case 'hover':
+ case "hover":
useStore.setState({ childHover: msg });
break;
default:
- console.log(`unknown iframe message with key [${key}] and msg [${msg}]`);
+ console.log(
+ `unknown iframe message with key [${key}] and msg [${msg}]`
+ );
break;
}
} catch (error) {
@@ -33,20 +36,20 @@ function MyApp({ Component, pageProps }: AppProps) {
}
};
- window.addEventListener('focus', onFocus);
- window.addEventListener('blur', onBlur);
- window.addEventListener('message', onMsg);
+ window.addEventListener("focus", onFocus);
+ window.addEventListener("blur", onBlur);
+ window.addEventListener("message", onMsg);
- document.addEventListener('mouseenter', onMouseenter);
- document.addEventListener('mouseleave', onMouseleave);
+ document.addEventListener("mouseenter", onMouseenter);
+ document.addEventListener("mouseleave", onMouseleave);
return function removeListeners() {
- window.removeEventListener('focus', onFocus);
- window.removeEventListener('blur', onBlur);
- window.removeEventListener('message', onMsg);
+ window.removeEventListener("focus", onFocus);
+ window.removeEventListener("blur", onBlur);
+ window.removeEventListener("message", onMsg);
- document.removeEventListener('mouseenter', onMouseenter);
- document.removeEventListener('mouseleave', onMouseleave);
+ document.removeEventListener("mouseenter", onMouseenter);
+ document.removeEventListener("mouseleave", onMouseleave);
};
}, []);
diff --git a/renderer/pages/_document.tsx b/renderer/pages/_document.tsx
index 26356cd..3e57ff4 100644
--- a/renderer/pages/_document.tsx
+++ b/renderer/pages/_document.tsx
@@ -1,4 +1,10 @@
-import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
+import Document, {
+ DocumentContext,
+ Head,
+ Html,
+ Main,
+ NextScript,
+} from "next/document";
class FlotDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
diff --git a/renderer/pages/index.tsx b/renderer/pages/index.tsx
index 0342d5d..91513d3 100644
--- a/renderer/pages/index.tsx
+++ b/renderer/pages/index.tsx
@@ -1,12 +1,12 @@
-import cn from 'classnames';
-import Head from 'next/head';
-import React from 'react';
-import FlotBar from '../components/FlotBar';
-import FlotControls from '../components/FlotControls';
-import FlotEmbed from '../components/FlotEmbed';
-import { useStore } from '../store';
+import cn from "classnames";
+import Head from "next/head";
+import React from "react";
+import FlotBar from "../components/FlotBar";
+import FlotControls from "../components/FlotControls";
+import FlotEmbed from "../components/FlotEmbed";
+import { useStore } from "../store";
-const magicHeight = 'max-h-[calc(100%-0px)]';
+const magicHeight = "max-h-[calc(100%-0px)]";
function PageHome() {
const url = useStore((state) => state.url);
@@ -29,7 +29,10 @@ function PageHome() {
diff --git a/renderer/postcss.config.js b/renderer/postcss.config.js
index 29a2231..9b904f9 100644
--- a/renderer/postcss.config.js
+++ b/renderer/postcss.config.js
@@ -1,7 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {
- config: './renderer/tailwind.config.js',
+ config: "./renderer/tailwind.config.js",
},
autoprefixer: {},
},
diff --git a/renderer/public/css/embed.css b/renderer/public/css/embed.css
index 915714f..78889b2 100644
--- a/renderer/public/css/embed.css
+++ b/renderer/public/css/embed.css
@@ -67,7 +67,7 @@
.flotapp.flot-video video::before,
.flotapp.flot-video video::after {
- content: '' !important;
+ content: "" !important;
background: #000 !important;
}
diff --git a/renderer/store.ts b/renderer/store.ts
index 0ec32a1..c6403c4 100644
--- a/renderer/store.ts
+++ b/renderer/store.ts
@@ -1,5 +1,9 @@
-import create from 'zustand';
-import { prepareDailyMotionUrl, prepareVimeoUrl, prepareYoutubeUrl } from './util/magic-urls';
+import create from "zustand";
+import {
+ prepareDailyMotionUrl,
+ prepareVimeoUrl,
+ prepareYoutubeUrl,
+} from "./util/magic-urls";
interface FlotState {
url: string;
@@ -18,16 +22,23 @@ interface FlotState {
let unlockTimeout: NodeJS.Timeout | null = null;
export const useStore = create((set, get) => ({
- url: typeof window === 'undefined' ? '' : localStorage.getItem('flot-last-url') ?? '',
+ url:
+ typeof window === "undefined"
+ ? ""
+ : localStorage.getItem("flot-last-url") ?? "",
setUrl: (nextUrl: string) => {
if (get().urlLocked) {
- console.log('%cSkipping attempt to set the url when it is not possible to do so', 'color:blue;');
+ console.log(
+ "%cSkipping attempt to set the url when it is not possible to do so",
+ "color:blue;"
+ );
} else {
- let target = (nextUrl ?? '').trim();
+ let target = (nextUrl ?? "").trim();
if (!target) {
- if (typeof window !== 'undefined') localStorage.setItem('flot-last-url', '');
- set(() => ({ url: '' }));
+ if (typeof window !== "undefined")
+ localStorage.setItem("flot-last-url", "");
+ set(() => ({ url: "" }));
} else {
try {
const parsedURL = preprocessURL(target);
@@ -38,27 +49,32 @@ export const useStore = create((set, get) => ({
} catch {
set(() => ({ urlLocked: false }));
- if (target) console.log(`%cSkipping invalid url [${target}]`, 'color:red;');
+ if (target)
+ console.log(`%cSkipping invalid url [${target}]`, "color:red;");
}
}
}
},
urlLocked: false,
- opacity: typeof window === 'undefined' ? 1 : Number(localStorage.getItem('flot-opacity') ?? 1),
+ opacity:
+ typeof window === "undefined"
+ ? 1
+ : Number(localStorage.getItem("flot-opacity") ?? 1),
setOpacity: (nextOpacity: number) => {
if (nextOpacity > 100) nextOpacity = 100;
if (nextOpacity < 5) nextOpacity = 5;
const roundedOpacity = Math.round(nextOpacity) / 100;
- localStorage.setItem('flot-opacity', `${roundedOpacity}`);
+ localStorage.setItem("flot-opacity", `${roundedOpacity}`);
set(() => ({ opacity: roundedOpacity }));
},
childLoading: false,
setChildLoading: (loading: boolean) => {
if (unlockTimeout) clearTimeout(unlockTimeout);
- if (loading) unlockTimeout = setTimeout(() => set(() => ({ urlLocked: false })), 3000); // One way or another, unlock the url in a few seconds
+ if (loading)
+ unlockTimeout = setTimeout(() => set(() => ({ urlLocked: false })), 3000); // One way or another, unlock the url in a few seconds
set(() => ({ urlLocked: loading }));
set(() => ({ childLoading: loading }));
@@ -70,7 +86,7 @@ export const useStore = create((set, get) => ({
}));
function preprocessURL(target: string) {
- target = target.startsWith('http') ? target : `https://${target}`;
+ target = target.startsWith("http") ? target : `https://${target}`;
target = prepareYoutubeUrl(target);
target = prepareVimeoUrl(target);
diff --git a/renderer/styles/globals.css b/renderer/styles/globals.css
index 0e072f6..8e2ee02 100644
--- a/renderer/styles/globals.css
+++ b/renderer/styles/globals.css
@@ -4,21 +4,21 @@
@layer base {
@font-face {
- font-family: 'Work Sans';
+ font-family: "Work Sans";
font-weight: 100 900;
font-display: swap;
font-style: normal;
- font-named-instance: 'Regular';
- src: url('/fonts/WorkSans.ttf?v=1.0') format('truetype');
+ font-named-instance: "Regular";
+ src: url("/fonts/WorkSans.ttf?v=1.0") format("truetype");
}
@font-face {
- font-family: 'Work Sans';
+ font-family: "Work Sans";
font-weight: 100 900;
font-display: swap;
font-style: italic;
- font-named-instance: 'Italic';
- src: url('/fonts/WorkSans-Italic.ttf?v=1.0') format('truetype');
+ font-named-instance: "Italic";
+ src: url("/fonts/WorkSans-Italic.ttf?v=1.0") format("truetype");
}
:root {
@@ -61,19 +61,19 @@
@apply w-full h-full flex flex-col;
}
- input[type='range'] {
+ input[type="range"] {
@apply appearance-none h-4 bg-transparent w-full my-2 focus:outline-none focus:ring-2 focus:ring-teal-500 rounded;
}
- input[type='range']::-webkit-slider-runnable-track {
+ input[type="range"]::-webkit-slider-runnable-track {
@apply w-full h-1 cursor-pointer bg-white rounded border border-black;
}
- input[type='range']::-webkit-slider-thumb {
+ input[type="range"]::-webkit-slider-thumb {
@apply h-4 w-4 appearance-none bg-transparent border-0 rounded-2xl bg-teal-600 cursor-pointer -mt-1.5;
}
- input[type='range']:focus::-webkit-slider-runnable-track {
+ input[type="range"]:focus::-webkit-slider-runnable-track {
@apply bg-white;
}
}
diff --git a/renderer/tsconfig.json b/renderer/tsconfig.json
index 80e174c..5d3c56d 100644
--- a/renderer/tsconfig.json
+++ b/renderer/tsconfig.json
@@ -1,13 +1,7 @@
{
"extends": "../tsconfig.json",
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx"
- ],
- "exclude": [
- "node_modules"
- ],
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"],
"compilerOptions": {
"incremental": true
}
diff --git a/renderer/util/magic-urls.ts b/renderer/util/magic-urls.ts
index 84be1b4..e37c739 100644
--- a/renderer/util/magic-urls.ts
+++ b/renderer/util/magic-urls.ts
@@ -8,28 +8,28 @@
export function prepareYoutubeUrl(target: string) {
const asURL = new URL(target);
- if (!asURL.host.includes('youtube.com') && !asURL.host.includes('youtu.be')) {
+ if (!asURL.host.includes("youtube.com") && !asURL.host.includes("youtu.be")) {
return target;
}
const queryParams = asURL.searchParams || {};
- let videoId = queryParams.get('v') || asURL.pathname;
+ let videoId = queryParams.get("v") || asURL.pathname;
// Remove slashes – pathname is prefixed with slash
- videoId = videoId.replace(/\//g, '');
+ videoId = videoId.replace(/\//g, "");
// Empty URL or already an embed link
- if (!videoId || videoId.includes('embed')) {
+ if (!videoId || videoId.includes("embed")) {
return target;
}
- queryParams.delete('v');
- queryParams.delete('index');
- queryParams.delete('list');
- queryParams.delete('modestbranding');
- queryParams.delete('autoplay');
+ queryParams.delete("v");
+ queryParams.delete("index");
+ queryParams.delete("list");
+ queryParams.delete("modestbranding");
+ queryParams.delete("autoplay");
- queryParams.set('autoplay', '1');
- queryParams.set('modestbranding', '1');
+ queryParams.set("autoplay", "1");
+ queryParams.set("modestbranding", "1");
return `https://www.youtube.com/embed/${videoId}?${queryParams}`;
}
@@ -37,34 +37,40 @@ export function prepareYoutubeUrl(target: string) {
export function prepareVimeoUrl(target: string) {
const asURL = new URL(target);
- if (!asURL.host.includes('vimeo.com')) {
+ if (!asURL.host.includes("vimeo.com")) {
return target;
}
const queryParams = asURL.searchParams || {};
let videoId = asURL.pathname;
- videoId = videoId.split('/').slice(-1).join() ?? '';
+ videoId = videoId.split("/").slice(-1).join() ?? "";
// Vimeo video ids are all numeric
if (!/^\d+$/.test(videoId)) {
return target;
}
- queryParams.set('autoplay', '1');
+ queryParams.set("autoplay", "1");
return `https://player.vimeo.com/video/${videoId}?${queryParams}`;
}
export function prepareDailyMotionUrl(target: string) {
- const normalized = target.replace(/^http(s)?:\/\/dai\.ly\//, 'http://www.dailymotion.com/video/');
+ const normalized = target.replace(
+ /^http(s)?:\/\/dai\.ly\//,
+ "http://www.dailymotion.com/video/"
+ );
const asURL = new URL(normalized);
- if (!asURL.host.includes('dailymotion.com') || !asURL.pathname.includes('/video')) {
+ if (
+ !asURL.host.includes("dailymotion.com") ||
+ !asURL.pathname.includes("/video")
+ ) {
return target;
}
- const videoId = asURL.pathname.replace(/\/video\//g, '');
+ const videoId = asURL.pathname.replace(/\/video\//g, "");
if (!videoId) {
return target;
}
diff --git a/script/serve-website.js b/script/serve-website.js
index 0a61708..d0d4252 100644
--- a/script/serve-website.js
+++ b/script/serve-website.js
@@ -1,13 +1,13 @@
-const server = require('live-server');
-const path = require('path');
+const server = require("live-server");
+const path = require("path");
const options = {
- host: '0.0.0.0',
+ host: "0.0.0.0",
port: 5500,
- root: path.join(__dirname, '..', 'website'),
+ root: path.join(__dirname, "..", "website"),
open: true,
- file: 'index.html',
- watch: path.join(__dirname, '..', 'website'),
+ file: "index.html",
+ watch: path.join(__dirname, "..", "website"),
noCssInject: true,
};
diff --git a/website/index.html b/website/index.html
index 8bf358a..b88e84f 100644
--- a/website/index.html
+++ b/website/index.html
@@ -7,7 +7,10 @@
-
+
-
+
Flōt | Keep a website always-on-top and translucent
-
-
-
+
+
+
@@ -30,38 +50,40 @@