Witaj Gościu! ( Zaloguj | Rejestruj )

Forum PHP.pl

 
Reply to this topicStart new topic
> [Symfony]Upload wielu plików, problem z CollectionType
PawelC
post 27.02.2018, 23:20:37
Post #1





Grupa: Zarejestrowani
Postów: 1 173
Pomógł: 121
Dołączył: 24.09.2007
Skąd: Toruń

Ostrzeżenie: (0%)
-----


Witam,
Piszę aplikację w symfony która daje możliwość uploadu wielu plików na raz. Niestety mam problem z formularzem. Mam następujący kod
Klasa Entity
  1. <?php
  2.  
  3. namespace AppBundle\Entity;
  4.  
  5. use Doctrine\Common\Collections\ArrayCollection;
  6. use Doctrine\ORM\Mapping as ORM;
  7.  
  8. /**
  9.  * @ORM\Entity
  10.  */
  11. class DiaryContent
  12. {
  13. /**
  14.   * @ORM\Column(type="integer")
  15.   * @ORM\Id
  16.   * @ORM\GeneratedValue(strategy="AUTO")
  17.   */
  18. protected $id;
  19.  
  20. /**
  21.   * @ORM\Column(type="integer")
  22.   */
  23. protected $author_id;
  24.  
  25. /**
  26.   * @ORM\Column(type="text")
  27.   */
  28. protected $title;
  29.  
  30. /**
  31.   * @ORM\Column(type="text")
  32.   */
  33. protected $content;
  34.  
  35. /**
  36.   * @ORM\Column(type="datetime")
  37.   */
  38. protected $data;
  39.  
  40.  
  41. /**
  42.   * @ORM\Column(type="text")
  43.   */
  44. protected $images;
  45.  
  46. public function __construct()
  47. {
  48. $this->images = new ArrayCollection();
  49. }
  50.  
  51. /**
  52.   * @return mixed
  53.   */
  54. public function getId()
  55. {
  56. return $this->id;
  57. }
  58.  
  59. /**
  60.   * @param mixed $id
  61.   */
  62. public function setId($id)
  63. {
  64. $this->id = $id;
  65. }
  66.  
  67. /**
  68.   * @return mixed
  69.   */
  70. public function getAuthorId()
  71. {
  72. return $this->author_id;
  73. }
  74.  
  75. /**
  76.   * @param mixed $author_id
  77.   */
  78. public function setAuthorId($author_id)
  79. {
  80. $this->author_id = $author_id;
  81. }
  82.  
  83. /**
  84.   * @return mixed
  85.   */
  86. public function getTitle()
  87. {
  88. return $this->title;
  89. }
  90.  
  91. /**
  92.   * @param mixed $title
  93.   */
  94. public function setTitle($title)
  95. {
  96. $this->title = $title;
  97. }
  98.  
  99. /**
  100.   * @return mixed
  101.   */
  102. public function getContent()
  103. {
  104. return $this->content;
  105. }
  106.  
  107. /**
  108.   * @param mixed $content
  109.   */
  110. public function setContent($content)
  111. {
  112. $this->content = $content;
  113. }
  114.  
  115. /**
  116.   * @return mixed
  117.   */
  118. public function getData()
  119. {
  120. return $this->data;
  121. }
  122.  
  123. /**
  124.   * @param mixed $data
  125.   */
  126. public function setData($data)
  127. {
  128. $this->data = $data;
  129. }
  130.  
  131. /**
  132.   * @return mixed
  133.   */
  134. public function getImages()
  135. {
  136. return $this->images;
  137. }
  138.  
  139. /**
  140.   * @param mixed $images
  141.   */
  142. public function setImages($images)
  143. {
  144. $this->images = $images;
  145. }
  146. }


