This commit is contained in:
2026-06-04 10:50:24 +08:00
parent 8f9f3a9779
commit 250f21a2b8
67 changed files with 1192 additions and 159 deletions

View File

@@ -0,0 +1,44 @@
/// 剪切板内容数据模型
class ClipboardRecordingModel {
final String title;
final int startTimestamp;
final int endTimestamp;
final String address;
ClipboardRecordingModel({
required this.title,
required this.startTimestamp,
required this.endTimestamp,
required this.address,
});
factory ClipboardRecordingModel.fromJson(Map<String, dynamic> json) {
return ClipboardRecordingModel(
title: _readString(json, 'title'),
startTimestamp: _readInt(json, 'startTimestamp'),
endTimestamp: _readInt(json, 'endTimestamp'),
address: _readString(json, 'address'),
);
}
Map<String, dynamic> toJson() {
return {
'title': title,
'startTimestamp': startTimestamp,
'endTimestamp': endTimestamp,
'address': address,
};
}
static String _readString(Map<String, dynamic> json, String key) {
final value = json[key];
if (value is String) return value;
throw FormatException('Clipboard field "$key" must be a String.');
}
static int _readInt(Map<String, dynamic> json, String key) {
final value = json[key];
if (value is int) return value;
throw FormatException('Clipboard field "$key" must be an int.');
}
}

View File

@@ -0,0 +1,26 @@
import 'package:recording_tool/features/recording/model/model_clipboard.dart';
class RecordingModel {
/// 剪切板内容
final ClipboardRecordingModel clipboardRecordingModel;
RecordingModel({required this.clipboardRecordingModel});
factory RecordingModel.fromJson(Map<String, dynamic> json) {
return RecordingModel(
clipboardRecordingModel: ClipboardRecordingModel.fromJson(
json['clipboardRecordingModel'],
),
);
}
Map<String, dynamic> toJson() {
return {'clipboardRecordingModel': clipboardRecordingModel.toJson()};
}
RecordingModel copyWith({ClipboardRecordingModel? clipboardRecordingModel}) {
return RecordingModel(
clipboardRecordingModel:
clipboardRecordingModel ?? this.clipboardRecordingModel,
);
}
}

View File

@@ -0,0 +1,5 @@
abstract final class RecordingChannelNames {
static const packageName = 'com.gdfw.fxjk';
static const method = '$packageName/recording';
static const events = '$packageName/recording_events';
}

View File

