@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 +48 -0
- package/android/templates/App.java +41 -0
- package/android/templates/MainActivity.kt +104 -0
- package/android/templates/TemplateProvider.java +30 -0
- package/ios/templates/AppDelegate.swift +13 -0
- package/ios/templates/LynxInitProcessor.swift +37 -0
- package/ios/templates/LynxProvider.swift +17 -0
- package/ios/templates/SceneDelegate.swift +12 -0
- package/ios/templates/ViewController.swift +68 -0
- package/lynx.ext.json +12 -0
- package/package.json +18 -0
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
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
|
+
}
|