DeX: DensityScaleWrapperでtransform scaleアプローチに切替、babel plugin wrapStyleForDensityを除去
This commit is contained in:
56
App.tsx
56
App.tsx
@@ -1,8 +1,7 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Linking, Platform, UIManager } from "react-native";
|
||||
import { Linking, Platform, UIManager, View, useWindowDimensions, PixelRatio } from "react-native";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import "./utils/disableFontScaling"; // グローバルなフォントスケーリング無効化
|
||||
import "./utils/scaleTextForDensity"; // 低密度ディスプレイ(DeX等)でのテキスト自動拡大
|
||||
import { AppContainer } from "./Apps";
|
||||
import { UpdateAsync } from "./UpdateAsync";
|
||||
import { LogBox } from "react-native";
|
||||
@@ -138,13 +137,56 @@ export default function App() {
|
||||
<DeviceOrientationChangeProvider>
|
||||
<SafeAreaProvider>
|
||||
<StatusbarDetect />
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ProviderTree>
|
||||
<AppContainer />
|
||||
</ProviderTree>
|
||||
</GestureHandlerRootView>
|
||||
<DensityScaleWrapper>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ProviderTree>
|
||||
<AppContainer />
|
||||
</ProviderTree>
|
||||
</GestureHandlerRootView>
|
||||
</DensityScaleWrapper>
|
||||
</SafeAreaProvider>
|
||||
</DeviceOrientationChangeProvider>
|
||||
</AppThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 低密度ディスプレイ(DeX等)で全体を transform scale で拡大。
|
||||
* テキストもアイコンもコンテナも一括で拡大される。
|
||||
* 内部レイアウトは width/height を 1/scale にしてはみ出し防止。
|
||||
*/
|
||||
function DensityScaleWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { width, height } = useWindowDimensions();
|
||||
const pr = PixelRatio.get();
|
||||
|
||||
if (pr >= 2.0) {
|
||||
// 通常密度 → スケーリング不要
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// DeX(PR=0.756): 2.625/0.756=3.47 → 大きすぎるので 1.8 に制限
|
||||
const scale = Math.min(1.8, 2.0 / pr);
|
||||
const innerWidth = width / scale;
|
||||
const innerHeight = height / scale;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: innerWidth,
|
||||
height: innerHeight,
|
||||
transform: [{ scale }],
|
||||
transformOrigin: "top left",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
1
app.json
1
app.json
@@ -106,6 +106,7 @@
|
||||
},
|
||||
"plugins": [
|
||||
"./plugins/with-android-local-properties",
|
||||
"./plugins/with-nfc-widget-guard",
|
||||
"@bacons/apple-targets",
|
||||
[
|
||||
"expo-font",
|
||||
|
||||
BIN
assets/icons/icon_2048.webp
Normal file
BIN
assets/icons/icon_2048.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
@@ -3,12 +3,15 @@ import { FlexWidget, TextWidget } from "react-native-android-widget";
|
||||
import { getDelayData } from "./TraInfoEXWidget";
|
||||
import { getInfoString } from "./InfoWidget";
|
||||
import { getFelicaQuickAccessData } from "./FelicaQuickAccessWidget";
|
||||
import { isAvailable as isNfcAvailable } from "../../modules/expo-felica-reader/src";
|
||||
|
||||
export async function getShortcutData() {
|
||||
const hasNfc = isNfcAvailable();
|
||||
|
||||
const [delayResult, infoResult, felicaResult] = await Promise.allSettled([
|
||||
getDelayData(),
|
||||
getInfoString(),
|
||||
getFelicaQuickAccessData(),
|
||||
hasNfc ? getFelicaQuickAccessData() : Promise.resolve({ amountText: "" }),
|
||||
]);
|
||||
|
||||
const delayCount =
|
||||
@@ -26,13 +29,14 @@ export async function getShortcutData() {
|
||||
? felicaResult.value.amountText
|
||||
: "未読取";
|
||||
|
||||
return { delayCount, hasInfo, amountText };
|
||||
return { delayCount, hasInfo, amountText, hasNfc };
|
||||
}
|
||||
|
||||
export type ShortcutWidgetProps = {
|
||||
delayCount: number;
|
||||
hasInfo: boolean;
|
||||
amountText: string;
|
||||
hasNfc: boolean;
|
||||
};
|
||||
|
||||
const TILE_BG = "#E8F4FB";
|
||||
@@ -130,7 +134,7 @@ function GridTile({
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutWidget({ delayCount, hasInfo, amountText }: ShortcutWidgetProps) {
|
||||
export function ShortcutWidget({ delayCount, hasInfo, amountText, hasNfc }: ShortcutWidgetProps) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
@@ -184,16 +188,20 @@ export function ShortcutWidget({ delayCount, hasInfo, amountText }: ShortcutWidg
|
||||
badgeText={hasInfo ? "あり" : "なし"}
|
||||
badgeColor={hasInfo ? "#FF7043" : "#9E9E9E"}
|
||||
/>
|
||||
<FlexWidget style={{ width: SPACING, height: "match_parent" }}>
|
||||
<TextWidget text="" style={{ fontSize: 1 }} />
|
||||
</FlexWidget>
|
||||
<GridTile
|
||||
icon="💳"
|
||||
label="ICカード"
|
||||
sub={amountText}
|
||||
subColor="#0099CC"
|
||||
uri="jrshikoku://open/felica"
|
||||
/>
|
||||
{hasNfc && (
|
||||
<>
|
||||
<FlexWidget style={{ width: SPACING, height: "match_parent" }}>
|
||||
<TextWidget text="" style={{ fontSize: 1 }} />
|
||||
</FlexWidget>
|
||||
<GridTile
|
||||
icon="💳"
|
||||
label="ICカード"
|
||||
sub={amountText}
|
||||
subColor="#0099CC"
|
||||
uri="jrshikoku://open/felica"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FlexWidget>
|
||||
|
||||
{/* Spacer between rows */}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Babel plugin for Text/TextInput:
|
||||
* 1. allowFontScaling={false} を全要素に追加
|
||||
* 2. style プロップを global.__scaleTextStyle() でラップ
|
||||
* (低密度ディスプレイでの fontSize 自動拡大用)
|
||||
* Babel plugin to add allowFontScaling={false} to all <Text> and <TextInput> JSX elements.
|
||||
*
|
||||
* React 19 で defaultProps が関数コンポーネントで非推奨になったため、
|
||||
* Text.defaultProps.allowFontScaling = false の代替として使用する。
|
||||
*/
|
||||
module.exports = function ({ types: t }) {
|
||||
const TARGET_COMPONENTS = new Set(["Text", "TextInput"]);
|
||||
@@ -12,27 +12,20 @@ module.exports = function ({ types: t }) {
|
||||
JSXOpeningElement(path) {
|
||||
const nameNode = path.node.name;
|
||||
|
||||
let isTarget = false;
|
||||
|
||||
// <Text> or <TextInput>
|
||||
// <Text> or <TextInput> (simple identifiers only)
|
||||
if (
|
||||
nameNode.type === "JSXIdentifier" &&
|
||||
TARGET_COMPONENTS.has(nameNode.name)
|
||||
) {
|
||||
isTarget = true;
|
||||
addAllowFontScaling(path, t);
|
||||
}
|
||||
|
||||
// <Animated.Text>
|
||||
// <Animated.Text> (member expression)
|
||||
if (
|
||||
nameNode.type === "JSXMemberExpression" &&
|
||||
nameNode.property.name === "Text"
|
||||
) {
|
||||
isTarget = true;
|
||||
}
|
||||
|
||||
if (isTarget) {
|
||||
addAllowFontScaling(path, t);
|
||||
wrapStyleForDensity(path, t);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -55,56 +48,3 @@ function addAllowFontScaling(path, t) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* style プロップを global.__scaleTextStyle() でラップ:
|
||||
* - style={EXPR} → style={global.__scaleTextStyle ? global.__scaleTextStyle(EXPR) : EXPR}
|
||||
* - styleなし → style={global.__scaleTextStyle ? global.__scaleTextStyle() : undefined}
|
||||
*/
|
||||
function wrapStyleForDensity(path, t) {
|
||||
const makeGlobalRef = () =>
|
||||
t.memberExpression(t.identifier("global"), t.identifier("__scaleTextStyle"));
|
||||
|
||||
const styleAttrIndex = path.node.attributes.findIndex(
|
||||
(attr) =>
|
||||
attr.type === "JSXAttribute" &&
|
||||
attr.name &&
|
||||
attr.name.name === "style"
|
||||
);
|
||||
|
||||
if (styleAttrIndex >= 0) {
|
||||
const styleAttr = path.node.attributes[styleAttrIndex];
|
||||
let originalExpr;
|
||||
|
||||
if (styleAttr.value && styleAttr.value.type === "JSXExpressionContainer") {
|
||||
originalExpr = styleAttr.value.expression;
|
||||
} else if (styleAttr.value) {
|
||||
originalExpr = styleAttr.value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// style={global.__scaleTextStyle ? global.__scaleTextStyle(EXPR) : EXPR}
|
||||
styleAttr.value = t.jsxExpressionContainer(
|
||||
t.conditionalExpression(
|
||||
makeGlobalRef(),
|
||||
t.callExpression(makeGlobalRef(), [originalExpr]),
|
||||
t.cloneNode(originalExpr, true)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// style属性なし → デフォルトfontSize(14)もスケーリング対象
|
||||
path.node.attributes.push(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier("style"),
|
||||
t.jsxExpressionContainer(
|
||||
t.conditionalExpression(
|
||||
makeGlobalRef(),
|
||||
t.callExpression(makeGlobalRef(), []),
|
||||
t.identifier("undefined")
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
82
plugins/with-nfc-widget-guard.js
Normal file
82
plugins/with-nfc-widget-guard.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const { withDangerousMod } = require("@expo/config-plugins");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Disables the JR_shikoku_felica_balance widget on devices without NFC.
|
||||
* Injects code into MainApplication.kt to check NFC availability at startup
|
||||
* and disable the widget provider component if NFC is not supported.
|
||||
*/
|
||||
const withNfcWidgetGuard = (config) => {
|
||||
return withDangerousMod(config, [
|
||||
"android",
|
||||
async (config) => {
|
||||
const mainAppPath = path.join(
|
||||
config.modRequest.projectRoot,
|
||||
"android/app/src/main/java/jrshikokuinfo/xprocess/hrkn/MainApplication.kt"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(mainAppPath)) {
|
||||
console.warn("[withNfcWidgetGuard] MainApplication.kt not found, skipping.");
|
||||
return config;
|
||||
}
|
||||
|
||||
let contents = fs.readFileSync(mainAppPath, "utf-8");
|
||||
|
||||
// Skip if already patched
|
||||
if (contents.includes("disableFelicaWidgetIfNoNfc")) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// Add necessary imports
|
||||
contents = contents.replace(
|
||||
"import android.app.Application",
|
||||
"import android.app.Application\n" +
|
||||
"import android.content.ComponentName\n" +
|
||||
"import android.content.pm.PackageManager\n" +
|
||||
"import android.nfc.NfcAdapter"
|
||||
);
|
||||
|
||||
// Add the NFC check method and call it from onCreate
|
||||
const nfcGuardMethod = `
|
||||
/**
|
||||
* Disable the Felica balance widget on devices without NFC hardware,
|
||||
* so it won't appear in the Android widget picker.
|
||||
*/
|
||||
private fun disableFelicaWidgetIfNoNfc() {
|
||||
if (NfcAdapter.getDefaultAdapter(this) == null) {
|
||||
packageManager.setComponentEnabledSetting(
|
||||
ComponentName(this, jrshikokuinfo.xprocess.hrkn.widget.JR_shikoku_felica_balance::class.java),
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
} else {
|
||||
packageManager.setComponentEnabledSetting(
|
||||
ComponentName(this, jrshikokuinfo.xprocess.hrkn.widget.JR_shikoku_felica_balance::class.java),
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
}`;
|
||||
|
||||
// Insert method before the closing brace of the class
|
||||
contents = contents.replace(
|
||||
/\n}\s*$/,
|
||||
`\n${nfcGuardMethod}\n}\n`
|
||||
);
|
||||
|
||||
// Insert call in onCreate after ApplicationLifecycleDispatcher
|
||||
contents = contents.replace(
|
||||
"ApplicationLifecycleDispatcher.onApplicationCreate(this)",
|
||||
"ApplicationLifecycleDispatcher.onApplicationCreate(this)\n disableFelicaWidgetIfNoNfc()"
|
||||
);
|
||||
|
||||
fs.writeFileSync(mainAppPath, contents);
|
||||
console.log("[withNfcWidgetGuard] Patched MainApplication.kt to disable Felica widget on non-NFC devices.");
|
||||
|
||||
return config;
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
module.exports = withNfcWidgetGuard;
|
||||
Reference in New Issue
Block a user