Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc6fb3cce2 |
@@ -12,7 +12,6 @@ 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. |
|
| [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. |
|
| [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. |
|
| [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-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. |
|
| [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`. |
|
| [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`. |
|
||||||
@@ -20,7 +19,6 @@ 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. |
|
| [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. |
|
| [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. |
|
| [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. |
|
| [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. |
|
| [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. |
|
| [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. |
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{{flutter_js}}
|
|
||||||
{{flutter_build_config}}
|
|
||||||
|
|
||||||
_flutter.loader.load({
|
|
||||||
onEntrypointLoaded: function(engineInitializer) {
|
|
||||||
engineInitializer.initializeEngine({
|
|
||||||
hostElement: document.getElementById('flutter-host')
|
|
||||||
}).then(function(appRunner) {
|
|
||||||
appRunner.runApp();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
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