|
|
|
@ -5,9 +5,83 @@ interface ViewPaddings { |
|
|
|
right: number; |
|
|
|
} |
|
|
|
|
|
|
|
interface LayoutInfo { |
|
|
|
breakpoint: string; |
|
|
|
screenWidth: number; |
|
|
|
screenHeight: number; |
|
|
|
isMobile: boolean; |
|
|
|
isTablet: boolean; |
|
|
|
isDesktop: boolean; |
|
|
|
} |
|
|
|
|
|
|
|
interface PlatformInfo { |
|
|
|
os: string; |
|
|
|
version: string; |
|
|
|
} |
|
|
|
|
|
|
|
interface LocaleInfo { |
|
|
|
languageCode: string; |
|
|
|
isRTL: boolean; |
|
|
|
} |
|
|
|
|
|
|
|
interface InitialConfig { |
|
|
|
paddings: ViewPaddings; |
|
|
|
viewInsets: ViewPaddings; |
|
|
|
safeArea: ViewPaddings; |
|
|
|
keyboardHeight: number; |
|
|
|
layout: LayoutInfo | null; |
|
|
|
platform: PlatformInfo | null; |
|
|
|
locale: LocaleInfo | null; |
|
|
|
} |
|
|
|
|
|
|
|
type EdgeSource = { |
|
|
|
top?: number; |
|
|
|
bottom?: number; |
|
|
|
left?: number; |
|
|
|
right?: number; |
|
|
|
} | null |
|
|
|
| undefined; |
|
|
|
|
|
|
|
function num(value: unknown): number { |
|
|
|
return typeof value === "number" && Number.isFinite(value) ? value : 0; |
|
|
|
} |
|
|
|
|
|
|
|
function readEdges(source: EdgeSource): ViewPaddings { |
|
|
|
return { |
|
|
|
top: num(source?.top), |
|
|
|
bottom: num(source?.bottom), |
|
|
|
left: num(source?.left), |
|
|
|
right: num(source?.right), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* فضای امن هر ضلع = بیشینهی viewInsets و safeArea همان ضلع. |
|
|
|
* منطبق بر منطق مستند: navigationBarHeight = max(insets, padding). |
|
|
|
* هیچ عدد ثابتی استفاده نمیشود؛ همهچیز از دیتای دریافتی محاسبه میشود. |
|
|
|
*/ |
|
|
|
function mergeEdges(insets: ViewPaddings, safe: ViewPaddings): ViewPaddings { |
|
|
|
return { |
|
|
|
top: Math.max(insets.top, safe.top), |
|
|
|
bottom: Math.max(insets.bottom, safe.bottom), |
|
|
|
left: Math.max(insets.left, safe.left), |
|
|
|
right: Math.max(insets.right, safe.right), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
class ViewPaddingsBridge { |
|
|
|
private paddings: ViewPaddings = { top: 0, bottom: 0, left: 0, right: 0 }; |
|
|
|
private config: InitialConfig = { |
|
|
|
paddings: { top: 0, bottom: 0, left: 0, right: 0 }, |
|
|
|
viewInsets: { top: 0, bottom: 0, left: 0, right: 0 }, |
|
|
|
safeArea: { top: 0, bottom: 0, left: 0, right: 0 }, |
|
|
|
keyboardHeight: 0, |
|
|
|
layout: null, |
|
|
|
platform: null, |
|
|
|
locale: null, |
|
|
|
}; |
|
|
|
private listeners: Array<(paddings: ViewPaddings) => void> = []; |
|
|
|
private configListeners: Array<(config: InitialConfig) => void> = []; |
|
|
|
private flutterUnsubscribe?: () => void; |
|
|
|
|
|
|
|
constructor() { |
|
|
|
@ -18,7 +92,7 @@ class ViewPaddingsBridge { |
|
|
|
if (typeof window === "undefined") return; |
|
|
|
|
|
|
|
this.setupFlutterListener(); |
|
|
|
this.requestPaddings(); |
|
|
|
this.requestConfig(); |
|
|
|
} |
|
|
|
|
|
|
|
private setupFlutterListener() { |
|
|
|
@ -32,15 +106,16 @@ class ViewPaddingsBridge { |
|
|
|
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(); |
|
|
|
if (!event || event.success === false || !event.data) return; |
|
|
|
|
|
|
|
if (event.action === "initial_config") { |
|
|
|
this.applyInitialConfig(event.data); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// سازگاری عقبرو: پاسخ سادهی get_view_paddings که فقط لبهها را دارد
|
|
|
|
if (event.action === "get_view_paddings") { |
|
|
|
this.applyEdges(readEdges(event.data)); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
@ -54,32 +129,99 @@ class ViewPaddingsBridge { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private requestPaddings() { |
|
|
|
private requestConfig() { |
|
|
|
if (typeof window === "undefined") return; |
|
|
|
|
|
|
|
const app = (window as any).HabibApp; |
|
|
|
if (app?.postMessage) { |
|
|
|
// اعلام آمادگی وب؛ فلاتر با initial_config پاسخ میدهد.
|
|
|
|
app.postMessage(JSON.stringify({ action: "web_ready" })); |
|
|
|
// درخواست سازگاری عقبرو برای نسخههای قدیمیتر اپ.
|
|
|
|
app.postMessage(JSON.stringify({ action: "get_view_paddings" })); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private applyInitialConfig(data: any) { |
|
|
|
const viewInsets = readEdges(data?.viewInsets); |
|
|
|
const safeArea = readEdges(data?.safeArea); |
|
|
|
const paddings = mergeEdges(viewInsets, safeArea); |
|
|
|
|
|
|
|
this.config = { |
|
|
|
paddings, |
|
|
|
viewInsets, |
|
|
|
safeArea, |
|
|
|
keyboardHeight: viewInsets.bottom, |
|
|
|
layout: data?.layout |
|
|
|
? { |
|
|
|
breakpoint: String(data.layout.breakpoint ?? ""), |
|
|
|
screenWidth: num(data.layout.screenWidth), |
|
|
|
screenHeight: num(data.layout.screenHeight), |
|
|
|
isMobile: Boolean(data.layout.isMobile), |
|
|
|
isTablet: Boolean(data.layout.isTablet), |
|
|
|
isDesktop: Boolean(data.layout.isDesktop), |
|
|
|
} |
|
|
|
: null, |
|
|
|
platform: data?.platform |
|
|
|
? { |
|
|
|
os: String(data.platform.os ?? ""), |
|
|
|
version: String(data.platform.version ?? ""), |
|
|
|
} |
|
|
|
: null, |
|
|
|
locale: data?.locale |
|
|
|
? { |
|
|
|
languageCode: String(data.locale.languageCode ?? ""), |
|
|
|
isRTL: Boolean(data.locale.isRTL ?? data.locale.isRtl), |
|
|
|
} |
|
|
|
: null, |
|
|
|
}; |
|
|
|
|
|
|
|
this.applyEdges(paddings); |
|
|
|
this.applyKeyboard(viewInsets.bottom); |
|
|
|
this.notifyConfigListeners(); |
|
|
|
} |
|
|
|
|
|
|
|
private applyEdges(paddings: ViewPaddings) { |
|
|
|
this.paddings = paddings; |
|
|
|
this.applyPaddings(); |
|
|
|
this.notifyListeners(); |
|
|
|
} |
|
|
|
|
|
|
|
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`); |
|
|
|
const root = document.documentElement.style; |
|
|
|
root.setProperty("--safe-top", `${this.paddings.top}px`); |
|
|
|
root.setProperty("--safe-bottom", `${this.paddings.bottom}px`); |
|
|
|
root.setProperty("--safe-left", `${this.paddings.left}px`); |
|
|
|
root.setProperty("--safe-right", `${this.paddings.right}px`); |
|
|
|
} |
|
|
|
|
|
|
|
private applyKeyboard(height: number) { |
|
|
|
if (typeof document === "undefined") return; |
|
|
|
document.documentElement.style.setProperty("--kb-height", `${height}px`); |
|
|
|
} |
|
|
|
|
|
|
|
private notifyListeners() { |
|
|
|
this.listeners.forEach((listener) => listener(this.paddings)); |
|
|
|
} |
|
|
|
|
|
|
|
private notifyConfigListeners() { |
|
|
|
this.configListeners.forEach((listener) => listener(this.config)); |
|
|
|
} |
|
|
|
|
|
|
|
public getPaddings(): ViewPaddings { |
|
|
|
return { ...this.paddings }; |
|
|
|
} |
|
|
|
|
|
|
|
public getConfig(): InitialConfig { |
|
|
|
return { |
|
|
|
...this.config, |
|
|
|
paddings: { ...this.config.paddings }, |
|
|
|
viewInsets: { ...this.config.viewInsets }, |
|
|
|
safeArea: { ...this.config.safeArea }, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
public subscribe(listener: (paddings: ViewPaddings) => void): () => void { |
|
|
|
this.listeners.push(listener); |
|
|
|
return () => { |
|
|
|
@ -87,6 +229,17 @@ class ViewPaddingsBridge { |
|
|
|
if (index >= 0) this.listeners.splice(index, 1); |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
public subscribeConfig( |
|
|
|
listener: (config: InitialConfig) => void, |
|
|
|
): () => void { |
|
|
|
this.configListeners.push(listener); |
|
|
|
return () => { |
|
|
|
const index = this.configListeners.indexOf(listener); |
|
|
|
if (index >= 0) this.configListeners.splice(index, 1); |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
export type { ViewPaddings, InitialConfig, LayoutInfo, PlatformInfo, LocaleInfo }; |
|
|
|
export const viewPaddingsBridge = new ViewPaddingsBridge(); |