This commit is contained in:
otsmr 2025-01-21 00:30:57 +01:00
parent 809f677626
commit 3b3c5e61b9
11 changed files with 378 additions and 418 deletions

View file

@ -6,6 +6,12 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure
This app was started because of the three main features I missed out by popular alternatives. This app was started because of the three main features I missed out by popular alternatives.
## Background Notification
The server will first try to send via an open websocket connection.
If not available then it sends a wakeup via FCM. This will trigger the app to reopen the websocket.
### Three to rule them all. ### Three to rule them all.
1. Security by design: No one except your device can access your data. 1. Security by design: No one except your device can access your data.

View file

@ -1,4 +1,5 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart'; import 'package:twonly/src/providers/db_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,7 +10,6 @@ import 'src/settings/settings_controller.dart';
import 'src/settings/settings_service.dart'; import 'src/settings/settings_service.dart';
late DbProvider dbProvider; late DbProvider dbProvider;
late ApiProvider apiProvider;
void main() async { void main() async {
// Set up the SettingsController, which will glue user settings to multiple // Set up the SettingsController, which will glue user settings to multiple
@ -27,40 +27,34 @@ void main() async {
// check if release build or debug build // check if release build or debug build
final kDebugMode = true; final kDebugMode = true;
Logger.root.level = Level.ALL; // defaults to Level.INFO Logger.root.level = Level.FINEST; // defaults to Level.INFO
Logger.root.onRecord.listen((record) { Logger.root.onRecord.listen((record) {
if (kDebugMode) { // if (kDebugMode) {
// ignore: avoid_print // ignore: avoid_print
print('${record.level.name}: ${record.time}: ${record.message}'); print('${record.level.name}: ${record.time}: ${record.message}');
} // }
}); });
var cameras = await availableCameras(); var cameras = await availableCameras();
t
// Create or open the database // Create or open the database
dbProvider = DbProvider(); dbProvider = DbProvider();
await dbProvider.ready; await dbProvider.ready;
// Create an option to select different servers. // Create an option to select different servers.
var apiUrl = "ws://api.theconnectapp.de/v0/"; var apiUrl = "ws://api.theconnectapp.de/v-1/";
if (kDebugMode) { if (true) {
// Overwrite the domain in your local network so you can test the app locally // Overwrite the domain in your local network so you can test the app locally
apiUrl = "ws://10.99.0.6:3030/api/client"; apiUrl = "ws://9.99.0.6:3030/api/client";
} }
apiProvider = ApiProvider(apiUrl: apiUrl);
// TODO: Open the connection in the background so the app launch is not delayed.
//await apiProvider.connect();
// Workmanager.executeTask((task, inputData) async { // Workmanager.executeTask((task, inputData) async {
// await _HomeState().manager(); // await _HomeState().manager();
// print('Background Services are Working!');//This is Working // print('Background Services are Working!');//This is Working
// return true; // return true;
// }); // });
// Run the app and pass in the SettingsController. The app listens to the runApp(ChangeNotifierProvider<ApiProvider>(
// SettingsController for changes, then passes it further down to the child: MyApp(settingsController: settingsController, cameras: cameras),
// SettingsView. create: (_) => ApiProvider(apiUrl: apiUrl, backupApiUrl: apiUrl)));
runApp(MyApp(settingsController: settingsController, cameras: cameras));
} }

View file

@ -1,11 +1,15 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:path/path.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'views/home_view.dart'; import 'views/home_view.dart';
import 'views/register_view.dart'; import 'views/register_view.dart';
import 'utils.dart'; import 'utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'dart:isolate';
import 'dart:async';
import 'settings/settings_controller.dart'; import 'settings/settings_controller.dart';
/// The Widget that configures your application. /// The Widget that configures your application.
@ -22,9 +26,41 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
Future<bool> _isUserCreated = isUserCreated(); Future<bool> _isUserCreated = isUserCreated();
int redColorOpacity = 0; // Start with dark red
bool redColorGoUp = true;
bool isConnected = false;
@override
void initState() {
super.initState();
// Start the color animation
_startColorAnimation();
}
void _startColorAnimation() {
// Change the color every second
Future.delayed(Duration(milliseconds: 200), () {
setState(() {
if (redColorOpacity <= 100) {
redColorGoUp = true;
}
if (redColorOpacity >= 150) {
redColorGoUp = false;
}
if (redColorGoUp) {
redColorOpacity += 10;
} else {
redColorOpacity -= 10;
}
});
_startColorAnimation(); // Repeat the animation
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
var isConnected = context.watch<ApiProvider>().isConnected;
// Glue the SettingsController to the MaterialApp. // Glue the SettingsController to the MaterialApp.
// //
// The ListenableBuilder Widget listens to the SettingsController for changes. // The ListenableBuilder Widget listens to the SettingsController for changes.
@ -58,27 +94,59 @@ class _MyAppState extends State<MyApp> {
const InputDecorationTheme(border: OutlineInputBorder()), const InputDecorationTheme(border: OutlineInputBorder()),
), ),
themeMode: widget.settingsController.themeMode, themeMode: widget.settingsController.themeMode,
home: FutureBuilder<bool>( home: Stack(
future: _isUserCreated, children: [
builder: (context, snapshot) { FutureBuilder<bool>(
if (snapshot.hasData) { future: context.watch<ApiProvider>().startBackend(),
return snapshot.data! builder: (context, snapshot) {
? HomeView( return FutureBuilder<bool>(
settingsController: widget.settingsController, future: _isUserCreated,
cameras: widget.cameras) builder: (context, snapshot) {
: RegisterView(callbackOnSuccess: () { if (snapshot.hasData) {
_isUserCreated = isUserCreated(); return snapshot.data!
setState(() {}); ? HomeView(
settingsController:
widget.settingsController,
cameras: widget.cameras)
: RegisterView(
callbackOnSuccess: () {
_isUserCreated = isUserCreated();
setState(() {});
},
); // Show the red line if not connected
} else {
return Container();
}
}); });
} else { }),
return Center( if (!isConnected)
child: SizedBox( Positioned(
width: 60, top: 3, // Position it at the top
height: 60, left: (screenWidth * 0.5) / 2, // Center it horizontally
child: CircularProgressIndicator(), child: AnimatedContainer(
)); duration: Duration(milliseconds: 100),
} width: screenWidth * 0.5, // 50% of the screen width
}), decoration: BoxDecoration(
border: Border.all(
color: Colors.red[600]!.withAlpha(redColorOpacity),
width: 2.0), // Red border
borderRadius: BorderRadius.all(
Radius.circular(10.0)), // Rounded top corners
),
// child: Padding(
// padding: const EdgeInsets.all(
// 8.0), // Padding around the child
// child: Center(
// child: Text(
// 'Not Connected',
// style: TextStyle(fontSize: 24),
// ),
// ),
// ),
),
),
],
),
); );
}, },
); );

