diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8295500..7f95d5e 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -10,9 +10,9 @@ on: env: # Use docker.io for Docker Hub if empty - REGISTRY: docker.io + REGISTRY: ghcr.io # github.repository as / - IMAGE_NAME: dyzulk/mivo + IMAGE_NAME: ${{ github.repository }} jobs: build: @@ -30,13 +30,21 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - # Login against a Docker registry except on PR - # https://github.com/docker/login-action + # Login against GHCR - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Login against Docker Hub (Optional fallback if secrets exist) + - name: Log into Docker Hub + if: github.event_name != 'pull_request' && secrets.DOCKER_USERNAME != '' + uses: docker/login-action@v3 + with: + registry: docker.io username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -46,13 +54,12 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + docker.io/${{ env.IMAGE_NAME }} tags: | - # Branch main -> dyzulk/mivo:edge type=raw,value=edge,enable={{is_default_branch}} - # Tag v1.0.0 -> dyzulk/mivo:1.0.0 type=ref,event=tag - # Tag v1.0.0 -> dyzulk/mivo:latest type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} # Build and push Docker image with Buildx (don't push on PR) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6af229d..774acb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,11 +32,25 @@ jobs: # Export source using git archive (respects .gitattributes) git archive --format=tar HEAD | tar -x -C release_temp + - name: Install Development Dependencies (for Build) + run: npm install + + - name: Build Localized Assets & Editor Bundle + run: | + npm run sync:assets + npm run build:editor + - name: Install Production Dependencies run: | cd release_temp composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-reqs + - name: Copy Build Artifacts to Release + run: | + cp -r public/assets/vendor/ release_temp/public/assets/ + mkdir -p release_temp/public/assets/js/vendor/ + cp public/assets/js/vendor/editor.bundle.js release_temp/public/assets/js/vendor/ + - name: Build Zip Artifact run: | cd release_temp diff --git a/DOCKER_README.md b/DOCKER_README.md index 8cdaf9d..3cc9657 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -1,5 +1,5 @@

- MIVO Logo + MIVO Logo

