Commit 35d3725a by Tecnicos

Implementados los formatos reconocidos y soporte para calidad

parent 3d96b3bf
......@@ -37,7 +37,6 @@ class Carpeta extends ElementoSeleccionable {
InfoFormato (
carpeta: this,
formato: f,
subCarpeta: false
)
);
_elementos.add(fse);
......@@ -60,13 +59,15 @@ class Carpeta extends ElementoSeleccionable {
return null;
}
void setFormatoDestino(Formato original, Formato? destino){
void setFormatoDestino(Formato original, Formato? destino, Calidad? calidad){
final formatosAlt = formatos.toList();
for(InfoFormato i in formatosAlt){
if(i.formatoOriginal == original){
i.formatoDestino = destino;
i.calidadSalida = calidad;
}
}
}
/*
......
......@@ -5,11 +5,12 @@ import 'formato.dart';
class Convertible extends ElementoSeleccionable{
final Formato _formatoOriginal;
Formato? formatoDestino;
Calidad? calidadSalida;
Formato get formatoOriginal => _formatoOriginal;
Convertible({required super.id, required super.nombre, required super.icon,
required Formato formatoOriginal, this.formatoDestino}):
required Formato formatoOriginal}):
_formatoOriginal = formatoOriginal;
@override
......
......@@ -2,74 +2,75 @@ import 'package:flutter/material.dart';
// TODO: AÑADIR CALIDAD
enum Formato {
png('Portable Network Graphics', TipoMultimedia.imagen, Clasificacion.calidad, [],
png(['png'], [], 'Portable Network Graphics', TipoMultimedia.imagen, Clasificacion.calidad, [],
'ClaseFormato gráfico basado en un algoritmo de compresión sin pérdida para bitmaps no sujeto a patentes. Fue desarrollado en buena parte para solventar las deficiencias del formato GIF y permite almacenar imágenes con una mayor profundidad de contraste y otros datos importantes.'
),
jpg('Joint Photographic Experts Group', TipoMultimedia.imagen, Clasificacion.ligero, [],
jpg(['jpeg','jpg','jpe'], Calidad.values, 'Joint Photographic Experts Group', TipoMultimedia.imagen, Clasificacion.ligero, [],
'A pesar de ser un método de compresión, es a menudo considerado como un formato de archivo. Es el formato de imagen más común, utilizado por las cámaras fotográficas digitales y otros dispositivos de captura de imagen'
),
jpeg('Joint Photographic Experts Group', TipoMultimedia.imagen, Clasificacion.ligero, [],
'A pesar de ser un método de compresión, es a menudo considerado como un formato de archivo. Es el formato de imagen más común, utilizado por las cámaras fotográficas digitales y otros dispositivos de captura de imagen'
tif(['tif','tiff'], [], 'Tagged Image File Format', TipoMultimedia.imagen, Clasificacion.calidad, [],
'Un formato de archivo informático para almacenar imágenes de mapa de bits. Es prevalente en la industria gráfica y en la fotografía profesional por su versatilidad y compresión no destructiva.'
),
tif('Tagged Image File Format', TipoMultimedia.imagen, Clasificacion.calidad, [],
'Un formato de archivo informático para almacenar imágenes de mapa de bits. Es prevalente en la industria gráfica y en la fotografía profesional por su versatilidad y compresión no destructiva.'
webp(['webp'], Calidad.values, 'Web Picture', TipoMultimedia.imagen, Clasificacion.versatil, [],
'Formato desarrollado por Google, basándose en tecnología de On2 Technologies. Es un formato gráfico en forma de contenedor, que sustenta tanto compresión con pérdida como sin ella.'
),
mp3('MPEG-1 Layer III', TipoMultimedia.audio, Clasificacion.calidad,
mp3(['mp3'], [], 'MPEG-1 Layer III', TipoMultimedia.audio, Clasificacion.calidad,
[ MetadatoInfo.title, MetadatoInfo.artist, MetadatoInfo.album, MetadatoInfo.genre,
MetadatoInfo.composer, MetadatoInfo.track, MetadatoInfo.language],
'Un formato de compresión de audio digital que usa un algoritmo con pérdida para conseguir un menor tamaño de archivo. Es un formato de audio común utilizado para música tanto en computadoras como en reproductores de audio portátil.'
),
ogg('Xiph.org Ogg', TipoMultimedia.audio, Clasificacion.versatil, [],
oga(['oga','opus'], [], 'Xiph.org Ogg Vorbis', TipoMultimedia.audio, Clasificacion.versatil, [],
'Un formato contenedor libre y abierto, desarrollado y mantenido por la Fundación Xiph.Org que no está restringido por las patentes de software, y está diseñado para proporcionar una difusión de flujo eficiente y manipulación de multimedios digitales de alta calidad.'
),
wav('Waveform Audio File Format', TipoMultimedia.audio, Clasificacion.calidad, [],
wav(['wav','wave'], [], 'Waveform Audio File Format', TipoMultimedia.audio, Clasificacion.calidad, [],
'Un formato de audio digital con o sin compresión de datos desarrollado por Microsoft e IBM que se utiliza para almacenar flujos digitales de audio en el PC, mono y estéreo a diversas resoluciones y velocidades de muestreo.'
),
mp4('MPEG-4 Parte 14', TipoMultimedia.video, Clasificacion.calidad,
flac(['flac'], [], 'Free Lossless Audio Codec', TipoMultimedia.audio, Clasificacion.calidad, [],
'Es un formato abierto con licencia libre de derechos de autor y una implementación de referencia la cual es software libre. FLAC cuenta con soporte para etiquetado de metadatos, inclusión de la portada del álbum, y la búsqueda rápida.'
),
mp4(['mp4','m4a','m4p','m4v','m4b','m4r'], [], 'MPEG-4 Parte 14', TipoMultimedia.video, Clasificacion.versatil,
[ MetadatoInfo.title, MetadatoInfo.author, MetadatoInfo.album, MetadatoInfo.year,
MetadatoInfo.genre, MetadatoInfo.composer, MetadatoInfo.track,
MetadatoInfo.description, MetadatoInfo.comment],
'Un formato contenedor especificado como parte del estándar internacional MPEG-4 de ISO/IEC. Es utilizado para almacenar los formatos audiovisuales especificados por ISO/IEC y el grupo MPEG (Moving Picture Experts Group) al igual que otros formatos audiovisuales disponibles.'
),
mkv('Matroshka', TipoMultimedia.video, Clasificacion.calidad,
mkv(['mkv','mk3d','mka','mks'], [], 'Matroshka', TipoMultimedia.video, Clasificacion.calidad,
[ MetadatoInfo.title, MetadatoInfo.description, MetadatoInfo.language ],
'Un formato contenedor abierto que puede almacenar una cantidad muy grande de vídeo, audio, imagen o pistas de subtítulos dentro de un solo archivo. Su finalidad es la de servir como formato universal para el almacenamiento de contenidos audiovisuales y multimedia, como películas o programas de televisión, imágenes y textos.'
),
wmv('Windows Media Video', TipoMultimedia.video, Clasificacion.calidad,
[ MetadatoInfo.title, MetadatoInfo.author, MetadatoInfo.rating, MetadatoInfo.comment ],
'Un formato de vídeo desarrollado por Microsoft, que forma parte del framework Windows Media. No está contruida solo con tecnología interna de Microsoft. Desde la versión 7 (WMV1), Microsoft ha utilizado su propia versión no estandarizada de MPEG-4. El vídeo a menudo se combina con sonido en formato Windows Media Audio.'
ogv(['ogv', 'ogm'], [], 'Xiph.org Ogg Theora', TipoMultimedia.video, Clasificacion.ligero, [],
'Un códec de vídeo libre que está siendo desarrollado por la Fundación Xiph.Org, como parte de su proyecto Ogg. Basado en el códec VP3 donado por On2 Technologies, Xiph.Org lo ha refinado y extendido dándole el mismo alcance futuro para mejoras en el codificador como el que posee el códec de audio Vorbis.'
);
final String _nombreCompleto;
final TipoMultimedia _tipoMultimedia;
final Clasificacion _clasificacion;
final List<MetadatoInfo> _metadatos;
final String _descripcion;
final List<String> listExtensiones;
final List<Calidad> listCalidades;
final String nombre;
final TipoMultimedia tipoMultimedia;
final Clasificacion clasificacion;
final List<MetadatoInfo> metadatos;
final String descripcion;
String get extension => name;
String get nombre => _nombreCompleto;
Clasificacion get clasificacion => _clasificacion;
TipoMultimedia get tipoMultimedia => _tipoMultimedia;
List<MetadatoInfo> get metadatos => _metadatos;
String get descripcion => _descripcion;
const Formato(this._nombreCompleto, this._tipoMultimedia, this._clasificacion,
this._metadatos, this._descripcion);
const Formato(this.listExtensiones, this.listCalidades, this.nombre,
this.tipoMultimedia, this.clasificacion, this.metadatos, this.descripcion);
/// Devuelve aquellos formatos de TipoMultimedia que no sean excepcion
static List<Formato> listadoFormatos({required TipoMultimedia tipo, Formato? excepcion}){
final toRet = <Formato>[];
for(var formato in Formato.values){
if(formato._tipoMultimedia == tipo && formato != excepcion) {
if(formato.tipoMultimedia == tipo && formato != excepcion) {
toRet.add(formato);
}
}
......@@ -78,9 +79,16 @@ enum Formato {
/// Si la extensión no es valida devuelve nulo
static Formato? fromExtension(String extension){
// COMPROBAR SI UN ARCHIVO OGG CONTIENE VÍDEO O SOLO AUDIO
// ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of csv=p=0 [ARCHIVO].ogg
// Outputs either video or no output at all.
// https://stackoverflow.com/questions/56397732/how-can-i-know-a-certain-file-is-a-video-file
for(Formato f in Formato.values) {
if(f.name == extension){
return f;
for(String ext in f.listExtensiones){
if(ext == extension){
return f;
}
}
}
return null;
......@@ -98,10 +106,14 @@ enum TipoMultimedia {
}
enum Clasificacion {
calidad("Calidad"), ligero("Ligero"), versatil("Versátil");
calidad("Calidad", Icons.high_quality_outlined),
ligero("Ligero", Icons.electric_bolt_outlined),
versatil("Versátil", Icons.auto_mode_outlined);
final String nombre;
const Clasificacion(this.nombre);
final IconData icono;
const Clasificacion(this.nombre, this.icono);
}
enum Calidad {
......
......@@ -54,9 +54,11 @@ class _PaginaConversionState extends State<PaginaConversion>
_tabController = TabController(length: 3, vsync: this);
if(widget._carpeta != null){
_formatoConvertido = widget._infoFormato!.formatoDestino;
_calidadActual = widget._infoFormato!.calidadSalida;
}
else{
_formatoConvertido = widget._archivo!.formatoDestino;
_calidadActual = widget._archivo!.calidadSalida;
}
}
......@@ -139,8 +141,8 @@ class _PaginaConversionState extends State<PaginaConversion>
shape: StadiumBorder(),
label: Text(elemento.texto),
// TODO: CAMBIAR CALIDAD FORMATO
onSelected: (_formatoConvertido == null)? null:
(selected) { setState((){_calidadActual = elemento;}); },
onSelected: (_formatoConvertido != null && _formatoConvertido!.listCalidades.contains(elemento))?
(selected) { setState((){_calidadActual = elemento;}); } : null,
);
}).toList();
......@@ -406,13 +408,43 @@ class _PaginaConversionState extends State<PaginaConversion>
_formatoConvertido = _calidadActual = null;
_cambiarFormatoCalidad(null, null);
});
} : () => _cambiarFormatoCalidad(elemento, Calidad.media),
} : () {
Calidad? qual = null;
if(elemento.listCalidades.isNotEmpty){
if(elemento.listCalidades.contains(Calidad.media)) {
qual = Calidad.media;
} else {
qual = elemento.listCalidades.first;
}
}
_cambiarFormatoCalidad(elemento, qual);
},
onLongPress: () {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
content: Text(elemento.descripcion),
title: Text(
elemento.nombre,
textAlign: TextAlign.center,
),
content: SizedBox(
child: Wrap(
children: [
Text(
'Clasificación: ${elemento.clasificacion.nombre}',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold
),
),
Text('\n'),
Text(
elemento.descripcion,
style: Theme.of(context).textTheme.bodyLarge,
)
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
......@@ -439,9 +471,12 @@ class _PaginaConversionState extends State<PaginaConversion>
style: Theme.of(context).textTheme.titleLarge,
textScaler: TextScaler.linear(1.5),
),
const SizedBox(
SizedBox(
height: 15.0,
child: Icon(Icons.airplanemode_active),
child: Icon(
elemento.clasificacion.icono,
size: (elemento.clasificacion == Clasificacion.calidad)? 32.0 : 28.0,
),
)
],
)
......@@ -459,11 +494,12 @@ class _PaginaConversionState extends State<PaginaConversion>
ElementoSeleccionable resultado;
if(widget._carpeta != null){
widget._carpeta!.setFormatoDestino(widget._formatoOriginal, _formatoConvertido);
widget._carpeta!.setFormatoDestino(widget._formatoOriginal, _formatoConvertido, _calidadActual);
resultado = widget._carpeta!;
}
else{
widget._archivo!.formatoDestino = _formatoConvertido;
widget._archivo!.calidadSalida = _calidadActual;
resultado = widget._archivo!;
}
......
import 'package:flutter/material.dart';
import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter/ffmpeg_session.dart';
import 'package:ffmpeg_kit_flutter/return_code.dart';
class PaginaPrincipalVacia extends StatelessWidget {
const PaginaPrincipalVacia({super.key});
......@@ -19,7 +22,7 @@ class PaginaPrincipalVacia extends StatelessWidget {
Text(
'Pulsa el botón + para agregarlos',
style: Theme.of(context).textTheme.bodyLarge,
)
),
],
),
)
......
......@@ -7,8 +7,10 @@ import Foundation
import ffmpeg_kit_flutter
import file_picker
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FFmpegKitFlutterPlugin.register(with: registry.registrar(forPlugin: "FFmpegKitFlutterPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}
......@@ -5,42 +5,42 @@ packages:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.12.0"
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.19.1"
version: "1.19.0"
cross_file:
dependency: transitive
description:
......@@ -69,10 +69,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.1"
ffi:
dependency: transitive
description:
......@@ -148,18 +148,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
......@@ -180,10 +180,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
......@@ -196,10 +196,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.15.0"
nested:
dependency: transitive
description:
......@@ -212,10 +212,58 @@ packages:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
......@@ -264,6 +312,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
......@@ -289,10 +345,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.0"
sprintf:
dependency: transitive
description:
......@@ -305,42 +361,42 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.3.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.2"
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.3"
typed_data:
dependency: transitive
description:
......@@ -369,10 +425,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.3.1"
version: "14.3.0"
web:
dependency: transitive
description:
......@@ -389,6 +445,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.10.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.7.0-0 <4.0.0"
dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.0"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment