@tamer4lynx/tamer-host 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # tamer-host
2
+
3
+ Production Lynx host templates for injecting LynxView into existing Android and iOS apps.
4
+
5
+ ## Overview
6
+
7
+ `tamer-host` provides the minimal native infrastructure needed to run a Lynx bundle in an existing app:
8
+
9
+ - **Android**: `App.java`, `TemplateProvider.java`, `MainActivity.kt`
10
+ - **iOS**: `AppDelegate.swift`, `SceneDelegate.swift`, `ViewController.swift`, `LynxProvider.swift`, `LynxInitProcessor.swift`
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @tamer4lynx/tamer-host
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Inject into an existing project
21
+
22
+ After creating an Android or iOS project (or if you already have one), run:
23
+
24
+ ```bash
25
+ t4l android inject
26
+ t4l ios inject
27
+ ```
28
+
29
+ Use `--force` to overwrite existing files:
30
+
31
+ ```bash
32
+ t4l android inject --force
33
+ ```
34
+
35
+ ### Prerequisites
36
+
37
+ - `tamer.config.json` with `android.packageName` / `ios.appName` and `ios.bundleId`
38
+ - An existing Android project (`android/app/src/main/java/`, `android/app/src/main/kotlin/`) or iOS project (`ios/<AppName>/`)
39
+ - Run `t4l link` after injecting to register native modules
40
+
41
+ ### Create flow
42
+
43
+ `t4l android create` and `t4l ios create` use `tamer-host` templates when the package is installed. If `tamer-host` is not installed, the CLI falls back to inline templates.
44
+
45
+ ## Related
46
+
47
+ - [tamer-dev-client](https://github.com/tamer4lynx/tamer-dev-client) — Adds dev launcher (QR scan, HMR) on top of the host
48
+ - [Embedding LynxView into Native View](https://lynxjs.org/guide/embed-lynx-to-native) — Lynx guide for embedding LynxView in existing layouts
@@ -0,0 +1,41 @@
1
+ package {{PACKAGE_NAME}};
2
+
3
+ import android.app.Application;
4
+ import com.facebook.drawee.backends.pipeline.Fresco;
5
+ import com.facebook.imagepipeline.core.ImagePipelineConfig;
6
+ import com.facebook.imagepipeline.memory.PoolConfig;
7
+ import com.facebook.imagepipeline.memory.PoolFactory;
8
+ import com.lynx.service.http.LynxHttpService;
9
+ import com.lynx.service.image.LynxImageService;
10
+ import com.lynx.service.log.LynxLogService;
11
+ import com.lynx.tasm.LynxEnv;
12
+ import com.lynx.tasm.service.LynxServiceCenter;
13
+ import {{PACKAGE_NAME}}.generated.GeneratedLynxExtensions;
14
+
15
+ public class App extends Application {
16
+ @Override
17
+ public void onCreate() {
18
+ super.onCreate();
19
+ initLynxService();
20
+ initFresco();
21
+ initLynxEnv();
22
+ }
23
+
24
+ private void initLynxEnv() {
25
+ GeneratedLynxExtensions.INSTANCE.register(this);
26
+ LynxEnv.inst().init(this, null, new TemplateProvider(this), null);
27
+ }
28
+
29
+ private void initLynxService() {
30
+ LynxServiceCenter.inst().registerService(LynxLogService.INSTANCE);
31
+ LynxServiceCenter.inst().registerService(LynxImageService.getInstance());
32
+ LynxServiceCenter.inst().registerService(LynxHttpService.INSTANCE);
33
+ }
34
+
35
+ private void initFresco() {
36
+ final PoolFactory factory = new PoolFactory(PoolConfig.newBuilder().build());
37
+ ImagePipelineConfig.Builder builder =
38
+ ImagePipelineConfig.newBuilder(getApplicationContext()).setPoolFactory(factory);
39
+ Fresco.initialize(getApplicationContext(), builder.build());
40
+ }
41
+ }
@@ -0,0 +1,104 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.os.Bundle
4
+ import android.view.MotionEvent
5
+ import android.view.inputmethod.InputMethodManager
6
+ import android.widget.EditText
7
+ import androidx.appcompat.app.AppCompatActivity
8
+ import androidx.core.view.WindowCompat
9
+ import androidx.core.view.WindowInsetsCompat
10
+ import androidx.core.view.WindowInsetsControllerCompat
11
+ import androidx.core.view.ViewCompat
12
+ import androidx.core.view.updatePadding
13
+ import com.lynx.tasm.LynxView
14
+ import com.lynx.tasm.LynxViewBuilder
15
+ import {{PACKAGE_NAME}}.generated.GeneratedLynxExtensions
16
+ import {{PACKAGE_NAME}}.generated.GeneratedActivityLifecycle
17
+
18
+ class MainActivity : AppCompatActivity() {
19
+ private var lynxView: LynxView? = null
20
+ private val handler = android.os.Handler(android.os.Looper.getMainLooper())
21
+
22
+ override fun onCreate(savedInstanceState: Bundle?) {
23
+ super.onCreate(savedInstanceState)
24
+ GeneratedActivityLifecycle.onCreate(intent)
25
+ WindowCompat.setDecorFitsSystemWindows(window, false)
26
+ WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true
27
+ lynxView = buildLynxView()
28
+ setContentView(lynxView)
29
+ ViewCompat.setOnApplyWindowInsetsListener(lynxView!!) { view, insets ->
30
+ val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
31
+ val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
32
+ view.updatePadding(bottom = if (imeVisible) imeHeight else 0)
33
+ insets
34
+ }
35
+ GeneratedActivityLifecycle.onViewAttached(lynxView)
36
+ GeneratedLynxExtensions.onHostViewChanged(lynxView)
37
+ lynxView?.renderTemplateUrl("main.lynx.bundle", "")
38
+ GeneratedActivityLifecycle.onCreateDelayed(handler)
39
+ }
40
+
41
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
42
+ super.onWindowFocusChanged(hasFocus)
43
+ GeneratedActivityLifecycle.onWindowFocusChanged(hasFocus)
44
+ }
45
+
46
+ override fun onNewIntent(intent: android.content.Intent) {
47
+ super.onNewIntent(intent)
48
+ setIntent(intent)
49
+ GeneratedActivityLifecycle.onNewIntent(intent)
50
+ }
51
+
52
+ override fun onPause() {
53
+ super.onPause()
54
+ GeneratedActivityLifecycle.onPause()
55
+ }
56
+
57
+ override fun onResume() {
58
+ super.onResume()
59
+ GeneratedActivityLifecycle.onResume()
60
+ }
61
+
62
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
63
+ if (ev.action == MotionEvent.ACTION_DOWN) maybeClearFocusedInput(ev)
64
+ return super.dispatchTouchEvent(ev)
65
+ }
66
+
67
+ private fun maybeClearFocusedInput(ev: MotionEvent) {
68
+ val focused = currentFocus
69
+ if (focused is EditText) {
70
+ val loc = IntArray(2)
71
+ focused.getLocationOnScreen(loc)
72
+ val x = ev.rawX.toInt()
73
+ val y = ev.rawY.toInt()
74
+ if (x < loc[0] || x > loc[0] + focused.width || y < loc[1] || y > loc[1] + focused.height) {
75
+ focused.clearFocus()
76
+ (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
77
+ ?.hideSoftInputFromWindow(focused.windowToken, 0)
78
+ }
79
+ }
80
+ }
81
+
82
+ @Deprecated("Deprecated in Java")
83
+ override fun onBackPressed() {
84
+ GeneratedActivityLifecycle.onBackPressed { consumed ->
85
+ if (!consumed) {
86
+ runOnUiThread { super.onBackPressed() }
87
+ }
88
+ }
89
+ }
90
+
91
+ override fun onDestroy() {
92
+ GeneratedActivityLifecycle.onViewDetached()
93
+ GeneratedLynxExtensions.onHostViewChanged(null)
94
+ lynxView?.destroy()
95
+ lynxView = null
96
+ super.onDestroy()
97
+ }
98
+
99
+ private fun buildLynxView(): LynxView {
100
+ val viewBuilder = LynxViewBuilder()
101
+ viewBuilder.setTemplateProvider(TemplateProvider(this))
102
+ return viewBuilder.build(this)
103
+ }
104
+ }
@@ -0,0 +1,30 @@
1
+ package {{PACKAGE_NAME}};
2
+
3
+ import com.lynx.tasm.provider.AbsTemplateProvider;
4
+
5
+ public class TemplateProvider extends AbsTemplateProvider {
6
+ private final android.content.Context context;
7
+
8
+ public TemplateProvider(android.content.Context context) {
9
+ this.context = context.getApplicationContext();
10
+ }
11
+
12
+ @Override
13
+ public void loadTemplate(String url, final Callback callback) {
14
+ new Thread(() -> {
15
+ try {
16
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
17
+ try (java.io.InputStream is = context.getAssets().open(url)) {
18
+ byte[] buf = new byte[1024];
19
+ int n;
20
+ while ((n = is.read(buf)) != -1) {
21
+ baos.write(buf, 0, n);
22
+ }
23
+ }
24
+ callback.onSuccess(baos.toByteArray());
25
+ } catch (java.io.IOException e) {
26
+ callback.onFailed(e.getMessage());
27
+ }
28
+ }).start();
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ import UIKit
2
+
3
+ @UIApplicationMain
4
+ class AppDelegate: UIResponder, UIApplicationDelegate {
5
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
6
+ LynxInitProcessor.shared.setupEnvironment()
7
+ return true
8
+ }
9
+
10
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
11
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
12
+ }
13
+ }
@@ -0,0 +1,37 @@
1
+ // Copyright 2024 The Lynx Authors. All rights reserved.
2
+ // Licensed under the Apache License Version 2.0 that can be found in the
3
+ // LICENSE file in the root directory of this source tree.
4
+
5
+ import Foundation
6
+
7
+ // GENERATED IMPORTS START
8
+ // This section is automatically generated by Tamer4Lynx.
9
+ // Manual edits will be overwritten.
10
+ // GENERATED IMPORTS END
11
+
12
+ final class LynxInitProcessor {
13
+ static let shared = LynxInitProcessor()
14
+ private init() {}
15
+
16
+ func setupEnvironment() {
17
+ TamerIconElement.registerFonts()
18
+ setupLynxEnv()
19
+ setupLynxService()
20
+ }
21
+
22
+ private func setupLynxEnv() {
23
+ let env = LynxEnv.sharedInstance()
24
+ let globalConfig = LynxConfig(provider: env.config.templateProvider)
25
+
26
+ // GENERATED AUTOLINK START
27
+
28
+ // GENERATED AUTOLINK END
29
+
30
+ env.prepareConfig(globalConfig)
31
+ }
32
+
33
+ private func setupLynxService() {
34
+ let webPCoder = SDImageWebPCoder.shared
35
+ SDImageCodersManager.shared.addCoder(webPCoder)
36
+ }
37
+ }
@@ -0,0 +1,17 @@
1
+ import Foundation
2
+
3
+ class LynxProvider: NSObject, LynxTemplateProvider {
4
+ func loadTemplate(withUrl url: String!, onComplete callback: LynxTemplateLoadBlock!) {
5
+ DispatchQueue.global(qos: .background).async {
6
+ guard let url = url,
7
+ let bundleUrl = Bundle.main.url(forResource: url, withExtension: nil),
8
+ let data = try? Data(contentsOf: bundleUrl) else {
9
+ let err = NSError(domain: "LynxProvider", code: 404,
10
+ userInfo: [NSLocalizedDescriptionKey: "Bundle not found: \(url ?? "nil")"])
11
+ callback?(nil, err)
12
+ return
13
+ }
14
+ callback?(data, nil)
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ import UIKit
2
+
3
+ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
4
+ var window: UIWindow?
5
+
6
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
7
+ guard let windowScene = scene as? UIWindowScene else { return }
8
+ window = UIWindow(windowScene: windowScene)
9
+ window?.rootViewController = ViewController()
10
+ window?.makeKeyAndVisible()
11
+ }
12
+ }
@@ -0,0 +1,68 @@
1
+ import UIKit
2
+ import Lynx
3
+ import tamerinsets
4
+
5
+ class ViewController: UIViewController {
6
+ private var lynxView: LynxView?
7
+
8
+ override func viewDidLoad() {
9
+ super.viewDidLoad()
10
+ view.backgroundColor = .black
11
+ edgesForExtendedLayout = .all
12
+ extendedLayoutIncludesOpaqueBars = true
13
+ additionalSafeAreaInsets = .zero
14
+ view.insetsLayoutMarginsFromSafeArea = false
15
+ view.preservesSuperviewLayoutMargins = false
16
+ viewRespectsSystemMinimumLayoutMargins = false
17
+ }
18
+
19
+ override func viewDidLayoutSubviews() {
20
+ super.viewDidLayoutSubviews()
21
+ guard view.bounds.width > 0, view.bounds.height > 0 else { return }
22
+ if lynxView == nil {
23
+ setupLynxView()
24
+ } else {
25
+ applyFullscreenLayout(to: lynxView!)
26
+ }
27
+ }
28
+
29
+ override func viewSafeAreaInsetsDidChange() {
30
+ super.viewSafeAreaInsetsDidChange()
31
+ TamerInsetsModule.reRequestInsets()
32
+ }
33
+
34
+ override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
35
+
36
+ private func buildLynxView() -> LynxView {
37
+ let bounds = view.bounds
38
+ let lv = LynxView { builder in
39
+ builder.config = LynxConfig(provider: LynxProvider())
40
+ builder.screenSize = bounds.size
41
+ builder.fontScale = 1.0
42
+ }
43
+ lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
44
+ lv.insetsLayoutMarginsFromSafeArea = false
45
+ lv.preservesSuperviewLayoutMargins = false
46
+ applyFullscreenLayout(to: lv)
47
+ return lv
48
+ }
49
+
50
+ private func setupLynxView() {
51
+ let lv = buildLynxView()
52
+ view.addSubview(lv)
53
+ lv.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
54
+ self.lynxView = lv
55
+ }
56
+
57
+ private func applyFullscreenLayout(to lynxView: LynxView) {
58
+ let bounds = view.bounds
59
+ let size = bounds.size
60
+ lynxView.frame = bounds
61
+ lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
62
+ lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
63
+ lynxView.preferredLayoutWidth = size.width
64
+ lynxView.preferredLayoutHeight = size.height
65
+ lynxView.layoutWidthMode = .exact
66
+ lynxView.layoutHeightMode = .exact
67
+ }
68
+ }
package/lynx.ext.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "hostTransforms": {
3
+ "android": {
4
+ "templateDir": "android/templates",
5
+ "replaceTemplateProvider": true
6
+ },
7
+ "ios": {
8
+ "templateDir": "ios/templates",
9
+ "injectViewControllers": ["ViewController"]
10
+ }
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@tamer4lynx/tamer-host",
3
+ "version": "0.0.1",
4
+ "publishConfig": { "access": "public", "tag": "prerelease" },
5
+ "type": "module",
6
+ "description": "Production Lynx host templates for injecting LynxView into existing apps",
7
+ "files": ["android/templates", "ios", "lynx.ext.json"],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/tamer4lynx/tamer-host.git"
11
+ },
12
+ "homepage": "https://github.com/tamer4lynx/tamer-host#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/tamer4lynx/tamer-host/issues"
15
+ },
16
+ "author": "Nanofuxion",
17
+ "license": "MIT"
18
+ }