@@ -3,11 +3,12 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/features/recording/recording_platform.dart';
import 'package:flutter_template/features/recording/recording_session_controller.dart';
import 'package:flutter_template/features/recording/widgets/camera_preview_widget.dart';
import 'package:flutter_template/features/recording/widgets/recording_touch_lock_overlay.dart';
import 'package:flutter_template/shared/widgets/widgets.dart';
import 'package:recording_tool/features/recording/recording_platform.dart';
import 'package:recording_tool/features/recording/recording_session_controller.dart';
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
import 'package:recording_tool/features/recording/widgets/camera_preview_widget.dart';
import 'package:recording_tool/features/recording/widgets/recording_touch_lock_overlay.dart';
import 'package:recording_tool/shared/widgets/widgets.dart';
import 'package:permission_handler/permission_handler.dart';
class RecordingPage extends ConsumerStatefulWidget {
@@ -27,6 +28,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
}
Future<void> _bootstrap() async {
await ref.read(recordingViewModelProvider.notifier).getClipboardContent();
await _enterRecordingMode();
// Allow PlatformView to attach before binding CameraX preview.
await Future<void>.delayed(const Duration(milliseconds: 400));

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:recording_tool/features/recording/recording_channel_names.dart';
enum RecordingState {
idle,
@@ -47,10 +48,10 @@ class RecordingPlatform {
RecordingPlatform._();
static const MethodChannel _channel = MethodChannel(
'com.example.flutter_template/recording',
RecordingChannelNames.method,
);
static const EventChannel _events = EventChannel(
'com.example.flutter_template/recording_events',
RecordingChannelNames.events,
);
static bool get isSupported =>

View File

@@ -3,8 +3,8 @@ import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/permission/permission_service.dart';
import 'package:flutter_template/features/recording/recording_platform.dart';
import 'package:recording_tool/core/permission/permission_service.dart';
import 'package:recording_tool/features/recording/recording_platform.dart';
import 'package:permission_handler/permission_handler.dart';
class RecordingSessionState {
@@ -74,8 +74,8 @@ class RecordingSessionState {
final recordingSessionControllerProvider =
NotifierProvider<RecordingSessionController, RecordingSessionState>(
RecordingSessionController.new,
);
RecordingSessionController.new,
);
class RecordingSessionController extends Notifier<RecordingSessionState> {
StreamSubscription<RecordingStatus>? _statusSubscription;
@@ -161,9 +161,7 @@ class RecordingSessionController extends Notifier<RecordingSessionState> {
if (!shouldRetry) {
rethrow;
}
await Future<void>.delayed(
Duration(milliseconds: 150 * (attempt + 1)),
);
await Future<void>.delayed(Duration(milliseconds: 150 * (attempt + 1)));
}
}
throw StateError('initializePreview retry exhausted');

View File

@@ -0,0 +1,56 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:recording_tool/core/logging/app_logger.dart';
import 'package:recording_tool/features/recording/model/model_clipboard.dart';
import 'package:recording_tool/features/recording/model/model_recording.dart';
final recordingViewModelProvider =
StateNotifierProvider<RecordingViewModel, RecordingModel>((ref) {
return RecordingViewModel(ref);
});
class RecordingViewModel extends StateNotifier<RecordingModel> {
RecordingViewModel(this.ref)
: super(
RecordingModel(
clipboardRecordingModel: ClipboardRecordingModel(
title: '',
startTimestamp: 0,
endTimestamp: 0,
address: '',
),
),
);
final Ref ref;
/// 从剪切板获取内容
Future<void> getClipboardContent() async {
try {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
final text = clipboardData?.text;
AppLogger.debug('读取剪切板内容:$text');
if (text == null || text.trim().isEmpty) {
AppLogger.info('剪切板内容为空,跳过录制信息解析');
return;
}
final decoded = jsonDecode(text);
if (decoded is! Map<String, dynamic>) {
AppLogger.warning('剪切板内容不是 JSON 对象,跳过录制信息解析');
return;
}
final clipboardRecordingModel = ClipboardRecordingModel.fromJson(decoded);
state = state.copyWith(clipboardRecordingModel: clipboardRecordingModel);
AppLogger.info('剪切板录制信息解析成功:${clipboardRecordingModel.toJson()}');
} on FormatException catch (error) {
AppLogger.warning('剪切板录制信息格式错误:$error');
} catch (error, stackTrace) {
AppLogger.debug('读取剪切板录制信息失败', error: error, stackTrace: stackTrace);
}
}
}

View File

@@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>复制赛事录制码</title>
</head>
<body>
<button onclick="copyEventInfo()">复制赛事录制码</button>
<textarea id="copyText" style="position: fixed; left: -9999px;"></textarea>
<script>
function copyEventInfo() {
const data = {
title: '王东方 丨李想 空中格斗赛',
startTimestamp: 1717334400,
endTimestamp: 1717334400,
address: '广州市番禺区·粤港澳大湾区青年人才双创小镇',
}
const jsonStr = JSON.stringify(data)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(jsonStr)
.then(() => {
alert('赛事录制码已复制')
})
.catch(() => {
fallbackCopy(jsonStr)
})
} else {
fallbackCopy(jsonStr)
}
}
function fallbackCopy(text) {
const textarea = document.getElementById('copyText')
textarea.value = text
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
try {
document.execCommand('copy')
alert('赛事录制码已复制')
} catch (err) {
console.error('复制失败:', err)
alert('复制失败,请手动复制')
}
}
</script>
</body>
</html>