mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:07:20 +00:00
fix: separate platform detection from responsive design, fix boot screen crash on web
The boot screen was rendering as a black screen on web because useNativeFeatures() called Capacitor's Network.getStatus() without checking if the app was running on a native platform. This crashed the entire AeThexOS component during mount. Additionally, tablet testing code in use-platform-layout.ts was mixing viewport width checks (responsive design) with native platform detection, causing layout confusion between web and mobile builds. Changes: - Add isMobile() guards to all Capacitor plugin calls in useNativeFeatures - Remove tablet viewport-width branch from usePlatformLayout (platform detection should not check window.innerWidth) - Rename isMobileDevice() to isSmallViewport() in embed-utils to clarify it's a responsive check, not a platform check - Rename local isMobile state to isNarrowViewport in os.tsx DesktopWidgets to prevent shadowing the platform.ts isMobile() import - Remove dead PlatformAdaptiveExample.tsx (not imported anywhere) - Fix watchLocation TypeScript error (watchId is Promise<string>) - Add web fallbacks for clipboard and browser in useNativeFeatures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
33e0a26d35
commit
51ed8371b9
5 changed files with 60 additions and 201 deletions
|
|
@ -1,163 +0,0 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePlatformLayout, usePlatformClasses, PlatformSwitch } from '@/hooks/use-platform-layout';
|
||||
import { Home, Users, Settings, Plus } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Example component showing how to adapt UI for different platforms
|
||||
*/
|
||||
export function PlatformAdaptiveExample() {
|
||||
const layout = usePlatformLayout();
|
||||
const classes = usePlatformClasses();
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
{/* Platform-specific header */}
|
||||
<PlatformSwitch
|
||||
mobile={<MobileHeader />}
|
||||
desktop={<DesktopHeader />}
|
||||
web={<WebHeader />}
|
||||
/>
|
||||
|
||||
{/* Content that adapts to platform */}
|
||||
<div className={classes.spacing}>
|
||||
<Card className={classes.card}>
|
||||
<CardHeader>
|
||||
<CardTitle className={classes.heading}>
|
||||
Platform: {layout.isMobile ? 'Mobile' : layout.isDesktop ? 'Desktop' : 'Web'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={classes.spacing}>
|
||||
<p className={classes.fontSize}>
|
||||
This component automatically adapts its layout and styling based on the platform.
|
||||
</p>
|
||||
|
||||
{/* Platform-specific buttons */}
|
||||
<div className="flex gap-2">
|
||||
<Button className={classes.button}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{layout.isMobile ? 'Add' : 'Add New Item'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Grid that adapts to screen size and platform */}
|
||||
<div className={`grid gap-4 ${
|
||||
layout.isMobile ? 'grid-cols-1' :
|
||||
layout.isDesktop ? 'grid-cols-3' :
|
||||
'grid-cols-2'
|
||||
}`}>
|
||||
<Card className={classes.card}>
|
||||
<CardContent className="pt-6">
|
||||
<Home className="h-8 w-8 mb-2" />
|
||||
<h3 className={classes.subheading}>Dashboard</h3>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className={classes.card}>
|
||||
<CardContent className="pt-6">
|
||||
<Users className="h-8 w-8 mb-2" />
|
||||
<h3 className={classes.subheading}>Team</h3>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className={classes.card}>
|
||||
<CardContent className="pt-6">
|
||||
<Settings className="h-8 w-8 mb-2" />
|
||||
<h3 className={classes.subheading}>Settings</h3>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Platform-specific navigation */}
|
||||
<PlatformSwitch
|
||||
mobile={<MobileBottomNav />}
|
||||
desktop={<DesktopTopNav />}
|
||||
web={<WebStickyNav />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile: Bottom navigation bar
|
||||
function MobileBottomNav() {
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 bg-background border-t">
|
||||
<div className="flex justify-around items-center h-16 px-4">
|
||||
<NavItem icon={<Home />} label="Home" />
|
||||
<NavItem icon={<Users />} label="Team" />
|
||||
<NavItem icon={<Settings />} label="Settings" />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: Top navigation bar
|
||||
function DesktopTopNav() {
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 bg-background border-b">
|
||||
<div className="flex items-center justify-between h-16 px-8">
|
||||
<div className="flex items-center gap-8">
|
||||
<span className="text-xl font-bold">AeThex OS</span>
|
||||
<NavItem icon={<Home />} label="Dashboard" />
|
||||
<NavItem icon={<Users />} label="Team" />
|
||||
</div>
|
||||
<NavItem icon={<Settings />} label="Settings" />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// Web: Sticky navigation
|
||||
function WebStickyNav() {
|
||||
return (
|
||||
<nav className="sticky top-0 bg-background/95 backdrop-blur border-b z-50">
|
||||
<div className="flex items-center justify-between h-14 px-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<span className="text-lg font-bold">AeThex OS</span>
|
||||
<NavItem icon={<Home />} label="Home" />
|
||||
<NavItem icon={<Users />} label="Team" />
|
||||
</div>
|
||||
<NavItem icon={<Settings />} label="Settings" />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ icon, label }: { icon: React.ReactNode; label: string }) {
|
||||
return (
|
||||
<button className="flex flex-col items-center gap-1 text-muted-foreground hover:text-foreground transition-colors">
|
||||
{icon}
|
||||
<span className="text-xs">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile-specific header
|
||||
function MobileHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 bg-background border-b z-10 px-4 py-3">
|
||||
<h1 className="text-xl font-bold">AeThex OS</h1>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop-specific header
|
||||
function DesktopHeader() {
|
||||
return (
|
||||
<header className="mb-6">
|
||||
<h1 className="text-3xl font-bold mb-2">AeThex OS Desktop</h1>
|
||||
<p className="text-muted-foreground">Native desktop experience</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// Web-specific header
|
||||
function WebHeader() {
|
||||
return (
|
||||
<header className="mb-4">
|
||||
<h1 className="text-2xl font-bold mb-1">AeThex OS</h1>
|
||||
<p className="text-sm text-muted-foreground">Web desktop platform</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { ScreenOrientation } from '@capacitor/screen-orientation';
|
|||
import { Browser } from '@capacitor/browser';
|
||||
import { App } from '@capacitor/app';
|
||||
import { Haptics, ImpactStyle } from '@capacitor/haptics';
|
||||
import { isMobile } from '@/lib/platform';
|
||||
|
||||
interface UseNativeFeaturesReturn {
|
||||
// Camera
|
||||
|
|
@ -59,19 +60,25 @@ interface UseNativeFeaturesReturn {
|
|||
export function useNativeFeatures(): UseNativeFeaturesReturn {
|
||||
const [networkStatus, setNetworkStatus] = useState({ connected: true, connectionType: 'unknown' });
|
||||
|
||||
// Initialize network monitoring
|
||||
// Initialize network monitoring - only on native platforms
|
||||
useEffect(() => {
|
||||
if (!isMobile()) return;
|
||||
|
||||
const initNetwork = async () => {
|
||||
const status = await Network.getStatus();
|
||||
setNetworkStatus({ connected: status.connected, connectionType: status.connectionType });
|
||||
|
||||
Network.addListener('networkStatusChange', status => {
|
||||
try {
|
||||
const status = await Network.getStatus();
|
||||
setNetworkStatus({ connected: status.connected, connectionType: status.connectionType });
|
||||
});
|
||||
|
||||
Network.addListener('networkStatusChange', status => {
|
||||
setNetworkStatus({ connected: status.connected, connectionType: status.connectionType });
|
||||
});
|
||||
} catch (error) {
|
||||
console.debug('[NativeFeatures] Network init failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
initNetwork();
|
||||
|
||||
|
||||
return () => {
|
||||
Network.removeAllListeners();
|
||||
};
|
||||
|
|
@ -79,6 +86,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// Camera functions
|
||||
const takePhoto = async (): Promise<string | null> => {
|
||||
if (!isMobile()) return null;
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
|
|
@ -94,6 +102,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const pickPhoto = async (): Promise<string | null> => {
|
||||
if (!isMobile()) return null;
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
|
|
@ -110,6 +119,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// File system functions
|
||||
const saveFile = async (data: string, filename: string): Promise<boolean> => {
|
||||
if (!isMobile()) return false;
|
||||
try {
|
||||
await Filesystem.writeFile({
|
||||
path: filename,
|
||||
|
|
@ -126,6 +136,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const readFile = async (filename: string): Promise<string | null> => {
|
||||
if (!isMobile()) return null;
|
||||
try {
|
||||
const result = await Filesystem.readFile({
|
||||
path: filename,
|
||||
|
|
@ -140,14 +151,14 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const pickFile = async (): Promise<string | null> => {
|
||||
// Note: Capacitor doesn't have a built-in file picker
|
||||
// You'd need to use a plugin like @capacitor-community/file-picker
|
||||
if (!isMobile()) return null;
|
||||
console.log('File picker not implemented - need @capacitor-community/file-picker');
|
||||
return null;
|
||||
};
|
||||
|
||||
// Share functions
|
||||
const shareText = async (text: string, title?: string): Promise<void> => {
|
||||
if (!isMobile()) return;
|
||||
try {
|
||||
await Share.share({
|
||||
text: text,
|
||||
|
|
@ -161,6 +172,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const shareUrl = async (url: string, title?: string): Promise<void> => {
|
||||
if (!isMobile()) return;
|
||||
try {
|
||||
await Share.share({
|
||||
url: url,
|
||||
|
|
@ -175,6 +187,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// Notification functions
|
||||
const requestNotificationPermission = async (): Promise<boolean> => {
|
||||
if (!isMobile()) return false;
|
||||
try {
|
||||
const result = await PushNotifications.requestPermissions();
|
||||
if (result.receive === 'granted') {
|
||||
|
|
@ -189,6 +202,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const sendLocalNotification = async (title: string, body: string): Promise<void> => {
|
||||
if (!isMobile()) return;
|
||||
try {
|
||||
await LocalNotifications.schedule({
|
||||
notifications: [
|
||||
|
|
@ -211,6 +225,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// Location functions
|
||||
const getCurrentLocation = async (): Promise<Position | null> => {
|
||||
if (!isMobile()) return null;
|
||||
try {
|
||||
const position = await Geolocation.getCurrentPosition({
|
||||
enableHighAccuracy: true,
|
||||
|
|
@ -224,20 +239,27 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const watchLocation = (callback: (position: Position) => void) => {
|
||||
const watchId = Geolocation.watchPosition(
|
||||
if (!isMobile()) return () => {};
|
||||
let watchIdPromise: Promise<string> | null = null;
|
||||
watchIdPromise = Geolocation.watchPosition(
|
||||
{ enableHighAccuracy: true },
|
||||
(position, err) => {
|
||||
if (position) callback(position);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
return () => {
|
||||
if (watchId) Geolocation.clearWatch({ id: watchId });
|
||||
watchIdPromise?.then(id => Geolocation.clearWatch({ id }));
|
||||
};
|
||||
};
|
||||
|
||||
// Clipboard functions
|
||||
const copyToClipboard = async (text: string): Promise<void> => {
|
||||
if (!isMobile()) {
|
||||
// Fallback to web clipboard API
|
||||
try { await navigator.clipboard.writeText(text); } catch {}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Clipboard.write({ string: text });
|
||||
await showToast('Copied to clipboard', 'short');
|
||||
|
|
@ -248,6 +270,10 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const pasteFromClipboard = async (): Promise<string> => {
|
||||
if (!isMobile()) {
|
||||
// Fallback to web clipboard API
|
||||
try { return await navigator.clipboard.readText(); } catch { return ''; }
|
||||
}
|
||||
try {
|
||||
const result = await Clipboard.read();
|
||||
return result.value;
|
||||
|
|
@ -259,6 +285,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// Screen orientation
|
||||
const lockOrientation = async (orientation: 'portrait' | 'landscape'): Promise<void> => {
|
||||
if (!isMobile()) return;
|
||||
try {
|
||||
await ScreenOrientation.lock({ orientation: orientation });
|
||||
} catch (error) {
|
||||
|
|
@ -267,6 +294,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
};
|
||||
|
||||
const unlockOrientation = async (): Promise<void> => {
|
||||
if (!isMobile()) return;
|
||||
try {
|
||||
await ScreenOrientation.unlock();
|
||||
} catch (error) {
|
||||
|
|
@ -276,6 +304,11 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// Browser
|
||||
const openInBrowser = async (url: string): Promise<void> => {
|
||||
if (!isMobile()) {
|
||||
// Fallback to web: open in new tab
|
||||
window.open(url, '_blank');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Browser.open({ url });
|
||||
} catch (error) {
|
||||
|
|
@ -285,6 +318,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// Toast
|
||||
const showToast = async (text: string, duration: 'short' | 'long' = 'short'): Promise<void> => {
|
||||
if (!isMobile()) return;
|
||||
try {
|
||||
await Toast.show({
|
||||
text: text,
|
||||
|
|
@ -298,6 +332,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
|
|||
|
||||
// Haptics
|
||||
const vibrate = async (style: 'light' | 'medium' | 'heavy' = 'medium'): Promise<void> => {
|
||||
if (!isMobile()) return;
|
||||
try {
|
||||
const styleMap = {
|
||||
light: ImpactStyle.Light,
|
||||
|
|
|
|||
|
|
@ -27,24 +27,9 @@ export function usePlatformLayout(): LayoutConfig {
|
|||
|
||||
const config = useMemo((): LayoutConfig => {
|
||||
if (platformCheck.isMobile) {
|
||||
// Tablet Optimization: Check if screen is large enough (e.g. iPad/Tablet)
|
||||
const isTablet = typeof window !== 'undefined' && window.innerWidth >= 768;
|
||||
|
||||
if (isTablet) {
|
||||
return {
|
||||
...platformCheck,
|
||||
// Tablet styling (Hybrid)
|
||||
containerClass: 'px-6 py-4 max-w-2xl mx-auto', // Centered content for tablets
|
||||
cardClass: 'rounded-xl shadow-md border p-5',
|
||||
navClass: 'fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur border-t z-50',
|
||||
spacing: 'space-y-4',
|
||||
fontSize: 'text-base',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...platformCheck,
|
||||
// Mobile-first container styling (Phone)
|
||||
// Native mobile app styling (Capacitor/Flutter/Cordova)
|
||||
containerClass: 'px-4 py-3 max-w-full',
|
||||
cardClass: 'rounded-lg shadow-sm border p-4',
|
||||
navClass: 'fixed bottom-0 left-0 right-0 bg-background border-t',
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ export const isEmbedded = (): boolean => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Detect if running on mobile device
|
||||
* Detect if viewport is small (phone-sized).
|
||||
* This is a responsive/viewport check, NOT a platform check.
|
||||
* For native platform detection, use isMobile() from '@/lib/platform'.
|
||||
*/
|
||||
export const isMobileDevice = (): boolean => {
|
||||
export const isSmallViewport = (): boolean => {
|
||||
return typeof window !== 'undefined' && window.innerWidth < 768;
|
||||
};
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ export const getMobileTheme = () => {
|
|||
*/
|
||||
export const getResponsiveStyles = () => {
|
||||
const embedded = isEmbedded();
|
||||
const mobile = isMobileDevice();
|
||||
const mobile = isSmallViewport();
|
||||
const theme = getMobileTheme();
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2034,13 +2034,13 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
});
|
||||
const [showWidgetSettings, setShowWidgetSettings] = useState(false);
|
||||
const [mobileWidgetsOpen, setMobileWidgetsOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [isNarrowViewport, setIsNarrowViewport] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
const checkViewport = () => setIsNarrowViewport(window.innerWidth < 768);
|
||||
checkViewport();
|
||||
window.addEventListener('resize', checkViewport);
|
||||
return () => window.removeEventListener('resize', checkViewport);
|
||||
}, []);
|
||||
|
||||
const toggleWidgetVisibility = (id: string) => {
|
||||
|
|
@ -2115,7 +2115,7 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
return { color: 'text-cyan-400', icon: <Users className="w-3 h-3" /> };
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
if (isNarrowViewport) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
|
|
|||
Loading…
Reference in a new issue