mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
wip: Add expo plugin
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./plugin/build').default;
|
||||||
+7
-2
@@ -10,8 +10,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd example && bun start",
|
"dev": "cd example && bun start",
|
||||||
"android": " cd example && bunx react-native run-android --no-packager --port 8082",
|
"android": " cd example && bunx react-native run-android --no-packager --port 8082",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit && tsc -p plugin/tsconfig.json --noEmit",
|
||||||
"build": "bun run typecheck && bob build",
|
"build:plugin": "tsc -p plugin/tsconfig.json",
|
||||||
|
"build": "bun run typecheck && bob build && bun run build:plugin",
|
||||||
"codegen": "nitrogen --logLevel=\"debug\" && node post-script.js"
|
"codegen": "nitrogen --logLevel=\"debug\" && node post-script.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"ios/**/*.cpp",
|
"ios/**/*.cpp",
|
||||||
"ios/**/*.swift",
|
"ios/**/*.swift",
|
||||||
"app.plugin.js",
|
"app.plugin.js",
|
||||||
|
"plugin/build",
|
||||||
"*.podspec",
|
"*.podspec",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
@@ -53,6 +55,9 @@
|
|||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://registry.npmjs.org/"
|
"registry": "https://registry.npmjs.org/"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/config-plugins": "^10.0.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.10",
|
"@biomejs/biome": "2.4.10",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { createRunOncePlugin } from '@expo/config-plugins';
|
||||||
|
import type { ConfigPlugin } from '@expo/config-plugins';
|
||||||
|
import pkg from '../../package.json';
|
||||||
|
import { withMediaNotifications } from './withMediaNotifications';
|
||||||
|
import { withPip } from './withPip';
|
||||||
|
|
||||||
|
const withOmni: ConfigPlugin = (config) => {
|
||||||
|
let nextConfig = withPip(config);
|
||||||
|
nextConfig = withMediaNotifications(nextConfig);
|
||||||
|
return nextConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createRunOncePlugin(withOmni, pkg.name, pkg.version);
|
||||||
|
|
||||||
|
export { withPip, withMediaNotifications };
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { AndroidConfig, withAndroidManifest, type ConfigPlugin } from '@expo/config-plugins';
|
||||||
|
|
||||||
|
function ensureUsesPermission(
|
||||||
|
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
||||||
|
permission: string
|
||||||
|
): void {
|
||||||
|
const usesPermissions = androidManifest.manifest['uses-permission'] ?? [];
|
||||||
|
const alreadyDefined = usesPermissions.some((item) => item.$['android:name'] === permission);
|
||||||
|
|
||||||
|
if (!alreadyDefined) {
|
||||||
|
usesPermissions.push({
|
||||||
|
$: {
|
||||||
|
'android:name': permission,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
androidManifest.manifest['uses-permission'] = usesPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchManifestForMediaNotifications(
|
||||||
|
androidManifest: AndroidConfig.Manifest.AndroidManifest
|
||||||
|
): AndroidConfig.Manifest.AndroidManifest {
|
||||||
|
const mainApplication = AndroidConfig.Manifest.getMainApplication(androidManifest);
|
||||||
|
if (!mainApplication) {
|
||||||
|
console.warn(
|
||||||
|
'AndroidManifest.xml is missing a <application android:name=".MainApplication" /> element - skipping Omni media service config.'
|
||||||
|
);
|
||||||
|
return androidManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = mainApplication.service ?? [];
|
||||||
|
const serviceName = 'dev.zoriya.omni.OmniPlayerService';
|
||||||
|
const hasService = services.some((service) => service.$['android:name'] === serviceName);
|
||||||
|
if (!hasService) {
|
||||||
|
services.push({
|
||||||
|
$: {
|
||||||
|
'android:name': serviceName,
|
||||||
|
'android:enabled': 'true',
|
||||||
|
'android:exported': 'true',
|
||||||
|
'android:foregroundServiceType': 'mediaPlayback',
|
||||||
|
},
|
||||||
|
'intent-filter': [
|
||||||
|
{
|
||||||
|
action: [
|
||||||
|
{
|
||||||
|
$: {
|
||||||
|
'android:name': 'androidx.media3.session.MediaSessionService',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mainApplication.service = services;
|
||||||
|
|
||||||
|
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE');
|
||||||
|
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK');
|
||||||
|
return androidManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withMediaNotifications: ConfigPlugin = (config) => {
|
||||||
|
return withAndroidManifest(config, (manifestConfig) => {
|
||||||
|
manifestConfig.modResults = patchManifestForMediaNotifications(manifestConfig.modResults);
|
||||||
|
return manifestConfig;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { AndroidConfig, withAndroidManifest, withMainActivity, type ConfigPlugin } from '@expo/config-plugins';
|
||||||
|
import { addImports, appendContentsInsideDeclarationBlock } from '@expo/config-plugins/build/android/codeMod';
|
||||||
|
|
||||||
|
const MODE_MARKER = 'OmniView.onActivityPipModeChanged(';
|
||||||
|
const UI_STATE_MARKER = 'OmniView.onActivityPipTransitionToPip(';
|
||||||
|
|
||||||
|
function patchMainActivityForPip(mainActivity: string, language: 'java' | 'kt'): string {
|
||||||
|
const isJava = language === 'java';
|
||||||
|
|
||||||
|
if (mainActivity.includes(MODE_MARKER) && mainActivity.includes(UI_STATE_MARKER)) {
|
||||||
|
return mainActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withRequiredImports = addImports(
|
||||||
|
mainActivity,
|
||||||
|
['android.app.PictureInPictureUiState', 'android.os.Build', 'dev.zoriya.omni.OmniView'],
|
||||||
|
isJava
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = withRequiredImports;
|
||||||
|
|
||||||
|
if (!output.includes(MODE_MARKER)) {
|
||||||
|
const modeChangedBlock = isJava
|
||||||
|
? [
|
||||||
|
'\n @Override',
|
||||||
|
' public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {',
|
||||||
|
' super.onPictureInPictureModeChanged(isInPictureInPictureMode);',
|
||||||
|
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);',
|
||||||
|
' }\n',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {',
|
||||||
|
' super.onPictureInPictureModeChanged(isInPictureInPictureMode)',
|
||||||
|
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)',
|
||||||
|
' }\n',
|
||||||
|
];
|
||||||
|
|
||||||
|
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', modeChangedBlock.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output.includes(UI_STATE_MARKER)) {
|
||||||
|
const uiStateChangedBlock = isJava
|
||||||
|
? [
|
||||||
|
'\n @Override',
|
||||||
|
' public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {',
|
||||||
|
' super.onPictureInPictureUiStateChanged(pipState);',
|
||||||
|
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {',
|
||||||
|
' OmniView.onActivityPipTransitionToPip(this);',
|
||||||
|
' }',
|
||||||
|
' }\n',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {',
|
||||||
|
' super.onPictureInPictureUiStateChanged(pipState)',
|
||||||
|
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {',
|
||||||
|
' OmniView.onActivityPipTransitionToPip(this)',
|
||||||
|
' }',
|
||||||
|
' }\n',
|
||||||
|
];
|
||||||
|
|
||||||
|
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', uiStateChangedBlock.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUsesFeature(
|
||||||
|
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
||||||
|
featureName: string,
|
||||||
|
required: 'true' | 'false'
|
||||||
|
): void {
|
||||||
|
const usesFeature = androidManifest.manifest['uses-feature'] ?? [];
|
||||||
|
const existing = usesFeature.find((feature) => feature.$['android:name'] === featureName);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.$['android:required'] = required;
|
||||||
|
} else {
|
||||||
|
usesFeature.push({
|
||||||
|
$: {
|
||||||
|
'android:name': featureName,
|
||||||
|
'android:required': required,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
androidManifest.manifest['uses-feature'] = usesFeature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureConfigChanges(mainActivity: AndroidConfig.Manifest.ManifestActivity): void {
|
||||||
|
const requiredChanges = ['screenSize', 'smallestScreenSize', 'screenLayout', 'orientation'];
|
||||||
|
const existing = mainActivity.$['android:configChanges'] ?? '';
|
||||||
|
const existingSet = new Set(
|
||||||
|
existing
|
||||||
|
.split('|')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
requiredChanges.forEach((item) => existingSet.add(item));
|
||||||
|
mainActivity.$['android:configChanges'] = Array.from(existingSet).join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchManifestForPip(
|
||||||
|
androidManifest: AndroidConfig.Manifest.AndroidManifest
|
||||||
|
): AndroidConfig.Manifest.AndroidManifest {
|
||||||
|
ensureUsesFeature(androidManifest, 'android.software.picture_in_picture', 'false');
|
||||||
|
|
||||||
|
const mainActivity = AndroidConfig.Manifest.getMainActivityOrThrow(androidManifest);
|
||||||
|
mainActivity.$['android:supportsPictureInPicture'] = 'true';
|
||||||
|
mainActivity.$['android:resizeableActivity'] = 'true';
|
||||||
|
ensureConfigChanges(mainActivity);
|
||||||
|
|
||||||
|
return androidManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withPip: ConfigPlugin = (config) => {
|
||||||
|
const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
|
||||||
|
activityConfig.modResults.contents = patchMainActivityForPip(
|
||||||
|
activityConfig.modResults.contents,
|
||||||
|
activityConfig.modResults.language
|
||||||
|
);
|
||||||
|
return activityConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
|
||||||
|
manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
|
||||||
|
return manifestConfig;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user