8081d98c67
Initial public release. Anonymous on-device identity (no phone number, e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double Ratchet, sealed sender) via libsignal, a metadata-minimizing relay, group chats, disappearing messages, encrypted local storage, optional Tor transport, and a UI localized into Czech, English, German, Russian, and Italian. Monorepo: Flutter client (app/), Rust relay + wire types (rust/), protobuf definitions (proto/), self-host deploy (deploy/). Licensed under AGPL-3.0 with an app-store distribution exception (§7).
113 lines
3.2 KiB
Dart
113 lines
3.2 KiB
Dart
/// Numerická klávesnice pro PIN + řádek teček.
|
||
///
|
||
/// Vytaženo z `pin_setup_screen`, aby ji mohla použít i změna PINu.
|
||
/// Systémovou klávesnici záměrně nepoužíváme — nechceme, aby PIN prošel
|
||
/// IME (predikce, schránka, klávesnice třetích stran).
|
||
library;
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:sepot/core/theme/app_theme.dart';
|
||
|
||
class PinDots extends StatelessWidget {
|
||
const PinDots({super.key, required this.filled, this.length = 6});
|
||
|
||
final int filled;
|
||
final int length;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: List.generate(length, (i) {
|
||
final on = i < filled;
|
||
return Container(
|
||
width: 16,
|
||
height: 16,
|
||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: on ? SepotColors.ember : SepotColors.border,
|
||
border: Border.all(
|
||
color: on ? SepotColors.ember : SepotColors.border,
|
||
width: 2,
|
||
),
|
||
),
|
||
);
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
|
||
class PinKeypad extends StatelessWidget {
|
||
const PinKeypad({super.key, required this.onKey, this.enabled = true});
|
||
|
||
/// Dostane číslici '0'–'9' nebo 'DEL'.
|
||
final void Function(String key) onKey;
|
||
final bool enabled;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
for (final row in const [
|
||
['1', '2', '3'],
|
||
['4', '5', '6'],
|
||
['7', '8', '9'],
|
||
['', '0', 'DEL'],
|
||
])
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: row
|
||
.map((k) => k.isEmpty
|
||
? const SizedBox(width: 88, height: 64)
|
||
: _klavesa(k))
|
||
.toList(),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _klavesa(String key) {
|
||
final delete = key == 'DEL';
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
child: InkWell(
|
||
onTap: enabled
|
||
? () {
|
||
HapticFeedback.lightImpact();
|
||
onKey(key);
|
||
}
|
||
: null,
|
||
borderRadius: BorderRadius.circular(32),
|
||
child: Container(
|
||
width: 64,
|
||
height: 64,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: delete ? Colors.transparent : SepotColors.surfaceVariant,
|
||
border: delete ? null : Border.all(color: SepotColors.border),
|
||
),
|
||
child: Center(
|
||
child: delete
|
||
? const Icon(Icons.backspace_outlined,
|
||
color: SepotColors.textSecondary, size: 24)
|
||
: Text(
|
||
key,
|
||
style: const TextStyle(
|
||
fontFamily: kFontSans,
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.w500,
|
||
color: SepotColors.textPrimary,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|