diff --git a/css/hyperware.css b/css/hyperware.css index 37855305e..6548890f1 100644 --- a/css/hyperware.css +++ b/css/hyperware.css @@ -2654,6 +2654,10 @@ left: calc(var(--spacing)*2) } + .right-2 { + right: calc(var(--spacing)*2) + } + .z-0 { z-index: 0 } @@ -2666,6 +2670,86 @@ z-index: 20 } + .bg-white { + background-color: var(--color-white); + } + + @media (prefers-color-scheme: dark) { + .dark\:bg-black { + background-color: var(--color-black); + } + + .dark\:shadow-white\/10 { + box-shadow: 0 10px 15px -3px rgba(255, 255, 255, 0.1), 0 4px 6px -2px rgba(255, 255, 255, 0.05); + } + } + + .shadow-lg { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + } + + .p-4 { + padding: calc(var(--spacing)*4); + } + + .rounded-lg { + border-radius: var(--radius-lg); + } + + .relative { + position: relative; + } + + .w-full { + width: 100%; + } + + .max-w-md { + max-width: var(--container-md); + } + + .min-h-0 { + min-height: 0; + } + + .max-h-screen { + max-height: 100vh; + } + + .overflow-y-auto { + overflow-y: auto; + } + + .backdrop-blur-sm { + backdrop-filter: blur(4px); + } + + .items-center { + align-items: center; + } + + .justify-center { + justify-content: center; + } + + .z-50 { + z-index: 50; + } + + .hidden { + display: none; + } + + .fixed { + position: fixed; + } + + .inset-0 { + inset: 0; + } + + + .container { width: 100% } @@ -2946,6 +3030,11 @@ line-height: var(--tw-leading, var(--text-5xl--line-height)) } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)) + } + .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)) diff --git a/hyperdrive/packages/app-store/app-store/src/icon b/hyperdrive/packages/app-store/app-store/src/icon index c351c301b..722e8e068 100644 --- a/hyperdrive/packages/app-store/app-store/src/icon +++ b/hyperdrive/packages/app-store/app-store/src/icon @@ -1,234 +1 @@ -data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAJYCAYAAAC+ZpjcAAAABGdBTUEAALGPC/xhBQAAAAFzUkdC -AdnJLH8AAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAlwSFlz -AAAuIwAALiMBeKU/dgAAAAd0SU1FB+kHDg8oL1KwnwcAACAASURBVHja7d13mJXVuT/8e2YYyqBI -rwpIk16liWJBERH1HJVjEhsmsZfEo8eSc+VNtyYmMTGaWFE0GqOiMYIYFbHBIL13BKkiUofBYWbe -P/I7uZIoSpny7Gc+n//Emb3Xuu+1Z77zPHuvlRVnlZYGAABlJlsJAAAELAAAAQsAQMACAEDAAgAQ -sAAABCwAAAQsAAABCwBAwAIAQMACABCwAAAELAAAAQsAAAELAEDAAgAQsAAAELAAAAQsAAABCwAA -AQsAQMACABCwAAAQsAAABCwAAAELAEDAAgBAwAIAELAAAAQsAAAELAAAAQsAQMACAEDAAgAQsAAA -BCwAAAQsAAABCwBAwAIAELAAABCwAAAELAAAAQsAAAELAEDAAgAQsAAAELAAAAQsAAABCwAAAQsA -QMACABCwAAAELAAABCwAAAELAEDAAgBAwAIAELAAAAQsAAAELAAAAQsAQMACABCwAAAQsAAABCwA -AAELAAABCwBAwAIAELAAABCwAAAELAAAAQsAAAELAEDAAgAQsAAABCwAAAQsAAABCwBAwAIAQMAC -ABCwAAAELAAABCwAAAELAEDAAgBAwAIAELAAAAQsAAABCwAAAQsAQMACABCwAAAQsAAABCwAAAEL -AAABCwBAwAIAELAAABCwAAAELAAAAQsAQMACAEDAAgAQsAAABCwAAAQsAAABCwBAwAIAQMACABCw -AAAELAAAAQsAAAELAEDAAgAQsAAAELAAAAQsAAABCwAAAQsAQMACABCwAAAQsAAABCwAAAELAEDA -AgBAwAIAELAAAAQsAAAELAAAAQsAQMACAEDAAgAQsAAABCwAAAQsAAABCwBAwAIAELAAABCwAAAE -LAAAAQsAAAELAEDAAgAQsAAAELAAAAQsAAABCwAAAQsAQMACABCwAAAELAAABCwAAAELAEDAAgBA -wAIAELAAAAQsSLNnvjcp8rJKFQIAAQvKwtWDP44+/cbG/ddPVQwABCwoC4OPXRAREQOPfSa+f9Yq -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 -gAUAIGABAAhYAAAIWAAAAhYAgIAFACBgAQAgYAEACFgAAAIWAAACFgCAgAUAIGABACBgAQAIWAAA -AhYAAAIWAICABQAgYAEACFgAAAhYAAACFgCAgAUAgIAFACBgAQAIWAAACFgAAAIWAICABQCAgAUA -IGABAAhYAAACFgAAAhYAgIAFACBgAQAgYAEACFgAAAIWAAACFgCAgAUAIGABAAhYAAAIWAAAAhYA -gIAFAICABQAgYAEACFgAAAhYAAACFgCAgAUAgIAFACBgAQAIWAAAAhYAAAIWAICABQAgYAEAIGAB -AAhYAAACFgAAAhYAgIAFACBgAQAgYAEACFgAAAIWAICABQCAgAUAIGABAAhYAAAIWAAAAhYAgIAF -AICABQAgYAEACFgAAAhYAAACFgCAgAUAIGABACBgAQAIWAAAAhYAAAIWAICABQAgYAEAIGABAAhY -AAACFgCAgAUAgIAFACBgAQAIWAAACFgAAAIWAICABQCAgAUAIGABAAhYAAAIWAAAAhYAgIAFACBg -AQAgYAEACFgAAAIWAAACFgCAgAUAIGABACBgAQAIWAAAAhYAAAIWAICABQAgYAEACFgAAAhYAAAC -FgCAgAUAgIAFACBgAQAIWAAACFgAAAIWAICABQCAgAUAIGABAAhYAAACFgAAAhYAgIAFACBgAQAg -YAEACFgAAOnw/wN7CrzJjcWUdAAAAABJRU5ErkJggg== +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAKACAYAAAAMzckjAAAAAXNSR0IB2cksfwAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+kIDQ8OKJ/vsb8AAA7hSURBVHja7d1PiFVlGMDh1+YugmBMZiF0N24ESdONE5W1MESE2UhmOKBYimRBaBAMEYgEJsJEmlITYkSNGUG7EYbLoIv+DOgqzRhx2wQuggoGWyi2yKCwZJy598453/s82wLnvt+B++P7zjl3Ue/WmdsBAEAaDxgBAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACACAAAQAQgAAACEAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACACAAAQAQgAAACEAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACACAAAQAQgAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACACAAAQAQgAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACACAAAQAEIAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACACAAAQAEIAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACACAAAQAEIAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACAAhAAAAEIAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAQAACAAhAAAAEIAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAEIAAAAhAAAAEIAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCACAAAQAEIAAAAhAAAAEIAAAAhAAAAEIAIAABABAAAIAIAABABCAAAAIQAAABCAAAAIQAAABCAAgAAEA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 diff --git a/hyperdrive/packages/app-store/app-store/src/icon.png b/hyperdrive/packages/app-store/app-store/src/icon.png index e707e025d..340a2049b 100644 Binary files a/hyperdrive/packages/app-store/app-store/src/icon.png and b/hyperdrive/packages/app-store/app-store/src/icon.png differ diff --git a/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx b/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx index 1470ee7fb..0984e256e 100644 --- a/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx @@ -3,8 +3,12 @@ import { FaExclamationTriangle } from 'react-icons/fa'; import useAppsStore from '../store'; import { BsArrowClockwise } from 'react-icons/bs'; import { Modal } from './Modal'; +import classNames from 'classnames' -const ResetButton: React.FC = () => { +interface ResetButtonProps { + className?: string +} +const ResetButton: React.FC = ({ className }) => { const resetStore = useAppsStore(state => state.resetStore); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -26,10 +30,11 @@ const ResetButton: React.FC = () => { <> {isOpen && ( diff --git a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx index addb09043..aa873d547 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx @@ -6,6 +6,7 @@ import { ResetButton } from "../components"; import { AppCard } from "../components/AppCard"; import { BsSearch } from "react-icons/bs"; import classNames from "classnames"; +import { useLocation } from "react-router-dom"; const mockApps: AppListing[] = [ { package_id: { @@ -102,6 +103,17 @@ export default function StorePage() { const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); + // if we have ?search=something, set the search query to that + const location = useLocation(); + useEffect(() => { + console.log({ location }) + const search = new URLSearchParams(location.search).get("search"); + if (search) { + setSearchQuery(search); + setCurrentPage(1); + } + }, [location]); + const onInputChange = (e: React.ChangeEvent) => { console.log(e.target.value, searchQuery); setSearchQuery(e.target.value); @@ -161,6 +173,7 @@ export default function StorePage() { value={searchQuery} onChange={onInputChange} className="grow text-sm !bg-transparent" + autoFocus /> @@ -233,9 +246,9 @@ export default function StorePage() { )} -
-

