Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 804913dcbe | |||
| 63f759c1e5 | |||
| 884e397d68 | |||
| 5cc939e120 | |||
| fb890a9204 |
@@ -12,6 +12,7 @@ npx skills add rodydavis/skills
|
||||
| [async-preact-signals](./skills/async-preact-signal/SKILL.md) | Explore how to effectively manage asynchronous data with Preact Signals by creating a custom `asyncSignal` that handles loading, error, and data states without breaking the synchronous nature of signals. |
|
||||
| [automate-flutter-app-releases](./skills/automate-flutter-apps/SKILL.md) | Automate your Flutter app releases to beta or production with this handy shell script that handles version bumping, formatting, cleaning, rebuilding, and deployment via Fastlane. |
|
||||
| [dynamic-themes-with-codemirror](./skills/codemirror-dynamic-theme/SKILL.md) | Learn how to create a Lit web component with CodeMirror, dynamically themed using Material Design's color utilities, for a customizable code editing experience. |
|
||||
| [create-flutter-app-native-ui](./skills/create-flutter-app-native-ui/SKILL.md) | Guide to bootstrap, coordinate, and maintain a headless Flutter engine integrated with 100% native platforms (SwiftUI, Jetpack Compose, HTML5 Web Components). |
|
||||
| [how-to-create-html-web-components-with-dart](./skills/dart-html-web-components/SKILL.md) | Discover the power of Web Components and how to build them with both JavaScript and Dart for reusable, framework-agnostic UI elements. |
|
||||
| [how-to-do-bitwise-operations-in-dart](./skills/dart_bitwise/SKILL.md) | Explore Dart's bitwise operations for both integers and booleans, including AND, OR (inclusive & exclusive), NAND, NOR, and XNOR, with practical code examples. |
|
||||
| [various-ways-to-invoke-functions-in-dart](./skills/dart_function-invoking/SKILL.md) | Discover the surprising flexibility of calling Dart functions, including mixed positional and named arguments, the `.call` operator, and dynamic invocation with `Function.apply`. |
|
||||
@@ -19,6 +20,7 @@ npx skills add rodydavis/skills
|
||||
| [check-if-an-object-is-truthy-in-dart](./skills/dart_truthy/SKILL.md) | Learn how to extend Dart's functionality to implement JavaScript-style "truthy" checks for easier conditional logic and value evaluations. |
|
||||
| [deep-linking-for-flutter-web](./skills/deep-linking-flutter-web/SKILL.md) | Learn how to implement proper URL navigation in your Flutter application, including deep linking to specific pages, handling protected routes, and creating custom transitions for a seamless user experience. |
|
||||
| [displaying-html-in-flutter](./skills/display-html-in-flutter/SKILL.md) | Easily display and interact with HTML content in your Flutter app using the `easy_web_view` package, which supports both web and mobile platforms. |
|
||||
| [dotfiles-management](./skills/dotfiles/SKILL.md) | Guidelines and rules for managing the user's dotfiles repository, config files, and installation scripts. |
|
||||
| [how-to-export-sqlite-tables-to-create-statements](./skills/export-sqlite-dart/SKILL.md) | Learn how to export your entire SQLite database schema, including tables and indexes, into runnable CREATE statements at runtime using Flutter and the `sqlite3` package. |
|
||||
| [using-fastlane-in-flutter-and-ci](./skills/fastlane-and-flutter/SKILL.md) | Automate Flutter app builds and deployments to both the App Store and Google Play using Fastlane with this step-by-step guide. |
|
||||
| [lit-and-figma](./skills/figma-and-lit/SKILL.md) | Learn how to create a Figma plugin using Lit web components, including project setup, component creation, WASM integration, and building the final plugin for use in Figma. |
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: create-flutter-app-native-ui
|
||||
description: Guide to bootstrap, coordinate, and maintain a headless Flutter engine integrated with 100% native platforms (SwiftUI, Jetpack Compose, HTML5 Web Components).
|
||||
---
|
||||
|
||||
# Creating a Flutter App with 100% Native UIs
|
||||
|
||||
This skill provides step-by-step guidance on implementing a decoupled architectural design where a **headless Flutter engine** acts as the shared single source of truth (SSOT) running in the background, while the user interfaces are built using platform-native toolkits.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Native Frontends
|
||||
iOS[SwiftUI View]
|
||||
Android[Jetpack Compose]
|
||||
Web[Web Components / JS]
|
||||
end
|
||||
|
||||
subgraph Headless Flutter Context
|
||||
Engine[Flutter Engine]
|
||||
StateManager[State Manager / Tasks]
|
||||
Bridge[Platform Channels / Interop]
|
||||
end
|
||||
|
||||
iOS <-->|Pigeon IPC| Bridge
|
||||
Android <-->|Pigeon IPC| Bridge
|
||||
Web <-->|dart:js_interop| Bridge
|
||||
Bridge <--> StateManager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Interop Layer Setup
|
||||
|
||||
### Mobile (Pigeon Code-Gen)
|
||||
Pigeon is used for type-safe IPC communication between Dart and Swift/Kotlin.
|
||||
Create a pigeon definition file at `pigeons/tasks_api.dart`:
|
||||
- Use `@HostApi` for calls originating from native code that Dart answers.
|
||||
- Use `@FlutterApi` for calls originating from Dart that native platforms listen to.
|
||||
|
||||
Compile the interfaces using the Pigeon CLI:
|
||||
```bash
|
||||
dart run pigeon --input pigeons/tasks_api.dart
|
||||
```
|
||||
|
||||
### Web (JS Interop)
|
||||
Use `dart:js_interop` to bind functions to the global `window` object:
|
||||
- Define `@JS('window.methodName')` setters to bind Dart triggers.
|
||||
- Define `@JS('window.callbackName')` getters to notify host JS components of updates.
|
||||
|
||||
---
|
||||
|
||||
## 2. Bootstrapping Headless Engines
|
||||
|
||||
### iOS (AppDelegate.swift)
|
||||
Instantiate `FlutterEngine` without a visual viewport controller:
|
||||
1. Declare `var flutterEngine: FlutterEngine?`.
|
||||
2. Instantiate `let engine = FlutterEngine(name: "my-engine")` and call `engine.run()`.
|
||||
3. Set up the custom event handler store (`TaskStore`) to listen on the binary messenger:
|
||||
`store.setup(binaryMessenger: engine.binaryMessenger)`.
|
||||
4. Wrap your SwiftUI view using `UIHostingController` and present it in the main `UIWindow`.
|
||||
|
||||
### Android (MainActivity.kt)
|
||||
Instantiate a headless `FlutterEngine` inside a standard Compose activity:
|
||||
1. Override `onCreate` in a subclass of `ComponentActivity`.
|
||||
2. Instantiate `FlutterEngine(this)` and invoke `executeDartEntrypoint(...)`.
|
||||
3. Connect your Pigeon host/client endpoints using the engine's `dartExecutor.binaryMessenger`.
|
||||
4. Set up Compose UI using `setContent { ... }`.
|
||||
5. Call `flutterEngine.destroy()` on `onDestroy()`.
|
||||
|
||||
### Web (flutter_bootstrap.js)
|
||||
Boot Flutter Web headlessly by specifying a hidden target container:
|
||||
1. Create a `flutter_bootstrap.js` mapping:
|
||||
```javascript
|
||||
engineInitializer.initializeEngine({
|
||||
hostElement: document.getElementById('flutter-host') // Styled with display: none
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. UI Synchronization & State Management
|
||||
|
||||
Native clients should observe updates from the headless engine reactively:
|
||||
* **iOS**: Implement `ObservableObject` and dispatch events to SwiftUI views via `@Published` properties on `DispatchQueue.main.async`.
|
||||
* **Android**: Use a Kotlin `StateFlow` updated via the Pigeon host API callback. Connect Compose views using `.collectAsState()`.
|
||||
* **Web**: Create custom HTML5 Web Components dispatching custom bubbles events (`toggle-task`, `delete-task`) up the DOM hierarchy.
|
||||
|
||||
---
|
||||
|
||||
## 4. Resource & Verification Scripts
|
||||
|
||||
The resource folder contains CLI compilation check scripts:
|
||||
- `resources/check_ios.sh`: Validates Swift compilation and framework linkage.
|
||||
- `resources/check_android.sh`: Triggers Kotlin compile tasks to catch build errors before simulator deployment.
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.example.tasks_interop
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class MainActivity : ComponentActivity(), TasksHostApi {
|
||||
private lateinit var flutterEngine: FlutterEngine
|
||||
private lateinit var flutterApi: TasksFlutterApi
|
||||
|
||||
private val _tasks = MutableStateFlow<List<TaskData>>(emptyList())
|
||||
val tasks: StateFlow<List<TaskData>> = _tasks.asStateFlow()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
flutterEngine = FlutterEngine(this)
|
||||
TasksHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, this)
|
||||
flutterApi = TasksFlutterApi(flutterEngine.dartExecutor.binaryMessenger)
|
||||
|
||||
flutterEngine.dartExecutor.executeDartEntrypoint(
|
||||
DartExecutor.DartEntrypoint.createDefault()
|
||||
)
|
||||
|
||||
flutterApi.getTasks { result ->
|
||||
if (result.isSuccess) {
|
||||
_tasks.value = result.getOrNull() ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val tasksState = tasks.collectAsState()
|
||||
TasksApp(
|
||||
tasks = tasksState.value,
|
||||
onAddTask = { title ->
|
||||
flutterApi.addTask(title) { }
|
||||
},
|
||||
onToggleTask = { id ->
|
||||
flutterApi.toggleTask(id) { }
|
||||
},
|
||||
onDeleteTask = { id ->
|
||||
flutterApi.deleteTask(id) { }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTasksUpdated(tasks: List<TaskData>) {
|
||||
_tasks.value = tasks
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
TasksHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, null)
|
||||
flutterEngine.destroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.example.tasks_interop
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TasksApp(
|
||||
tasks: List<TaskData>,
|
||||
onAddTask: (String) -> Unit,
|
||||
onToggleTask: (String) -> Unit,
|
||||
onDeleteTask: (String) -> Unit
|
||||
) {
|
||||
var newTaskTitle by remember { mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextField(
|
||||
value = newTaskTitle,
|
||||
onValueChange = { newTaskTitle = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (newTaskTitle.isNotBlank()) {
|
||||
onAddTask(newTaskTitle.trim())
|
||||
newTaskTitle = ""
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add")
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn {
|
||||
items(tasks, key = { it.id }) { task ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = task.isCompleted,
|
||||
onCheckedChange = { onToggleTask(task.id) }
|
||||
)
|
||||
Text(
|
||||
text = task.title,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = { onDeleteTask(task.id) }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
var flutterEngine: FlutterEngine?
|
||||
let taskStore = TaskStore()
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
let engine = FlutterEngine(name: "headless-tasks-engine")
|
||||
engine.run()
|
||||
self.flutterEngine = engine
|
||||
|
||||
GeneratedPluginRegistrant.register(with: engine)
|
||||
taskStore.setup(binaryMessenger: engine.binaryMessenger)
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
let contentView = ContentView(store: taskStore)
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@ObservedObject var store: TaskStore
|
||||
@State private var newTaskTitle: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
if store.tasks.isEmpty {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "checklist")
|
||||
.font(.system(size: 80))
|
||||
.foregroundColor(.indigo.opacity(0.4))
|
||||
|
||||
Text("No Tasks Yet")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
} else {
|
||||
List {
|
||||
ForEach(store.tasks) { task in
|
||||
TaskRow(task: task) {
|
||||
store.toggleTask(id: task.id)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteTasks)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
||||
Divider()
|
||||
HStack(spacing: 12) {
|
||||
TextField("Enter task title...", text: $newTaskTitle)
|
||||
.padding(12)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(10)
|
||||
.onSubmit(addTask)
|
||||
|
||||
Button(action: addTask) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 38))
|
||||
.foregroundColor(.indigo)
|
||||
}
|
||||
.disabled(newTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Tasks")
|
||||
}
|
||||
}
|
||||
|
||||
private func addTask() {
|
||||
let trimmed = newTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
store.addTask(title: trimmed)
|
||||
newTaskTitle = ""
|
||||
}
|
||||
|
||||
private func deleteTasks(at offsets: IndexSet) {
|
||||
for index in offsets {
|
||||
let task = store.tasks[index]
|
||||
store.deleteTask(id: task.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import SwiftUI
|
||||
|
||||
extension TaskData: Identifiable {}
|
||||
|
||||
struct TaskRow: View {
|
||||
let task: TaskData
|
||||
let onToggle: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
Button(action: onToggle) {
|
||||
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(task.isCompleted ? .indigo : .gray)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text(task.title)
|
||||
.strikethrough(task.isCompleted, color: .gray)
|
||||
.foregroundColor(task.isCompleted ? .gray : .primary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 16)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import Flutter
|
||||
|
||||
class TaskStore: NSObject, ObservableObject, TasksHostApi {
|
||||
@Published var tasks: [TaskData] = []
|
||||
private var flutterApi: TasksFlutterApi?
|
||||
|
||||
func setup(binaryMessenger: FlutterBinaryMessenger) {
|
||||
TasksHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: self)
|
||||
self.flutterApi = TasksFlutterApi(binaryMessenger: binaryMessenger)
|
||||
self.flutterApi?.getTasks { [weak self] result in
|
||||
if case .success(let list) = result {
|
||||
DispatchQueue.main.async {
|
||||
self?.tasks = list
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onTasksUpdated(tasks: [TaskData]) throws {
|
||||
DispatchQueue.main.async {
|
||||
self.tasks = tasks
|
||||
}
|
||||
}
|
||||
|
||||
func addTask(title: String) {
|
||||
flutterApi?.addTask(title: title) { _ in }
|
||||
}
|
||||
|
||||
func toggleTask(id: String) {
|
||||
flutterApi?.toggleTask(id: id) { _ in }
|
||||
}
|
||||
|
||||
func deleteTask(id: String) {
|
||||
flutterApi?.deleteTask(id: id) { _ in }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:pigeon/pigeon.dart';
|
||||
|
||||
@ConfigurePigeon(PigeonOptions(
|
||||
dartOut: 'lib/src/tasks_api.g.dart',
|
||||
swiftOut: 'ios/Runner/TasksApi.g.swift',
|
||||
kotlinOut: 'android/app/src/main/kotlin/com/example/tasks_interop/TasksApi.g.kt',
|
||||
kotlinOptions: KotlinOptions(package: 'com.example.tasks_interop'),
|
||||
))
|
||||
|
||||
class TaskData {
|
||||
final String id;
|
||||
final String title;
|
||||
final bool isCompleted;
|
||||
|
||||
TaskData({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.isCompleted,
|
||||
});
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
abstract class TasksHostApi {
|
||||
void onTasksUpdated(List<TaskData> tasks);
|
||||
}
|
||||
|
||||
@FlutterApi()
|
||||
abstract class TasksFlutterApi {
|
||||
List<TaskData> getTasks();
|
||||
void addTask(String title);
|
||||
void toggleTask(String id);
|
||||
void deleteTask(String id);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
class TaskItem extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._id = '';
|
||||
this._title = '';
|
||||
this._completed = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['task-id', 'title', 'completed'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (oldValue === newValue) return;
|
||||
if (name === 'task-id') this._id = newValue;
|
||||
if (name === 'title') this._title = newValue;
|
||||
if (name === 'completed') this._completed = newValue === 'true';
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div>
|
||||
<input type="checkbox" ${this._completed ? 'checked' : ''}>
|
||||
<span>${this._title}</span>
|
||||
<button class="delete-btn">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.querySelector('input').addEventListener('change', () => {
|
||||
this.dispatchEvent(new CustomEvent('toggle-task', {
|
||||
bubbles: true,
|
||||
detail: { id: this._id }
|
||||
}));
|
||||
});
|
||||
|
||||
this.querySelector('.delete-btn').addEventListener('click', () => {
|
||||
this.dispatchEvent(new CustomEvent('delete-task', {
|
||||
bubbles: true,
|
||||
detail: { id: this._id }
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('task-item', TaskItem);
|
||||
|
||||
class TaskApp extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div>
|
||||
<form id="task-form">
|
||||
<input type="text" placeholder="Add a new task..." required>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
<div id="task-list"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.querySelector('#task-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const val = this.querySelector('input').value.trim();
|
||||
if (val && window.addTaskOnWeb) {
|
||||
window.addTaskOnWeb(val);
|
||||
this.querySelector('input').value = '';
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener('toggle-task', (e) => {
|
||||
if (window.toggleTaskOnWeb) window.toggleTaskOnWeb(e.detail.id);
|
||||
});
|
||||
|
||||
this.addEventListener('delete-task', (e) => {
|
||||
if (window.deleteTaskOnWeb) window.deleteTaskOnWeb(e.detail.id);
|
||||
});
|
||||
}
|
||||
|
||||
setTasks(tasks) {
|
||||
const list = this.querySelector('#task-list');
|
||||
list.innerHTML = '';
|
||||
tasks.forEach(t => {
|
||||
const item = document.createElement('task-item');
|
||||
item.setAttribute('task-id', t.id);
|
||||
item.setAttribute('title', t.title);
|
||||
item.setAttribute('completed', t.isCompleted ? 'true' : 'false');
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('task-app', TaskApp);
|
||||
|
||||
window.onTasksUpdatedOnWeb = function(tasks) {
|
||||
const app = document.querySelector('task-app');
|
||||
if (app) app.setTasks(tasks);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{{flutter_js}}
|
||||
{{flutter_build_config}}
|
||||
|
||||
_flutter.loader.load({
|
||||
onEntrypointLoaded: function(engineInitializer) {
|
||||
engineInitializer.initializeEngine({
|
||||
hostElement: document.getElementById('flutter-host')
|
||||
}).then(function(appRunner) {
|
||||
appRunner.runApp();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}Starting Android Kotlin compilation check...${NC}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$(dirname "$(dirname "$SCRIPT_DIR")")")"
|
||||
ANDROID_DIR="$PROJECT_ROOT/android"
|
||||
|
||||
if [ ! -d "$ANDROID_DIR" ]; then
|
||||
echo -e "${RED}Error: android directory not found at $ANDROID_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running gradle compileDebugKotlin..."
|
||||
set +e
|
||||
"$ANDROID_DIR/gradlew" -p "$ANDROID_DIR" compileDebugKotlin --quiet
|
||||
|
||||
RESULT=$?
|
||||
set -e
|
||||
|
||||
if [ $RESULT -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Android Compilation Succeeded! No errors found.${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Android compilation failed with exit code $RESULT.${NC}"
|
||||
exit $RESULT
|
||||
fi
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}Starting iOS compilation check...${NC}"
|
||||
|
||||
if [ -d "/Applications/Xcode-16.2.app" ]; then
|
||||
echo "Using Xcode 16.2 toolchain..."
|
||||
export DEVELOPER_DIR="/Applications/Xcode-16.2.app/Contents/Developer"
|
||||
else
|
||||
echo "Using system default Xcode toolchain..."
|
||||
fi
|
||||
|
||||
echo "Running xcodebuild..."
|
||||
set +e
|
||||
xcodebuild \
|
||||
-workspace ios/Runner.xcworkspace \
|
||||
-scheme Runner \
|
||||
-configuration Debug \
|
||||
-sdk iphonesimulator \
|
||||
build \
|
||||
-quiet
|
||||
|
||||
RESULT=$?
|
||||
set -e
|
||||
|
||||
if [ $RESULT -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ iOS Compilation Succeeded! No errors found.${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${YELLOW}Xcode build finished with code $RESULT.${NC}"
|
||||
echo "If the only failing step in the output is 'CompileAssetCatalog', your Swift and interop codebase compiles successfully."
|
||||
exit $RESULT
|
||||
fi
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: dotfiles-management
|
||||
description: Guidelines and rules for managing the user's dotfiles repository, config files, and installation scripts.
|
||||
---
|
||||
|
||||
# Dotfiles Management Skill
|
||||
|
||||
Use this skill when modifying system configurations, shell profiles, editor settings, or the installer scripts within the dotfiles repository.
|
||||
|
||||
## Repository Location
|
||||
The dotfiles repository is stored at:
|
||||
`/Users/rodydavis/dev/git/dotfiles`
|
||||
|
||||
## Rules
|
||||
- When updating or modifying configuration files (like `.zshrc`, `.gitconfig`, `.gitignore_global`, or Zed settings), always make changes inside the dotfiles repository rather than directly editing files in the home directory (`~`).
|
||||
- After modifying configurations, run the `./install.sh` script to link/sync them.
|
||||
- Always run the installer in dry-run mode (`./install.sh --dry-run` or `-d`) first to verify changes.
|
||||
|
||||
## Bootstrapping & Installation Guidelines
|
||||
|
||||
### 1. SSH & Git Clones
|
||||
* When cloning private repositories in the background, check if the host is in `~/.ssh/known_hosts` first. Use `ssh-keyscan` to populate it before running `git clone` to prevent the task from being suspended on interactive host key prompts.
|
||||
|
||||
### 2. Homebrew Bundle & Tap Trust
|
||||
* **Tap Trust:** Modern Homebrew requires explicit tap trust approval. When running `brew bundle` in unattended background scripts, prepend `HOMEBREW_NO_REQUIRE_TAP_TRUST=1` to bypass interactive prompts.
|
||||
* **Sudo Casks:** Avoid including casks that require administrative (`sudo`) privileges (e.g. `basictex`) in unattended installations. Comment them out in the `Brewfile` and instruct the user to install them manually in their terminal.
|
||||
* **Deprecated/Private Taps:** Keep the `Brewfile` clean of deprecated taps (like `homebrew/cask-fonts`) or private/unauthorized taps (like `robotsandpencils/made`) to avoid failing the entire bundle installation.
|
||||
|
||||
### 3. Node.js/NPM Runtime
|
||||
* Installing `nvm` via Homebrew does not install a Node runtime. Before executing global NPM package installations, initialize `nvm` and run `nvm install --lts` to verify a node/npm binary is active.
|
||||
|
||||
### 4. Java & Android SDK Configuration
|
||||
* Homebrew's `openjdk` is keg-only. To ensure Java is detected by Android CLI and Flutter tools, configure `.zshrc` to export `/opt/homebrew/opt/openjdk/bin` in the `PATH` and set `JAVA_HOME` to the Homebrew JDK path.
|
||||
Reference in New Issue
Block a user