apps/mobile is a Matrix client for Android on the famedly Matrix Dart SDK, with vodozemac (compiled from Rust during the build) for end-to-end encryption. It speaks the plain Client-Server API and uses the web client's design tokens, fonts and symbol. - Sign in by server name (.well-known), homeserver address or full user ID; sign up with the dummy and registration token stages. Sessions survive restarts: SQLite in private storage, credentials also in the Keystore, both excluded from backups and device transfers. - Room list with unread and mention counts, invites, search and filters. Timeline with grouping, day separators, notices, history on scroll, links, images and files (authenticated media). Sending text, photos, camera shots and files, with Sending/Sent/Read/Not sent and Retry. Typing, receipts, deleting. New rooms and DMs, encrypted by default; joining by address; room info with members, invite, rename, topic, notification level, leave. - Encryption: one-step setup behind a recovery key, verifying a new session with the recovery key (restoring the backup) or another session, emoji verification of people and sessions, reset. Undecryptable messages say why. Session list with verify and sign out. - Notifications while running, and push through UnifiedPush (no Firebase): the distributor's endpoint becomes an event_id_only pusher, the gateway is discovered, and a push starts the app headless to fetch, decrypt and show the event. Make targets: mobile (release APKs per ABI into bin/), mobile-run, mobile-test. Kotlin incremental compilation is off (Windows, project and pub cache on different drives), and Tink's JVM artifact is excluded in favour of tink-android. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
285 lines
9.0 KiB
Dart
285 lines
9.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Warp's design tokens (apps/ui/src/styles/tokens.css) as a Flutter theme.
|
|
///
|
|
/// The token names are kept so a colour can be traced back to the brand
|
|
/// package: [WarpColors.light] and [WarpColors.dark] are `:root` and
|
|
/// `[data-theme="dark"]`.
|
|
@immutable
|
|
class WarpColors extends ThemeExtension<WarpColors> {
|
|
const WarpColors({
|
|
required this.surface,
|
|
required this.surfaceSunken,
|
|
required this.surfaceRaised,
|
|
required this.line,
|
|
required this.lineStrong,
|
|
required this.ink,
|
|
required this.inkMuted,
|
|
required this.teal,
|
|
required this.tealStrong,
|
|
required this.onTeal,
|
|
required this.tealText,
|
|
required this.tealSoft,
|
|
required this.bubbleOther,
|
|
required this.danger,
|
|
required this.dangerSoft,
|
|
required this.warning,
|
|
required this.warningSoft,
|
|
required this.info,
|
|
required this.infoSoft,
|
|
required this.presenceOnline,
|
|
required this.presenceAway,
|
|
required this.presenceOffline,
|
|
});
|
|
|
|
final Color surface;
|
|
final Color surfaceSunken;
|
|
final Color surfaceRaised;
|
|
final Color line;
|
|
final Color lineStrong;
|
|
final Color ink;
|
|
final Color inkMuted;
|
|
final Color teal;
|
|
final Color tealStrong;
|
|
final Color onTeal;
|
|
final Color tealText;
|
|
final Color tealSoft;
|
|
final Color bubbleOther;
|
|
final Color danger;
|
|
final Color dangerSoft;
|
|
final Color warning;
|
|
final Color warningSoft;
|
|
final Color info;
|
|
final Color infoSoft;
|
|
final Color presenceOnline;
|
|
final Color presenceAway;
|
|
final Color presenceOffline;
|
|
|
|
static const light = WarpColors(
|
|
surface: Color(0xFFFFFFFF),
|
|
surfaceSunken: Color(0xFFF3F5F4),
|
|
surfaceRaised: Color(0xFFFFFFFF),
|
|
line: Color(0xFFE1E6E4),
|
|
lineStrong: Color(0xFF7B8682),
|
|
ink: Color(0xFF111114),
|
|
inkMuted: Color(0xFF5B6461),
|
|
teal: Color(0xFF00A389),
|
|
tealStrong: Color(0xFF00806A),
|
|
onTeal: Color(0xFFFFFFFF),
|
|
tealText: Color(0xFF00735F),
|
|
tealSoft: Color(0xFFDDF4EE),
|
|
bubbleOther: Color(0xFFEFF2F1),
|
|
danger: Color(0xFFB42318),
|
|
dangerSoft: Color(0xFFFDE8E6),
|
|
warning: Color(0xFF8A5300),
|
|
warningSoft: Color(0xFFFFF2DB),
|
|
info: Color(0xFF1D5FD1),
|
|
infoSoft: Color(0xFFE3EDFF),
|
|
presenceOnline: Color(0xFF00A389),
|
|
presenceAway: Color(0xFFD99A00),
|
|
presenceOffline: Color(0xFF8A9490),
|
|
);
|
|
|
|
static const dark = WarpColors(
|
|
surface: Color(0xFF0F1213),
|
|
surfaceSunken: Color(0xFF161A1C),
|
|
surfaceRaised: Color(0xFF1B2022),
|
|
line: Color(0xFF2A3134),
|
|
lineStrong: Color(0xFF6E7976),
|
|
ink: Color(0xFFECEEEE),
|
|
inkMuted: Color(0xFF9AA5A1),
|
|
teal: Color(0xFF22C3A6),
|
|
tealStrong: Color(0xFF22C3A6),
|
|
onTeal: Color(0xFF062A23),
|
|
tealText: Color(0xFF3ED6B8),
|
|
tealSoft: Color(0xFF123A33),
|
|
bubbleOther: Color(0xFF1E2427),
|
|
danger: Color(0xFFF97066),
|
|
dangerSoft: Color(0xFF3A1512),
|
|
warning: Color(0xFFF5B547),
|
|
warningSoft: Color(0xFF33260B),
|
|
info: Color(0xFF7EB1FF),
|
|
infoSoft: Color(0xFF14233F),
|
|
presenceOnline: Color(0xFF22C3A6),
|
|
presenceAway: Color(0xFFF5B547),
|
|
presenceOffline: Color(0xFF6E7976),
|
|
);
|
|
|
|
@override
|
|
WarpColors copyWith() => this;
|
|
|
|
@override
|
|
WarpColors lerp(WarpColors? other, double t) =>
|
|
other == null || t < 0.5 ? this : other;
|
|
}
|
|
|
|
extension WarpTheme on BuildContext {
|
|
WarpColors get warp => Theme.of(this).extension<WarpColors>()!;
|
|
}
|
|
|
|
const fontDisplay = 'Outfit';
|
|
const fontSans = 'Figtree';
|
|
const fontMono = 'JetBrains Mono';
|
|
|
|
ThemeData warpTheme(Brightness brightness) {
|
|
final c = brightness == Brightness.dark ? WarpColors.dark : WarpColors.light;
|
|
final scheme = ColorScheme(
|
|
brightness: brightness,
|
|
primary: c.tealStrong,
|
|
onPrimary: c.onTeal,
|
|
primaryContainer: c.tealSoft,
|
|
onPrimaryContainer: c.tealText,
|
|
secondary: c.teal,
|
|
onSecondary: c.onTeal,
|
|
error: c.danger,
|
|
onError: brightness == Brightness.dark ? c.surface : Colors.white,
|
|
errorContainer: c.dangerSoft,
|
|
onErrorContainer: c.danger,
|
|
surface: c.surface,
|
|
onSurface: c.ink,
|
|
onSurfaceVariant: c.inkMuted,
|
|
surfaceContainerLowest: c.surface,
|
|
surfaceContainerLow: c.surfaceSunken,
|
|
surfaceContainer: c.surfaceSunken,
|
|
surfaceContainerHigh: c.surfaceRaised,
|
|
surfaceContainerHighest: c.bubbleOther,
|
|
outline: c.lineStrong,
|
|
outlineVariant: c.line,
|
|
);
|
|
|
|
TextStyle display(double size, double height, FontWeight w) => TextStyle(
|
|
fontFamily: fontDisplay,
|
|
fontSize: size,
|
|
height: height / size,
|
|
fontWeight: w,
|
|
color: c.ink,
|
|
);
|
|
TextStyle sans(double size, double height, FontWeight w, [Color? color]) =>
|
|
TextStyle(
|
|
fontFamily: fontSans,
|
|
fontSize: size,
|
|
height: height / size,
|
|
fontWeight: w,
|
|
color: color ?? c.ink,
|
|
);
|
|
|
|
final text = TextTheme(
|
|
displaySmall: display(40, 44, FontWeight.w700),
|
|
headlineMedium: display(28, 34, FontWeight.w600),
|
|
headlineSmall: display(24, 30, FontWeight.w600),
|
|
titleLarge: display(20, 26, FontWeight.w600),
|
|
titleMedium: display(16, 22, FontWeight.w600),
|
|
titleSmall: sans(14, 20, FontWeight.w600),
|
|
bodyLarge: sans(15, 22, FontWeight.w400),
|
|
bodyMedium: sans(14, 20, FontWeight.w400),
|
|
bodySmall: sans(13, 18, FontWeight.w400, c.inkMuted),
|
|
labelLarge: sans(14, 20, FontWeight.w600),
|
|
labelMedium: sans(12, 16, FontWeight.w500),
|
|
labelSmall: sans(11, 14, FontWeight.w500, c.inkMuted),
|
|
);
|
|
|
|
final radius = BorderRadius.circular(12);
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
brightness: brightness,
|
|
colorScheme: scheme,
|
|
fontFamily: fontSans,
|
|
textTheme: text,
|
|
scaffoldBackgroundColor: c.surface,
|
|
extensions: [c],
|
|
dividerTheme: DividerThemeData(color: c.line, space: 1, thickness: 1),
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: c.surface,
|
|
foregroundColor: c.ink,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
titleTextStyle: display(18, 24, FontWeight.w600),
|
|
shape: Border(bottom: BorderSide(color: c.line)),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: c.surfaceSunken,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
border: OutlineInputBorder(
|
|
borderRadius: radius,
|
|
borderSide: BorderSide(color: c.line),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: radius,
|
|
borderSide: BorderSide(color: c.line),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: radius,
|
|
borderSide: BorderSide(color: c.tealStrong, width: 2),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: radius,
|
|
borderSide: BorderSide(color: c.danger),
|
|
),
|
|
labelStyle: sans(14, 20, FontWeight.w500, c.inkMuted),
|
|
hintStyle: sans(14, 20, FontWeight.w400, c.inkMuted),
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: c.tealStrong,
|
|
foregroundColor: c.onTeal,
|
|
minimumSize: const Size(64, 44),
|
|
textStyle: sans(14, 20, FontWeight.w600),
|
|
shape: RoundedRectangleBorder(borderRadius: radius),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: c.ink,
|
|
minimumSize: const Size(64, 44),
|
|
side: BorderSide(color: c.line),
|
|
textStyle: sans(14, 20, FontWeight.w600),
|
|
shape: RoundedRectangleBorder(borderRadius: radius),
|
|
),
|
|
),
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: c.tealText,
|
|
textStyle: sans(14, 20, FontWeight.w600),
|
|
),
|
|
),
|
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
|
backgroundColor: c.tealStrong,
|
|
foregroundColor: c.onTeal,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
|
),
|
|
dialogTheme: DialogThemeData(
|
|
backgroundColor: c.surfaceRaised,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
|
titleTextStyle: display(20, 26, FontWeight.w600),
|
|
),
|
|
bottomSheetTheme: BottomSheetThemeData(
|
|
backgroundColor: c.surfaceRaised,
|
|
surfaceTintColor: Colors.transparent,
|
|
showDragHandle: true,
|
|
),
|
|
snackBarTheme: SnackBarThemeData(
|
|
behavior: SnackBarBehavior.floating,
|
|
backgroundColor: c.ink,
|
|
contentTextStyle: sans(14, 20, FontWeight.w500, c.surface),
|
|
shape: RoundedRectangleBorder(borderRadius: radius),
|
|
),
|
|
listTileTheme: ListTileThemeData(
|
|
iconColor: c.inkMuted,
|
|
titleTextStyle: sans(15, 22, FontWeight.w500),
|
|
subtitleTextStyle: sans(13, 18, FontWeight.w400, c.inkMuted),
|
|
),
|
|
switchTheme: SwitchThemeData(
|
|
thumbColor: WidgetStateProperty.resolveWith(
|
|
(s) => s.contains(WidgetState.selected) ? c.onTeal : c.lineStrong,
|
|
),
|
|
trackColor: WidgetStateProperty.resolveWith(
|
|
(s) => s.contains(WidgetState.selected) ? c.tealStrong : c.line,
|
|
),
|
|
),
|
|
progressIndicatorTheme: ProgressIndicatorThemeData(color: c.teal),
|
|
);
|
|
}
|