Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.io.File
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit

Expand Down Expand Up @@ -85,6 +86,12 @@ class CodexForegroundService : Service() {
sendOrQueue(payload)
}
}
ACTION_SEND_FILE -> {
val path = intent.getStringExtra(EXTRA_PAYLOAD_FILE)
if (!path.isNullOrBlank()) {
sendPayloadFile(path)
}
}
ACTION_DISCONNECT -> {
disconnect(stopService = true)
}
Expand Down Expand Up @@ -201,6 +208,29 @@ class CodexForegroundService : Service() {
}
}

private fun sendPayloadFile(path: String) {
val file = File(path)
if (!file.exists()) {
CodexServiceBridge.pushEvent(
"""{"method":"android/transportStatus","params":{"status":"error","message":"Payload file missing."}}"""
)
return
}
val payload = try {
file.readText()
} catch (t: Throwable) {
CodexServiceBridge.pushEvent(
"""{"method":"android/transportStatus","params":{"status":"error","message":${(t.message ?: "Failed to read payload file").quoteJson()}}}"""
)
return
} finally {
file.delete()
}
if (payload.isNotBlank()) {
sendOrQueue(payload)
}
}

private fun flushPendingMessages(webSocket: WebSocket) {
while (socket === webSocket && isSocketOpen) {
val payload = pendingMessages.poll() ?: break
Expand Down Expand Up @@ -275,10 +305,12 @@ class CodexForegroundService : Service() {
private const val NOTIFICATION_ID = 31041
const val ACTION_CONNECT = "codex_remote.action.CONNECT"
const val ACTION_SEND = "codex_remote.action.SEND"
const val ACTION_SEND_FILE = "codex_remote.action.SEND_FILE"
const val ACTION_DISCONNECT = "codex_remote.action.DISCONNECT"
const val EXTRA_URL = "url"
const val EXTRA_BEARER_TOKEN = "bearerToken"
const val EXTRA_PAYLOAD = "payload"
const val EXTRA_PAYLOAD_FILE = "payloadFile"

fun startService(context: Context, intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
import java.io.File

class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
Expand Down Expand Up @@ -45,6 +46,25 @@ class MainActivity : FlutterActivity() {
result.success(null)
}

"sendFile" -> {
val path = call.argument<String>("path")
if (path.isNullOrBlank()) {
result.error("invalid_args", "Missing payload file path", null)
return@setMethodCallHandler
}
val file = File(path)
if (!file.exists()) {
result.error("invalid_args", "Payload file does not exist", null)
return@setMethodCallHandler
}
val intent = Intent(this, CodexForegroundService::class.java).apply {
action = CodexForegroundService.ACTION_SEND_FILE
putExtra(CodexForegroundService.EXTRA_PAYLOAD_FILE, path)
}
CodexForegroundService.startService(this, intent)
result.success(null)
}

"disconnect" -> {
val intent = Intent(this, CodexForegroundService::class.java).apply {
action = CodexForegroundService.ACTION_DISCONNECT
Expand Down
15 changes: 12 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'src/app.dart';
import 'src/app_controller.dart';
import 'src/app/bootstrap/app_bootstrap.dart';
import 'src/app/providers.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final controller = await AppController.bootstrap();
runApp(CodexRemoteApp(controller: controller));
final controller = await AppBootstrap.load();
runApp(
ProviderScope(
overrides: <Override>[
appControllerProvider.overrideWith((Ref ref) => controller),
],
child: const CodexRemoteApp(),
),
);
}
200 changes: 1 addition & 199 deletions lib/src/app.dart
Original file line number Diff line number Diff line change
@@ -1,199 +1 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

import 'app_controller.dart';
import 'home_page.dart';

class CodexRemoteApp extends StatelessWidget {
const CodexRemoteApp({super.key, required this.controller});

final AppController controller;

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (BuildContext context, Widget? child) {
return MaterialApp(
title: 'Codex Remote',
debugShowCheckedModeBanner: false,
themeMode: controller.settings.materialThemeMode,
theme: _buildLightTheme(),
darkTheme: _buildDarkTheme(),
home: HomePage(controller: controller),
);
},
);
}
}

ThemeData _buildLightTheme() {
const background = Color(0xFFFAF8F5);
const surface = Color(0xFFFFFFFF);
const primary = Color(0xFFB45309);
const secondary = Color(0xFFD97706);
const accent = Color(0xFF059669);
const text = Color(0xFF451A03);
const muted = Color(0xFF9A7B63);
const border = Color(0xFFE7DED5);

final scheme = const ColorScheme(
brightness: Brightness.light,
primary: primary,
onPrimary: Colors.white,
secondary: secondary,
onSecondary: Colors.white,
error: Color(0xFFB42318),
onError: Colors.white,
surface: surface,
onSurface: text,
);

return _buildTheme(
scheme: scheme,
background: background,
muted: muted,
border: border,
accent: accent,
);
}

ThemeData _buildDarkTheme() {
const background = Color(0xFF0F0F0F);
const surface = Color(0xFF1A1A1A);
const primary = Color(0xFF00D4AA);
const secondary = Color(0xFF00A3CC);
const accent = Color(0xFFFF6B9D);
const text = Color(0xFFF5F5F5);
const muted = Color(0xFF9D9D9D);
const border = Color(0xFF2A2A2A);

final scheme = const ColorScheme(
brightness: Brightness.dark,
primary: primary,
onPrimary: Color(0xFF07110F),
secondary: secondary,
onSecondary: Color(0xFF071217),
error: Color(0xFFFF8A8A),
onError: Color(0xFF2A0608),
surface: surface,
onSurface: text,
);

return _buildTheme(
scheme: scheme,
background: background,
muted: muted,
border: border,
accent: accent,
);
}

ThemeData _buildTheme({
required ColorScheme scheme,
required Color background,
required Color muted,
required Color border,
required Color accent,
}) {
final base = ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: background,
);
final textTheme = GoogleFonts.ibmPlexSansTextTheme(base.textTheme).copyWith(
titleLarge: GoogleFonts.ibmPlexSans(
fontSize: 18,
fontWeight: FontWeight.w600,
color: scheme.onSurface,
),
titleMedium: GoogleFonts.ibmPlexSans(
fontSize: 15,
fontWeight: FontWeight.w600,
color: scheme.onSurface,
),
bodyLarge: GoogleFonts.ibmPlexSans(
fontSize: 15,
height: 1.45,
color: scheme.onSurface,
),
bodyMedium: GoogleFonts.ibmPlexSans(
fontSize: 14,
height: 1.45,
color: scheme.onSurface,
),
bodySmall: GoogleFonts.ibmPlexSans(
fontSize: 13,
height: 1.35,
color: muted,
),
labelLarge: GoogleFonts.ibmPlexSans(
fontSize: 14,
fontWeight: FontWeight.w600,
color: scheme.onSurface,
),
);

return base.copyWith(
textTheme: textTheme,
appBarTheme: AppBarTheme(
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: background,
foregroundColor: scheme.onSurface,
surfaceTintColor: Colors.transparent,
titleTextStyle: textTheme.titleLarge,
),
dividerColor: border,
cardTheme: CardThemeData(
elevation: 0,
color: scheme.surface,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: border),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: scheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: scheme.primary),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
side: BorderSide(color: border),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
foregroundColor: scheme.onSurface,
),
),
chipTheme: base.chipTheme.copyWith(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
side: BorderSide(color: border),
backgroundColor: scheme.surface,
selectedColor: accent.withValues(alpha: 0.16),
labelStyle: textTheme.bodySmall?.copyWith(color: scheme.onSurface),
),
);
}
export 'app/app.dart';
43 changes: 43 additions & 0 deletions lib/src/app/app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../app_controller.dart';
import '../home_page.dart';
import 'providers.dart';
import 'theme/app_theme.dart';