Can't find the app you're looking for?

- +
+

Can't find the app?

+
); diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx index 815b43d6f..50c7321a6 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx @@ -5,6 +5,7 @@ import { useNavigationStore } from '../../../stores/navigationStore'; import { usePersistenceStore } from '../../../stores/persistenceStore'; import { AppIcon } from './AppIcon'; import { BsSearch, BsX } from 'react-icons/bs'; +import classNames from 'classnames'; export const AppDrawer: React.FC = () => { const { apps } = useAppStore(); @@ -28,28 +29,46 @@ export const AppDrawer: React.FC = () => { if (!isAppDrawerOpen) return null; + const isMobile = window.innerWidth < 768; + return ( -
-
+
+

My Apps

-
- +
+ setSearchQuery(e.target.value)} - className="grow self-stretch bg-transparent" - autoFocus + className="grow self-stretch !bg-transparent !p-0" + autoFocus={!isMobile} />
-
+
0, + 'grid-cols-2': filteredApps.length === 0, + })}> {filteredApps.map(app => ( -
-
openApp(app)}> +
+
{ + e.stopPropagation(); + openApp(app); + }}>
{!homeScreenApps.includes(app.id) && ( @@ -62,16 +81,26 @@ export const AppDrawer: React.FC = () => { )}
))} + {filteredApps.length === 0 && ( +
+ No installed apps found. + { + e.stopPropagation(); + setSearchQuery('') + openApp(apps.find(a => a.id === 'main:app-store:sys')!, `?search=${searchQuery}`) + }} + > + Search the app store + +
+ )}
- -
); }; \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx index 0977996d8..5b4d71d59 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx @@ -6,14 +6,14 @@ import classNames from 'classnames'; interface AppIconProps { app: HomepageApp; isEditMode: boolean; - showLabel?: boolean; + isUndocked?: boolean; isFloating?: boolean; } export const AppIcon: React.FC = ({ app, isEditMode, - showLabel = true, + isUndocked = true, isFloating = false }) => { const { openApp } = useNavigationStore(); @@ -33,7 +33,7 @@ export const AppIcon: React.FC = ({ 'animate-wiggle': isEditMode && isFloating, 'hover:scale-110': !isEditMode && isFloating, 'opacity-50': !app.path && !(app.process && app.publisher) && !app.base64_icon, - 'p-2': showLabel, + 'p-2': isUndocked, })} onMouseDown={() => setIsPressed(true)} onMouseUp={() => setIsPressed(false)} @@ -45,8 +45,8 @@ export const AppIcon: React.FC = ({ data-app-publisher={app.publisher} > -
{app.base64_icon ? ( {app.label} @@ -57,11 +57,13 @@ export const AppIcon: React.FC = ({ )}
- {showLabel && ( - - {app.label} - - )} + + {app.label} +
); -}; \ No newline at end of file +}; diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx deleted file mode 100644 index 00db68f3b..000000000 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useNavigationStore } from '../../../stores/navigationStore'; -import classNames from 'classnames'; -import { BsChevronLeft, BsClock } from 'react-icons/bs'; - -export const GestureZone: React.FC = () => { - const { toggleRecentApps, runningApps, currentAppId, switchToApp, isRecentAppsOpen } = useNavigationStore(); - const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null); - const [isActive, setIsActive] = useState(false); - const [_isHovered, setIsHovered] = useState(false); - - // Touch handlers - const handleTouchStart = (e: React.TouchEvent) => { - const touch = e.touches[0]; - setTouchStart({ x: touch.clientX, y: touch.clientY }); - setIsActive(true); - }; - - const handleTouchMove = (e: React.TouchEvent) => { - if (!touchStart) return; - - const touch = e.touches[0]; - const deltaX = touchStart.x - touch.clientX; - const deltaY = touch.clientY - touchStart.y; - - // Swipe left (show recent apps) - if (deltaX > 50 && Math.abs(deltaY) < 30) { - toggleRecentApps(); - setTouchStart(null); - } - - // Swipe up/down (switch apps) - if (Math.abs(deltaY) > 50 && Math.abs(deltaX) < 30) { - const currentIndex = runningApps.findIndex(app => app.id === currentAppId); - if (currentIndex !== -1) { - const newIndex = deltaY > 0 - ? Math.min(currentIndex + 1, runningApps.length - 1) - : Math.max(currentIndex - 1, 0); - if (newIndex !== currentIndex) { - switchToApp(runningApps[newIndex].id); - } - } - setTouchStart(null); - } - }; - - const handleTouchEnd = () => { - setTouchStart(null); - setIsActive(false); - }; - - // Desktop click handler - const handleClick = () => { - toggleRecentApps(); - }; - - useEffect(() => { - if (!isRecentAppsOpen) { - setTouchStart(null); - setIsActive(false); - } - }, [isRecentAppsOpen]); - - return ( - <> -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {!isActive &&
- - -
} -
- - {/* {isHovered && !isActive && ( -
-
- Click - or - S - Recent apps -
-
- A - All apps -
-
- H - Home -
-
- )} */} - - ); -}; \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index ce1b28b51..52eb7b138 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -6,21 +6,40 @@ import { Draggable } from './Draggable'; import { AppIcon } from './AppIcon'; import { Widget } from './Widget'; import type { HomepageApp } from '../../../types/app.types'; -import { BsCheck, BsClock, BsGridFill, BsImage, BsLayers, BsSearch, BsX } from 'react-icons/bs'; +import { BsCheck, BsClock, BsEnvelope, BsGridFill, BsImage, BsLayers, BsPencilSquare, BsSearch, BsX } from 'react-icons/bs'; import classNames from 'classnames'; +import { Modal } from './Modal'; export const HomeScreen: React.FC = () => { const { apps } = useAppStore(); - const { homeScreenApps, dockApps, appPositions, widgetSettings, removeFromHomeScreen, toggleWidget, moveItem, backgroundImage, setBackgroundImage, addToDock, removeFromDock, isInitialized, setIsInitialized, addToHomeScreen } = usePersistenceStore(); + const { + homeScreenApps, + dockApps, + appPositions, + widgetSettings, + removeFromHomeScreen, + toggleWidget, + moveItem, + backgroundImage, + setBackgroundImage, + addToDock, + removeFromDock, + isInitialized, + setIsInitialized, + addToHomeScreen, + doNotShowOnboardingAgain, + setDoNotShowOnboardingAgain, + } = usePersistenceStore(); const { isEditMode, setEditMode } = useAppStore(); - const { toggleAppDrawer, toggleRecentApps } = useNavigationStore(); + const { openApp, toggleAppDrawer, toggleRecentApps } = useNavigationStore(); const [draggedAppId, setDraggedAppId] = React.useState(null); const [touchDragPosition, setTouchDragPosition] = React.useState<{ x: number; y: number } | null>(null); const [showBackgroundSettings, setShowBackgroundSettings] = React.useState(false); const [showWidgetSettings, setShowWidgetSettings] = React.useState(false); - const [searchQuery, setSearchQuery] = React.useState(''); + const [showOnboarding, setShowOnboarding] = React.useState(!doNotShowOnboardingAgain); + const [showWidgetOnboarding, setShowWidgetOnboarding] = React.useState(!doNotShowOnboardingAgain); - console.log({ appPositions }) + // console.log({ appPositions }) useEffect(() => { console.log('isInitialized', isInitialized); @@ -53,8 +72,8 @@ export const HomeScreen: React.FC = () => { }; const handleDockDrop = (e: React.DragEvent, index: number) => { - e.preventDefault(); - e.stopPropagation(); + try {e.preventDefault();} catch {} + try {e.stopPropagation();} catch {} const appId = e.dataTransfer.getData('appId'); if (appId) { // Add to dock at the specified index @@ -64,7 +83,8 @@ export const HomeScreen: React.FC = () => { }; const handleDockDragOver = (e: React.DragEvent) => { - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } e.dataTransfer.dropEffect = 'move'; }; @@ -79,7 +99,8 @@ export const HomeScreen: React.FC = () => { const handleTouchMove = (e: React.TouchEvent) => { if (!draggedAppId || !touchDragPosition) return; - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } const touch = e.touches[0]; setTouchDragPosition({ x: touch.clientX, y: touch.clientY }); }; @@ -184,8 +205,8 @@ export const HomeScreen: React.FC = () => { }, [apps, homeScreenApps]); const widgetApps = useMemo(() => { - return homeApps.filter(app => app.widget && !widgetSettings[app.id]?.hide); - }, [homeApps, widgetSettings]); + return homeApps.filter(app => app.widget); + }, [homeApps]); // Get actual dock app objects from IDs const dockAppsList = useMemo(() => { @@ -234,19 +255,21 @@ export const HomeScreen: React.FC = () => { data-is-dark-mode={isDarkMode} > - {backgroundImage && ( -
- )} + {/* {backgroundImage && ( +
+ )} */}
{ - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } e.dataTransfer.dropEffect = 'move'; }} onDrop={(e) => { - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } const appId = e.dataTransfer.getData('appId'); // Only handle drops from dock apps or if dropping outside dock area const isDroppingOnDock = (e.target as HTMLElement).closest('.dock-area'); @@ -267,7 +290,8 @@ export const HomeScreen: React.FC = () => { const touch = e.touches[0]; const element = document.elementFromPoint(touch.clientX, touch.clientY); if (element?.closest('.dock-area')) { - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } } }} > @@ -275,7 +299,7 @@ export const HomeScreen: React.FC = () => { {floatingApps .filter(app => { return !app.id.includes('homepage:homepage:sys') // don't show the clock icon because it does nothing. - && (!searchQuery || app.label.toLowerCase().includes(searchQuery.toLowerCase())) + // && (!searchQuery || app.label.toLowerCase().includes(searchQuery.toLowerCase())) }) .map((app, index, allApps) => { const position = appPositions[app.id] || calculateAppIconPosition(app.id, index, allApps.length); @@ -312,9 +336,24 @@ export const HomeScreen: React.FC = () => { {widgetApps - .filter(app => !searchQuery || app.label.toLowerCase().includes(searchQuery.toLowerCase())) .map((app, index) => ( - + + {showWidgetOnboarding && index === 0 &&
setShowWidgetOnboarding(false)} + > + This is a widget. Drag it, resize it, or hide it! +
} +
))} @@ -323,7 +362,7 @@ export const HomeScreen: React.FC = () => { onDragOver={handleDockDragOver} onDrop={(e) => handleDockDrop(e, dockAppsList.length)} > -
+
{Array.from({ length: 4 }).map((_, index) => { const app = dockAppsList[index]; @@ -331,10 +370,14 @@ export const HomeScreen: React.FC = () => {
{ - e.stopPropagation(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } handleDockDrop(e, index); }} > @@ -375,7 +418,7 @@ export const HomeScreen: React.FC = () => {
) : ( @@ -384,21 +427,29 @@ export const HomeScreen: React.FC = () => {
); })} -
- - + My apps +
+
- - Recent - + + Recent +
@@ -414,13 +465,13 @@ export const HomeScreen: React.FC = () => { a.id === draggedAppId)!} isEditMode={false} - showLabel={false} + isUndocked={false} />
)} -
+
Hyperdrive { alt="Hyperdrive" className="h-8 hidden md:block self-start" /> - {!isEditMode && <> -
- - setSearchQuery(e.target.value)} - value={searchQuery} - /> -
- - } {isEditMode && (
)}
)} {showWidgetSettings && ( -
- Widget Manager +
+ Widgets
{homeApps.filter(app => app.widget).map(app => ( -
+
{app.label}
)} + + {!isEditMode && <> + {/* + + */} + + + } +
+ {showOnboarding && ( + setShowOnboarding(false)} + title="Welcome to Hyperware" + > +

Your gateway to the internet, reimagined.

+

Your node, your device: customize the interface, pin your favorite apps.

+

Your node, your data: take full control over your information.

+
+ + +
+
+ )} + {/*
diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx new file mode 100644 index 000000000..6f51bd43b --- /dev/null +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx @@ -0,0 +1,35 @@ +import classNames from "classnames"; +import React, { ReactNode } from "react"; +import { BsX } from "react-icons/bs"; + +interface ModalProps { + children: ReactNode; + onClose: () => void; + backdropClassName?: string; + modalClassName?: string; + title?: string; +} + +export const Modal: React.FC = ({ + children, + backdropClassName, + modalClassName, + onClose, + title +}) => { + return ( +
+
+
+ {title &&

{title}

} + +
+ {children} +
+
+ ); +}; \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx new file mode 100644 index 000000000..e1a03d3c6 --- /dev/null +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx @@ -0,0 +1,160 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { useNavigationStore } from '../../../stores/navigationStore'; +import classNames from 'classnames'; +import { usePersistenceStore } from '../../../stores/persistenceStore'; +export const OmniButton: React.FC = () => { + const { toggleRecentApps, isRecentAppsOpen, closeAllOverlays } = useNavigationStore(); + const { omnibuttonPosition, setOmnibuttonPosition } = usePersistenceStore(); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState<{ x: number; y: number; buttonX: number; buttonY: number } | null>(null); + const dragThreshold = 5; // pixels - swipes smaller than this will be treated as taps + const buttonRef = useRef(null); + const isMobile = window.innerWidth < 768; + + // Touch handlers for drag and tap + const handleTouchStart = (e: React.TouchEvent) => { + e.stopPropagation(); + const touch = e.touches[0]; + setDragStart({ + x: touch.clientX, + y: touch.clientY, + buttonX: omnibuttonPosition.x, + buttonY: omnibuttonPosition.y + }); + }; + + const handleTouchMove = (e: React.TouchEvent) => { + if (!dragStart) return; + e.stopPropagation(); + + const touch = e.touches[0]; + const deltaX = touch.clientX - dragStart.x; + const deltaY = touch.clientY - dragStart.y; + + // Check if movement exceeds threshold to start dragging + if (!isDragging && (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold)) { + setIsDragging(true); + } + + // Update position if dragging + if (isDragging || Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { + const newX = Math.max(30, Math.min(window.innerWidth - 30, dragStart.buttonX + deltaX)); + const newY = Math.max(30, Math.min(window.innerHeight - 30, dragStart.buttonY + deltaY)); + setOmnibuttonPosition({ x: newX, y: newY }); + } + }; + + const handleTouchEnd = () => { + if (!isDragging && dragStart) { + // Tap - open recent apps + if (!isRecentAppsOpen) toggleRecentApps(); + else closeAllOverlays(); + } + setDragStart(null); + setIsDragging(false); + }; + + // Mouse handlers for desktop + const handleMouseDown = (e: React.MouseEvent) => { + if (isMobile) return; + e.stopPropagation(); + setDragStart({ + x: e.clientX, + y: e.clientY, + buttonX: omnibuttonPosition.x, + buttonY: omnibuttonPosition.y + }); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isMobile) return; + e.stopPropagation(); + if (!dragStart) return; + + const deltaX = e.clientX - dragStart.x; + const deltaY = e.clientY - dragStart.y; + + if (!isDragging && (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold)) { + setIsDragging(true); + } + + if (isDragging || Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { + const newX = Math.max(30, Math.min(window.innerWidth - 30, dragStart.buttonX + deltaX)); + const newY = Math.max(30, Math.min(window.innerHeight - 30, dragStart.buttonY + deltaY)); + setOmnibuttonPosition({ x: newX, y: newY }); + } + }; + + const handleMouseUp = () => { + if (isMobile) return; + if (!isDragging && dragStart) { + if (!isRecentAppsOpen) toggleRecentApps(); + else closeAllOverlays(); + } + setDragStart(null); + setIsDragging(false); + }; + + // Mouse event listeners + useEffect(() => { + if (dragStart) { + document.addEventListener('mousemove', handleMouseMove), { passive: false }; + document.addEventListener('mouseup', handleMouseUp), { passive: false }; + return () => { + document.removeEventListener('mousemove', handleMouseMove), { passive: false }; + document.removeEventListener('mouseup', handleMouseUp), { passive: false }; + }; + } + }, [dragStart, isDragging]); + + // Handle window resize to keep button in bounds + useEffect(() => { + const handleResize = () => { + setOmnibuttonPosition({ + x: Math.max(30, Math.min(window.innerWidth - 30, omnibuttonPosition.x)), + y: Math.max(30, Math.min(window.innerHeight - 30, omnibuttonPosition.y)) + }); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [omnibuttonPosition]); + + useEffect(() => { + if (!isRecentAppsOpen) { + setDragStart(null); + setIsDragging(false); + } + }, [isRecentAppsOpen]); + + return ( +
+ {/* Black rounded square background */} +
+ + {/* White circle with icon */} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx index 08eb213a1..255b158a3 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useRef } from 'react'; import { useNavigationStore } from '../../../stores/navigationStore'; import { BsX } from 'react-icons/bs'; import dayjs from 'dayjs'; @@ -6,39 +6,149 @@ import dayjs from 'dayjs'; export const RecentApps: React.FC = () => { const { runningApps, isRecentAppsOpen, switchToApp, closeApp, toggleRecentApps, closeAllOverlays } = useNavigationStore(); + // Touch handling state + const [swipeStates, setSwipeStates] = useState<{ [key: string]: { + translateX: number; + opacity: number; + isDragging: boolean; + } }>({}); + const touchStartRef = useRef<{ [key: string]: { x: number; y: number } }>({}); + + const handleTouchStart = (e: React.TouchEvent, appId: string) => { + const touch = e.touches[0]; + touchStartRef.current[appId] = { x: touch.clientX, y: touch.clientY }; + + setSwipeStates(prev => ({ + ...prev, + [appId]: { ...prev[appId], isDragging: true } + })); + }; + + const handleTouchMove = (e: React.TouchEvent, appId: string) => { + if (!touchStartRef.current[appId]) return; + + const touch = e.touches[0]; + const startX = touchStartRef.current[appId].x; + const deltaX = touch.clientX - startX; + const deltaY = Math.abs(touch.clientY - touchStartRef.current[appId].y); + + // Only handle horizontal swipes (avoid interfering with vertical scrolling) + if (deltaY > 30) return + + const maxTranslate = window.innerWidth * 0.4; // 40% of screen width + const clampedDeltaX = Math.max(-maxTranslate, Math.min(maxTranslate, deltaX)); + const opacity = Math.max(0.3, 1 - Math.abs(clampedDeltaX) / maxTranslate); + + setSwipeStates(prev => ({ + ...prev, + [appId]: { + translateX: clampedDeltaX, + opacity, + isDragging: true + } + })); + }; + + const handleTouchEnd = (e: React.TouchEvent, appId: string) => { + if (!touchStartRef.current[appId]) return; + + const currentSwipe = swipeStates[appId]; + const threshold = window.innerWidth * 0.25; // 25% of screen width to close + + if (currentSwipe && Math.abs(currentSwipe.translateX) > threshold) { + // Close the app with animation + const direction = currentSwipe.translateX > 0 ? 1 : -1; + setSwipeStates(prev => ({ + ...prev, + [appId]: { + translateX: direction * window.innerWidth, + opacity: 0, + isDragging: false + } + })); + + // Close app after animation + setTimeout(() => { + closeApp(appId); + setSwipeStates(prev => { + const newState = { ...prev }; + delete newState[appId]; + return newState; + }); + + if (runningApps.length === 1) { + closeAllOverlays(); + } + }, 200); + } else { + // Snap back to original position + setSwipeStates(prev => ({ + ...prev, + [appId]: { + translateX: 0, + opacity: 1, + isDragging: false + } + })); + } + + delete touchStartRef.current[appId]; + }; + if (!isRecentAppsOpen) return null; return ( -
+
{runningApps.length === 0 ? ( -
+
📱

No running apps

Open an app to see it here

-
) : ( <>
- {runningApps.map(app => ( + {runningApps.map(app => { + const swipeState = swipeStates[app.id] || { translateX: 0, opacity: 1, isDragging: false }; + + return (
switchToApp(app.id)} + className={` + relative flex-shrink-0 w-72 h-96 + bg-gradient-to-b from-black/10 to-black/20 dark:from-white/10 dark:to-white/20 + rounded-3xl overflow-hidden cursor-pointer select-none + group hover:scale-105 hover:shadow-2xl + ${swipeState.isDragging ? '' : 'transition-all duration-200'} + `} + style={{ + transform: `translateX(${swipeState.translateX}px) ${swipeState.isDragging ? '' : 'scale(1)'}`, + opacity: swipeState.opacity, + transition: swipeState.isDragging ? 'none' : 'all 0.2s ease-out', + }} + onClick={(e) => { + if (swipeState.isDragging) return; + try { e.stopPropagation(); } catch { } + try { e.preventDefault(); } catch { } + switchToApp(app.id); + }} + onTouchStart={(e) => handleTouchStart(e, app.id)} + onTouchMove={(e) => handleTouchMove(e, app.id)} + onTouchEnd={(e) => handleTouchEnd(e, app.id)} >
{app.label}
- {/*
⧉
-

App Preview

*/} - {app.base64_icon ? ( {app.label} ) : ( @@ -64,24 +171,10 @@ export const RecentApps: React.FC = () => {

opened {dayjs(runningApps.find(a => a.id === app.id)?.openedAt || 0).fromNow()}

- ))} + ); + })}
- -
- - -
)}
diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx index 99efb5255..85354b3c6 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx @@ -9,9 +9,11 @@ interface WidgetProps { app: HomepageApp; index: number; totalWidgets: number; + children?: React.ReactNode; + className?: string; } -export const Widget: React.FC = ({ app, index, totalWidgets }) => { +export const Widget: React.FC = ({ app, index, totalWidgets, children, className }) => { const { toggleWidget, widgetSettings, setWidgetPosition, setWidgetSize } = usePersistenceStore(); const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); @@ -77,7 +79,6 @@ export const Widget: React.FC = ({ app, index, totalWidgets }) => { }; const handleResize = (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); e.stopPropagation(); const isTouch = 'touches' in e; @@ -103,7 +104,6 @@ export const Widget: React.FC = ({ app, index, totalWidgets }) => { const handleMouseMove = (e: MouseEvent) => handleMove(e.clientX, e.clientY); const handleTouchMove = (e: TouchEvent) => { - e.preventDefault(); handleMove(e.touches[0].clientX, e.touches[0].clientY); }; @@ -129,7 +129,7 @@ export const Widget: React.FC = ({ app, index, totalWidgets }) => { position={position} onMove={(pos) => setWidgetPosition(app.id, pos)} enableHtmlDrag={false} - className="z-20" + className={classNames("z-20", className)} >
-
-
+
+
Loading Hyperware...
@@ -123,7 +123,7 @@ export default function AndroidHomescreen() { - + diff --git a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts index 03ea8d3f0..ac12dc79d 100644 --- a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts +++ b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts @@ -7,7 +7,7 @@ interface NavigationStore { isAppDrawerOpen: boolean; isRecentAppsOpen: boolean; - openApp: (app: HomepageApp) => void; + openApp: (app: HomepageApp, query?: string) => void; closeApp: (appId: string) => void; switchToApp: (appId: string) => void; toggleAppDrawer: () => void; @@ -65,7 +65,7 @@ export const useNavigationStore = create((set, get) => ({ // If already on homepage, let default browser behavior handle it }, - openApp: async (app) => { + openApp: async (app: HomepageApp, query?: string) => { console.log('openApp called with:', app); // Don't open apps without a valid path @@ -128,7 +128,7 @@ export const useNavigationStore = create((set, get) => ({ const hostname = currentHost.split(':')[0]; // 'localhost' from 'localhost:3000' const baseDomain = hostname; // For localhost, we just use 'localhost' - const subdomainUrl = `${protocol}//${expectedSubdomain}.${baseDomain}${port}${appUrl}`; + const subdomainUrl = `${protocol}//${expectedSubdomain}.${baseDomain}${port}${appUrl}${query || ''}`; // Debug logging console.log('Opening secure subdomain app in new tab:', { @@ -139,7 +139,8 @@ export const useNavigationStore = create((set, get) => ({ expectedSubdomain, subdomainUrl, protocol, - port + port, + query, }); const newWindow = window.open(subdomainUrl, '_blank'); @@ -166,13 +167,11 @@ export const useNavigationStore = create((set, get) => ({ const existingApp = runningApps.find(a => a.id === app.id); // Add to browser history for back button support - if (typeof window !== 'undefined') { - window.history.pushState( - { type: 'app', appId: app.id, previousAppId: currentAppId }, - '', - `#app-${app.id}` - ); - } + window?.history?.pushState( + { type: 'app', appId: app.id, previousAppId: currentAppId }, + '', + `#app-${app.id}${query || ''}` + ); if (existingApp) { set({ @@ -182,7 +181,11 @@ export const useNavigationStore = create((set, get) => ({ }); } else { set({ - runningApps: [...runningApps, { ...app, openedAt: Date.now() }], + runningApps: [...runningApps, { + ...app, + path: `${app.path}${query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''}`, + openedAt: Date.now() + }], currentAppId: app.id, isAppDrawerOpen: false, isRecentAppsOpen: false, diff --git a/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts b/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts index 358e12292..9fd208558 100644 --- a/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts +++ b/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts @@ -20,6 +20,10 @@ interface PersistenceStore { setWidgetSize: (appId: string, size: Size) => void; setBackgroundImage: (imageUrl: string | null) => void; setIsInitialized: (isInitialized: boolean) => void; + doNotShowOnboardingAgain: boolean; + setDoNotShowOnboardingAgain: (doNotShowOnboardingAgain: boolean) => void; + omnibuttonPosition: Position; + setOmnibuttonPosition: (position: Position) => void; } export const usePersistenceStore = create()( @@ -31,7 +35,10 @@ export const usePersistenceStore = create()( appPositions: {}, widgetSettings: {}, backgroundImage: null, - + doNotShowOnboardingAgain: false, + setDoNotShowOnboardingAgain: (doNotShowOnboardingAgain) => set({ doNotShowOnboardingAgain }), + omnibuttonPosition: { x: window.innerWidth - 80, y: 80 }, + setOmnibuttonPosition: (position) => set({ omnibuttonPosition: position }), setIsInitialized: (isInitialized) => set({ isInitialized }), addToHomeScreen: (appId) => { diff --git a/hyperdrive/packages/settings/settings/src/lib.rs b/hyperdrive/packages/settings/settings/src/lib.rs index 00451d69b..1e4328380 100644 --- a/hyperdrive/packages/settings/settings/src/lib.rs +++ b/hyperdrive/packages/settings/settings/src/lib.rs @@ -243,7 +243,7 @@ fn initialize(our: Address) { while let Err(e) = state.fetch() { println!("failed to fetch settings: {e}, trying again in 5s..."); homepage::add_to_homepage( - "Settings", + "Node settings", Some(ICON), Some("/"), Some(&make_widget(&state)), diff --git a/hyperdrive/packages/settings/ui/src/App.tsx b/hyperdrive/packages/settings/ui/src/App.tsx index 5df023fde..3408cc46f 100644 --- a/hyperdrive/packages/settings/ui/src/App.tsx +++ b/hyperdrive/packages/settings/ui/src/App.tsx @@ -209,7 +209,7 @@ function App() {
-

System diagnostics and settings

+

Node settings and system diagnostics

-
+
+
+
+ Need help? +
+