# MIVO (Mikrotik Voucher) Docker Image @@ -24,7 +24,7 @@ docker run -d \ -e APP_ENV=production \ -v mivo_data:/var/www/html/app/Database \ -v mivo_config:/var/www/html/.env \ - dyzulk/mivo:latest + ghcr.io/mivodev/mivo:latest ``` Open your browser and navigate to `http://localhost:8080`. @@ -39,7 +39,7 @@ For a more permanent setup, use `docker-compose.yml`: ```yaml services: mivo: - image: dyzulk/mivo:latest + image: ghcr.io/mivodev/mivo:latest container_name: mivo restart: unless-stopped ports: @@ -111,4 +111,4 @@ If you find MIVO useful, please consider supporting its development. Your contri [![SociaBuzz Tribe](https://img.shields.io/badge/SociaBuzz-Tribe-green?style=for-the-badge&logo=sociabuzz&logoColor=white)](https://sociabuzz.com/dyzulkdev/tribe) --- -*Created by DyzulkDev* +*Created by MivoDev* diff --git a/README.md b/README.md index cfda3c6..85d7121 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ MIVO is a next-generation **Mikrotik Voucher Management System** with a modern M 1. **Install via Composer** ```bash - composer create-project dyzulk/mivo + composer create-project mivodev/mivo cd mivo ``` > **Alternative (Docker):** > ```bash - > docker pull dyzulk/mivo + > docker pull ghcr.io/mivodev/mivo > ``` > *See [INSTALLATION.md](docs/INSTALLATION.md) for more tags.* @@ -83,4 +83,4 @@ If you find MIVO useful, please consider supporting its development. Your contri This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). --- -*Created by DyzulkDev* +*Created by MivoDev* diff --git a/app/Config/SiteConfig.php b/app/Config/SiteConfig.php index a974164..8d202c1 100644 --- a/app/Config/SiteConfig.php +++ b/app/Config/SiteConfig.php @@ -5,10 +5,10 @@ class SiteConfig { const APP_NAME = 'MIVO'; const APP_VERSION = 'v1.1.0'; const APP_FULL_NAME = 'MIVO - Mikrotik Voucher'; - const CREDIT_NAME = 'DyzulkDev'; - const CREDIT_URL = 'https://dyzulk.com'; + const CREDIT_NAME = 'MivoDev'; + const CREDIT_URL = 'https://github.com/mivodev'; const YEAR = '2026'; - const REPO_URL = 'https://github.com/dyzulk/mivo'; + const REPO_URL = 'https://github.com/mivodev/mivo'; // Security Keys // Fetched from .env or fallback to default diff --git a/app/Core/Hooks.php b/app/Core/Hooks.php new file mode 100644 index 0000000..40e5525 --- /dev/null +++ b/app/Core/Hooks.php @@ -0,0 +1,120 @@ + $callback, + 'accepted_args' => $accepted_args + ]; + } + + /** + * Execute an action + * + * @param string $tag The name of the action hook + * @param mixed ...$args Optional arguments to pass to the callback + */ + public static function doAction($tag, ...$args) + { + if (empty(self::$actions[$tag])) { + return; + } + + // Sort by priority + ksort(self::$actions[$tag]); + + foreach (self::$actions[$tag] as $priority => $callbacks) { + foreach ($callbacks as $callbackData) { + call_user_func_array($callbackData['function'], array_slice($args, 0, $callbackData['accepted_args'])); + } + } + } + + /** + * Register a new filter + * + * @param string $tag The name of the filter hook + * @param callable $callback The function to call + * @param int $priority Lower numbers correspond to earlier execution + * @param int $accepted_args The number of arguments the function accepts + */ + public static function addFilter($tag, $callback, $priority = 10, $accepted_args = 1) + { + self::$filters[$tag][$priority][] = [ + 'function' => $callback, + 'accepted_args' => $accepted_args + ]; + } + + /** + * Apply filters to a value + * + * @param string $tag The name of the filter hook + * @param mixed $value The value to be filtered + * @param mixed ...$args Optional extra arguments + * @return mixed The filtered value + */ + public static function applyFilters($tag, $value, ...$args) + { + if (empty(self::$filters[$tag])) { + return $value; + } + + // Sort by priority + ksort(self::$filters[$tag]); + + foreach (self::$filters[$tag] as $priority => $callbacks) { + foreach ($callbacks as $callbackData) { + // Prepend value to args + $params = array_merge([$value], array_slice($args, 0, $callbackData['accepted_args'] - 1)); + $value = call_user_func_array($callbackData['function'], $params); + } + } + + return $value; + } + + /** + * Check if any action has been registered for a hook. + * + * @param string $tag The name of the action hook. + * @return bool True if action exists, false otherwise. + */ + public static function hasAction($tag) + { + return isset(self::$actions[$tag]); + } + + /** + * Check if any filter has been registered for a hook. + * + * @param string $tag The name of the filter hook. + * @return bool True if filter exists, false otherwise. + */ + public static function hasFilter($tag) + { + return isset(self::$filters[$tag]); + } +} diff --git a/app/Core/PluginManager.php b/app/Core/PluginManager.php new file mode 100644 index 0000000..deef5e6 --- /dev/null +++ b/app/Core/PluginManager.php @@ -0,0 +1,78 @@ +pluginsDir = dirname(__DIR__, 2) . '/plugins'; // Root/plugins + } + + /** + * Load all active plugins + */ + public function loadPlugins() + { + // Ensure plugins directory exists + if (!is_dir($this->pluginsDir)) { + return; + } + + // 1. Get List of Active Plugins (For now, we load ALL folders as active) + // TODO: Implement database/config check for active status + $plugins = scandir($this->pluginsDir); + + foreach ($plugins as $pluginName) { + if ($pluginName === '.' || $pluginName === '..') { + continue; + } + + $pluginPath = $this->pluginsDir . '/' . $pluginName; + + // Check if it is a directory and has specific plugin file + if (is_dir($pluginPath) && file_exists($pluginPath . '/plugin.php')) { + $this->loadPlugin($pluginName, $pluginPath); + } + } + + // Fire 'plugins_loaded' action after all plugins are loaded + Hooks::doAction('plugins_loaded'); + } + + /** + * Load a single plugin + * + * @param string $name Plugin folder name + * @param string $path Full path to plugin directory + */ + private function loadPlugin($name, $path) + { + try { + require_once $path . '/plugin.php'; + $this->activePlugins[] = $name; + } catch (\Exception $e) { + error_log("Failed to load plugin [$name]: " . $e->getMessage()); + } + } + + /** + * Get list of loaded plugins + * + * @return array + */ + public function getActivePlugins() + { + return $this->activePlugins; + } +} diff --git a/app/Core/Router.php b/app/Core/Router.php index 437bc01..a277032 100644 --- a/app/Core/Router.php +++ b/app/Core/Router.php @@ -92,6 +92,9 @@ class Router { } public function dispatch($uri, $method) { + // Fire hook to allow plugins to register routes + \App\Core\Hooks::doAction('router_init', $this); + $path = parse_url($uri, PHP_URL_PATH); // Handle subdirectory diff --git a/app/Helpers/TemplateHelper.php b/app/Helpers/TemplateHelper.php index 2ee1ec8..0069721 100644 --- a/app/Helpers/TemplateHelper.php +++ b/app/Helpers/TemplateHelper.php @@ -51,8 +51,8 @@ class TemplateHelper { $content = str_replace(array_keys($dummyData), array_values($dummyData), $content); - // QR Code replacement - $content = preg_replace('/\{\{\s*qrcode.*?\}\}/i', '', $content); + // QR Code replacement - Using canvas for client-side rendering with QRious + $content = preg_replace('/\{\{\s*qrcode\s*(.*?)\s*\}\}/i', '', $content); return $content; } @@ -69,6 +69,7 @@ class TemplateHelper { body { display: flex; align-items: center; justify-content: center; background-color: transparent; } #wrapper { display: inline-block; transform-origin: center center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } +
' . $mockContent . '
@@ -76,6 +77,48 @@ class TemplateHelper { window.addEventListener("load", () => { const wrap = document.getElementById("wrapper"); if(!wrap) return; + + // Render QR Codes + document.querySelectorAll(".qrcode-placeholder").forEach(canvas => { + const optionsStr = canvas.dataset.options || ""; + const options = {}; + + // Robust parser for "fg=red bg=#fff size=100" format + const regex = /([a-z]+)=([^ \t\r\n\f\v"]+|"[^"]*"|\'[^\']*\')/gi; + let match; + while ((match = regex.exec(optionsStr)) !== null) { + let key = match[1].toLowerCase(); + let val = match[2].replace(/["\']/g, ""); + options[key] = val; + } + + new QRious({ + element: canvas, + value: "http://hotspot.lan/login?username=u-5829&password=5912", + size: parseInt(options.size) || 100, + foreground: options.fg || "#000000", + backgroundAlpha: 0, + level: "M" + }); + + // Handle styles via CSS for better compatibility with rounding and padding + if (options.size) { + canvas.style.width = options.size + "px"; + canvas.style.height = options.size + "px"; + } + + if (options.bg) { + canvas.style.backgroundColor = options.bg; + } + + if (options.padding) { + canvas.style.padding = options.padding + "px"; + } + + if (options.rounded) { + canvas.style.borderRadius = options.rounded + "px"; + } + }); const updateScale = () => { const w = wrap.offsetWidth; diff --git a/app/Views/layouts/footer_main.php b/app/Views/layouts/footer_main.php index 7b7785a..c551f8d 100644 --- a/app/Views/layouts/footer_main.php +++ b/app/Views/layouts/footer_main.php @@ -9,15 +9,15 @@