class CodexRemoteApp extends StatelessWidget {
const CodexRemoteApp({super.key, this.controller});

final AppController? controller;

@override
Widget build(BuildContext context) {
if (controller == null) {
return const _CodexRemoteAppView();
}
return ProviderScope(
overrides: <Override>[
appControllerProvider.overrideWith((Ref ref) => controller!),
],
child: const _CodexRemoteAppView(),
);
}
}

class _CodexRemoteAppView extends ConsumerWidget {
const _CodexRemoteAppView();

@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = ref.watch(appControllerProvider);
return MaterialApp(
title: 'Codex Remote',
debugShowCheckedModeBanner: false,
themeMode: controller.settings.materialThemeMode,
theme: buildLightTheme(),
darkTheme: buildDarkTheme(),
home: HomePage(controller: controller),
);
}
}
9 changes: 9 additions & 0 deletions lib/src/app/bootstrap/app_bootstrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import '../../app_controller.dart';

class AppBootstrap {
const AppBootstrap._();

static Future<AppController> load() {
return AppController.bootstrap();
}
}
9 changes: 9 additions & 0 deletions lib/src/app/providers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../app_controller.dart';

final appControllerProvider = ChangeNotifierProvider<AppController>((ref) {
throw UnimplementedError(
'appControllerProvider must be overridden during bootstrap.',
);
});
Loading
Loading