View file

@ -0,0 +1,26 @@
import 'dart:typed_data';
import 'package:cv/cv.dart';
class DbMessages extends CvModelBase {
static const tableName = "messages";
static const columnMessageId = "messageId";
final messageId = CvField<int>(columnMessageId);
static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(columnCreatedAt);
static String getCreateTableString() {
return """
CREATE TABLE $tableName (
$columnMessageId INTEGER NOT NULL,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ($columnMessageId)
)
""";
}
@override
List<CvField> get fields => [messageId, createdAt];
}

View file

@ -1,15 +1,17 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/src/proto/api/client_to_server.pb.dart'; import 'package:twonly/src/proto/api/client_to_server.pb.dart' as c;
import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server;
import 'package:twonly/src/proto/api/server_to_client.pbserver.dart';
import 'package:twonly/src/signal/signal_helper.dart'; import 'package:twonly/src/signal/signal_helper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
@ -25,22 +27,176 @@ class Result<T, E> {
Result.error(this.error) : value = null; Result.error(this.error) : value = null;
} }
class ApiProvider { enum ExchangeKind { connectionStateChange, sendRequestV0 }
ApiProvider({required this.apiUrl});
class Exchange {
Exchange({required this.kind, required this.body, required this.seq});
final int seq;
final ExchangeKind kind;
final dynamic body;
}
c.ClientToServer createClientToServerFromHandshake(c.Handshake handshake) {
// Create the V0 message
var v0 = c.V0()
..seq = Int64(0) // You can set this to the appropriate sequence number
..handshake = handshake;
// Create the ClientToServer message
var clientToServer = c.ClientToServer()..v0 = v0;
return clientToServer;
}
Result asResult(server.ServerToClient msg) {
if (msg.v0.response.hasOk()) {
return Result.success(msg.v0.response.ok);
} else {
return Result.error(msg.v0.response.error);
}
}
class BackendIsolatedArgs {
BackendIsolatedArgs(
{required this.apiUrl,
required this.backupApiUrl,
required this.receivePort,
required this.sendPort});
final String apiUrl;
final String backupApiUrl;
final ReceivePort receivePort;
final SendPort sendPort;
}
class ApiProvider with ChangeNotifier {
ApiProvider({required this.apiUrl, required this.backupApiUrl});
final String apiUrl; final String apiUrl;
final log = Logger("connect::ApiProvider"); final String backupApiUrl;
final HashMap<String, List<Function>> _callbacks = HashMap(); bool _isConnected = false;
final Map<int, ServerToClient> _sendRequestV0Resp = HashMap();
final log = Logger("twonly::ApiProvider");
final ReceivePort _receivePort = ReceivePort();
SendPort? _sendPort;
Future<ServerToClient?> _waitForResponse(int seq) async {
final startTime = DateTime.now();
final timeout = Duration(seconds: 5);
while (true) {
if (_sendRequestV0Resp[seq] != null) {
final tmp = _sendRequestV0Resp[seq];
_sendRequestV0Resp.remove(seq);
return tmp;
}
if (DateTime.now().difference(startTime) > timeout) {
log.shout("Timeout for message $seq");
return null;
}
await Future.delayed(Duration(milliseconds: 10));
}
}
static void _startBackendIsolated(BackendIsolatedArgs args) async {
final backend =
Backend(apiUrl: args.apiUrl, backupApiUrl: args.backupApiUrl);
await backend.connect();
args.receivePort.listen((msg) async {
switch (msg.kind) {
case ExchangeKind.sendRequestV0:
final resp = await backend._sendRequestV0(msg.body);
args.sendPort.send(Exchange(
kind: ExchangeKind.sendRequestV0, body: resp, seq: msg.seq));
break;
default:
}
});
args.sendPort.send(
Exchange(kind: ExchangeKind.connectionStateChange, body: true, seq: 0));
}
Future<bool> startBackend() async {
ReceivePort port = ReceivePort();
_sendPort = port.sendPort;
await Isolate.spawn(
_startBackendIsolated,
BackendIsolatedArgs(
sendPort: _receivePort.sendPort,
receivePort: port,
apiUrl: apiUrl,
backupApiUrl: backupApiUrl));
_receivePort.listen((msg) {
switch (msg.kind) {
case ExchangeKind.sendRequestV0:
_sendRequestV0Resp[msg.seq] = msg.body;
// final resp = await backend._sendRequestV0(msg.body);
// args.sendPort.send(Exchange(
// kind: ExchangeKind.sendRequestV0, body: resp, seq: msg.seq));
break;
case ExchangeKind.connectionStateChange:
_isConnected = msg.body;
default:
}
});
return true;
}
bool get isConnected => _isConnected;
Future<Result> register(String username, String? inviteCode) async {
if (_sendPort == null) return Result.error("Unknown error");
final reqSignal = await SignalHelper.getRegisterData();
if (reqSignal == null) {
return Result.error(
"There was an fatal error. Try reinstalling the app.");
}
var register = c.Handshake_Register()
..username = username
..publicIdentityKey = reqSignal["identityKey"]
..signedPrekey = reqSignal["signedPreKey"]?["key"]
..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"]
..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]);
if (inviteCode != null && inviteCode != "") {
register.inviteCode = inviteCode;
}
// Create the Handshake message
var handshake = c.Handshake()..register = register;
var req = createClientToServerFromHandshake(handshake);
var seq = Random().nextInt(4294967296);
final tmp = Exchange(seq: seq, kind: ExchangeKind.sendRequestV0, body: req);
_sendPort!.send(tmp);
final resp = await _waitForResponse(seq);
if (resp == null) {
return Result.error("Server is not reachable!");
}
return asResult(resp);
}
}
class Backend {
Backend({required this.apiUrl, required this.backupApiUrl});
final String apiUrl;
final String? backupApiUrl;
final log = Logger("twonly::backend");
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap(); final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
WebSocketChannel? _channel; WebSocketChannel? _channel;
Future<bool> connect() async { Future<bool> _connectTo(String apiUrl) async {
if (_channel != null && _channel!.closeCode != null) {
return true;
}
log.info("Trying to connect to the backend $apiUrl!");
try { try {
var channel = WebSocketChannel.connect( var channel = WebSocketChannel.connect(
Uri.parse(apiUrl), Uri.parse(apiUrl),
@ -49,6 +205,7 @@ class ApiProvider {
_channel!.stream.listen(_onData); _channel!.stream.listen(_onData);
await _channel!.ready; await _channel!.ready;
log.info("Websocket is connected!"); log.info("Websocket is connected!");
print("Websocket is connected!");
return true; return true;
} on WebSocketChannelException catch (e) { } on WebSocketChannelException catch (e) {
log.shout("Error: $e"); log.shout("Error: $e");
@ -56,25 +213,39 @@ class ApiProvider {
} }
} }
Future<bool> connect() async {
print("Trying to connect to the backend $apiUrl!");
if (_channel != null && _channel!.closeCode != null) {
return true;
}
log.info("Trying to connect to the backend $apiUrl!");
if (await _connectTo(apiUrl)) {
return true;
}
if (backupApiUrl != null) {
log.info("Trying to connect to the backup backend $backupApiUrl!");
if (await _connectTo(backupApiUrl!)) {
return true;
}
}
return false;
}
bool get isConnected => _channel != null && _channel!.closeCode != null;
void _onData(dynamic msgBuffer) { void _onData(dynamic msgBuffer) {
try { try {
final msg = server.ServerToClient.fromBuffer(msgBuffer); final msg = server.ServerToClient.fromBuffer(msgBuffer);
print("New message: $msg"); if (msg.v0.hasResponse()) {
messagesV0[msg.v0.seq] = msg; messagesV0[msg.v0.seq] = msg;
} else {
print("Got a new message from the server: $msg");
}
} catch (e) { } catch (e) {
log.shout("Error parsing the servers message: $e"); log.shout("Error parsing the servers message: $e");
} }
} }
// void _reconnect() {}
void addNotifier(String messageType, Function callBackFunction) {
if (!_callbacks.containsKey(messageType)) {
_callbacks[messageType] = [];
}
_callbacks[messageType]!.add(callBackFunction);
}
// TODO: There must be a smarter move to do that :/ // TODO: There must be a smarter move to do that :/
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async { Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
final startTime = DateTime.now(); final startTime = DateTime.now();
@ -95,7 +266,8 @@ class ApiProvider {
} }
} }
Future<server.ServerToClient?> _sendRequestV0(ClientToServer request) async { Future<server.ServerToClient?> _sendRequestV0(
c.ClientToServer request) async {
var seq = Int64(Random().nextInt(4294967296)); var seq = Int64(Random().nextInt(4294967296));
while (messagesV0.containsKey(seq)) { while (messagesV0.containsKey(seq)) {
seq = Int64(Random().nextInt(4294967296)); seq = Int64(Random().nextInt(4294967296));
@ -115,18 +287,6 @@ class ApiProvider {
return await _waitForResponse(seq); return await _waitForResponse(seq);
} }
ClientToServer createClientToServerFromHandshake(Handshake handshake) {
// Create the V0 message
var v0 = V0()
..seq = Int64(0) // You can set this to the appropriate sequence number
..handshake = handshake;
// Create the ClientToServer message
var clientToServer = ClientToServer()..v0 = v0;
return clientToServer;
}
static String getLocalizedString(BuildContext context, ErrorCode code) { static String getLocalizedString(BuildContext context, ErrorCode code) {
switch (code.toString()) { switch (code.toString()) {
case "Unknown": case "Unknown":
@ -159,41 +319,4 @@ class ApiProvider {
return code.toString(); // Fallback for unrecognized keys return code.toString(); // Fallback for unrecognized keys
} }
} }
Result _asResult(server.ServerToClient msg) {
if (msg.v0.response.hasOk()) {
return Result.success(msg.v0.response.ok);
} else {
return Result.error(msg.v0.response.error);
}
}
Future<Result> register(String username, String? inviteCode) async {
final reqSignal = await SignalHelper.getRegisterData();
if (reqSignal == null) {
return Result.error(
"There was an fatal error. Try reinstalling the app.");
}
var register = Handshake_Register()
..username = username
..publicIdentityKey = reqSignal["identityKey"]
..signedPrekey = reqSignal["signedPreKey"]?["key"]
..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"]
..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]);
if (inviteCode != null && inviteCode != "") {
register.inviteCode = inviteCode;
}
// Create the Handshake message
var handshake = Handshake()..register = register;
var req = createClientToServerFromHandshake(handshake);
final resp = await _sendRequestV0(req);
if (resp == null) {
return Result.error("Server is not reachable!");
}
return _asResult(resp);
}
} }

View file

@ -1,9 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/signal/signal_helper.dart'; import 'package:twonly/src/signal/signal_helper.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'model/user_data_json.dart'; import 'model/user_data_json.dart';
import 'package:provider/provider.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
// Just a helper function to get the secure storage // Just a helper function to get the secure storage
@ -44,13 +46,18 @@ Future<bool> deleteLocalUserData() async {
return true; return true;
} }
Future<Result> createNewUser(String username, String inviteCode) async { Future<Result> createNewUser(
BuildContext context, String username, String inviteCode) async {
final storage = getSecureStorage(); final storage = getSecureStorage();
if (!context.mounted) {
return Result.error("not mounted");
}
await SignalHelper.createIfNotExistsSignalIdentity(); await SignalHelper.createIfNotExistsSignalIdentity();
// TODO: API call to server to check username and inviteCode // TODO: API call to server to check username and inviteCode
final res = await apiProvider.register(username, inviteCode); // final res = await apiProvider.register(username, inviteCode);
final res = await context.watch<ApiProvider>().register(username, inviteCode);
if (res.isSuccess) { if (res.isSuccess) {
print("Got user_id ${res.value}"); print("Got user_id ${res.value}");

View file

@ -1,288 +0,0 @@
// Dart imports:
import 'dart:io';
import 'dart:math';
// Flutter imports:
// import 'package:example/widgets/demo_build_stickers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// Package imports:
import 'package:google_fonts/google_fonts.dart';
import 'package:pro_image_editor/designs/frosted_glass/frosted_glass.dart';
import 'package:pro_image_editor/pro_image_editor.dart';
// Project imports:
// import '../../utils/example_helper.dart';
/// The frosted glass design example
class CameraEditorView extends StatefulWidget {
/// Creates a new [CameraEditorView] widget.
const CameraEditorView({
super.key,
required this.imagePath,
});
/// The URL of the image to display.
final String imagePath;
@override
State<CameraEditorView> createState() => _CameraEditorViewState();
}
class _CameraEditorViewState extends State<CameraEditorView> {
final bool _useMaterialDesign =
platformDesignMode == ImageEditorDesignModeE.material;
/// Opens the sticker/emoji editor.
void _openStickerEditor(ProImageEditorState editor) async {
Layer? layer = await editor.openPage(FrostedGlassStickerPage(
configs: editor.configs,
callbacks: editor.callbacks,
));
if (layer == null || !mounted) return;
if (layer.runtimeType != StickerLayerData) {
layer.scale = editor.configs.emojiEditorConfigs.initScale;
}
editor.addLayer(layer);
}
/// Calculates the number of columns for the EmojiPicker.
int _calculateEmojiColumns(BoxConstraints constraints) =>
max(1, (_useMaterialDesign ? 6 : 10) / 400 * constraints.maxWidth - 1)
.floor();
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return ProImageEditor.file(
File(widget.imagePath),
// key: editorKey,
callbacks: ProImageEditorCallbacks(
onImageEditingStarted: () {},
onImageEditingComplete: (image) async {
Navigator.pop(context);
},
onCloseEditor: () {},
stickerEditorCallbacks: StickerEditorCallbacks(
onSearchChanged: (value) {
/// Filter your stickers
debugPrint(value);
},
)),
configs: ProImageEditorConfigs(
designMode: platformDesignMode,
theme: Theme.of(context).copyWith(
iconTheme:
Theme.of(context).iconTheme.copyWith(color: Colors.white)),
icons: const ImageEditorIcons(
paintingEditor: IconsPaintingEditor(
bottomNavBar: Icons.edit,
),
),
imageEditorTheme: ImageEditorTheme(
textEditor: TextEditorTheme(
textFieldMargin: const EdgeInsets.only(top: kToolbarHeight),
bottomBarBackgroundColor: Colors.transparent,
bottomBarMainAxisAlignment: !_useMaterialDesign
? MainAxisAlignment.spaceEvenly
: MainAxisAlignment.start),
paintingEditor: const PaintingEditorTheme(
initialStrokeWidth: 5,
),
// filterEditor: const FilterEditorTheme(
// filterListSpacing: 7,
// filterListMargin: EdgeInsets.fromLTRB(8, 15, 8, 10),
// ),
emojiEditor: EmojiEditorTheme(
backgroundColor: Colors.transparent,
textStyle: DefaultEmojiTextStyle.copyWith(
fontFamily:
!kIsWeb ? null : GoogleFonts.notoColorEmoji().fontFamily,
fontSize: _useMaterialDesign ? 48 : 30,
),
emojiViewConfig: EmojiViewConfig(
gridPadding: EdgeInsets.zero,
horizontalSpacing: 0,
verticalSpacing: 0,
recentsLimit: 40,
backgroundColor: Colors.transparent,
buttonMode: !_useMaterialDesign
? ButtonMode.CUPERTINO
: ButtonMode.MATERIAL,
loadingIndicator:
const Center(child: CircularProgressIndicator()),
columns: _calculateEmojiColumns(constraints),
emojiSizeMax: !_useMaterialDesign ? 32 : 64,
replaceEmojiOnLimitExceed: false,
),
bottomActionBarConfig:
const BottomActionBarConfig(enabled: false),
),
layerInteraction: const ThemeLayerInteraction(
removeAreaBackgroundInactive: Colors.black12,
),
),
textEditorConfigs: TextEditorConfigs(
customTextStyles: [
GoogleFonts.roboto(),
GoogleFonts.averiaLibre(),
GoogleFonts.lato(),
GoogleFonts.comicNeue(),
GoogleFonts.actor(),
GoogleFonts.odorMeanChey(),
GoogleFonts.nabla(),
],
),
emojiEditorConfigs: const EmojiEditorConfigs(
checkPlatformCompatibility: !kIsWeb,
),
customWidgets: ImageEditorCustomWidgets(
loadingDialog: (message, configs) => FrostedGlassLoadingDialog(
message: message,
configs: configs,
),
mainEditor: CustomWidgetsMainEditor(
closeWarningDialog: (editor) async {
if (!context.mounted) return false;
return await showDialog<bool>(
context: context,
builder: (BuildContext context) =>
FrostedGlassCloseDialog(editor: editor),
) ??
false;
},
appBar: (editor, rebuildStream) => null,
bottomBar: (editor, rebuildStream, key) => null,
bodyItems: _buildMainBodyWidgets,
),
paintEditor: CustomWidgetsPaintEditor(
appBar: (paintEditor, rebuildStream) => null,
bottomBar: (paintEditor, rebuildStream) => null,
colorPicker:
(paintEditor, rebuildStream, currentColor, setColor) => null,
bodyItems: _buildPaintEditorBody,
),
textEditor: CustomWidgetsTextEditor(
appBar: (textEditor, rebuildStream) => null,
colorPicker:
(textEditor, rebuildStream, currentColor, setColor) => null,
bottomBar: (textEditor, rebuildStream) => null,
bodyItems: _buildTextEditorBody,
),
cropRotateEditor: CustomWidgetsCropRotateEditor(),
),
),
);
});
}
List<ReactiveCustomWidget> _buildMainBodyWidgets(
ProImageEditorState editor,
Stream<dynamic> rebuildStream,
) {
return [
if (editor.selectedLayerIndex < 0)
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => FrostedGlassActionBar(
editor: editor,
openStickerEditor: () => _openStickerEditor(editor),
),
),
];
}
List<ReactiveCustomWidget> _buildPaintEditorBody(
PaintingEditorState paintEditor,
Stream<dynamic> rebuildStream,
) {
return [
/// Appbar
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) {
return paintEditor.activePainting
? const SizedBox.shrink()
: FrostedGlassPaintingAppbar(paintEditor: paintEditor);
},
),
/// Bottombar
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => FrostedGlassPaintBottomBar(paintEditor: paintEditor),
),
];
}
List<ReactiveCustomWidget> _buildTuneEditorBody(
TuneEditorState tuneEditor,
Stream<dynamic> rebuildStream,
) {
return [
/// Appbar
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) {
return FrostedGlassTuneAppbar(tuneEditor: tuneEditor);
},
),
/// Bottombar
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => FrostedGlassTuneBottombar(tuneEditor: tuneEditor),
),
];
}
List<ReactiveCustomWidget> _buildTextEditorBody(
TextEditorState textEditor,
Stream<dynamic> rebuildStream,
) {
return [
/// Background
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => const FrostedGlassEffect(
radius: BorderRadius.zero,
child: SizedBox.expand(),
),
),
/// Slider Text size
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => Padding(
padding: const EdgeInsets.only(top: kToolbarHeight),
child: FrostedGlassTextSizeSlider(textEditor: textEditor),
),
),
/// Appbar
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) {
return FrostedGlassTextAppbar(textEditor: textEditor);
},
),
/// Bottombar
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => FrostedGlassTextBottomBar(
configs: textEditor.configs,
initColor: textEditor.primaryColor,
onColorChanged: (color) {
textEditor.primaryColor = color;
},
selectedStyle: textEditor.selectedTextStyle,
onFontChange: textEditor.setTextStyle,
),
),
];
}
}

View file

@ -1,5 +1,4 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'camera_editor_view.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// import 'package:camerawesome/camerawesome_plugin.dart'; // import 'package:camerawesome/camerawesome_plugin.dart';
@ -71,13 +70,13 @@ class CameraPreviewViewState extends State<CameraPreviewView> {
final image = await _controller.takePicture(); final image = await _controller.takePicture();
if (!context.mounted) return; if (!context.mounted) return;
await Navigator.of(context).push( // await Navigator.of(context).push(
MaterialPageRoute( // MaterialPageRoute(
builder: (context) => CameraEditorView( // builder: (context) => CameraEditorView(
imagePath: image.path, // imagePath: image.path,
), // ),
), // ),
); // );
} catch (e) { } catch (e) {
// If an error occurs, log the error to the console. // If an error occurs, log the error to the console.
print(e); print(e);

View file

@ -120,7 +120,7 @@ class _RegisterViewState extends State<RegisterView> {
setState(() { setState(() {
_isTryingToRegister = true; _isTryingToRegister = true;
}); });
final res = await createNewUser( final res = await createNewUser(context,
usernameController.text, inviteCodeController.text); usernameController.text, inviteCodeController.text);
setState(() { setState(() {
_isTryingToRegister = false; _isTryingToRegister = false;
@ -130,7 +130,7 @@ class _RegisterViewState extends State<RegisterView> {
return; return;
} }
final errMsg = final errMsg =
ApiProvider.getLocalizedString(context, res.error); Backend.getLocalizedString(context, res.error);
showAlertDialog(context, "Oh no!", errMsg); showAlertDialog(context, "Oh no!", errMsg);
}, },
style: ButtonStyle( style: ButtonStyle(

View file

@ -306,10 +306,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: emoji_picker_flutter name: emoji_picker_flutter
sha256: "08567e6f914d36c32091a96cf2f51d2558c47aa2bd47a590dc4f50e42e0965f6" sha256: "63dee6be976c51c8b971eccbc73fc637f021b6b679eed1b2ec3b503947304734"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "4.2.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -642,6 +642,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "6.0.3"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
optional: optional:
dependency: transitive dependency: transitive
description: description:
@ -814,10 +822,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: pro_image_editor name: pro_image_editor
sha256: "27190b0333af71e9949f366ac303496511ef6d67607f6f9797c9f136371a321f" sha256: "918f156f28a72b9185d950f865032f6aa83da485d6d45b22fb63d78b63bf7e21"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.3" version: "7.6.4"
protobuf: protobuf:
dependency: transitive dependency: transitive
description: description:
@ -826,6 +834,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
provider:
dependency: "direct main"
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -1047,6 +1063,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -30,7 +30,8 @@ dependencies:
path: ^1.9.0 path: ^1.9.0
path_provider: ^2.1.5 path_provider: ^2.1.5
permission_handler: ^11.3.1 permission_handler: ^11.3.1
pro_image_editor: ^6.1.3 pro_image_editor: ^7.6.4
provider: ^6.1.2
restart_app: ^1.3.2 restart_app: ^1.3.2
sqflite_sqlcipher: ^3.1.0+1 sqflite_sqlcipher: ^3.1.0+1
web_socket_channel: ^3.0.1 web_socket_channel: ^3.0.1