From 454c88e5dd3badc26d04b97d81a7ea290193fe1a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 27 Jan 2026 19:17:52 +0000 Subject: [PATCH] Autocue 1 --- frontend/src/components/NavBarItem.tsx | 5 +- frontend/src/layouts/Authenticated.tsx | 5 +- frontend/src/menuAside.ts | 7 +- frontend/src/pages/autoscroller.tsx | 242 +++++++++++++++++++++++++ frontend/src/pages/index.tsx | 202 ++++++++------------- 5 files changed, 326 insertions(+), 135 deletions(-) create mode 100644 frontend/src/pages/autoscroller.tsx diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..4ced3eb 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' @@ -126,4 +125,4 @@ export default function LayoutAuthenticated({ ) -} +} \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 0ddc339..08f8dac 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -2,6 +2,11 @@ import * as icon from '@mdi/js'; import { MenuAsideItem } from './interfaces' const menuAside: MenuAsideItem[] = [ + { + href: '/autoscroller', + icon: icon.mdiSpeedometer, + label: 'Autoscroller', + }, { href: '/dashboard', icon: icon.mdiViewDashboardOutline, @@ -88,4 +93,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/autoscroller.tsx b/frontend/src/pages/autoscroller.tsx new file mode 100644 index 0000000..419ff9c --- /dev/null +++ b/frontend/src/pages/autoscroller.tsx @@ -0,0 +1,242 @@ +import { mdiPlay, mdiPause, mdiFullscreen, mdiArrowLeft, mdiArrowRight, mdiFormatSize, mdiSpeedometer, mdiFolderOpen } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useRef, useState } from 'react' +import CardBox from '../components/CardBox' +import LayoutAuthenticated from '../layouts/Authenticated' +import SectionMain from '../components/SectionMain' +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' +import { getPageTitle } from '../config' +import { useAppDispatch, useAppSelector } from '../stores/hooks' +import { fetch as fetchTexts } from '../stores/texts/textsSlice' +import { fetch as fetchFolders } from '../stores/folders/foldersSlice' +import BaseButton from '../components/BaseButton' +import BaseButtons from '../components/BaseButtons' +import BaseIcon from '../components/BaseIcon' + +const AutoscrollerPage = () => { + const dispatch = useAppDispatch() + const texts = useAppSelector((state) => state.texts.texts) + const folders = useAppSelector((state) => state.folders.folders) + const [selectedTextId, setSelectedTextId] = useState(null) + const [isPlaying, setIsPlaying] = useState(false) + const [speed, setSpeed] = useState(2) + const [fontSize, setFontSize] = useState(24) + const scrollRef = useRef(null) + const requestRef = useRef() + + const selectedText = texts.find((t: any) => t.id === selectedTextId) + + useEffect(() => { + dispatch(fetchTexts({})) + dispatch(fetchFolders({})) + }, [dispatch]) + + const scroll = () => { + if (scrollRef.current && isPlaying) { + scrollRef.current.scrollTop += speed / 2 + } + requestRef.current = requestAnimationFrame(scroll) + } + + useEffect(() => { + requestRef.current = requestAnimationFrame(scroll) + return () => { + if (requestRef.current) { + cancelAnimationFrame(requestRef.current) + } + } + }, [isPlaying, speed]) + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen() + } else { + if (document.exitFullscreen) { + document.exitFullscreen() + } + } + } + + const nextPage = () => { + if (scrollRef.current) { + scrollRef.current.scrollTop += scrollRef.current.clientHeight + } + } + + const prevPage = () => { + if (scrollRef.current) { + scrollRef.current.scrollTop -= scrollRef.current.clientHeight + } + } + + return ( + <> + + {getPageTitle('Autoscroller')} + + + + + + + + +
+ {/* Sidebar - Title Selection */} +
+ +
+
+ + Choose Title +
+ {folders.map((folder: any) => ( +
+

+ {folder.name} +

+
+ {texts + .filter((t: any) => t.folderId === folder.id) + .map((text: any) => ( + + ))} +
+
+ ))} + {/* Texts without folders */} + {texts.filter((t: any) => !t.folderId).length > 0 && ( +
+

+ Other +

+ {texts + .filter((t: any) => !t.folderId) + .map((text: any) => ( + + ))} +
+ )} +
+
+
+ + {/* Main Scroller Area */} +
+ {selectedText ? ( + <> + +
+

{selectedText.title}

+
+
+ + setFontSize(parseInt(e.target.value))} + className='w-24' + /> +
+
+ + setSpeed(parseInt(e.target.value))} + className='w-24' + /> +
+
+
+ +
+
+ {selectedText.content} +
+ {/* Padding bottom to allow scrolling to the very end */} +
+
+ +
+ + setIsPlaying(!isPlaying)} + label={isPlaying ? 'Pause' : 'Play'} + className='w-32' + /> + +
+ + + ) : ( + +
+ +

Select a title from the menu to start scrolling

+
+
+ )} +
+
+ + + ) +} + +AutoscrollerPage.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default AutoscrollerPage \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index df28ae4..a122328 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,112 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; -import Link from 'next/link'; import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - +import { mdiSpeedometer, mdiPlayCircleOutline } from '@mdi/js'; +import { getPexelsImage } from '../helpers/pexels'; export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('background'); + const [illustrationImage, setIllustrationImage] = useState(null) const textColor = useAppSelector((state) => state.style.linkColor); - const title = 'Autoscroller App' + const title = 'Autoscroller Pro' - // Fetch Pexels image/video useEffect(() => { async function fetchData() { const image = await getPexelsImage(); - const video = await getPexelsVideo(); setIllustrationImage(image); - setIllustrationVideo(video); } fetchData(); }, []); - const imageBlock = (image) => ( - - ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; - return (
- {getPageTitle('Starter Page')} + {getPageTitle('Home')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - + +
+
+
+ + + +
+

+ Smooth Autoscrolling for Professionals +

+

+ The ultimate tool for speakers, musicians, and readers. Organize your texts into folders, adjust speed in real-time, and read with comfort. +

+
- - -
-
+ +
+

Ready to start your session?

+ + + + + +
+ + +
+
+
+ + {illustrationImage && ( + + )} +
-
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+
+

© 2026 {title}. All rights reserved.

+
); } Starter.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file