feat(calls): Я ЕБЛАН ХАХАХХА Я ШАШКИ ЧЕРЕЗ ЗВОНКИ СДЕЛАЛ
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart'
|
||||
show TargetPlatform, defaultTargetPlatform;
|
||||
@@ -35,6 +36,14 @@ class CallParticipant {
|
||||
});
|
||||
}
|
||||
|
||||
class CallChatMessage {
|
||||
final String text;
|
||||
final bool mine;
|
||||
final DateTime time;
|
||||
|
||||
CallChatMessage({required this.text, required this.mine, required this.time});
|
||||
}
|
||||
|
||||
class CallSession {
|
||||
final Ws2Config ws2Config;
|
||||
|
||||
@@ -90,6 +99,17 @@ class CallSession {
|
||||
static const String _probeQuestion = 'AreYouKomet?';
|
||||
static const String _probeAnswer = 'YesImKomet😎';
|
||||
|
||||
final List<CallChatMessage> _chat = [];
|
||||
final _chatController = StreamController<CallChatMessage>.broadcast();
|
||||
final _gameController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
|
||||
List<CallChatMessage> get chatLog => List.unmodifiable(_chat);
|
||||
Stream<CallChatMessage> get chatMessages => _chatController.stream;
|
||||
Stream<Map<String, dynamic>> get gameMessages => _gameController.stream;
|
||||
|
||||
int get selfUserId => ws2Config.userId;
|
||||
int? get peerUserId => _peerId;
|
||||
|
||||
bool get localVideo => _localVideo;
|
||||
bool get localScreen => _localScreen;
|
||||
MediaStream? get localVideoStream => _localVideoStream;
|
||||
@@ -160,6 +180,7 @@ class CallSession {
|
||||
Future<void> _sampleLevels() async {
|
||||
final pc = _pc;
|
||||
if (pc == null || _ended) return;
|
||||
if (!_mediaConnected || _current != CallSessionState.active) return;
|
||||
|
||||
var local = 0.0;
|
||||
var remote = 0.0;
|
||||
@@ -571,6 +592,21 @@ class CallSession {
|
||||
void _onProbeMessage(RTCDataChannel channel, RTCDataChannelMessage message) {
|
||||
if (message.isBinary) return;
|
||||
final text = message.text;
|
||||
|
||||
final frame = _decodeFrame(text);
|
||||
if (frame != null && frame['t'] == 'chat') {
|
||||
final body = frame['text'];
|
||||
if (body is String && body.isNotEmpty) {
|
||||
_addChat(CallChatMessage(text: body, mine: false, time: DateTime.now()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (frame != null && frame['t'] == 'game') {
|
||||
final data = Map<String, dynamic>.of(frame)..remove('t');
|
||||
if (!_gameController.isClosed) _gameController.add(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text == _probeQuestion) {
|
||||
_sendProbe(channel, _probeAnswer);
|
||||
} else if (text == _probeAnswer) {
|
||||
@@ -578,12 +614,46 @@ class CallSession {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _decodeFrame(String text) {
|
||||
try {
|
||||
final v = jsonDecode(text);
|
||||
return v is Map<String, dynamic> ? v : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _sendProbe(RTCDataChannel channel, String text) {
|
||||
try {
|
||||
channel.send(RTCDataChannelMessage(text));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void sendChatMessage(String text) {
|
||||
final body = text.trim();
|
||||
final channel = _probeChannel;
|
||||
if (body.isEmpty || channel == null) return;
|
||||
try {
|
||||
channel.send(RTCDataChannelMessage(jsonEncode({'t': 'chat', 'text': body})));
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
_addChat(CallChatMessage(text: body, mine: true, time: DateTime.now()));
|
||||
}
|
||||
|
||||
void sendGame(Map<String, dynamic> data) {
|
||||
final channel = _probeChannel;
|
||||
if (channel == null) return;
|
||||
try {
|
||||
channel.send(RTCDataChannelMessage(jsonEncode({'t': 'game', ...data})));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _addChat(CallChatMessage message) {
|
||||
_chat.add(message);
|
||||
if (!_chatController.isClosed) _chatController.add(message);
|
||||
}
|
||||
|
||||
void _markPeerKomet() {
|
||||
if (_peerIsKomet) return;
|
||||
_peerIsKomet = true;
|
||||
@@ -1097,6 +1167,8 @@ class CallSession {
|
||||
if (!_remoteStream.isClosed) await _remoteStream.close();
|
||||
if (!_info.isClosed) await _info.close();
|
||||
if (!_kometDetected.isClosed) await _kometDetected.close();
|
||||
if (!_chatController.isClosed) await _chatController.close();
|
||||
if (!_gameController.isClosed) await _gameController.close();
|
||||
}
|
||||
|
||||
void _applyConnectionInfo(Map<String, dynamic> msg, List iceServers) {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
enum CheckersSide { white, black }
|
||||
|
||||
class Checkers {
|
||||
static const int size = 8;
|
||||
static const int empty = 0;
|
||||
static const int whiteMan = 1;
|
||||
static const int whiteKing = 2;
|
||||
static const int blackMan = 3;
|
||||
static const int blackKing = 4;
|
||||
|
||||
static const List<List<int>> _dirs = [
|
||||
[-1, -1],
|
||||
[-1, 1],
|
||||
[1, -1],
|
||||
[1, 1],
|
||||
];
|
||||
|
||||
static List<int> initial() {
|
||||
final board = List<int>.filled(size * size, empty);
|
||||
for (var r = 0; r < size; r++) {
|
||||
for (var c = 0; c < size; c++) {
|
||||
if ((r + c) % 2 == 0) continue;
|
||||
final i = r * size + c;
|
||||
if (r <= 2) board[i] = blackMan;
|
||||
if (r >= 5) board[i] = whiteMan;
|
||||
}
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
static CheckersSide? sideOf(int piece) {
|
||||
if (piece == whiteMan || piece == whiteKing) return CheckersSide.white;
|
||||
if (piece == blackMan || piece == blackKing) return CheckersSide.black;
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool isKing(int piece) => piece == whiteKing || piece == blackKing;
|
||||
|
||||
static CheckersSide opponent(CheckersSide side) =>
|
||||
side == CheckersSide.white ? CheckersSide.black : CheckersSide.white;
|
||||
|
||||
static int _row(int i) => i ~/ size;
|
||||
static int _col(int i) => i % size;
|
||||
static bool _inB(int r, int c) => r >= 0 && r < size && c >= 0 && c < size;
|
||||
static int _idx(int r, int c) => r * size + c;
|
||||
static int _lastRow(CheckersSide side) =>
|
||||
side == CheckersSide.white ? 0 : size - 1;
|
||||
static int _kingOf(CheckersSide side) =>
|
||||
side == CheckersSide.white ? whiteKing : blackKing;
|
||||
|
||||
static List<List<int>> legalMoves(List<int> board, CheckersSide side) {
|
||||
final captures = <List<int>>[];
|
||||
for (var i = 0; i < board.length; i++) {
|
||||
if (sideOf(board[i]) != side) continue;
|
||||
_collectCaptures(List<int>.of(board), i, side, [i], <int>{}, captures);
|
||||
}
|
||||
if (captures.isNotEmpty) return captures;
|
||||
|
||||
final quiet = <List<int>>[];
|
||||
for (var i = 0; i < board.length; i++) {
|
||||
if (sideOf(board[i]) != side) continue;
|
||||
_collectQuiet(board, i, side, quiet);
|
||||
}
|
||||
return quiet;
|
||||
}
|
||||
|
||||
static void _collectCaptures(List<int> work, int at, CheckersSide side,
|
||||
List<int> path, Set<int> captured, List<List<int>> out) {
|
||||
final steps = _captureSteps(work, at, captured);
|
||||
if (steps.isEmpty) {
|
||||
if (path.length > 1) out.add(List<int>.of(path));
|
||||
return;
|
||||
}
|
||||
final piece = work[at];
|
||||
for (final step in steps) {
|
||||
final landing = step[0];
|
||||
final victim = step[1];
|
||||
final promote = !isKing(piece) && _row(landing) == _lastRow(side);
|
||||
final moved = promote ? _kingOf(side) : piece;
|
||||
|
||||
work[at] = empty;
|
||||
work[landing] = moved;
|
||||
captured.add(victim);
|
||||
path.add(landing);
|
||||
|
||||
_collectCaptures(work, landing, side, path, captured, out);
|
||||
|
||||
path.removeLast();
|
||||
captured.remove(victim);
|
||||
work[landing] = empty;
|
||||
work[at] = piece;
|
||||
}
|
||||
}
|
||||
|
||||
static List<List<int>> _captureSteps(
|
||||
List<int> work, int at, Set<int> captured) {
|
||||
final piece = work[at];
|
||||
final side = sideOf(piece);
|
||||
if (side == null) return const [];
|
||||
final king = isKing(piece);
|
||||
final r0 = _row(at);
|
||||
final c0 = _col(at);
|
||||
final result = <List<int>>[];
|
||||
|
||||
for (final d in _dirs) {
|
||||
var r = r0 + d[0];
|
||||
var c = c0 + d[1];
|
||||
if (king) {
|
||||
while (_inB(r, c) && work[_idx(r, c)] == empty) {
|
||||
r += d[0];
|
||||
c += d[1];
|
||||
}
|
||||
if (!_inB(r, c)) continue;
|
||||
final vi = _idx(r, c);
|
||||
if (sideOf(work[vi]) == side || captured.contains(vi)) continue;
|
||||
var lr = r + d[0];
|
||||
var lc = c + d[1];
|
||||
while (_inB(lr, lc) && work[_idx(lr, lc)] == empty) {
|
||||
result.add([_idx(lr, lc), vi]);
|
||||
lr += d[0];
|
||||
lc += d[1];
|
||||
}
|
||||
} else {
|
||||
if (!_inB(r, c)) continue;
|
||||
final vi = _idx(r, c);
|
||||
if (work[vi] == empty ||
|
||||
sideOf(work[vi]) == side ||
|
||||
captured.contains(vi)) {
|
||||
continue;
|
||||
}
|
||||
final lr = r + d[0];
|
||||
final lc = c + d[1];
|
||||
if (_inB(lr, lc) && work[_idx(lr, lc)] == empty) {
|
||||
result.add([_idx(lr, lc), vi]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void _collectQuiet(
|
||||
List<int> board, int at, CheckersSide side, List<List<int>> out) {
|
||||
final piece = board[at];
|
||||
final r0 = _row(at);
|
||||
final c0 = _col(at);
|
||||
if (isKing(piece)) {
|
||||
for (final d in _dirs) {
|
||||
var r = r0 + d[0];
|
||||
var c = c0 + d[1];
|
||||
while (_inB(r, c) && board[_idx(r, c)] == empty) {
|
||||
out.add([at, _idx(r, c)]);
|
||||
r += d[0];
|
||||
c += d[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final forward = side == CheckersSide.white ? -1 : 1;
|
||||
for (final dc in const [-1, 1]) {
|
||||
final r = r0 + forward;
|
||||
final c = c0 + dc;
|
||||
if (_inB(r, c) && board[_idx(r, c)] == empty) {
|
||||
out.add([at, _idx(r, c)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<int> applyMove(List<int> board, List<int> path) {
|
||||
final next = List<int>.of(board);
|
||||
if (path.length < 2) return next;
|
||||
final from = path.first;
|
||||
final side = sideOf(board[from]);
|
||||
if (side == null) return next;
|
||||
final piece = board[from];
|
||||
next[from] = empty;
|
||||
var promoted = isKing(piece);
|
||||
|
||||
for (var k = 0; k < path.length - 1; k++) {
|
||||
final a = path[k];
|
||||
final b = path[k + 1];
|
||||
final dr = (_row(b) - _row(a)).sign;
|
||||
final dc = (_col(b) - _col(a)).sign;
|
||||
var r = _row(a) + dr;
|
||||
var c = _col(a) + dc;
|
||||
while (r != _row(b) || c != _col(b)) {
|
||||
final vi = _idx(r, c);
|
||||
if (next[vi] != empty && sideOf(next[vi]) != side) {
|
||||
next[vi] = empty;
|
||||
}
|
||||
r += dr;
|
||||
c += dc;
|
||||
}
|
||||
if (_row(b) == _lastRow(side)) promoted = true;
|
||||
}
|
||||
|
||||
next[path.last] = promoted ? _kingOf(side) : piece;
|
||||
return next;
|
||||
}
|
||||
|
||||
static bool _hasPieces(List<int> board, CheckersSide side) =>
|
||||
board.any((p) => sideOf(p) == side);
|
||||
|
||||
static CheckersSide? winner(List<int> board, CheckersSide toMove) {
|
||||
if (!_hasPieces(board, toMove) || legalMoves(board, toMove).isEmpty) {
|
||||
return opponent(toMove);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:komet/core/storage/app_instance.dart';
|
||||
import 'package:komet/core/utils/logger.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
class ProfileData {
|
||||
@@ -155,10 +157,34 @@ class AppDatabase {
|
||||
return _db!;
|
||||
}
|
||||
|
||||
static Future<String> _databasesDir() async {
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
return dir.path;
|
||||
}
|
||||
return getDatabasesPath();
|
||||
}
|
||||
|
||||
static Future<void> _migrateLegacyDb(String target) async {
|
||||
if (AppInstance.isNamed) return;
|
||||
if (!(Platform.isLinux || Platform.isWindows || Platform.isMacOS)) return;
|
||||
try {
|
||||
if (await File(target).exists()) return;
|
||||
final legacy = File(join(await getDatabasesPath(), 'komet.db'));
|
||||
if (legacy.path == target) return;
|
||||
if (await legacy.exists()) {
|
||||
await legacy.copy(target);
|
||||
logger.i('[db] перенёс komet.db -> $target');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<Database> _open() async {
|
||||
final dbPath = await getDatabasesPath();
|
||||
final dbPath = await _databasesDir();
|
||||
final target = join(dbPath, 'komet${AppInstance.suffix}.db');
|
||||
await _migrateLegacyDb(target);
|
||||
return openDatabase(
|
||||
join(dbPath, 'komet.db'),
|
||||
target,
|
||||
version: 11,
|
||||
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
||||
onCreate: (db, _) => _createTables(db),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class AppInstance {
|
||||
AppInstance._();
|
||||
|
||||
static const String id = String.fromEnvironment('KOMET_INSTANCE');
|
||||
|
||||
static bool get isNamed => id.isNotEmpty;
|
||||
|
||||
static String get suffix => isNamed ? '_$id' : '';
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../config/app_media_cache.dart';
|
||||
import '../storage/app_instance.dart';
|
||||
|
||||
/// Постоянный дисковый кэш скачанных медиа (файлы, видео).
|
||||
///
|
||||
@@ -21,7 +22,7 @@ class MediaCache {
|
||||
final cached = _dir;
|
||||
if (cached != null) return cached;
|
||||
final base = await getApplicationSupportDirectory();
|
||||
final dir = Directory(p.join(base.path, 'media_cache'));
|
||||
final dir = Directory(p.join(base.path, 'media_cache${AppInstance.suffix}'));
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user