main
parent
41e7c49467
commit
bfae90faca
29
README.md
29
README.md
|
@ -1,16 +1,23 @@
|
|||
# optictext
|
||||
|
||||
A new Flutter project.
|
||||
Ein Tool, das dem Benutzer die Option gibt, OCR auf Bildern anzuwenden oder Text in Bildern zu übersetzen und den übersetzten Text wieder auf das Bild zu setzen.
|
||||
|
||||
Screen1: Wahl zwischen Image tools und Audio Tools(falls ich in der Zukunft weiter daran Arbeiten werde).
|
||||
|
||||
Image tools screen: DoOCR und ImageTranslate.
|
||||
DoOCR: User kann ein Bild hochladen und OCR darauf anwenden.
|
||||
ImageTranslate: User kann ein Bild hochladen welches dann übersetzt wird.
|
||||
|
||||
|
||||
Den user die Sprache wählen zu lassen und dann in eine gewählte sprache zu übersetzen ist leicht gemacht,
|
||||
allerding ist es mir wichtig, das die spracherkennung auch automatisch funktioniert.
|
||||
Im ersten schritt war es wichtig diese funktionalität zu haben.
|
||||
Aktuell läuft das auf einem vps, dafür ist der http request da.
|
||||
Wenn möglich implementiere ich die langauge classfication in dart.
|
||||
|
||||
aktuell teste ich auf einem android phone, es soll aber am ende auch auf IOS laufen.
|
||||
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.8.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:io';
|
||||
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class OCRPage extends StatefulWidget {
|
||||
const OCRPage({super.key});
|
||||
|
||||
@override
|
||||
State<OCRPage> createState() => _OCRPageState();
|
||||
}
|
||||
|
||||
class _OCRPageState extends State<OCRPage> {
|
||||
String _extractedText = '';
|
||||
|
||||
Future<String> uploadImage() async {
|
||||
var postUri = Uri.parse("http://130.61.88.150/upload");
|
||||
http.MultipartRequest request = http.MultipartRequest("POST", postUri);
|
||||
String imageName = "lorem.png";
|
||||
ByteData bytes = await rootBundle.load('assets/$imageName');
|
||||
Uint8List imageBytes = bytes.buffer.asUint8List();
|
||||
|
||||
http.MultipartFile multipartFile = http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
imageBytes,
|
||||
filename: 'lorem.png',
|
||||
contentType: MediaType('image', 'png'),
|
||||
);
|
||||
|
||||
request.files.add(multipartFile);
|
||||
http.StreamedResponse response = await request.send();
|
||||
http.Response finalResponse = await http.Response.fromStream(response);
|
||||
var lang = "";
|
||||
if (finalResponse.statusCode == 200) {
|
||||
Map<String, dynamic> jsonData = jsonDecode(finalResponse.body);
|
||||
lang = jsonData['language'];
|
||||
} else {
|
||||
throw ('Error: ${finalResponse.statusCode}');
|
||||
}
|
||||
|
||||
Directory tempDir = await getTemporaryDirectory();
|
||||
String tempPath = tempDir.path;
|
||||
File tempImageFile = File('$tempPath/$imageName');
|
||||
await tempImageFile.writeAsBytes(imageBytes);
|
||||
|
||||
String text = await FlutterTesseractOcr.extractText(tempImageFile.path,
|
||||
language: lang,
|
||||
args: {
|
||||
"psm": "4",
|
||||
"preserve_interword_spaces": "1",
|
||||
});
|
||||
setState(() {
|
||||
_extractedText = text;
|
||||
});
|
||||
print(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('OCR'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: uploadImage,
|
||||
child: const Text('Test Upload Image'),
|
||||
),
|
||||
Text(_extractedText),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: _extractedText));
|
||||
},
|
||||
child: const Text('Copy to Clipboard'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: library_private_types_in_public_api
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
//might be useless. keep for now
|
||||
|
@ -10,6 +12,8 @@ class BottomBar extends StatefulWidget {
|
|||
|
||||
class _BottomBarState extends State<BottomBar> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
// ignore: unused_field
|
||||
static const List<Widget> _widgetOptions = <Widget>[
|
||||
Text('Home Page'),
|
||||
Text('todo'),
|
|
@ -0,0 +1,32 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
class HttpUtils {
|
||||
var client = http.Client();
|
||||
Future<String> performHttpRequest(
|
||||
Uint8List imageBytes, String imageName) async {
|
||||
var postUri = Uri.parse("http://130.61.88.150/upload");
|
||||
http.MultipartRequest request = http.MultipartRequest("POST", postUri);
|
||||
|
||||
http.MultipartFile multipartFile = http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
imageBytes,
|
||||
filename: 'lorem.png',
|
||||
contentType: MediaType('image', 'png'),
|
||||
);
|
||||
|
||||
request.files.add(multipartFile);
|
||||
http.StreamedResponse response = await client.send(request);
|
||||
http.Response finalResponse = await http.Response.fromStream(response);
|
||||
var lang = "";
|
||||
if (finalResponse.statusCode == 200) {
|
||||
Map<String, dynamic> jsonData = jsonDecode(finalResponse.body);
|
||||
lang = jsonData['language'];
|
||||
} else {
|
||||
throw ('Error: ${finalResponse.statusCode}');
|
||||
}
|
||||
return lang;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class ImageUploader {
|
||||
//final http.Client client;
|
||||
|
||||
ImageUploader();
|
||||
|
||||
Future<Uint8List> buildImageFile(String img) async {
|
||||
String imageName = img;
|
||||
ByteData bytes = await rootBundle.load('assets/$imageName');
|
||||
return bytes.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Future<String> performOcr(
|
||||
Uint8List imageBytes, String imageName, String lang) async {
|
||||
Directory tempDir = await getTemporaryDirectory();
|
||||
String tempPath = tempDir.path;
|
||||
File tempImageFile = File('$tempPath/$imageName');
|
||||
await tempImageFile.writeAsBytes(imageBytes);
|
||||
|
||||
String text = await FlutterTesseractOcr.extractText(tempImageFile.path,
|
||||
language: lang,
|
||||
args: {
|
||||
"psm": "4",
|
||||
"preserve_interword_spaces": "1",
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:cpd_app/OCRPage.dart';
|
||||
import 'package:cpd_app/bottomBar.dart';
|
||||
import 'package:cpd_app/ocr_page.dart';
|
||||
import 'package:cpd_app/bottom_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:cpd_app/http_utils.dart';
|
||||
import 'package:cpd_app/image_uploader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class OCRPage extends StatefulWidget {
|
||||
const OCRPage({super.key});
|
||||
|
||||
@override
|
||||
State<OCRPage> createState() => _OCRPageState();
|
||||
}
|
||||
|
||||
class _OCRPageState extends State<OCRPage> {
|
||||
String _extractedText = '';
|
||||
final ImageUploader _imageUploader = ImageUploader();
|
||||
final HttpUtils _httpUtils = HttpUtils();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
uploadImage("lorem.png");
|
||||
}
|
||||
|
||||
Future<void> uploadImage(String img) async {
|
||||
//hier wird der user sein bild auswählen können
|
||||
//aktuell hardcoded bild zum testen
|
||||
String imageName = img;
|
||||
Uint8List imageBytes = await _imageUploader.buildImageFile(imageName);
|
||||
String lang = await _httpUtils.performHttpRequest(imageBytes, imageName);
|
||||
String text = await _imageUploader.performOcr(imageBytes, imageName, lang);
|
||||
|
||||
setState(() {
|
||||
_extractedText = text;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('OCR'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => uploadImage("lorem.png"),
|
||||
child: const Text('Test Upload Image'),
|
||||
),
|
||||
Text(_extractedText),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: _extractedText));
|
||||
},
|
||||
child: const Text('Copy to Clipboard'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
162
pubspec.lock
162
pubspec.lock
|
@ -1,6 +1,30 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "64.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -17,6 +41,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "723b4021e903217dfc445ec4cf5b42e27975aece1fc4ebbc1ca6329c2d9fb54e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.7.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -33,6 +81,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -41,6 +97,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.2"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -49,6 +113,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+6"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -57,6 +129,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -81,6 +161,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -121,6 +209,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -160,6 +256,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -169,7 +273,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
|
@ -256,6 +360,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -288,6 +400,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
mockito:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mockito
|
||||
sha256: "4b693867cee1853c9d1d7ecc1871f27f39b2ef2c13c0d8d8507dfe5bebd8aaf1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.3"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -360,11 +488,27 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -429,6 +573,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -453,6 +605,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.1.3 <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
|
|
|
@ -16,13 +16,13 @@ dependencies:
|
|||
path_provider: ^2.0.14
|
||||
http: ^1.1.0
|
||||
flutter_tesseract_ocr:
|
||||
http_parser: ^4.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
flutter_lints: ^2.0.0
|
||||
mockito: ^5.0.0
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
|
@ -37,4 +37,3 @@ flutter:
|
|||
- assets/tessdata/rus.traineddata
|
||||
|
||||
uses-material-design: true
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,9 +1,21 @@
|
|||
import 'package:cpd_app/image_uploader.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cpd_app/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const OpticText());
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('Test OCR functionality', () async {
|
||||
final ImageUploader imageUploader = ImageUploader();
|
||||
String imageName = "lorem.png";
|
||||
|
||||
var img = await imageUploader.buildImageFile(imageName);
|
||||
assert(img.lengthInBytes > 0);
|
||||
|
||||
// momentan auskommentiert, weil das pathproviderplugin irgentwie gemockt werden muss und ich es noch nicht hinbekommen habe
|
||||
//mock response, weil man keine http requests in tests machen kann
|
||||
// String mockResponse = "eng";
|
||||
|
||||
// String text = await imageUploader.performOcr(img, imageName, mockResponse);
|
||||
// assert(text.contains("Lorem ipsum"));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue