Component

Module

Gallery

Enhancement to allow multiple image upload 

Directory

File Folder Link
M - Album.php & Gallery.php \\SYNAS\Allan\DOCUMENTATION\Module\Gallery\rckuc\wolf\app\models
V - all files \\SYNAS\Allan\DOCUMENTATION\Module\Gallery\rckuc\wolf\app\views\gallery
C - GalleryController.php & Utilities.php \\SYNAS\Allan\DOCUMENTATION\Module\Gallery\rckuc\wolf\app\controllers
backend.php \\SYNAS\Allan\DOCUMENTATION\Module\Gallery\rckuc\wolf\app\layouts
style.css \\SYNAS\Allan\DOCUMENTATION\Module\Gallery\rckuc\wolf\admin\themes\black_and_white
backend.js, gallery.js, bulk_action.js, img_handler.js \\SYNAS\Allan\DOCUMENTATION\Module\Gallery\rckuc\wolf\admin\javascript

wolf_album.sql & wolf_gallery.sql

\\SYNAS\Allan\DOCUMENTATION\Module\Gallery

 

Step 1

Update: backend.php

  • Insert scripts file in <head> section
  • Add new tab for "Gallery"
<head> <link href="<?php echo URI_PUBLIC; ?>wolf/admin/themes/<?php echo Setting::get('theme'); ?>/switch_button.css" id="css_theme" media="screen" rel="Stylesheet" type="text/css" /> ... <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/backend.js"></script> <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/gallery.js"></script> <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/bulk_action.js"></script> <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/img_handler.js"></script> </head> <!-- Add 'Option' Tab --> <body id="body_<?php echo $ctrl.'_'.Dispatcher::getAction(); ?>"> <ul> <li id="page-plugin" class="plugin"><a href="<?php echo get_url('gallery'); ?>"<?php if ($ctrl=='gallery') echo ' class="current"'; ?>><?php echo __('Gallery'); ?></a></li> </ul> </body>
<head>
    <link href="<?php echo URI_PUBLIC; ?>wolf/admin/themes/<?php echo Setting::get('theme'); ?>/switch_button.css" id="css_theme" media="screen" rel="Stylesheet" type="text/css" />

    ...
    <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/backend.js"></script>
    <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/gallery.js"></script>
    <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/bulk_action.js"></script>
    <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/img_handler.js"></script>
</head>

<!-- Add 'Option' Tab -->
<body id="body_<?php echo $ctrl.'_'.Dispatcher::getAction(); ?>">
    <ul>
        <li id="page-plugin" class="plugin"><a href="<?php echo get_url('gallery'); ?>"<?php if ($ctrl=='gallery') echo ' class="current"'; ?>><?php echo __('Gallery'); ?></a></li>
    </ul>
</body>

Step 2

Update: style.css

/* FOR bulk_action_toolbar */ #bulk_delete_form { display: none; } #bulk_duplicate_form { display: none; } #bulk_action_toolbar { display: none; align-items: center; gap: 1rem; background-color: #f9fafb; padding: 0.5rem 1rem; border: 1px solid #e5e7eb; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.04); margin: 1rem 0; } #selection_info { flex-grow: 1; } .action_button { cursor: pointer; padding: 6px 12px; background-color: #3b82f6; color: white; font-size: 13px; border-radius: 6px; transition: background-color 0.2s ease; } .action_button:hover { background-color: #2563eb; } #bulk_action_toolbar img { height: 18px; transition: opacity 0.2s ease; } #bulk_action_toolbar img:hover { opacity: 0.7; } /* Drag and Drop Image Design */ .drop-area, #drop-area-multiple { border: 2px dashed #d0d5dd; padding: 1rem; text-align: center; border-radius: 12px; background-color: #ffffff; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); transition: border-color 0.3s ease, background-color 0.3s ease; position: relative; width: max-content; } .drop-area { cursor: pointer; } #drop-multiple-container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); height: 80%; width: 80%; display: none; align-items: center; justify-content: center; background-color: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); z-index: 100; } #drop-area-multiple { height: 80%; width: 80%; display: flex; flex-direction: column; align-items: center; justify-content: space-around; } .drop-area.dragging, #drop-area-multiple.dragging { border-color: #3b82f6; background-color: #f0f8ff; } .previewImg { box-sizing: border-box; width: 200px; height: 200px; background: center / contain no-repeat; background-color: rgb(237, 237, 237); border: 2px solid #e5e7eb; border-radius: 12px; cursor: pointer; transition: transform 0.2s ease; } #drop-area-multiple .previewImg { cursor: move; } .previewImg:hover { transform: scale(1.02); } .previewContainer, .multiplePreviewList { position: relative; display: inline-block; } .previewTemplate { display: none; } .multiplePreviewList { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 1rem; padding: 1rem; overflow-y: auto; border-radius: 12px; margin: 1rem 0; /* min-height: 15rem; */ } .img_icon { cursor: pointer; height: 10rem; width: 10rem; opacity: 0.1; transition: transform 0.2s ease; } .img_icon:hover { transform: scale(1.02); } .sequence { height: 1rem; width: 1rem; position: absolute; top: 0.2rem; left: 0.2rem; padding: 0.3rem; border-radius: 100%; background-color: lightgray; } .close_icon { position: absolute; top: 0; right: 0; height: 1.5rem; width: 1.5rem; padding: 1rem; cursor: pointer; } .close_icon:hover { opacity: 0.7; } #multiple-upload-btn { display: none; background-color: #007bff; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 8px; cursor: pointer; transition: background-color 0.3s ease; position: absolute; bottom: 1rem; right: 1rem; } #multiple-upload-btn:hover { background-color: #0056b3; } .plusIcon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 2.5rem; color: #9ca3af; pointer-events: none; transition: opacity 0.3s ease; opacity: 1; } .previewImg[style*="background-image"]:not([style*='none']) + .plusIcon { opacity: 0; } .drop-area p, #drop-area-multiple p { font-size: 0.95rem; color: #6b7280; margin: 0.5rem 0; } .drop-area span.clickUpload, #drop-area-multiple span.clickUpload { color: #3b82f6; text-decoration: underline; font-weight: 500; cursor: pointer; transition: color 0.2s ease; } .drop-area span.clickUpload:hover, #drop-area-multiple span.clickUpload:hover { color: #2563eb; } .drop-area .note, #drop-area-multiple .note { display: block; font-size: 0.75rem; color: #9ca3af; margin-top: 0.5rem; } /* For Cropper */ #cropModal { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.5); /* dark overlay */ display: none; z-index: 9999; } .modal-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); padding: 2rem; width: 90%; max-width: 600px; max-height: 90vh; display: flex; flex-direction: column; align-items: center; } .modal-title { margin-bottom: 1rem; font-size: 1.5rem; font-weight: 600; color: #333; } .crop-area { max-height: 400px; overflow: hidden; margin-bottom: 1.5rem; } .crop-area img { max-width: 100%; height: auto; display: block; } .modal-actions { display: flex; justify-content: center; gap: 1rem; width: 100%; } .modal-actions button#cropConfirm, .modal-actions button#closeCropModal { background-color: #007bff; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 8px; cursor: pointer; transition: background-color 0.3s ease; } .modal-actions button#closeCropModal { background-color: darkgray; } .modal-actions button#cropConfirm:hover { background-color: #0056b3; } .modal-actions button#closeCropModal:hover { background-color: gray; } .aspect-ratio-selector { margin-bottom: 1rem; display: flex; align-items: center; justify-content: center; gap: 1rem; } .aspect-ratio-selector select { padding: 0.5rem 0.75rem; border: 1px solid #ccc; border-radius: 8px; background-color: #fff; outline: none; min-width: 200px; } .crop_icon, .remove_icon { border-radius: 100%; background-color: rgba(255, 255, 255, 0.3); position: absolute; top: 0.2rem; right: 0.2rem; padding: 0.3rem; height: 1.2rem; width: 1.2rem; display: flex; align-items: center; justify-content: center; } #drop-area-multiple .crop_icon { right: 2.5rem; } .crop_icon > img { height: 100%; width: 100%; } .remove_icon > img { height: 75%; width: 75%; } .drop-area .crop_icon { display: none; } .crop_icon:hover, .remove_icon:hover { opacity: 0.7; } .ori_img { display: none; } /* FOR UPLOAD PROGRESS BAR */ #upload-progress { display: none; } .progress-container { position: absolute; top: 0; left: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 101; background-color: rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); } .progress-container p { font-size: 1.5rem; font-weight: 700; margin: 0 0 3rem 0; color: white; } .progress-container p::after { content: ''; display: inline-block; width: 1.2em; text-align: left; animation: dots 1s steps(4, end) infinite; } @keyframes dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75%,100% { content: '...'; } } .progress-info { display: flex; justify-content: space-between; margin-bottom: 1rem; font-size: 1rem; color: white; width: 65%; } .progress-bar-wrapper { width: 70%; height: 10px; background-color: #e5e7eb; border-radius: 999px; overflow: hidden; margin-bottom: 5rem; } .progress-bar-fill { height: 100%; width: 0%; background-color: #3b82f6; transition: width 0.2s ease; } /* FOR DUPLICATE */ .duplicate_sweetalert img { height: 14px; width: 14px; }
/* FOR bulk_action_toolbar */
#bulk_delete_form {
  display: none;
}

#bulk_duplicate_form {
  display: none;
}

#bulk_action_toolbar {
  display: none;
  align-items: center;
  gap: 1rem;
  background-color: #f9fafb;
  padding: 0.5rem 1rem;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.04);
  margin: 1rem 0;
}

#selection_info {
    flex-grow: 1;
}

.action_button {
  cursor: pointer;
  padding: 6px 12px;
  background-color: #3b82f6;
  color: white;
  font-size: 13px;
  border-radius: 6px;
  transition: background-color 0.2s ease;
}

.action_button:hover {
  background-color: #2563eb;
}

#bulk_action_toolbar img {
  height: 18px;
  transition: opacity 0.2s ease;
}

#bulk_action_toolbar img:hover {
  opacity: 0.7;
}


/* Drag and Drop Image Design */
.drop-area,
#drop-area-multiple {
    border: 2px dashed #d0d5dd;
    padding: 1rem;
    text-align: center;
    border-radius: 12px;
    background-color: #ffffff;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
    transition: border-color 0.3s ease, background-color 0.3s ease;
    position: relative;
    width: max-content;
}

.drop-area {
    cursor: pointer;
}

#drop-multiple-container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 80%;
  width: 80%;
  display: none;
  align-items: center;
  justify-content: center;
  background-color: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
  z-index: 100;
}

#drop-area-multiple {
  height: 80%;
  width: 80%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-around;
}

.drop-area.dragging,
#drop-area-multiple.dragging {
    border-color: #3b82f6;
    background-color: #f0f8ff;
}

.previewImg {
    box-sizing: border-box;
    width: 200px;
    height: 200px;
    background: center / contain no-repeat;
    background-color: rgb(237, 237, 237);
    border: 2px solid #e5e7eb;
    border-radius: 12px;
    cursor: pointer;
    transition: transform 0.2s ease;
}

#drop-area-multiple .previewImg {
  cursor: move;
}

.previewImg:hover {
    transform: scale(1.02);
}

.previewContainer, .multiplePreviewList {
    position: relative;
    display: inline-block;
}

.previewTemplate {
  display: none;
}

.multiplePreviewList {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 1rem;
  padding: 1rem;
  overflow-y: auto;
  border-radius: 12px;
  margin: 1rem 0;
  /* min-height: 15rem; */
}

.img_icon {
  cursor: pointer;
  height: 10rem;
  width: 10rem;
  opacity: 0.1;
  transition: transform 0.2s ease;
}

.img_icon:hover {
  transform: scale(1.02);
}

.sequence {
  height: 1rem;
  width: 1rem;
  position: absolute;
  top: 0.2rem;
  left: 0.2rem;
  padding: 0.3rem;
  border-radius: 100%;
  background-color: lightgray;
}

.close_icon {
  position: absolute;
  top: 0;
  right: 0;
  height: 1.5rem;
  width: 1.5rem;
  padding: 1rem;
  cursor: pointer;
}

.close_icon:hover {
  opacity: 0.7;
}

#multiple-upload-btn {
  display: none;
  background-color: #007bff;
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.3s ease;
  position: absolute;
  bottom: 1rem;
  right: 1rem;
}

#multiple-upload-btn:hover {
  background-color: #0056b3;
}

.plusIcon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 2.5rem;
    color: #9ca3af;
    pointer-events: none;
    transition: opacity 0.3s ease;
    opacity: 1;
}

.previewImg[style*="background-image"]:not([style*='none']) + .plusIcon {
    opacity: 0;
}

.drop-area p,
#drop-area-multiple p {
    font-size: 0.95rem;
    color: #6b7280;
    margin: 0.5rem 0;
}

.drop-area span.clickUpload,
#drop-area-multiple span.clickUpload {
    color: #3b82f6;
    text-decoration: underline;
    font-weight: 500;
    cursor: pointer;
    transition: color 0.2s ease;
}

.drop-area span.clickUpload:hover,
#drop-area-multiple span.clickUpload:hover {
    color: #2563eb;
}

.drop-area .note,
#drop-area-multiple .note {
    display: block;
    font-size: 0.75rem;
    color: #9ca3af;
    margin-top: 0.5rem;
}


/* For Cropper */
#cropModal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0, 0, 0, 0.5); /* dark overlay */
  display: none;
  z-index: 9999;
}

.modal-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  padding: 2rem;
  width: 90%;
  max-width: 600px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.modal-title {
  margin-bottom: 1rem;
  font-size: 1.5rem;
  font-weight: 600;
  color: #333;
}

.crop-area {
  max-height: 400px;
  overflow: hidden;
  margin-bottom: 1.5rem;
}

.crop-area img {
  max-width: 100%;
  height: auto;
  display: block;
}

.modal-actions {
  display: flex;
  justify-content: center;
  gap: 1rem;
  width: 100%;
}

.modal-actions button#cropConfirm,
.modal-actions button#closeCropModal {
  background-color: #007bff;
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.modal-actions button#closeCropModal {
  background-color: darkgray;
}

.modal-actions button#cropConfirm:hover {
  background-color: #0056b3;
}

.modal-actions button#closeCropModal:hover {
  background-color: gray;
}

.aspect-ratio-selector {
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}

.aspect-ratio-selector select {
  padding: 0.5rem 0.75rem;
  border: 1px solid #ccc;
  border-radius: 8px;
  background-color: #fff;
  outline: none;
  min-width: 200px;
}

.crop_icon,
.remove_icon {
  border-radius: 100%;
  background-color: rgba(255, 255, 255, 0.3);
  position: absolute;
  top: 0.2rem;
  right: 0.2rem;
  padding: 0.3rem;
  height: 1.2rem;
  width: 1.2rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

#drop-area-multiple .crop_icon {
  right: 2.5rem;
}

.crop_icon > img {
  height: 100%;
  width: 100%;
}

.remove_icon > img {
  height: 75%;
  width: 75%;
}

.drop-area .crop_icon {
  display: none;
}

.crop_icon:hover,
.remove_icon:hover {
  opacity: 0.7;
}

.ori_img {
  display: none;
}

/* FOR UPLOAD PROGRESS BAR */
#upload-progress {
  display: none;
}

.progress-container {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  z-index: 101;
  background-color: rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(10px);
}

.progress-container p {
  font-size: 1.5rem;
  font-weight: 700;
  margin: 0 0 3rem 0;
  color: white;
}

.progress-container p::after {
  content: '';
  display: inline-block;
  width: 1.2em;
  text-align: left;
  animation: dots 1s steps(4, end) infinite;
}

@keyframes dots {
  0%   { content: '';    }
  25%  { content: '.';   }
  50%  { content: '..';  }
  75%,100% { content: '...'; }
}

.progress-info {
  display: flex;
  justify-content: space-between;
  margin-bottom: 1rem;
  font-size: 1rem;
  color: white;
  width: 65%;
}

.progress-bar-wrapper {
  width: 70%;
  height: 10px;
  background-color: #e5e7eb;
  border-radius: 999px;
  overflow: hidden;
  margin-bottom: 5rem;
}

.progress-bar-fill {
  height: 100%;
  width: 0%;
  background-color: #3b82f6;
  transition: width 0.2s ease;
}

/* FOR DUPLICATE */
.duplicate_sweetalert img {
  height: 14px;
  width: 14px;
}

Step 3

Update: Utilities.php
Add new function duplicate_files()

public static function duplicate_files($module, $folder, $id, $new_folder_name) { $sourceDir = FILES_DIR . '/' . $module . '/' . $folder . '/' . $id; $targetDir = FILES_DIR . '/' . $module . '/' . $folder . '/' . $new_folder_name; if (!file_exists($sourceDir)) { return false; // source folder not found } // create target directory if not exists if (!file_exists($targetDir)) { mkdir($targetDir, 0755, true); } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { $targetPath = $targetDir . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); if ($item->isDir()) { if (!file_exists($targetPath)) { mkdir($targetPath, 0755, true); } } else { copy($item->getRealPath(), $targetPath); } } return true; }
  public static function duplicate_files($module, $folder, $id, $new_folder_name) {
      $sourceDir = FILES_DIR . '/' . $module . '/' . $folder . '/' . $id;
      $targetDir = FILES_DIR . '/' . $module . '/' . $folder . '/' . $new_folder_name;

      if (!file_exists($sourceDir)) {
          return false; // source folder not found
      }

      // create target directory if not exists
      if (!file_exists($targetDir)) {
          mkdir($targetDir, 0755, true);
      }

      $iterator = new RecursiveIteratorIterator(
          new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
          RecursiveIteratorIterator::SELF_FIRST
      );

      foreach ($iterator as $item) {
          $targetPath = $targetDir . DIRECTORY_SEPARATOR . $iterator->getSubPathName();

          if ($item->isDir()) {
              if (!file_exists($targetPath)) {
                  mkdir($targetPath, 0755, true);
              }
          } else {
              copy($item->getRealPath(), $targetPath);
          }
      }

      return true;
  }

Step 4

Update your every existing files and move new files to your project, and import latest sql table

Code Copied To Clipboard!