Kod formularza
  1. <?php
  2.  
  3. namespace AppBundle\Form;
  4.  
  5. use Symfony\Component\Form\AbstractType;
  6. use Symfony\Component\Form\Extension\Core\Type\CollectionType;
  7. use Symfony\Component\Form\Extension\Core\Type\FileType;
  8. use Symfony\Component\Form\FormBuilderInterface;
  9. use Symfony\Component\OptionsResolver\OptionsResolver;
  10.  
  11. class DiaryContentType extends AbstractType
  12. {
  13. /**
  14.   * {@inheritdoc}
  15.   */
  16. public function buildForm(FormBuilderInterface $builder, array $options)
  17. {
  18. $builder->add('author_id')->add('title')->add('content')->add('data')->add('images', CollectionType::class, array(
  19. 'entry_type' => FileType::class,
  20.  
  21. ));
  22. }
  23.  
  24. /**
  25.   * {@inheritdoc}
  26.   */
  27. public function configureOptions(OptionsResolver $resolver)
  28. {
  29. $resolver->setDefaults(array(
  30. 'data_class' => 'AppBundle\Entity\DiaryContent'
  31. ));
  32. }
  33.  
  34. /**
  35.   * {@inheritdoc}
  36.   */
  37. public function getBlockPrefix()
  38. {
  39. return 'appbundle_diarycontent';
  40. }
  41.  
  42.  
  43. }

oraz jego wywołanie w kontrolerze
  1. $diaryContent = new DiaryContent();
  2.  
  3. $form = $this->createFormBuilder($diaryContent)
  4. ->add("title", TextType::class)
  5. ->add('content', TextareaType::class)
  6. ->add('images', CollectionType::class, array(
  7. 'entry_type' => FileType::class,
  8. 'required' => false,
  9. 'mapped' => false,
  10. 'allow_add' => true,
  11. ))->getForm();
  12.  
  13. return $this->render('@App/Diary/new.html.twig', array(
  14. 'form' => $form->createView(),
  15. ));


Problem jest taki, że wyświetlają mi się wszystkie pola formularza, poza tym z CollectionType, jak zmienię na FileType to pokazuje tylko jeden plik. Ma ktoś jakieś sensowne pomysły? Ogólnie chciałbym zrobić formularz do uploadu zdjęć bez limitu, ale mi nie idzie.
Go to the top of the page
+Quote Post
newbie007
post 28.02.2018, 06:15:44
Post #2





Grupa: Zarejestrowani
Postów: 5
Pomógł: 0
Dołączył: 27.02.2018

Ostrzeżenie: (0%)
-----


A gdy dodasz do tego co masz w kontrolerze:
  1. 'entry_options' => [
  2. 'multiple' => true,
  3. ]


Strzelam ale może to to? Poza tym stworzyłbym DiaryContentType żebyś w kontrolerze nie ustawiał jakich pól oczekujesz.
Go to the top of the page
+Quote Post
PawelC
post 28.02.2018, 08:44:25
Post #3





Grupa: Zarejestrowani
Postów: 1 173
Pomógł: 121
Dołączył: 24.09.2007
Skąd: Toruń

Ostrzeżenie: (0%)
-----


Bez zmian, zrobiłem taki entity
  1. <?php
  2.  
  3. namespace AppBundle\Entity;
  4.  
  5. use Doctrine\Common\Collections\ArrayCollection;
  6. use Doctrine\ORM\Mapping as ORM;
  7.  
  8. /**
  9.  * @ORM\Entity
  10.  */
  11. class DiaryContent
  12. {
  13. /**
  14.   * @ORM\Column(type="integer")
  15.   * @ORM\Id
  16.   * @ORM\GeneratedValue(strategy="AUTO")
  17.   */
  18. protected $id;
  19.  
  20. /**
  21.   * @ORM\Column(type="integer")
  22.   */
  23. protected $author_id;
  24.  
  25. /**
  26.   * @ORM\Column(type="text")
  27.   */
  28. protected $title;
  29.  
  30. /**
  31.   * @ORM\Column(type="text")
  32.   */
  33. protected $content;
  34.  
  35. /**
  36.   * @ORM\Column(type="datetime")
  37.   */
  38. protected $data;
  39.  
  40.  
  41. /**
  42.   * @ORM\Column(type="text")
  43.   */
  44. protected $images;
  45.  
  46. public function __construct()
  47. {
  48. $this->images = new ArrayCollection();
  49. }
  50.  
  51. /**
  52.   * @return mixed
  53.   */
  54. public function getId()
  55. {
  56. return $this->id;
  57. }
  58.  
  59. /**
  60.   * @param mixed $id
  61.   */
  62. public function setId($id)
  63. {
  64. $this->id = $id;
  65. }
  66.  
  67. /**
  68.   * @return mixed
  69.   */
  70. public function getAuthorId()
  71. {
  72. return $this->author_id;
  73. }
  74.  
  75. /**
  76.   * @param mixed $author_id
  77.   */
  78. public function setAuthorId($author_id)
  79. {
  80. $this->author_id = $author_id;
  81. }
  82.  
  83. /**
  84.   * @return mixed
  85.   */
  86. public function getTitle()
  87. {
  88. return $this->title;
  89. }
  90.  
  91. /**
  92.   * @param mixed $title
  93.   */
  94. public function setTitle($title)
  95. {
  96. $this->title = $title;
  97. }
  98.  
  99. /**
  100.   * @return mixed
  101.   */
  102. public function getContent()
  103. {
  104. return $this->content;
  105. }
  106.  
  107. /**
  108.   * @param mixed $content
  109.   */
  110. public function setContent($content)
  111. {
  112. $this->content = $content;
  113. }
  114.  
  115. /**
  116.   * @return mixed
  117.   */
  118. public function getData()
  119. {
  120. return $this->data;
  121. }
  122.  
  123. /**
  124.   * @param mixed $data
  125.   */
  126. public function setData($data)
  127. {
  128. $this->data = $data;
  129. }
  130.  
  131. /**
  132.   * @return mixed
  133.   */
  134. public function getImages()
  135. {
  136. return $this->images;
  137. }
  138.  
  139. /**
  140.   * @param mixed $images
  141.   */
  142. public function setImages($images)
  143. {
  144. $this->images = $images;
  145. }

Na jego podstawie wygenerowałem taki formularz tzn DiaryContentType
  1. <?php
  2.  
  3. namespace AppBundle\Form;
  4.  
  5. use Symfony\Component\Form\AbstractType;
  6. use Symfony\Component\Form\Extension\Core\Type\CollectionType;
  7. use Symfony\Component\Form\Extension\Core\Type\FileType;
  8. use Symfony\Component\Form\FormBuilderInterface;
  9. use Symfony\Component\OptionsResolver\OptionsResolver;
  10.  
  11. class DiaryContentType extends AbstractType
  12. {
  13. /**
  14.   * {@inheritdoc}
  15.   */
  16. public function buildForm(FormBuilderInterface $builder, array $options)
  17. {
  18. $builder->add('author_id')->add('title')->add('content')->add('data')->add('images', CollectionType::class, array(
  19. 'entry_type' => FileType::class,
  20. 'allow_add' => true,
  21. 'entry_options' => array(
  22. 'multiple' => true,
  23. )
  24.  
  25. ));
  26. }/**
  27.   * {@inheritdoc}
  28.   */
  29. public function configureOptions(OptionsResolver $resolver)
  30. {
  31. $resolver->setDefaults(array(
  32. 'data_class' => 'AppBundle\Entity\DiaryContent'
  33. ));
  34. }
  35.  
  36. /**
  37.   * {@inheritdoc}
  38.   */
  39. public function getBlockPrefix()
  40. {
  41. return 'appbundle_diarycontent';
  42. }
  43.  
  44.  
  45. }
W kontrolerze mam taki kod
  1. $diaryContent = new DiaryContent();
  2.  
  3. $form = $this->createForm(DiaryContentType::class, $diaryContent);
  4. return $this->render('@App/Diary/new.html.twig', array(
  5. 'form' => $form->createView(),
  6. ));


Efekt jest taki, że pokazują się wszystkie pola poza images do wyboru plików. Wygląda to tak


Już nie mam pomysłu co jest nie tak, jak dam FileType to się pokazuje, ale ja potrzebuje obsłużyć upload x plików, a nie jednego.
Go to the top of the page
+Quote Post
aras785
post 28.02.2018, 11:18:31
Post #4





Grupa: Zarejestrowani
Postów: 859
Pomógł: 177
Dołączył: 29.10.2009

Ostrzeżenie: (0%)
-----


Podrzucam Ci działający przykład lub https://blog.theodo.fr/2015/07/manage-multi...oad-in-symfony/

  1. <?php
  2. namespace Acme\Bundle\AdminBundle\Entity;
  3. use Doctrine\ORM\Mapping as ORM;
  4. use Symfony\Component\HttpFoundation\File\UploadedFile;
  5. use Symfony\Component\Validator\Constraints as Assert;
  6. use Cocur\Slugify\Slugify;
  7. use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
  8. /**
  9.  * AlbumImages
  10.  *
  11.  * @ORM\Table()
  12.  * @ORM\Entity
  13.  * @ORM\HasLifecycleCallbacks
  14.  */
  15. class AlbumImages
  16. {
  17. /**
  18.   * @var integer
  19.   *
  20.   * @ORM\Column(name="id", type="integer", nullable=false)
  21.   * @ORM\Id
  22.   * @ORM\GeneratedValue(strategy="IDENTITY")
  23.   */
  24. private $id;
  25. /**
  26.   * @var string
  27.   *
  28.   * @ORM\Column(name="image", type="string", length=50, nullable=false)
  29.   */
  30. private $image;
  31. /**
  32.   * @var string
  33.   *
  34.   * @ORM\Column(name="title", type="string", length=50, nullable=false)
  35.   */
  36. private $title;
  37. /**
  38.   * @var \DateTime
  39.   *
  40.   * @ORM\Column(name="date_add", type="datetime", nullable=false)
  41.   */
  42. private $dateAdd;
  43. /**
  44.   * @var \DateTime
  45.   *
  46.   * @ORM\Column(name="date_update", type="datetime", nullable=false)
  47.   */
  48. private $dateUpdate;
  49. /**
  50.   * @var \Studio
  51.   *
  52.   * @ORM\ManyToOne(targetEntity="Albums")
  53.   * @ORM\JoinColumns({
  54.   * @ORM\JoinColumn(name="id_album", referencedColumnName="id", onDelete="CASCADE")
  55.   * })
  56.   */
  57. private $idAlbum;
  58. /**
  59.   * Get id
  60.   *
  61.   * @return integer
  62.   */
  63. public function getId()
  64. {
  65. return $this->id;
  66. }
  67. /**
  68.   * Set title
  69.   *
  70.   * @param string $title
  71.   * @return AlbumImages
  72.   */
  73. public function setTitle($title)
  74. {
  75. $this->title = $title;
  76.  
  77. return $this;
  78. }
  79. /**
  80.   * Get title
  81.   *
  82.   * @return string
  83.   */
  84. public function getTitle()
  85. {
  86. return $this->title;
  87. }
  88. /**
  89.   * Set dateAdd
  90.   *
  91.   * @param \DateTime $dateAdd
  92.   * @return AlbumImages
  93.   */
  94. public function setDateAdd($dateAdd)
  95. {
  96. $this->dateAdd = $dateAdd;
  97.  
  98. return $this;
  99. }
  100. /**
  101.   * Get dateAdd
  102.   *
  103.   * @return \DateTime
  104.   */
  105. public function getDateAdd()
  106. {
  107. return $this->dateAdd;
  108. }
  109. /**
  110.   * Set dateUpdate
  111.   *
  112.   * @param \DateTime $dateUpdate
  113.   * @return AlbumImages
  114.   */
  115. public function setDateUpdate($dateUpdate)
  116. {
  117. $this->dateUpdate = $dateUpdate;
  118.  
  119. return $this;
  120. }
  121. /**
  122.   * Get dateUpdate
  123.   *
  124.   * @return \DateTime
  125.   */
  126. public function getDateUpdate()
  127. {
  128. return $this->dateUpdate;
  129. }
  130. /**
  131.   * Set image
  132.   *
  133.   * @param string $image
  134.   * @return AlbumImages
  135.   */
  136. public function setImage($image)
  137. {
  138. $this->image = $image;
  139.  
  140. return $this;
  141. }
  142. /**
  143.   * Get image
  144.   *
  145.   * @return string
  146.   */
  147. public function getImage()
  148. {
  149. return $this->image;
  150. }
  151. /**
  152.   * @Assert\File(maxSize="6000000",mimeTypes={"image/*"})
  153.   */
  154. public $file_image;
  155. /**
  156.   * @var
  157.   * Old variable: $image
  158.   */
  159. public $old_image;
  160.  
  161. public function getAbsolutePath($name='')
  162. {
  163. if($name!='') return null === $name ? null : $this->getUploadRootDir().'/'.$name;
  164. return null === $this->image ? null : $this->getUploadRootDir().'/'.$this->image;
  165. }
  166. public function getWebPath($name='')
  167. {
  168. if($name!='') return null === $name ? null : $this->getUploadDir().'/'.$name;
  169. return null === $this->image ? null : $this->getUploadDir().'/'.$this->image;
  170. }
  171. protected function getUploadRootDir()
  172. {
  173. // the absolute directory path where uploaded documents should be saved
  174. return __DIR__.'/../../../../../web/'.$this->getUploadDir();
  175. }
  176. protected function getUploadDir()
  177. {
  178. // get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image in the view.
  179. return 'upload/studio/'.$this->getIdAlbum()->getidStudio()->getId().'/albums/'.$this->getIdAlbum()->getId();
  180. }
  181. /**
  182.   * @ORM\PreUpdate
  183.   */
  184. public function preUpdate()
  185. {
  186. $slug = new Slugify();
  187. $this->setDateUpdate(new \DateTime());
  188. $ext = pathinfo($this->getImage(), PATHINFO_EXTENSION);
  189.  
  190. $image_new_name = $slug->slugify($this->getTitle());
  191.  
  192. $this->old_image = $this->getImage();
  193.  
  194. if($image_new_name.'.'.$ext != $this->old_image) {
  195. if(file_exists($this->getUploadRootDir().'/'.$image_new_name.'.'.$ext)) {
  196. $file_exists_count = 1;
  197. while(file_exists($this->getUploadRootDir().'/'.$image_new_name.'_'.$file_exists_count.'.'.$ext)) {
  198. $file_exists_count++;
  199. }
  200. $image_new_name = $image_new_name.'_'.$file_exists_count;
  201. }
  202. rename($this->getAbsolutePath($this->old_image),$this->getAbsolutePath($image_new_name.'.'.$ext));
  203. $this->setImage($image_new_name.'.'.$ext);
  204. }
  205. }
  206. /**
  207.   * @ORM\PrePersist()
  208.   */
  209. public function preUpload()
  210. {
  211. $this->setDateAdd(new \DateTime());
  212. $this->setDateUpdate(new \DateTime());
  213. $slug = new Slugify();
  214. //photo
  215. if (is_object($this->file_image)) {
  216. $this->removeUpload();
  217. $slug = new Slugify();
  218. $image_new_name = $slug->slugify(pathinfo($this->file_image->getClientOriginalName(), PATHINFO_FILENAME));
  219. if(file_exists($this->getUploadRootDir().'/'.$image_new_name.'.'.$this->file_image->guessExtension())) {
  220. $file_exists_count = 1;
  221. while(file_exists($this->getUploadRootDir().'/'.$image_new_name.'_'.$file_exists_count.'.'.$this->file_image->guessExtension())) {
  222. $file_exists_count++;
  223. }
  224. $image_new_name = $image_new_name.'_'.$file_exists_count;
  225. }
  226. $this->setImage($image_new_name.'.'.$this->file_image->guessExtension());
  227. $this->setTitle($image_new_name);
  228. }
  229. }
  230. /**
  231.   * @ORM\PostPersist()
  232.   */
  233. public function upload()
  234. {
  235. if(is_object($this->file_image)) {
  236. $this->file_image->move($this->getUploadRootDir(), $this->getImage());
  237. }
  238. }
  239. /**
  240.   * @ORM\PostRemove()
  241.   */
  242. public function removeUpload()
  243. {
  244. if ($file = $this->getAbsolutePath() AND $this->getImage()!='') {
  245. if(file_exists($file)) {
  246. unlink($file);
  247. }
  248. }
  249. }
  250. /**
  251.   * Set idAlbum
  252.   *
  253.   * @param \Acme\Bundle\AdminBundle\Entity\Albums $idAlbum
  254.   * @return AlbumImages
  255.   */
  256. public function setIdAlbum(\Acme\Bundle\AdminBundle\Entity\Albums $idAlbum = null)
  257. {
  258. $this->idAlbum = $idAlbum;
  259.  
  260. return $this;
  261. }
  262. /**
  263.   * Get idAlbum
  264.   *
  265.   * @return \Acme\Bundle\AdminBundle\Entity\Albums
  266.   */
  267. public function getIdAlbum()
  268. {
  269. return $this->idAlbum;
  270. }
  271. }


  1. <?php
  2. public function album_images(Request $request, $id)
  3. {
  4. $em = $this->getDoctrine()->getManager();
  5. $album = $em->getRepository('AcmeAdminBundle:Albums')->find($id);
  6. if (!$album) return $this->redirectToRoute('admin_studio');
  7. $form = $this->createFormBuilder()
  8. ->add('file_image','file',array('label'=>'Zdjęcia','multiple'=>true))
  9. ->add('save', 'submit', array('label' => 'Dodaj zdjęcia do albumu'))
  10. ->getForm();
  11. $image_errors = array();
  12. if($request->isMethod('POST')) {
  13. $form->handleRequest($request);
  14. $file_count = count($form["file_image"]->getData());
  15. if($file_count>0) {
  16. foreach($form["file_image"]->getData() as $file_key=>$file) {
  17. $albumImage = new AlbumImages();
  18. $albumImage->setIdAlbum($album);
  19. $albumImage->file_image = $file;
  20. $validator = $this->get('validator');
  21. $errors = $validator->validate($albumImage);
  22. if(count($errors)==0) {
  23. $em->persist($albumImage);
  24. $em->flush();
  25. }else {
  26. foreach($errors as $error) {
  27. $image_errors[$file_key]['file_name'] = $file->getClientOriginalName();
  28. $image_errors[$file_key]['errors'][] = $error->getMessage();
  29. }
  30. }
  31. }
  32. $this->addFlash('success','Zdjęcia zostały dodane');
  33. if(count($image_errors)>0) {
  34. $error_message = '<b>Wystąpiły błędy:</b>';
  35. foreach($image_errors as $image_errors_k=>$image_errors_v) {
  36. $error_message .= '<br/>Plik: <u>'.$image_errors_v['file_name'].'</u>';
  37. $error_message .= '<ul><li>'.implode('</li><li>',$image_errors_v['errors']).'</li></ul>';
  38. }
  39. $this->addFlash('error',$error_message);
  40. }
  41. return $this->redirectToRoute('admin_studio_album_images', array('id' => $id));
  42. }
  43. }
  44. $albumImages = $em->getRepository('AcmeAdminBundle:AlbumImages')->findBy(array('idAlbum'=>$id));
  45. $this->view_data['title']['subheader'] = 'Zdjęcia do albumu: '.$album->getName();
  46. $this->view_data['form'] = $form->createView();
  47. $this->view_data['albumImages'] = $albumImages;
  48. $this->view_data['album'] = $album;
  49. $this->view_data['studio'] = $album->getIdStudio();
  50. return $this->render('@AcmeAdmin/Studio/albums/album_images.html.twig',$this->view_data);
  51. }


Ten post edytował aras785 28.02.2018, 11:32:22
Go to the top of the page
+Quote Post
PawelC
post 28.02.2018, 21:02:59
Post #5





Grupa: Zarejestrowani
Postów: 1 173
Pomógł: 121
Dołączył: 24.09.2007
Skąd: Toruń

Ostrzeżenie: (0%)
-----


Dziękuję już sobie poradziłem biggrin.gif Tak swoją drogą to masz pełną skrzynkę, albo mnie zablokowałeś tongue.gif
Go to the top of the page
+Quote Post
aras785
post 28.02.2018, 22:21:17
Post #6





Grupa: Zarejestrowani
Postów: 859
Pomógł: 177
Dołączył: 29.10.2009

Ostrzeżenie: (0%)
-----


Cytat(ExPlOiT @ 28.02.2018, 21:02:59 ) *
Dziękuję już sobie poradziłem biggrin.gif Tak swoją drogą to masz pełną skrzynkę, albo mnie zablokowałeś tongue.gif


nie zablokowałem Cię smile.gif Własnie zrobiłem porządek w skrzynce
Go to the top of the page
+Quote Post
PawelC
post 28.02.2018, 22:40:22
Post #7





Grupa: Zarejestrowani
Postów: 1 173
Pomógł: 121
Dołączył: 24.09.2007
Skąd: Toruń

Ostrzeżenie: (0%)
-----


Ok, problem z uploadem rozwiązany.
Go to the top of the page
+Quote Post

Reply to this topicStart new topic
1 Użytkowników czyta ten temat (1 Gości i 0 Anonimowych użytkowników)
0 Zarejestrowanych:

 



RSS Wersja Lo-Fi Aktualny czas: 29.03.2024 - 13:34