Browse Source
feat: implement view paddings management with ViewPaddingsBridge and related components
master
feat: implement view paddings management with ViewPaddingsBridge and related components
master
10 changed files with 178 additions and 3 deletions
-
6src/app/globals.css
-
9src/app/layout.tsx
-
6src/app/providers.tsx
-
8src/components/ui/sticky-header.tsx
-
22src/components/utils/fixed-bottom.tsx
-
12src/components/utils/view-paddings-provider.tsx
-
14src/hooks/use-view-paddings.ts
-
92src/lib/view-paddings.ts
-
9src/types/window.d.ts
-
3src/view-paddings.ts
@ -0,0 +1,22 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import type { ReactNode } from "react"; |
||||
|
import { useViewPaddings } from "@/hooks/use-view-paddings"; |
||||
|
|
||||
|
type FixedBottomProps = { |
||||
|
children: ReactNode; |
||||
|
className?: string; |
||||
|
}; |
||||
|
|
||||
|
export function FixedBottom({ children, className }: FixedBottomProps) { |
||||
|
const { bottom } = useViewPaddings(); |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
style={{ paddingBottom: `${bottom}px` }} |
||||
|
className={className} |
||||
|
> |
||||
|
{children} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useEffect } from "react"; |
||||
|
import { viewPaddingsBridge } from "@/lib/view-paddings"; |
||||
|
|
||||
|
export function ViewPaddingsProvider() { |
||||
|
useEffect(() => { |
||||
|
viewPaddingsBridge; |
||||
|
}, []); |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useEffect, useState } from "react"; |
||||
|
import { viewPaddingsBridge } from "@/lib/view-paddings"; |
||||
|
|
||||
|
export function useViewPaddings() { |
||||
|
const [paddings, setPaddings] = useState(() => viewPaddingsBridge.getPaddings()); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
return viewPaddingsBridge.subscribe(setPaddings); |
||||
|
}, []); |
||||
|
|
||||
|
return paddings; |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
interface ViewPaddings { |
||||
|
top: number; |
||||
|
bottom: number; |
||||
|
left: number; |
||||
|
right: number; |
||||
|
} |
||||
|
|
||||
|
class ViewPaddingsBridge { |
||||
|
private paddings: ViewPaddings = { top: 0, bottom: 0, left: 0, right: 0 }; |
||||
|
private listeners: Array<(paddings: ViewPaddings) => void> = []; |
||||
|
private flutterUnsubscribe?: () => void; |
||||
|
|
||||
|
constructor() { |
||||
|
this.init(); |
||||
|
} |
||||
|
|
||||
|
private init() { |
||||
|
if (typeof window === "undefined") return; |
||||
|
|
||||
|
this.setupFlutterListener(); |
||||
|
this.requestPaddings(); |
||||
|
} |
||||
|
|
||||
|
private setupFlutterListener() { |
||||
|
const win = window as Window & { |
||||
|
addFlutterResponseListener?: ( |
||||
|
listener: (event: any) => void, |
||||
|
) => () => void; |
||||
|
}; |
||||
|
|
||||
|
const attach = () => { |
||||
|
if (typeof win.addFlutterResponseListener !== "function") return false; |
||||
|
|
||||
|
this.flutterUnsubscribe = win.addFlutterResponseListener((event) => { |
||||
|
if (event.action === "get_view_paddings" && event.success && event.data) { |
||||
|
this.paddings = { |
||||
|
top: event.data.top || 0, |
||||
|
bottom: event.data.bottom || 0, |
||||
|
left: event.data.left || 0, |
||||
|
right: event.data.right || 0, |
||||
|
}; |
||||
|
this.applyPaddings(); |
||||
|
this.notifyListeners(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return true; |
||||
|
}; |
||||
|
|
||||
|
if (!attach()) { |
||||
|
const interval = setInterval(() => { |
||||
|
if (attach()) clearInterval(interval); |
||||
|
}, 50); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private requestPaddings() { |
||||
|
if (typeof window === "undefined") return; |
||||
|
|
||||
|
const app = (window as any).HabibApp; |
||||
|
if (app?.postMessage) { |
||||
|
app.postMessage(JSON.stringify({ action: "get_view_paddings" })); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private applyPaddings() { |
||||
|
if (typeof document === "undefined") return; |
||||
|
|
||||
|
document.documentElement.style.setProperty("--safe-top", `${this.paddings.top}px`); |
||||
|
document.documentElement.style.setProperty("--safe-bottom", `${this.paddings.bottom}px`); |
||||
|
document.documentElement.style.setProperty("--safe-left", `${this.paddings.left}px`); |
||||
|
document.documentElement.style.setProperty("--safe-right", `${this.paddings.right}px`); |
||||
|
} |
||||
|
|
||||
|
private notifyListeners() { |
||||
|
this.listeners.forEach((listener) => listener(this.paddings)); |
||||
|
} |
||||
|
|
||||
|
public getPaddings(): ViewPaddings { |
||||
|
return { ...this.paddings }; |
||||
|
} |
||||
|
|
||||
|
public subscribe(listener: (paddings: ViewPaddings) => void): () => void { |
||||
|
this.listeners.push(listener); |
||||
|
return () => { |
||||
|
const index = this.listeners.indexOf(listener); |
||||
|
if (index >= 0) this.listeners.splice(index, 1); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const viewPaddingsBridge = new ViewPaddingsBridge(); |
||||
@ -0,0 +1,3 @@ |
|||||
|
export { useViewPaddings } from "./hooks/use-view-paddings"; |
||||
|
export { viewPaddingsBridge } from "./lib/view-paddings"; |
||||
|
export { FixedBottom } from "./components/utils/fixed-bottom"; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue