diff --git a/.eslintignore b/.eslintignore index fa6ca0b..71ba64f 100755 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -**/vendors/** +/src/core/vendors/ **/src/php/** \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index b895c76..663de60 100755 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,6 +25,8 @@ "no-dupe-class-members": "error", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-empty-function": "off", "indent": [ "error", "tab", diff --git a/.gitattributes b/.gitattributes index 1a6aaa8..ae95dbb 100755 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ +# Git attributes + +# Mark vendored and documentation extras/* linguist-vendored=true -docs/* linguist-documentation=true \ No newline at end of file +docs/* linguist-documentation=true +build/* linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e7eed02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: '' +assignees: sixem + +--- + +

Attempt to fill out this form as well as possible. If you can't answer a question, you can skip it.

+ +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +If relevant, the steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Features used** +Please fill out the features that are enabled: + - Performance mode: [e.g. yes/no] + - Single page: [e.g. yes/no] + +**Information** +If possible, fill out the data: + - PHP version: [e.g. 7.4] + - Browser used: [e.g. FireFox/Chrome] + +**Errors** +If possible, copy any console errors that may occur with debugging enabled. If the page fails to load, you can check your web server logs for any errors. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..9cae32f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[REQUEST]" +labels: '' +assignees: sixem + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c6e6edb --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,45 @@ +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Cache modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test + + - name: Build + run: npm run build + + - name: Build standalone + run: npm run make-standalone + + - name: Archive production artifacts + uses: actions/upload-artifact@v4 + with: + name: build + path: build/ diff --git a/.gitignore b/.gitignore index b343f46..c0b5197 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ node_modules/ build/ docker/public -.DS_Store *.7z *.zip +*.ignore diff --git a/.stylelintrc.json b/.stylelintrc.json index 613f4f9..7fd8705 100755 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -3,6 +3,8 @@ "rules": { "selector-class-pattern": null, "selector-id-pattern": null, - "no-descending-specificity": null + "no-descending-specificity": null, + "color-function-notation": "modern", + "hue-degree-notation": "angle" } } \ No newline at end of file diff --git a/LICENSE b/LICENSE index a071c1d..0dcb2f7 100755 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ - Licensed under GPL-3.0 - Copyright (c) 2022 emy (sixem) | five.sh + Licensed under GPL-3.0 - Copyright (c) 2023 emy (sixem) | five.sh GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/README.md b/README.md index 37ce6a3..fabd0bc 100755 --- a/README.md +++ b/README.md @@ -1,172 +1,106 @@ -

- -

+
+
+ +

+

IVFi-PHP

+

The image and video friendly indexer

+
-

- An image and video friendly Indexer
-

+
-

-GitHub releases GitHub issues GitHub repo size -Travis (.com) -

- ---- - -## What is this project? - -This is a file directory browser script written in PHP and TypeScript. - -This is designed to be a image and video friendly Indexer, while also being an Indexer that has all of the other features that you can expect from most directory listers out there. It can be heavily customized, and has a design that attempts to be appealing while also being functional and easy to use. - -***Note: The Indexer can be used without JavaScript enabled, but it is needed for the extra functionality.*** - -## Demo -***You can visit the [demo](https://five.sh/demo/indexer/) to view the indexer in action.*** - -## Documentation -:link: - -## Feedback :bulb: -I'm open for any feedback. - -You can open an [issue](https://github.com/sixem/eyy-indexer/issues) if you encounter any **specific** problems or bugs of any kind. - -Or, you can start a [discussion](https://github.com/sixem/eyy-indexer/discussions) if you just have any general questions or minor issues you want to troubleshoot. You can also suggest any features or potential changes there. - -# Features -### **Authentication** -The script supports HTTP authentication, allowing you to add a bit of protection to your directories. -### **Gallery Mode** -A gallery mode where you can view images and videos of the current directory without needing to visit each URL separately. It has support for downloading files and reverse searching images. -### **Hover Previews** -Displays a preview of the image or video when hovering over the name. -### **Search Filter** -The search filter can be used to search for filenames or filetypes in the current directory. -Usage (Desktop): `Shift + F`. -### **Single file** -This script can be set up as a single file script (standalone setup). Only one file needed, nothing more. -### **Customizable** -This script can be customized in a number of ways. -### **Additonal Features** -It can be built with additional features, like support for displaying `README.md` files on each directory! -### **And much more ..** -+ All dates will match the timezone of the client. -+ Persistent client-set sorting settings. -+ Support for custom themes. -+ Server-side filtering which can help you hide specific files or folders. -+ Paths can be clicked, allowing for easy navigation between folders. -+ The client can set their own settings in the menu. -+ Direct download links. -+ Mobile friendly. - -# Setup - -### Download the latest release [HERE](https://github.com/sixem/eyy-indexer/releases)! +

GitHub releases GitHub issues GitHub repo size Build

-Alternatively, you can also build it from source yourself, for that see: [Building](#building). +
-You can also find every release and specific builds here: [https://five.sh/releases/eyy-indexer/](https://five.sh/releases/eyy-indexer/) -
-
-#### These instructions are aimed at servers using a regular Nginx or Apache setup. -#### For instructions on how to use it with Docker, see [this](https://sixem.github.io/eyy-indexer/#/setup?id=docker). +

+ Demo   + Documentation   + Configuration   + Building +

-## 1. Files +
-You can choose between having a **default** setup and a **standalone** setup. +# About -The default setup will import assets (`js`, `css` and fonts) as separate files, like most sites. This is the normal setup. +IVFi-PHP is a file directory browser script made in PHP and TypeScript. -The standalone setup will have all of these files bundled directly into the `.php` file instead, serving everything through HTML. +It is designed to be a comprehensive indexer, with a focus on efficiently handling image and video files. IVFi has a modern and user-friendly interface, offering features such as a gallery view, hoverable previews, and many customization options. -### Default: -Place the files from the `build` directory into your root web directory. -### Standalone (single file): -Place the file from the `standalone` directory into your root web directory. +This project can be easily set up on most web servers. -## 2. Server Configuration +
-The Indexer does require you to use it as an `index` file, that way it can be automatically applied to directories that do not have a defualt `index` file present. This makes it behave just like any other default and built-in directory indexes. This can all be done easily by following the steps below, **depending** on what web server you are using. +# Quick setup :zap: -### Nginx -To use this script for all directories without a default index you need append `/indexer.php` to the end of your `index` line in your server configuration. This will tell Nginx to use the Indexer if none of the default indexes exist. +* Download the latest release [here](https://github.com/sixem/ivfi-php/releases). +* Place the `/build/` files into your web root. For example `/var/www/html/`. +* Then, use the `indexer.php` as an index file for any of the directories where the script should be used: -Example usage: +#### Nginx - Example using the [index](https://nginx.org/en/docs/http/ngx_http_index_module.html#index) directive: ``` server { index index.html index.php /indexer.php; } - ``` -Another example, here it is only being applied to a few directories: -``` -server { - location ~ ^/(videos|images)/ { - index /indexer.php; - } -} -``` -### Apache -In order to automatically use this script as a default index, you need to edit your Apache configuration. To do this, you must place `/indexer.php` at the end of your `DirectoryIndex` directive. - -Example usage: +#### Apache - Example using the [DirectoryIndex](https://httpd.apache.org/docs/2.4/mod/mod_dir.html#directoryindex) directive: ``` DirectoryIndex index.html index.php /indexer.php ``` -This line can be placed in either your server's `.conf` file or your `.htaccess` file. This will tell Apache to use the Indexer if none of the default indexes exist. This can be set globally or on a per-directory basis depending on your usecase. - -# Configuration - -#### See [Configuration](https://sixem.github.io/eyy-indexer/) for a detailed overview over how this script can be customized. +
-## Additional features +For detailed instructions on how to configure the script, refer to [setup](https://git.five.sh/ivfi/docs/php/#/setup). -When building the Indexer, additional features can be enabled. These features are not bundled with the pre-built releases, and must be flagged as enabled prior to building the Indexer yourself (see below for how the building process is done). +The releases and individual builds are available [here](https://git.five.sh/ivfi/releases/php/). -The reason why these features are not included in the default version, is because they may rely on larger libraries and other scripts, which I don't feel should be pushed into the vanilla version unless the user desires to do so themselves! +
-See [Extra Features](https://sixem.github.io/eyy-indexer/#/extras) for a list over the features and how these can be implemented. - -# Building -:grey_exclamation: **This has been tested to work with node 18.3** +# Features -You can build this script from source using `node` and `npm`. +### **Authentication** +> It includes support for HTTP authentication, providing some added security for your directories. +### **Gallery Mode** +> A gallery mode that allows you to view images and videos from the current directory in one place, as well as the ability to download files and perform reverse image searches. +### **Hover Previews** +> An image or video preview is displayed when hovering over the file name. +### **Search Filter** +> The filter function allows you to search for specific filenames or file types within the directory.

Usage (Desktop): `Shift + F`. +### **Single file** +> The script can be configured as a standalone solution, where all required assets are combined into a single file, making it easy to use with just one file needed. +### **Customizable** +> This script offers multiple customization options. +### **Additonal Features** +> It can be built with added functionality, such as the ability to display `README.md` files in each directory! -**Clone repository and install dependencies:** -``` -git clone https://github.com/sixem/eyy-indexer -cd eyy-indexer -npm install -``` +
-## Production builds +### **And much more ...** ++ :clock12: The dates will be adjusted to match the time zone of the client. ++ :arrow_up_down: Client-defined sorting preferences are stored persistently. ++ :art: Support for custom themes. ++ :mag: Server-side filtering which can help you hide specific files or folders. ++ :link: Navigating between folders is made easy with the clickable paths. ++ :gear: The client has the option to personalize their settings through the menu. ++ :inbox_tray: Direct download links for all files. ++ :desktop_computer: Compatible with both mobile and desktop devices. -Build from source, creating minified files: -``` -npm run build -``` +
-Build a standalone file from source: -``` -npm run make-standalone -``` +# Feedback -## Development builds +If you have come across any specific problems or bugs that you would like to report, you have the option to open an [issue](https://github.com/sixem/ivfi-php/issues). This will allow us to better understand the issue at hand and take the necessary steps to resolve it. -Build source mapped, non-production files: -``` -npm run build-dev -``` +Alternatively, if you have any general questions, minor issues, or ideas for improvements that you would like to discuss, you can start a [discussion](https://github.com/sixem/ivfi-php/discussions). This is a good way for you to share your thoughts and ideas with us, and we would be more than happy to listen and consider them. -# Contributing -You can contribute by either submitting a pull request, reporting issues or bugs, or voicing good ideas. It's all very much welcome! :relaxed: +
## License -This project is licensed under GPL-3.0. It also includes external libraries that are available under a variety of licenses. See [LICENSE](LICENSE) for the full license text. +This project is licensed under GPL-3.0. It also includes external libraries that are available under a variety of licenses. + +See [LICENSE](LICENSE) for the full license text. ## Disclaimer -**As you with anything else, use this script at your own risk. There may exist bugs that i do not know of.** \ No newline at end of file +As with anything else, use this script at your own risk. There could exist bugs that I do not know of :v: diff --git a/build.helpers.js b/build.helpers.js index b6d5528..152b46c 100755 --- a/build.helpers.js +++ b/build.helpers.js @@ -1,4 +1,4 @@ -const fs = require('fs'); +import fs from 'fs'; /** * Strips the opening/closing tags from code. @@ -39,7 +39,7 @@ const stripOuterTags = (data, tags) => return data; }; -exports.exit = (...message) => +const exit = (...message) => { console.log(`\nExiting -`, ...message); process.exit(1); @@ -50,7 +50,7 @@ exports.exit = (...message) => * * @param {string} path */ -exports.readJson = (path) => +const readJson = (path) => { let data = false; @@ -73,7 +73,7 @@ exports.readJson = (path) => /** * Extractors for fetching/reading extra build files */ - exports.extractors = { +const extractors = { filePhp: (path, options = {}) => { let data = null; @@ -130,7 +130,7 @@ exports.readJson = (path) => } }; -exports.trimPartPath = (path) => +const trimPartPath = (path) => { if(path[0] === '/' || path[0] === '\\') { @@ -141,4 +141,11 @@ exports.trimPartPath = (path) => } return path; +}; + +export default { + exit, + readJson, + extractors, + trimPartPath }; \ No newline at end of file diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index f3ad496..0c4ba22 100755 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -1,4 +1,5 @@ server { + # HTTP listen 80; # Location of indexer files @@ -26,11 +27,12 @@ server { index /indexer.php; } + # Handle PHP files location ~ ^/.+\.php(/|$) { try_files $uri =404; + # FastCGI params fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass php:9000; fastcgi_index index.php; diff --git a/docs/README.md b/docs/README.md index 482586f..d4e2b0e 100755 --- a/docs/README.md +++ b/docs/README.md @@ -3,12 +3,12 @@

- An image and video friendly Indexer
+ The image and video friendly Indexer

-GitHub releases GitHub issues GitHub repo size -Travis (.com) +GitHub releases GitHub issues GitHub repo size +Travis (.com)

--- @@ -24,9 +24,9 @@ This is designed to be a image and video friendly Indexer, while also being an I ## Feedback I'm open for any feedback. -You can open an [issue](https://github.com/sixem/eyy-indexer/issues) if you encounter any **specific** problems or bugs of any kind. +You can open an [issue](https://github.com/sixem/ivfi-php/issues) if you encounter any **specific** problems or bugs of any kind. -Or, you can start a [discussion](https://github.com/sixem/eyy-indexer/discussions) if you just have any general questions or minor issues you want to troubleshoot. You can also suggest any features or potential changes there. +Or, you can start a [discussion](https://github.com/sixem/ivfi-php/discussions) if you just have any general questions or minor issues you want to troubleshoot. You can also suggest any features or potential changes there. ## Demo @@ -75,7 +75,7 @@ This project is licensed under GPL-3.0. It also includes external libraries that are available under a variety of licenses. -See [LICENSE](https://github.com/sixem/eyy-indexer/blob/master/LICENSE) for the full license text. +See [LICENSE](https://github.com/sixem/ivfi-php/blob/master/LICENSE) for the full license text. ## Disclaimer **As you with anything else, use this script at your own risk. There may exist bugs that i do not know of.** \ No newline at end of file diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 5d0751d..063b45d 100755 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -1,10 +1,12 @@ -# eyy-indexer docs +![logo](./greeterLogo.svg) -> An image and video friendly Indexer +# IVFi-PHP docs -* Customizable and easy to use +> Documentation -[GitHub](https://github.com/sixem/eyy-indexer) -[Get Started](README) \ No newline at end of file +* The image and video friendly indexer + +[GitHub](https://github.com/sixem/ivfi-php) +[Get Started](setup.md) \ No newline at end of file diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 195be0a..2ba179c 100755 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,12 +1,18 @@ -* [Overview](README.md "eyy-indexer (Documentation)") -* [Setup](setup.md "eyy-indexer (Setup)") -* [Configuration](config.md "eyy-indexer (Configuration)") -* [Advanced](advanced.md "eyy-indexer (Advanced)") - * [Performance Mode](performance.md "eyy-indexer (Performance Mode)") - * [Processor](processor.md "eyy-indexer (Processor)") - * [Extra Features](extras.md "eyy-indexer (Extras)") -* [Building](building.md "eyy-indexer (Building)") +* [Overview](README.md "IVFi-PHP (Documentation)") +* [Setup](setup.md "IVFi-PHP (Setup)") +* [Configuration](config.md "IVFi-PHP (Configuration)") +* [Themes](themes.md "IVFi-PHP (Themes)") +* [Advanced](advanced.md "IVFi-PHP (Advanced)") + * [Performance Mode](performance.md "IVFi-PHP (Performance Mode)") + * [Processor](processor.md "IVFi-PHP (Processor)") + * [Extra Features](extras.md "IVFi-PHP (Extras)") + * [Dotfile](dotfile.md "IVFi-PHP (Dotfile)") +* [Building](building.md "IVFi-PHP (Building)") ---- \ No newline at end of file +--- + +

+ IVFi-PHP +

\ No newline at end of file diff --git a/docs/advanced.md b/docs/advanced.md index 71669a2..f86b184 100755 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -11,4 +11,7 @@ * Handle processed data with a self-defined function. [Extra Features](extras.md) - * Compile the Indexer with extrea features. \ No newline at end of file + * Compile the Indexer with extrea features. + +[Dotfile](dotfile.md) + * Directory-specific configuration using dotfiles. \ No newline at end of file diff --git a/docs/building.md b/docs/building.md index 29be6bf..56ca060 100755 --- a/docs/building.md +++ b/docs/building.md @@ -8,8 +8,8 @@ You can build this script from source using `node` and `npm`. **Clone repository and install dependencies:** ```bash -git clone https://github.com/sixem/eyy-indexer -cd eyy-indexer +git clone https://github.com/sixem/ivfi-php +cd ivfi-php npm install ``` diff --git a/docs/config.md b/docs/config.md index 7b34d5b..671fa8d 100755 --- a/docs/config.md +++ b/docs/config.md @@ -164,6 +164,25 @@ If the value is a function, then the returned string of that function will be us * `$param['config']` contains the parsed and active configuration. +## Metadata +Key: **`metadata`** + +This option will add metadata to the header. This can be set globally using this option, or you can also set it on a per-directory basis using [dotfiles](dotfile.md). + +Example: +```php + [ + [ + 'property' => 'og:title', + 'content' => 'This is a title!' + ] + ], + ); +?> +``` + ## Style Key: **`style`** @@ -173,7 +192,7 @@ The `compact` setting can be changed by the client in the settings menu, as can | Child key | Type | Default | Description | |-----|------|---------|-------------| -| `themes => path` | Bool/String | `false` | Set to a path relative to the root directory containing `.css` files (example: `/indexer/css/themes/`). Every `.css` file in the set folder will be treated as a separate theme. +| `themes => path` | Bool/String | `/indexer/themes/` | Set to a path relative to the root directory containing `.css` files. Every `.css` file in the set folder will be treated as a separate theme. | `themes => default` | Bool/String | `false` | Default theme for new clients to use. Takes a filename **without** the `.css` extension. | `css => additional` | Array/String | `false` | Adds any additional CSS to the page. Can either be a pure CSS `string` or an `array` where the key is the selector and where the child keys and values are properties and values respectively. | `compact` | Bool | `false` | Makes the page use a more compact and centered style. @@ -183,13 +202,13 @@ Key: **`filter`** This option can be used if you want to filter the files or directories using `regular expressions`. -All filenames and directory names **matching** the `regex` will be shown. +All filenames and directory names **matching** the `regex` will be shown. Please note that directory names will always end in a slash (`/`), this is done to make them easier to differentiate from files. For example, setting `file` to `/^.{1,10}\.(jpg|png)$/` will only include `.jpg` and `.png` files with a filename between `1 - 10` characters in length when reading the directory files. #### Another is example is when you want to hide files with invalid characters or specific filenames: - `'/^(?:(?!\b(README.md|secret.pwd)\b).)*$/'`
+ `'/^(?!README\.md$|secret\.pwd$).*$/'`
This example will hide any `README.md` and `secret.pwd` files from the directory. `'/^[^\#\?]*$/'`
@@ -199,7 +218,7 @@ If you want to apply multiple filters easily, then you can also pass them as an ```php 'filter' => array( 'file' => array( - '/^(?:(?!\b(README.md|secret.pwd)\b).)*$/', + '/^(?!README\.md$|secret\.pwd$).*$/', '/^[^\#\?]*$/' ), 'directory' => false @@ -214,6 +233,20 @@ Setting the value to `false` will disable the filter. | `file` | Bool/String/Array | `false` | A `regexp` filter for what files should be included. | `directory` | Bool/String/Array | `false` | A `regexp` filter for what directories should be included. +## Exclude +Key: **`exclude`** + +This option will exclude certain extensions from showing up in any directory. + +An example that'll exclude any `jpg` or `jpeg` files: +```php + ["jpg", "jpeg"] + ); +?> +``` + ## Directory Sizes Key: **`directory_sizes`** diff --git a/docs/dotfile.md b/docs/dotfile.md new file mode 100644 index 0000000..501ec71 --- /dev/null +++ b/docs/dotfile.md @@ -0,0 +1,46 @@ +

Dotfile

+ +
+ +If a directory contains an `.ivfi` dotfile, the script will read the options from the file and enable those settings for that directory only. + +The script expects the file to be in a JSON format, like this template: +```json +{ + "metadata": [], + "metadataBehavior": "overwrite", + "ignore": [], + "exclude": [] +} +``` +### metadata +An array of objects containg the attributes (keys) and values for the metadata elements. + +* Example: ```[ { "property": "description", "content": "A description." } ]``` +* type: `` + +--- + +### metadataBehavior +Sets the override behavior of the metadata options. If using `overwrite` it'll override any existing values should they already exist, but keep any existing, unchanged values. Using `replace` will simply remove all previous metadata in favor of the new data, this also applies to any `charset` or `viewport` metadata set by the script. + +*The dotfile metadata will always take priority over the configuration values when using overwrite.* + +* default: `overwrite` +* type: `'string'` + +--- + +### ignore +An array of strings that will act as a filter for the current file data. Directories will always end in a `/` for easier identification. Any files or directories matching an item in this array will be hidden. It supports basic wildcards. + +* Example: ```[ "*.md", "image.png", "directory/"]``` +* type: `` + +--- + +### exclude +An array of strings that will act in a similar way as the ignore option, however, this will only apply to extensions, so any files matching the extensions that are in this array will be hidden. + +* Example: ```[ "md", "ini" ]``` +* type: `` diff --git a/docs/extras.md b/docs/extras.md index e1b2196..45f9022 100755 --- a/docs/extras.md +++ b/docs/extras.md @@ -6,7 +6,7 @@ The `extras` directory contains extra features for the Indexer. These features must be compiled independently, as they are not available in any official releases. -To build the Indexer with these features, find and edit [build.options.json](https://github.com/sixem/eyy-indexer/blob/master/build.options.json) in the base directory of the repository. +To build the Indexer with these features, find and edit [build.options.json](https://github.com/sixem/ivfi-php/blob/master/build.options.json) in the base directory of the repository. Simply set the value of any corresponding key under `extraFeatures` to `true` or `false` to enable and disable the feature respectively during compilation. diff --git a/docs/greeterLogo.svg b/docs/greeterLogo.svg new file mode 100644 index 0000000..f62e144 --- /dev/null +++ b/docs/greeterLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 604f279..ae74bb0 100755 --- a/docs/index.html +++ b/docs/index.html @@ -2,11 +2,19 @@ - eyy-indexer (Documentation) - + IVFi-PHP (Documentation) + + - + + + + + + + + - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md index fed1a02..a143ef5 100755 --- a/docs/setup.md +++ b/docs/setup.md @@ -5,9 +5,9 @@ ## Download -

Download the latest release HERE!

+

Download the latest release HERE!

Alternatively, you can also build it from source yourself, for that see: building

-

You can also find every release and specific builds here: https://five.sh/releases/eyy-indexer/

+

You can also find every release and specific builds here: https://git.five.sh/ivfi/releases/php/

## Files @@ -61,8 +61,8 @@ The script can also be run through a simple docker container using `docker compo #### Clone the repository and install dependencies: `npm install` can be skipped if you are manually creating `docker/public` without building from source. ```bash -git clone https://github.com/sixem/eyy-indexer -cd eyy-indexer +git clone https://github.com/sixem/ivfi-php +cd ivfi-php npm install ``` diff --git a/docs/themes.md b/docs/themes.md new file mode 100755 index 0000000..7386aef --- /dev/null +++ b/docs/themes.md @@ -0,0 +1,30 @@ +

Themes

+ +

Overview and theme usage.

+ + +## Overview +IVFi-PHP supports custom themes. By default, you can place any themes in `/indexer/themes/`, and they'll be automatically applied. If you want to customize the path or the default theme, then you can use the below instructions. + +You can find a list of official themes here: [IVFi-themes](https://github.com/sixem/ivfi-themes), or you can create your own! + +## Usage +* 1) Download or create the themes that you wish to use. +* 2) Place them in a publicly available directory. + * Example: `/indexer/themes/` +* 3) Edit the configuration: + * Set the `path` to the relative directory of the themes. + * If you want a theme to be the default, then set `default` to the theme's name. +```php + [ + 'themes' => [ + 'path' => '/indexer/themes/', + 'default' => false + ] + ] +]; +?> +``` +* 4) You should now be able to enable different themes in the settings menu (⚙️ in the top right corner). \ No newline at end of file diff --git a/extras/readmeSupport/css/stylesheet.css b/extras/readmeSupport/css/stylesheet.css deleted file mode 100755 index 0745a16..0000000 --- a/extras/readmeSupport/css/stylesheet.css +++ /dev/null @@ -1,40 +0,0 @@ -.readmeContainer -{ - background-color: #121318; - margin-bottom: 18px; - border-top: 1px solid #19191d; - border-bottom: 1px solid #19191d; -} - -.readmeContainer pre -{ - white-space: pre-wrap; -} - -.readmeContainer > div.readmeContents -{ - padding: 5px 8px; -} - -.readmeContainer a -{ - text-decoration: none; - color: #d6d3ce; - font-weight: bold; -} - -.readmeContainer a:hover -{ - text-decoration: underline; - color: #5e74ea; -} - -.readmeContainer::before -{ - content: "README.md"; - background-color: #15161e; - display: block; - padding: 3px 8px 5px 6px; - color: #939498; - border-bottom: 1px solid #151822; -} \ No newline at end of file diff --git a/extras/readmeSupport/data.json b/extras/readmeSupport/data.json index e1e3258..efe5663 100755 --- a/extras/readmeSupport/data.json +++ b/extras/readmeSupport/data.json @@ -15,14 +15,6 @@ "options": { "stripTags": false } - }, - "STYLESHEET": { - "path": "css/stylesheet.css", - "extractor": "fileCss", - "type": "additonalCss", - "options": { - "minimize": true - } } } } \ No newline at end of file diff --git a/extras/readmeSupport/display/displayReadme.php b/extras/readmeSupport/display/displayReadme.php index 622307f..9b85c23 100755 --- a/extras/readmeSupport/display/displayReadme.php +++ b/extras/readmeSupport/display/displayReadme.php @@ -7,8 +7,12 @@ ); echo sprintf( - '
%s
', - $readmeSupport['parsedown']->text($readmeSupport['contents']) + '
+ README.md +
%s
+
', +(isset($cookies['readme']['toggled']) && $cookies['readme']['toggled'] === true) ? ' open=""' : '', +$readmeSupport['parsedown']->text($readmeSupport['contents']) ); } ?> \ No newline at end of file diff --git a/logo.svg b/logo.svg index 62b8542..486b968 100755 --- a/logo.svg +++ b/logo.svg @@ -1,93 +1 @@ - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6b6ff07..ef80df6 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { - "name": "eyy-indexer", - "version": "1.2.1", + "name": "ivfi", + "version": "1.2.3", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "eyy-indexer", - "version": "1.2.1", + "name": "ivfi", + "version": "1.2.3", "license": "GPL-3.0", "dependencies": { - "js-cookie": "^2.2.1", - "vanilla-swipe": "^2.4.0" + "js-cookie": "^2.2.1" }, "devDependencies": { "@babel/eslint-parser": "^7.14.7", @@ -2756,9 +2755,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001375", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz", - "integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==", + "version": "1.0.30001606", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz", + "integrity": "sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==", "dev": true, "funding": [ { @@ -2768,6 +2767,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -4747,13 +4750,10 @@ "peer": true }, "node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, "bin": { "json5": "lib/cli.js" }, @@ -4816,9 +4816,9 @@ } }, "node_modules/loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -5061,9 +5061,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "peer": true, "dependencies": { @@ -5073,12 +5073,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, "node_modules/minimist-options": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", @@ -7587,11 +7581,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vanilla-swipe": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/vanilla-swipe/-/vanilla-swipe-2.4.0.tgz", - "integrity": "sha512-EdZdBWhPQvKz8DWHRXEHxsbJnU5wWKXrXblOV9ENElDnEK3tPkrBlAYK1pgPeuUb8k2DPQEK3tFi6W4cMZgUAg==" - }, "node_modules/vendors": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", @@ -9797,9 +9786,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001375", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz", - "integrity": "sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw==", + "version": "1.0.30001606", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz", + "integrity": "sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==", "dev": true }, "chalk": { @@ -11285,13 +11274,10 @@ "peer": true }, "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "kind-of": { "version": "6.0.3", @@ -11336,9 +11322,9 @@ "dev": true }, "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -11529,21 +11515,15 @@ } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "peer": true, "requires": { "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, "minimist-options": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", @@ -13332,11 +13312,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "vanilla-swipe": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/vanilla-swipe/-/vanilla-swipe-2.4.0.tgz", - "integrity": "sha512-EdZdBWhPQvKz8DWHRXEHxsbJnU5wWKXrXblOV9ENElDnEK3tPkrBlAYK1pgPeuUb8k2DPQEK3tFi6W4cMZgUAg==" - }, "vendors": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", diff --git a/package.json b/package.json index 93b4607..df87a9f 100755 --- a/package.json +++ b/package.json @@ -3,13 +3,13 @@ "name": "emy", "mail": "emy@five.sh" }, - "name": "eyy-indexer", - "version": "1.2.1", - "description": "A directory explorer", + "name": "ivfi", + "version": "1.2.3", + "description": "The image and video friendly indexer", "main": "./core/js/main.ts", + "type": "module", "dependencies": { - "js-cookie": "^2.2.1", - "vanilla-swipe": "^2.4.0" + "js-cookie": "^2.2.1" }, "devDependencies": { "@babel/eslint-parser": "^7.14.7", @@ -43,17 +43,18 @@ "make-standalone": "webpack --config webpack.config.js --mode=production && node ./scripts/make-standalone.js", "docker-populate": "rm -rf ./docker/public ./build && webpack --config webpack.config.js --mode=production && mv ./build ./docker/public", "lint": "eslint ./src/core/**/*.ts && stylelint ./src/css/**/*.scss", - "test": "npm run lint && npm run build && node ./scripts/make-standalone.js" + "test": "npm run lint && npm run build && node ./scripts/make-standalone.js", + "pack-release": "bash ./scripts/pack-release.sh" }, "repository": { "type": "git", - "url": "git+https://github.com/sixem/eyy-indexer.git", - "reference": "https://github.com/sixem/eyy-indexer" + "url": "git+https://github.com/sixem/ivfi-php.git", + "reference": "https://github.com/sixem/ivfi-php" }, "author": "emy", "license": "GPL-3.0", "bugs": { - "url": "https://github.com/sixem/eyy-indexer/issues" + "url": "https://github.com/sixem/ivfi-php/issues" }, - "homepage": "https://github.com/sixem/eyy-indexer#readme" + "homepage": "https://github.com/sixem/ivfi-php#readme" } diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..9a580f1 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +}; \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100755 index afeb286..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: [ - require('autoprefixer') - ] -} \ No newline at end of file diff --git a/scripts/make-standalone.js b/scripts/make-standalone.js index 80b91b4..1df5321 100755 --- a/scripts/make-standalone.js +++ b/scripts/make-standalone.js @@ -1,110 +1,140 @@ -const package = require('../package.json'); - -const fs = require('fs'); -const path = require('path'); +/** Imports */ +import fs from 'fs'; +import path from 'path'; +/** Data */ const data = { outFile : './build/standalone.php' }; -let stripComments = (source) => +/** + * Fired upon errors + * + * @param {*} error + */ +const onError = (error) => { - return source.replace(/(\/\*(?:(?!\*\/).|[\n\r])*\*\/)/, ''); -} + console.error('FAILED:' + error); + process.exit(1); +}; -let minifiedBanner = () => -{ - return ` -/*! - * eyy-indexer - ${package.description} (${package.version}) - * - * [https://github.com/sixem/eyy-indexer] - * - * Copyright (c) 2022 emy | five.sh | github.com/sixem - * Licensed under GPL-3.0 +/** + * Strips comments from a string + * + * @param {string} source */ - `; -} - -let replaceScriptTags = (binary) => +const stripComments = (source) => { - let matches = /(`; - } else { - throw new Error(`Unexisting asset file (${filePath}).`); + index = i; break; } } - return [binary, output]; -} + return index; +}; -let replaceStyleTags = (binary) => +/** + * Scans lines for a needle, finds the start and end of a block, removes it and returns the search item + * The search item is expected to be within the block lines + * + * @param {array} lines + * @param {string} needle + * @param {string} start + * @param {string} end + * @param {regexp} itemSearch + * @param {boolean} removeComment + * @param {integer} limit + */ +const spliceBySearch = (lines, needle, start, end, itemSearch, removeComment = true, limit = null) => { - let matches = /(\'\')/g.exec(binary); + /** Indexes (lines) removed */ + let splicedIndexes = []; - if(matches) - { - let filePath = `./build/${matches[2]}`; + /** Items fetched from block(s) */ + let splicedItems = []; - if(fs.existsSync(filePath)) + for(let i = (lines.length - 1); i > 0; i--) + { + if(lines[i].includes(needle)) { - console.log('\nReading stylesheet', '->', filePath); - - /* read .css file */ - let sheet = fs.readFileSync(filePath, 'utf-8'); - - let fonts = sheet.match(new RegExp(/(src\:\ ?url\(([A-Za-z0-9\.\/\-]+)\) format\("[A-Za-z0-9]+"\)\;)/g)); - - console.log('Found', fonts.length, 'font assets'); - - (fonts).forEach((font) => + /** Block start index */ + let startIndex = i; + + /** Find block start */ + const headerIndex = scanFromIndex( + lines, start, startIndex, true + ); + + if(headerIndex !== null) { - /* join asset path */ - const fontPath = path.join('./build', (font.split('url(')[1]).split(')')[0]); - - if(fs.existsSync(fontPath)) - { - console.log('Processing ->', fontPath); + if(removeComment && lines[headerIndex - 1].includes('/*')) + startIndex = (headerIndex - 1); - /* read font as base64 */ - let based = fs.readFileSync(fontPath, { - encoding : 'base64' - }); + /** Find block ending */ + const headerEnd = scanFromIndex(lines, end, headerIndex, false); - /* replace css font with base64 */ - sheet = sheet.replace(font, `src: url(data:application/font-woff2;charset=utf-8;base64,${based});`); - } else { - throw new Error(`Unexisting asset file (${fontPath}).`); + if(headerEnd !== null) + { + for(let i = startIndex; i <= headerEnd; i++) + { + /** Check for item match */ + const matches = itemSearch.exec(lines[i]); + + if(matches) + { + /** Get all lines removed */ + const spliceSpan = [ + ...Array((headerEnd + 1) - startIndex).keys() + ].map((i) => i + startIndex); + + /** Add to spliced indexes */ + splicedIndexes.push(...spliceSpan); + splicedItems.push(matches[1]); + + break; + } + } } - }); + } + } - binary = binary.replace(matches[1], `''`); - } else { - throw new Error(`Unexisting asset file (${filePath}).`); + /** Check for limits */ + if(limit && splicedItems.length >= limit) + { + break; } } - return binary; -} + /** Remove spliced lines */ + for(let i = splicedIndexes.length -1; i >= 0; i--) + { + lines.splice(splicedIndexes[i],1); + } + + return splicedItems.length > 0 ? { + lines, items: splicedItems, indexes: splicedIndexes + } : null; +}; console.log('Building standalone ..'); @@ -112,46 +142,121 @@ try { console.log('\nReading index ..'); - /* read php index */ - let source = fs.readFileSync('./build/indexer.php', 'binary'); - - /* replace script tags, return stripped source and script data */ - let [binary, scripts] = replaceScriptTags(source); - - /* set source without script tags */ - source = binary; + /* Read PHP index */ + const source = fs.readFileSync('./build/indexer.php', 'utf-8'); - /* split source by line */ + /* Split source by line */ let lines = source.split('\n'); - /* iterate over lines */ - for(let i = 0; i < lines.length; i++) + /** Extract and remove scripts */ + const splicedScripts = spliceBySearch( + lines, + "'type' => 'text/javascript'", '$header[]', ');', + /\'(\/[^\/\'\"]+\/main\.js)\?bust=%s\'/g + ); + + if(splicedScripts) { - let line = lines[i]; + /** Update lines */ + lines = splicedScripts.lines; + + let scriptsData = []; - /* check if line is ending body */ - if(line.includes('')) + /** Iterate over spliced scripts */ + for(const script of splicedScripts.items) { - /* insert script data before ending body (can't use defer in standalone) */ - lines.splice(i - 1, 0, scripts); + /** Construct script file relative */ + const scriptPath = script.split('/')[1]; + const scriptFile = `./build/${scriptPath}/${script.replace(`/${scriptPath}/`, '')}`; - console.log('Inserting scripts at line', i - 1); + console.log('\nReading script', '->', scriptFile); - /* break loop */ - break; + if(fs.existsSync(scriptFile)) + { + scriptsData.push(fs.readFileSync(scriptFile, 'binary')); + } else { + onError(`Script file: '${scriptFile}' does not exist!`); + } + } + + /** Get closing tag line */ + const bodyEnd = scanFromIndex(lines, '', lines.length - 1, true); + + if(bodyEnd !== null) + { + /** Inject script data */ + lines.splice(bodyEnd - 1, 0, scriptsData.map((data) => + { + return ``; + })); } } - /* create new source with inserted script data */ - source = lines.join('\n'); + /** Extract and remove stylesheets */ + const splicedStyles = spliceBySearch( + lines, + "$baseStylesheet", '$baseStylesheet', ');', + /\"(\/[^\/\'\"]+\/css\/style\.css)\?bust=%s\"/g, 1 + ); - /* replace stylesheets with raw css */ - source = replaceStyleTags(source); + if(splicedStyles) + { + /** Update lines */ + lines = splicedStyles.lines; + + /** Construct stylesheet relative path */ + const stylePath = `./build/${splicedStyles.items[0]}`; + + console.log('\nReading stylesheet', '->', stylePath); + + if(fs.existsSync(stylePath)) + { + /** Read stylesheet */ + let stylesheetData = stripComments(fs.readFileSync(stylePath, 'utf-8')); + + /** Find used fonts in stylesheet */ + const usedFonts = stylesheetData.match( + new RegExp(/(src\:\ ?url\(([A-Za-z0-9\.\/\-]+)\) format\("[A-Za-z0-9]+"\)\;)/g) + ); + + console.log('Found', usedFonts.length, 'font asset(s)'); + + /** Iterate over used fonts */ + for(const fontEntry of usedFonts) + { + /* Join asset path */ + const fontPath = path.join('./build', (fontEntry.split('url(')[1]).split(')')[0]); + + if(fs.existsSync(fontPath)) + { + console.log('Processing ->', fontPath); + + /* Read font as Base64 and replace it with the stylesheet entry */ + stylesheetData = stylesheetData.replace( + fontEntry, `src: url(data:application/font-woff2;charset=utf-8;base64,${fs.readFileSync(fontPath, { + encoding: 'base64' + })});` + ); + } else { + onError(`Font file: '${fontPath}' does not exist!`); + } + } + + lines.splice( + Math.min(...splicedStyles.indexes), 0, + `$baseStylesheet = '';` + ); + } else { + onError(`Stylesheet file: '${stylePath}' does not exist!`); + } + } console.log('\nWriting to', data.outFile); /* write output */ - fs.writeFileSync(data.outFile, source); + fs.writeFileSync(data.outFile, lines.join('\n')); /* get output stats */ let stats = fs.statSync(data.outFile) @@ -159,7 +264,5 @@ try console.log(`OK .. ${stats.size} (${Math.round(((stats.size / (1024)) + Number.EPSILON) * 100) / 100} kB)`); } catch(error) { - console.error(error); - - process.exit(1); + onError(error); } \ No newline at end of file diff --git a/scripts/pack-release.sh b/scripts/pack-release.sh index 49f3bac..3c351d8 100755 --- a/scripts/pack-release.sh +++ b/scripts/pack-release.sh @@ -1,4 +1,46 @@ -#!/bin/bash +#!/usr/bin/env bash + +# +# This script will package the script into a release zip file. +# +# It requires the following dependencies: 7z, jq and npm +# + + +set -o errexit # abort on nonzero exit status +set -o nounset # abort on unbound variable +set -o pipefail # don't hide errors within pipes + +no_dep_exit_code=3 + +where() { + local cmd + cmd="$(command -v "$1")" + echo "$cmd" +} + +e() { + >&2 echo "$1" +} + +jq_cmd="$(where jq)" +npm_cmd="$(where npm)" +sz_cmd="$(where 7z)" + +if ! [ -x "${jq_cmd}" ]; then + e "required dependency not found: jq not found in the path or not executable" + exit ${no_dep_exit_code} +fi + +if ! [ -x "${npm_cmd}" ]; then + e "required dependency not found: npm not found in the path or not executable" + exit ${no_dep_exit_code} +fi + +if ! [ -x "${sz_cmd}" ]; then + e "required dependency not found: 7z not found in the path or not executable" + exit ${no_dep_exit_code} +fi declare -a PACKAGE_FILES=( 'README.md' @@ -9,24 +51,29 @@ declare -a PACKAGE_FILES=( cd "$(dirname "$0")" && cd .. -VERSION=$(jq -r .version "package.json") -NAME=$(jq -r .name "package.json") +VERSION=$("$jq_cmd" -r .version "package.json") +NAME=$("$jq_cmd" -r .name "package.json") PACKAGED="$NAME-$VERSION.zip" -npm install -npm run make-standalone +e "Installing dependencies ..." +"$npm_cmd" install + +e "Building standalone ..." +"$npm_cmd" run make-standalone mkdir "standalone" mv "build/standalone.php" "standalone/indexer.php" -7z a "$PACKAGED" "standalone/" -7z a "$PACKAGED" "build/" +e "Creating release file ..." +"$sz_cmd" a "$PACKAGED" "standalone/" +"$sz_cmd" a "$PACKAGED" "build/" +e "Cleaning up ..." rm -rf "build" "standalone" for FILE in "${PACKAGE_FILES[@]}"; do - echo "Adding $FILE ..." - 7z a "$PACKAGED" "$FILE" + e "Adding $FILE ..." + "$sz_cmd" a "$PACKAGED" "$FILE" done -printf "\n\n>> $PACKAGED\n" \ No newline at end of file +e "> $PACKAGED" \ No newline at end of file diff --git a/src/core/classes/gallery/index.ts b/src/core/classes/gallery/index.ts index 85cc04f..175849d 100755 --- a/src/core/classes/gallery/index.ts +++ b/src/core/classes/gallery/index.ts @@ -1,6 +1,6 @@ /** Vendors */ import cookies from 'js-cookie'; -import Swipe, { EventData } from 'vanilla-swipe'; +import { SwipeEvent } from '../../vendors/swiped-events'; /** Config */ import data from '../../config/data'; import { user } from '../../config/config'; @@ -409,7 +409,6 @@ export default class galleryClass } DOM.style.set(body, { - 'max-height': 'calc(100vh - var(--height-gallery-top-bar))', 'overflow': 'hidden' }); } else { @@ -419,7 +418,6 @@ export default class galleryClass if(Object.prototype.hasOwnProperty.call(this.data, 'body')) { DOM.style.set(body, { - 'max-height': this.data.body['max-height'], 'overflow': this.data.body['overflow'] }); } @@ -1228,6 +1226,7 @@ export default class galleryClass eventHooks.listen(video, videoReadyEvents, 'awaitGalleryVideo', (): boolean | void => { /** Clear listener */ + //eventHooks.unlisten(video, videoReadyEvents, 'awaitGalleryVideo'); if(hasEvented || video.srcId !== this.data.selected.src) @@ -1401,7 +1400,7 @@ export default class galleryClass /* Trigger gallery item change event */ eventHooks.trigger('galleryItemChanged', { source: encodedItemSource, - index: index + index, image, video }); /* If selected item is an image */ @@ -1432,16 +1431,6 @@ export default class galleryClass wrapper.prepend(cover); cover.append(image); - - /* Listener for mouse enter on image cover */ - cover.addEventListener('mouseenter', (e: Event) => - { - if(this.options.reverseOptions) - { - /* Show reverse options if enabled */ - this.reverse(e.currentTarget as HTMLElement); - } - }); } /* Await image loading */ @@ -1459,12 +1448,17 @@ export default class galleryClass height: h } }); + + if(this.options.reverseOptions) + { + /* Show reverse options if enabled */ + this.reverse(image as HTMLElement); + } } } }).catch((error: unknown) => { /* Image could not be loaded */ - console.error(error); this.busy(false); @@ -1829,37 +1823,48 @@ export default class galleryClass onAdd: this.removeOnUnbind }); + /* Used to create a scroll break to avoid accidental multi-swipes */ + let swipeTimeout: null | number = null; + let swipeBreak = false; + if(this.options.mobile === true) { + const swipeTarget = document.querySelector('body > div.rootGallery div.wrapper'); + /* Handle swipe events */ - const handler = (event: Event, eventData: EventData): void => + swipeTarget.addEventListener('swiped', (e: SwipeEvent) => { - switch(eventData.directionX) + clearTimeout(swipeTimeout); + + if(!swipeBreak) { - case 'RIGHT': + if(e.detail.dir === 'down' || e.detail.dir === 'right') + { + /** Navigate forwards */ this.navigate(null, -1); - break; - case 'LEFT': + swipeBreak = true; + } else if(e.detail.dir === 'up' || e.detail.dir === 'left') + { + /** Navigate backwards */ this.navigate(null, 1); - break; - } - }; - /* Create swipe events */ - const swipeInstance = new Swipe({ - element: document.querySelector('body > div.rootGallery'), - onSwiped: handler, - mouseTrackingEnabled: true + swipeBreak = true; + } + } + + /** Effectively locks swiping for 200 ms */ + swipeTimeout = window.setTimeout(() => + { + swipeBreak = false; + }, 200); }); - - /* Initialize */ - swipeInstance.init(); } /* Scroll navigation listener */ - eventHooks.listen('body > div.rootGallery > div.galleryContent > \ - div.media', ['scroll', 'DOMMouseScroll', 'mousewheel'], 'galleryScrollNavigate', + eventHooks.listen( + 'body > div.rootGallery > div.galleryContent > div.media', + ['scroll', 'DOMMouseScroll', 'mousewheel'], 'galleryScrollNavigate', (event: WheelEvent): void | boolean => { if(this.options.scrollInterval > 0 && this.data.scrollbreak === true) diff --git a/src/core/classes/optimize/index.ts b/src/core/classes/optimize/index.ts index 6e73818..229dec2 100755 --- a/src/core/classes/optimize/index.ts +++ b/src/core/classes/optimize/index.ts @@ -200,8 +200,6 @@ export default class optimizeClass /* Store offsets */ const rowOffsets: { [key: string]: number; } = {}; - let recentHeight = 0; - /* Create updated structure */ for(let index = 0; index < (this.rows).length; index++) { @@ -216,15 +214,9 @@ export default class optimizeClass rowOffsets[index] = combinedHeight; combinedHeight += item._offsetHeight; - recentHeight = item._offsetHeight; } } - if(recentHeight > 0) - { - combinedHeight = (combinedHeight - recentHeight); - } - combinedHeight += this.padding; /* Update optimize structure */ diff --git a/src/core/classes/selector/index.ts b/src/core/classes/selector/index.ts index 7bfdadc..33e55e8 100755 --- a/src/core/classes/selector/index.ts +++ b/src/core/classes/selector/index.ts @@ -11,6 +11,7 @@ export default class selectorClass FILTER_INPUT: ':scope > div.filterContainer > input[type="text"]', TOP_EXTEND: ':scope > div.topBar > div.extend', TABLE_CONTAINER: ':scope > div.tableContainer', + README_CONTAINER: ':scope > .readmeContainer', TABLE: ':scope > div.tableContainer > table', PATH: ':scope > div.path' }; diff --git a/src/core/components/main/index.ts b/src/core/components/main/index.ts index f159fa4..b2673c5 100755 --- a/src/core/components/main/index.ts +++ b/src/core/components/main/index.ts @@ -292,13 +292,13 @@ main.dates.load = () => { const offset: number = main.dates.offsetGet(); const client: TUserClient = user.get(); - const update: boolean = client.timezone_offset !== offset; + const update: boolean = client.timezoneOffset !== offset; /* Only update if offset is changed or unset */ if(update) { /* Update client's offset */ - client.timezone_offset = offset; + client.timezoneOffset = offset; /* Save client */ user.set(client); diff --git a/src/core/components/settings/index.ts b/src/core/components/settings/index.ts index 77a8e99..bac8448 100755 --- a/src/core/components/settings/index.ts +++ b/src/core/components/settings/index.ts @@ -175,10 +175,7 @@ create.check = (options, selected = null) => */ update.style.theme = (value: any): void => { - theme.set( - value === false ? null : value, - false - ); + theme.set(value === false ? null : value, false); }; /** @@ -283,6 +280,8 @@ update.gallery.fitContent = (value: boolean) => /** * Update gallery autoplay option + * + * @param value new autoplay state */ update.gallery.autoplay = (value: boolean) => { @@ -295,8 +294,11 @@ update.gallery.autoplay = (value: boolean) => /** * Gathers set options + * + * @param container settings container + * @returns {object} object containing set options */ -options.gather = (container) => +options.gather = (container: HTMLElement) => { const gathered: MComponentSettings.TGathered = {}; @@ -313,6 +315,7 @@ options.gather = (container) => { const id: string = element.getAttribute('name'); + /** Get section identifier */ const section: string = element.hasAttribute('data-key') ? element.getAttribute('data-key') : element.closest('.section').getAttribute('data-key'); @@ -324,7 +327,11 @@ options.gather = (container) => if(element.tagName === 'SELECT') { - gathered[section][id] = element.selectedIndex; + const setValue = (id === 'theme' + ? element[element.selectedIndex].value + : element.selectedIndex); + + gathered[section][id] = setValue; } else if(element.tagName === 'INPUT' && element.getAttribute('type').toUpperCase() === 'CHECKBOX') @@ -338,18 +345,24 @@ options.gather = (container) => }; /** - * Apply options + * Applies the settings passed to the function + * + * @param setData settings to apply + * @param client user client instance + * @returns {object} an object containing the changed settings */ options.set = (setData: object, client: TUserClient) => { client = client || user.get(); + /** Perform reload flag */ + let performReload = false; + Object.keys(setData).forEach((key) => { const isMain: boolean = (key === 'main'); - if(!isMain - && !Object.prototype.hasOwnProperty.call(client, key)) + if(!isMain && !Object.prototype.hasOwnProperty.call(client, key)) { client[key] = {}; } @@ -358,35 +371,30 @@ options.set = (setData: object, client: TUserClient) => { let value = null; - config.get(`style.themes.pool.${setData[key][option]}`); - switch(option) { case 'theme': - if(setData[key][option] - <= (config.get('style.themes.pool').length - 1)) - { - const selected: string | boolean = config.get( - `style.themes.pool.${setData[key][option]}` - ); - - value = selected === 'default' - ? false - : selected; + if(Object.prototype.hasOwnProperty.call( + config.get('style.themes.pool'), setData[key][option] + )) { + const selected = setData[key][option]; + value = (selected === 'default' ? false : selected); } - + break; default: value = setData[key][option]; - + break; } + /** Check if the option has changed, and if so, flag it for updating */ const changed: boolean = (isMain ? (client[option] !== value) : (client[key][option] !== value) ); + /** Recreate object - set changed state and value */ setData[key][option] = { value, changed }; @@ -400,7 +408,7 @@ options.set = (setData: object, client: TUserClient) => if(changed) { - /* Call the live update function (if any) for the changed settings */ + /* Call any live updating functions for the changed setting */ if(isMain && Object.prototype.hasOwnProperty.call(update, option)) { @@ -409,24 +417,52 @@ options.set = (setData: object, client: TUserClient) => { update[key][option](value); } + + /** + * Themes can alter the way the page is being displayed, and + * it may therefor create different offsets that may mess with + * certain functions, like the optimizer etc. + * + * Attempting to force a reload between changing themes will ensure + * that the page is being properly displayed with the newly set theme. + */ + if(option === 'theme') + { + performReload = true; + } } }); }); log('settings', 'Set settings:', setData); + /** Save settings to client */ user.set(client); + /** Reload page if needed */ + if(performReload) + { + location.reload(); + } + return setData; }; /** * Sets a theme for the client + * + * @param theme the theme to set as active + * @param setCookie whether or not to set a cookie for the theme + * @returns {void | boolean} */ -theme.set = (theme: any = null, setCookie = true) => +theme.set = (theme: any = null, setCookie = true): void | boolean => { + /** Get the current themes path (relative to the indexer) */ const themesPath = config.get('style.themes.path'); + /** Get the set path of the theme that is to be applied */ + const setThemesPath = config.get(`style.themes.pool.${theme}.path`); + /* Get current stylesheets */ const activeStylesheets: NodeListOf = document.querySelectorAll( 'head > link[rel="stylesheet"]' @@ -451,7 +487,7 @@ theme.set = (theme: any = null, setCookie = true) => config.set('style.themes.set', theme); /* If null theme, then remove active sheets */ - if(theme === null || !theme) + if(!theme) { if(stylesheets.length > 0) { @@ -467,22 +503,25 @@ theme.set = (theme: any = null, setCookie = true) => } } - /* Create stylesheet element */ - const sheet = DOM.new('link', { - rel : 'stylesheet', - type : 'text/css', - href : `${themesPath}/${theme}.css?bust=${ - config.data.bust - }`.replace(/\/\//g, '/') - }); + if(setThemesPath) + { + /* Create stylesheet element */ + const sheet = DOM.new('link', { + rel : 'stylesheet', + type : 'text/css', + href : `${setThemesPath}?bust=${ + config.data.bust + }`.replace(/\/\//g, '/') + }); - /* Apply to document */ - document.querySelector('head').append(sheet); + /* Apply to document */ + document.querySelector('head').append(sheet); - /* Remove stylesheets that were active prior to change */ - if(stylesheets.length > 0) - { - stylesheets.forEach((sheet): void => sheet.remove()); + /* Remove stylesheets that were active prior to change */ + if(stylesheets.length > 0) + { + stylesheets.forEach((sheet): void => sheet.remove()); + } } }; @@ -503,7 +542,7 @@ export class componentSettings available = (): boolean => { if(config.exists('style.themes.pool') - && config.get('style.themes.pool').length > 0 + && Object.keys(config.get('style.themes.pool')).length > 0 || config.get('gallery.enabled') === true) { return true; @@ -521,6 +560,7 @@ export class componentSettings options.set(options.gather(element), client); + /** Call functions on settings applied */ data.components.settings.close(); data.layer.main.update(); } @@ -530,7 +570,7 @@ export class componentSettings */ close = (): void => { - /* Remove events */ + /** Remove events */ Object.keys(this.boundEvents).forEach((eventId: string) => { const { selector, events } = this.boundEvents[eventId]; @@ -598,7 +638,9 @@ export class componentSettings name : key }, () => { - return checkNested(this.client, 'gallery', key) ? (this.client.gallery[key]) : config.get(`gallery.${key}`); + return checkNested(this.client, 'gallery', key) + ? (this.client.gallery[key]) + : config.get(`gallery.${key}`); }), label, { class : 'interactable' }, description) @@ -616,8 +658,11 @@ export class componentSettings getSectionMain = (section: HTMLElement = create.section('main'), settings = 0) => { if(config.exists('style.themes.pool') - && config.get('style.themes.pool').length > 0) + && typeof config.get('style.themes.pool') === 'object') { + const configThemesPool = config.get('style.themes.pool'); + const configThemesKeys = Object.keys(configThemesPool); + type TPoolItem = { value: string; text: string; @@ -627,15 +672,15 @@ export class componentSettings const setTheme: string | null = config.get('style.themes.set'); - const themePool: TPoolCapsule = config.get( - 'style.themes.pool' - ).map((theme: TPoolItem) => - { - return { - value: theme, - text: theme - }; - }); + const themePool: TPoolCapsule = configThemesKeys.map( + (key: string) => + { + return { + value: key, + text: key + }; + } + ); const selectTemplate: [TPoolCapsule, object, any] = [themePool, { 'name': 'theme', diff --git a/src/core/config/config.ts b/src/core/config/config.ts index f189ff2..44a6926 100755 --- a/src/core/config/config.ts +++ b/src/core/config/config.ts @@ -16,7 +16,7 @@ const user: TUserClient = {}; config.init = (): void => { config.data = JSON.parse(document.getElementById(ScriptDataId).innerHTML); - config.data.mobile = Modernizr.mq('(max-width: 640px)'); + config.data.mobile = Modernizr.mq('(max-width: 768px)'); }; config.isMobile = (): boolean => diff --git a/src/core/constant/index.ts b/src/core/constant/index.ts index e2f72a4..b1b0b3a 100755 --- a/src/core/constant/index.ts +++ b/src/core/constant/index.ts @@ -15,9 +15,9 @@ const Keys: TKeyReferences = { l: 'KeyL' }; -const StorageKey = 'indexer'; -const CookieKey = 'ei-client'; -const ScriptDataId = '__INDEXER_DATA__'; +const StorageKey = 'IVFi'; +const CookieKey = 'IVFi'; +const ScriptDataId = '__IVFI_DATA__'; export { Keys, diff --git a/src/core/data.json b/src/core/data.json index 2c9c058..ae45876 100755 --- a/src/core/data.json +++ b/src/core/data.json @@ -35,9 +35,9 @@ } }, "reverseSearch" : { - "Google" : "https://www.google.com/searchbyimage?image_url={URL}&safe=off", + "Google" : "https://lens.google.com/uploadbyurl?url={URL}", "Yandex" : "https://yandex.com/images/search?rpt=imageview&url={URL}", "Bing" : "https://bing.com/images/search?q=imgurl:{URL}&view=detailv2&iss=sbi#enterInsights", "SauceNAO" : "https://saucenao.com/search.php?url={URL}" } -} \ No newline at end of file +} diff --git a/src/core/main.ts b/src/core/main.ts index a9c93ee..f136f85 100755 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -1,7 +1,8 @@ /** Vendors */ +import './vendors/swiped-events'; import hoverPreview from './vendors/hover-preview/hover-preview'; /** Config */ -import { config } from './config/config'; +import { config, user } from './config/config'; import data from './config/data'; /** Modules */ import { log } from './modules/logger'; @@ -133,6 +134,38 @@ eventHooks.listen(selector.use('FILTER_INPUT') as HTMLElement, 'input', 'filterI data.components.filter.apply(e.currentTarget.value); }); +/** + * Readme toggle event + */ +if(selector.use('README_CONTAINER')) +{ + eventHooks.listen(selector.use('README_CONTAINER') as HTMLElement, 'toggle', 'toggledReadme', (e) => + { + const client = user.get(); + + if(!client.readme) + { + client.readme = {}; + } + + client.readme.toggled = e.target.hasAttribute('open'); + + user.set(client); + + /** + * Refresh performance rows + * + * This is done because long readme content can lead to a deep set of + * rows previously being out of view, becoming "visible" without the + * optimizer rendering them as visible. + */ + if(data.instances.optimize.main.enabled) + { + data.instances.optimize.main.attemptRefresh(); + } + }); +} + /** * Item click event (show gallery if enabled and table sort) */ @@ -174,17 +207,26 @@ eventHooks.listen(selector.use('TABLE') as HTMLElement, 'click', 'sortClick', (e }); /** - * Recheck mobile sizing on resize + * Re-check mobile sizing on resize */ eventHooks.listen(window, 'resize', 'windowResize', debounce((): void => { - log('event', 'windowResize (main)', 'Resized.'); + log('event', 'windowResize (main)', 'Resized'); + + /** Get mobile status */ + const isMobile = Modernizr.mq('(max-width: 768px)'); - config.set('mobile', Modernizr.mq('(max-width: 640px)')); + if(config.get('mobile') !== isMobile) + { + /** Update mobile status */ + config.set('mobile', Modernizr.mq('(max-width: 768px)')); + + log('view', `Switched to ${isMobile ? 'mobile' : 'desktop'} view`); + } if(data.instances.gallery) { - data.instances.gallery.options.mobile = config.get('mobile'); + data.instances.gallery.options.mobile = isMobile; data.instances.gallery.update.listWidth(); } diff --git a/src/core/types/common/index.ts b/src/core/types/common/index.ts index 711c1db..033aebb 100755 --- a/src/core/types/common/index.ts +++ b/src/core/types/common/index.ts @@ -55,6 +55,8 @@ export type TExtensionArray = { export type TPayloadgalleryItemChanged = { source: string; index: number; + image: HTMLElement | undefined; + video: HTMLElement | undefined; }; /** diff --git a/src/core/types/module-config/index.ts b/src/core/types/module-config/index.ts index 5d78353..735e8bd 100755 --- a/src/core/types/module-config/index.ts +++ b/src/core/types/module-config/index.ts @@ -14,7 +14,7 @@ export type TUserClient = { style?: { compact?: boolean; }; - timezone_offset?: number | boolean; + timezoneOffset?: number | boolean; sort?: { ascending?: boolean | number; row?: number; @@ -38,6 +38,9 @@ export type TUserStorage = { compact?: boolean; theme?: boolean | string; }; + readme?: { + toggled?: boolean; + }; }; /** diff --git a/src/core/vendors/swiped-events/index.ts b/src/core/vendors/swiped-events/index.ts new file mode 100644 index 0000000..bebc993 --- /dev/null +++ b/src/core/vendors/swiped-events/index.ts @@ -0,0 +1,191 @@ +/** + * Global `window` extensions + */ +interface WindowCustomEvents extends Window { + CustomEvent?: any; +}; + +type SwipeEvent = CustomEvent<{ + // swipe direction + dir: 'up' | 'down' | 'left' | 'right', + // touch type - stylus=apple pencil and direct=finger + touchType: 'stylus' | 'direct', + // x coords of swipe start + xStart: number, + // x coords of swipe end + xEnd: number, + // y coords of swipe start + yStart: number, + // y coords of swipe end + yEnd: number +}>; + +/*! + * swiped-events.js - v@version@ + * Pure JavaScript swipe events + * https://github.com/john-doherty/swiped-events + * @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element + * @author John Doherty + * @license MIT + */ +(function (window, document) { + + 'use strict'; + + // patch CustomEvent to allow constructor creation (IE/Chrome) + if (typeof window.CustomEvent !== 'function') { + + (window as WindowCustomEvents).CustomEvent = function (event, params) { + + params = params || { bubbles: false, cancelable: false, detail: undefined }; + + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + }; + + (window as WindowCustomEvents).CustomEvent.prototype = window.Event.prototype; + } + + document.addEventListener('touchstart', handleTouchStart, false); + document.addEventListener('touchmove', handleTouchMove, false); + document.addEventListener('touchend', handleTouchEnd, false); + + var xDown = null; + var yDown = null; + var xDiff = null; + var yDiff = null; + var timeDown = null; + var startEl = null; + + /** + * Fires swiped event if swipe detected on touchend + * @param {object} e - browser event object + * @returns {void} + */ + function handleTouchEnd(e) { + + // if the user released on a different target, cancel! + if (startEl !== e.target) return; + + var swipeThreshold = parseInt(getNearestAttribute(startEl, 'data-swipe-threshold', '20'), 10); // default 20 units + var swipeUnit = getNearestAttribute(startEl, 'data-swipe-unit', 'px'); // default px + var swipeTimeout = parseInt(getNearestAttribute(startEl, 'data-swipe-timeout', '500'), 10); // default 500ms + var timeDiff = Date.now() - timeDown; + var eventType = ''; + var changedTouches = e.changedTouches || e.touches || []; + + if (swipeUnit === 'vh') { + swipeThreshold = Math.round((swipeThreshold / 100) * document.documentElement.clientHeight); // get percentage of viewport height in pixels + } + if (swipeUnit === 'vw') { + swipeThreshold = Math.round((swipeThreshold / 100) * document.documentElement.clientWidth); // get percentage of viewport height in pixels + } + + if (Math.abs(xDiff) > Math.abs(yDiff)) { // most significant + if (Math.abs(xDiff) > swipeThreshold && timeDiff < swipeTimeout) { + if (xDiff > 0) { + eventType = 'swiped-left'; + } + else { + eventType = 'swiped-right'; + } + } + } + else if (Math.abs(yDiff) > swipeThreshold && timeDiff < swipeTimeout) { + if (yDiff > 0) { + eventType = 'swiped-up'; + } + else { + eventType = 'swiped-down'; + } + } + + if (eventType !== '') { + + var eventData = { + dir: eventType.replace(/swiped-/, ''), + touchType: (changedTouches[0] || {}).touchType || 'direct', + xStart: parseInt(xDown, 10), + xEnd: parseInt((changedTouches[0] || {}).clientX || -1, 10), + yStart: parseInt(yDown, 10), + yEnd: parseInt((changedTouches[0] || {}).clientY || -1, 10) + }; + + // fire `swiped` event event on the element that started the swipe + startEl.dispatchEvent(new CustomEvent('swiped', { bubbles: true, cancelable: true, detail: eventData })); + + // fire `swiped-dir` event on the element that started the swipe + startEl.dispatchEvent(new CustomEvent(eventType, { bubbles: true, cancelable: true, detail: eventData })); + } + + // reset values + xDown = null; + yDown = null; + timeDown = null; + } + + /** + * Records current location on touchstart event + * @param {object} e - browser event object + * @returns {void} + */ + function handleTouchStart(e) { + + // if the element has data-swipe-ignore="true" we stop listening for swipe events + if (e.target.getAttribute('data-swipe-ignore') === 'true') return; + + startEl = e.target; + + timeDown = Date.now(); + xDown = e.touches[0].clientX; + yDown = e.touches[0].clientY; + xDiff = 0; + yDiff = 0; + } + + /** + * Records location diff in px on touchmove event + * @param {object} e - browser event object + * @returns {void} + */ + function handleTouchMove(e) { + + if (!xDown || !yDown) return; + + var xUp = e.touches[0].clientX; + var yUp = e.touches[0].clientY; + + xDiff = xDown - xUp; + yDiff = yDown - yUp; + } + + /** + * Gets attribute off HTML element or nearest parent + * @param {object} el - HTML element to retrieve attribute from + * @param {string} attributeName - name of the attribute + * @param {any} defaultValue - default value to return if no match found + * @returns {any} attribute value or defaultValue + */ + function getNearestAttribute(el, attributeName, defaultValue) { + + // walk up the dom tree looking for attributeName + while (el && el !== document.documentElement) { + + var attributeValue = el.getAttribute(attributeName); + + if (attributeValue) { + return attributeValue; + } + + el = el.parentNode; + } + + return defaultValue; + } + +}(window, document)); + +export { + SwipeEvent +}; \ No newline at end of file diff --git a/src/css/gallery.scss b/src/css/gallery.scss index 6039763..2aebd9e 100755 --- a/src/css/gallery.scss +++ b/src/css/gallery.scss @@ -304,6 +304,7 @@ html > body > .rootGallery { justify-content: center; div.error { + position: absolute; color: #d83232; display: block; width: 100%; diff --git a/src/css/main.scss b/src/css/main.scss index 461fef7..b6b5f59 100755 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -38,6 +38,63 @@ html > body { scrollbar-color: #222 #131315; background-color: map.get(variables.$root, "color-background-body"); + /* Readme container */ + .readmeContainer { + background-color: #121318; + margin-bottom: 18px; + border-top: 1px solid #19191d; + border-bottom: 1px solid #19191d; + text-align: center; + + > .readmeContents { + padding: 5px 8px; + position: relative; + } + + a { + text-decoration: none; + color: #d6d3ce; + font-weight: bold; + + &:hover { + text-decoration: underline; + color: #5e74ea; + } + } + + &:not([open]) { + summary { + &::before { + transform: rotate(180deg); + } + } + } + + summary { + text-align: right; + background-color: #15161e; + display: block; + padding: 3px 8px 5px 6px; + color: #939498; + border-bottom: 1px solid #151822; + user-select: none; + cursor: pointer; + + &::before { + content: "\25BE"; + transform: rotate(0deg); + display: inline-block; + margin-right: 5px; + transition: transform 0.25s; + } + } + + pre { + white-space: pre-wrap; + line-height: 145%; + } + } + > div.navigateLoad { $spinner-size: 26px; $spinner-fade-duration: 0.1s; @@ -472,87 +529,85 @@ html > body { letter-spacing: 1px; background-color: #121216f0; - > { - .wrapper { - max-height: 75vh; - overflow-y: auto; - - > div.section { - padding: 10px 0; - - > div.header { - padding: 4px 17px; - margin-bottom: 14px; - font-size: 13px; - color: #797979; - box-shadow: 0 0 1px #0d0d0d; - background-color: #17171d; - border-top: 1px solid #1d1d25; - border-bottom: 1px solid #1d1d25; - } + > .wrapper { + max-height: 75vh; + overflow-y: auto; + + > div.section { + padding: 10px 0; + + > div.header { + padding: 4px 17px; + margin-bottom: 14px; + font-size: 13px; + color: #797979; + box-shadow: 0 0 1px #0d0d0d; + background-color: #17171d; + border-top: 1px solid #1d1d25; + border-bottom: 1px solid #1d1d25; + } - &:first-child { - padding-top: 0; - } + &:first-child { + padding-top: 0; + } - .option { - padding: 0 10px; - display: table; - width: calc(100% - 20px); - height: 25px; + .option { + padding: 0 10px; + display: table; + width: calc(100% - 20px); + height: 25px; - &.interactable { - cursor: pointer; + &.interactable { + cursor: pointer; - input { - cursor: pointer; - } + input { + cursor: pointer; } + } - &:not(:first-child) { - margin-top: 10px; - } + &:not(:first-child) { + margin-top: 10px; + } - > div { - display: table-cell; - width: 50%; + > div { + display: table-cell; + width: 50%; - &:last-child { - width: auto; - text-align: right; - } + &:last-child { + width: auto; + text-align: right; } } } } + } - /* Settings apply and cancel buttons */ - div.bottom { - display: table; - width: 100%; - margin-top: 4px; - background-color: #17171d; - border-top: 1px solid #1d1d25; + /* Settings apply and cancel buttons */ + > div.bottom { + display: table; + width: 100%; + margin-top: 4px; + background-color: #17171d; + border-top: 1px solid #1d1d25; - > div { - text-align: center; - padding: 6px 7px 8px; - display: table-cell; - font-size: 12px; + > div { + text-align: center; + padding: 6px 7px 8px; + display: table-cell; + font-size: 12px; - &:first-child { - border-right: 1px solid #1c1c1d; - } + &:first-child { + border-right: 1px solid #1c1c1d; + } - &:not(:last-child) { - border-right: 1px solid #292929; - width: 50%; - } + &:not(:last-child) { + border-right: 1px solid #292929; + width: 50%; + } - &:hover { - cursor: pointer; - background-color: #1c1c25; - } + &:hover { + cursor: pointer; + background-color: #1c1c25; } } } diff --git a/src/php/template.php b/src/php/template.php index f67359e..5d2353d 100755 --- a/src/php/template.php +++ b/src/php/template.php @@ -1,9 +1,8 @@ [https://github.com/sixem/eyy-indexer] + * [https://github.com/sixem/ivfi-php] * - * @license https://github.com/sixem/eyy-indexer/blob/master/LICENSE GPL-3.0 + * @license https://github.com/sixem/ivfi-php/blob/master/LICENSE GPL-3.0 * @author emy (sixem@github) * @version <%= version %> */ @@ -11,89 +10,122 @@ /** * [Configuration] * A more in-depth overview can be found here: - * https://github.com/sixem/eyy-indexer/blob/master/CONFIG.md + * https://git.five.sh/ivfi/docs/php/#/config */ /* Used to bust the cache and to display footer version number */ $version = '<%= version %>'; -$config = array( - /* Authentication options */ +$config = [ + /** + * Authentication options + */ 'authentication' => false, - /* Enables single-page features */ + /** + * Enables single-page features + */ 'single_page' => false, - /* Formatting options */ - 'format' => array( + /** + * Formatting options + */ + 'format' => [ 'title' => 'Index of %s', /* Title format where %s is the current path */ - 'date' => array('d/m/y H:i', 'd/m/y'), /* Date formats (desktop, mobile) */ - 'sizes' => array(' B', ' KiB', ' MiB', ' GiB', ' TiB') /* Size formats */ - ), - /* Favicon options */ - 'icon' => array( + 'date' => ['d/m/y H:i', 'd/m/y'], /* Date formats (desktop, mobile) */ + 'sizes' => [' B', ' KiB', ' MiB', ' GiB', ' TiB'] /* Size formats */ + ], + /** + * Favicon options + */ + 'icon' => [ 'path' => '/favicon.ico', /* What favicon to use */ 'mime' => 'image/x-icon' /* Favicon mime type */ - ), - /* Sorting options. Used as default until the client sets their own sorting settings */ - 'sorting' => array( + ], + /** + * Sorting options. + * + * Used as default until the client sets their own sorting settings + */ + 'sorting' => [ 'enabled' => false, /* Whether the server should sort the items */ 'order' => SORT_ASC, /* Sorting order. asc or desc */ 'types' => 0, /* What item types to sort. 0 = both. 1 = files only. 2 = directories only */ 'sort_by' => 'name', /* What to sort by. available options are name, modified, type and size */ 'use_mbstring' => false /* Enabled mbstring when sorting */ - ), - /* Gallery options */ - 'gallery' => array( + ], + /** + * Gallery options + */ + 'gallery' => [ 'enabled' => true, /* Whether the gallery plugin should be enabled */ 'reverse_options' => false, /* Reverse search options for images (when hovering over them) */ 'scroll_interval' => 50, /* Break in ms between scroll navigation events */ 'list_alignment' => 0, /* List alignment where 0 is right and 1 is left */ 'fit_content' => true, /* Whether the media should be forced to fill the screen space */ 'image_sharpen' => false, /* Attempts to disable browser blurriness on images */ - ), - /* Preview options */ - 'preview' => array( + ], + /** + * Preview options + */ + 'preview' => [ 'enabled' => true, /* Whether the preview plugin should be enabled */ 'hover_delay' => 75, /* Delay in milliseconds before the preview is shown */ 'cursor_indicator' => true /* Displays a loading cursor while the preview is loading */ - ), - /* Extension that should be marked as media. - * These extensions will have potential previews and will be included in the gallery */ - 'extensions' => array( - 'image' => array('jpg', 'jpeg', 'png', 'gif', 'ico', 'svg', 'bmp', 'webp'), - 'video' => array('webm', 'mp4', 'ogg', 'ogv', 'mov') - ), - /* Injection options */ + ], + /** + * Extension that should be marked as media. + * These extensions will have potential previews and will be included in the gallery + */ + 'extensions' => [ + 'image' => ['jpg', 'jpeg', 'png', 'gif', 'ico', 'svg', 'bmp', 'webp'], + 'video' => ['webm', 'mp4', 'ogg', 'ogv', 'mov'] + ], + /** + * Injection options + */ 'inject' => false, - /* Styling options */ - 'style' => array( + /** + * Styling options + */ + 'style' => [ /* Set to a path relative to the root directory (location of this file) containg .css files. * Each .css file will be treated as a separate theme. Set to false to disable themes */ - 'themes' => array( - 'path' => false, + 'themes' => [ + 'path' => '/<%= indexerPath %>/themes/', 'default' => false - ), + ], /* Cascading style sheets options */ - 'css' => array( + 'css' => [ 'additional' => false - ), + ], /* Enables a more compact styling of the page */ 'compact' => false - ), - /* Filter what files or directories to show. - * Uses regular expressions. All names !matching! the regex will be shown. - * Setting the value to false will disable the respective filter */ - 'filter' => array( + ], + /** + * Filter what files or directories to show. + + * Uses regular expressions. All names *matching* the regex will be shown. + * Setting the value to false will disable the respective filter + */ + 'filter' => [ 'file' => false, 'directory' => false - ), - /* Calculates the size of directories. - * This can be intensive, especially with the recursive option, so be aware of that */ - 'directory_sizes' => array( + ], + /** Extensions to exclude */ + 'exclude' => false, + /** + * Calculates the size of directories. + + * This can be intensive, especially with the recursive + * option, so be aware of that + */ + 'directory_sizes' => [ /* Whether directory sizes should be calculated or not */ 'enabled' => false, /* Recursively scans the directories when calculating the size */ 'recursive' => false - ), + ], + /* Metadata options */ + 'metadata' => false, /* Processing functions */ 'processor' => false, /* Should ? and # characters be encoded when processing URLs */ @@ -101,54 +133,362 @@ /* Whether this .php file should be directly accessible */ 'allow_direct_access' => false, /* Set to 'strict' or 'weak'. - * 'strict' uses realpath() to avoid backwards directory traversal whereas 'weak' uses a similar string-based approach */ + * 'strict' uses realpath() to avoid backwards directory traversal + * whereas 'weak' uses a similar string-based approach */ 'path_checking' => 'strict', /* Enabled the performance mode */ 'performance' => false, - /* Whether extra information in the footer should be generated (page load time, path etc.) */ - 'footer' => array( + /* Whether extra information in the footer should be generated */ + 'footer' => [ 'enabled' => true, 'show_server_name' => true - ), - /* Displays a simple link to the git repository in the footer along with the current version. - * I would really appreciate it if you would keep this enabled */ + ], + /** + * Displays a simple link to the git repository in the + * footer along with the current version. + * + * I would really appreciate it if you would keep this enabled + */ 'credits' => true, - /* Enables console output in JS and PHP debugging. - * Also enables random query-strings for js/css files to bust the cache */ + /** + * Enables console output in JS and PHP debugging. + * Also enables random query-strings for js/css files to bust the cache + */ 'debug' => true -); - -/* Get current request URI */ -$currentUri = rawurldecode($_SERVER['REQUEST_URI']); - -/* Look for a config file in the current directory */ -$configFile = (basename(__FILE__, '.php') . '.config.php'); +]; /* Any potential libraries and so on for extra features will appear here */ <%= buildInject.readmeSupport && buildInject.readmeSupport.PARSEDOWN_LIBRARY ? buildInject.readmeSupport.PARSEDOWN_LIBRARY : null %> -/* If found, it'll override the above configuration values. - * Any unset values in the file will take the default values */ -if(file_exists($configFile)) -{ - $config = include($configFile); -} else if(file_exists('.' . $configFile)) /* Also check for hidden (.) file */ +/* Define current request URI */ +define('CURRENT_URI', rawurldecode($_SERVER['REQUEST_URI'])); +/* Define default configuration file */ +define('CONFIG_FILE', basename(__FILE__, '.php') . '.config.php'); +/* Define default dotfile name */ +define('DOTFILE_NAME', '.ivfi'); +/** Define script identifier */ +define('SCRIPT_ID', '__IVFI_DATA__'); +/* Define the base path of the Indexer */ +define('BASE_PATH', isset($_SERVER['INDEXER_BASE_PATH']) + ? $_SERVER['INDEXER_BASE_PATH'] + : dirname(__FILE__)); + +/* Check if cookie is set */ +$client = isset($_COOKIE['IVFi']) ? $_COOKIE['IVFi'] : NULL; + +/** Define the current theme */ +$currentTheme = NULL; + +/* If client cookie is set, then parse it using `json_decode()` */ +if($client) { - $config = include('.' . $configFile); + $client = json_decode($client, true); } -/* Default configuration values. Used if values from the above config are unset */ -$defaults = array('authentication' => false,'single_page' => false,'format' => array('title' => 'Index of %s','date' => array('m/d/y H:i', 'd/m/y'),'sizes' => array(' B', ' KiB', ' MiB', ' GiB', ' TiB')),'icon' => array('path' => '/favicon.png','mime' => 'image/png'),'sorting' => array('enabled' => false,'order' => SORT_ASC,'types' => 0,'sort_by' => 'name','use_mbstring' => false),'gallery' => array('enabled' => true,'reverse_options' => false,'scroll_interval' => 50,'list_alignment' => 0,'fit_content' => true,'image_sharpen' => false),'preview' => array('enabled' => true,'hover_delay' => 75,'cursor_indicator' => true),'extensions' => array('image' => array('jpg', 'jpeg', 'png', 'gif', 'ico', 'svg', 'bmp', 'webp'),'video' => array('webm', 'mp4', 'ogv', 'ogg', 'mov')),'inject' => false,'style' => array('themes' => array('path' => false,'default' => false),'css' => array('additional' => false),'compact' => false),'filter' => array('file' => false,'directory' => false),'directory_sizes' => array('enabled' => false, 'recursive' => false),'processor' => false,'encode_all' => false,'allow_direct_access' => false,'path_checking' => 'strict','performance' => false,'footer' => array('enabled' => true, 'show_server_name' => true),'credits' => true,'debug' => false); +/* Validate that the cookie is a valid array type */ +$validate = is_array($client); + +/** Define compact mode */ +$compact = NULL; + +/* Passed to any inject functions that are called from config */ +$injectPassableData = []; + +/* Set any additional CSS */ +$additionalCss = "<%= additonalCss ? additonalCss.join('') : null %>"; + +/** Define themes array */ +$themes = [ + 'default' => [ + 'path' => NULL + ] +]; + +/** + * Helper functions for the Indexer + */ +class Helpers +{ + /** + * Checks if a string starts with a string + * + * @param String $haystack The string to match against + * @param String $needle The string needle + * + * @return Boolean + */ + public static function startsWith($haystack, $needle) + { + return $needle === '' || strrpos($haystack, $needle, - strlen($haystack)) !== false; + } + + /** + * Creates a stringed HTML element + * + * @param String $tag Element type + * @param Array $attributes Element attributes + * @param String $text Inner text + * + * @return String + */ + public static function createElement($tag, $attributes, $text = NULL) + { + /** Avoid using closing tags for these element types */ + $useClosing = !in_array($tag, [ + 'link', 'meta' + ]); + + $HTML = ('<' . $tag); + + foreach($attributes as $key => $value) + { + $HTML .= $value == NULL + ? (' ' . $key) + : (' ' . $key . '="' . $value . '"'); + } + + $HTML .= $useClosing + ? ('>' . ($text ? $text : '') . '') + : ($text ? $text : '') . '>'; + + return $HTML; + } + + /** + * A realpath alternative that solves links by using + * a string-based approach instead + * + * @param String $input A path + * + * @return String + */ + private static function removeDotSegments($input) + { + $output = ''; + + while($input !== '') + { + if(($prefix = substr($input, 0, 3)) == '../' + || ($prefix = substr($input, 0, 2)) == './') + { + $input = substr($input, strlen($prefix)); + } else if(($prefix = substr($input, 0, 3)) == '/./' + || ($prefix = $input) == '/.') + { + $input = '/' . substr($input, strlen($prefix)); + } else if (($prefix = substr($input, 0, 4)) == '/../' + || ($prefix = $input) == '/..') + { + $input = '/' . substr($input, strlen($prefix)); + $output = substr($output, 0, strrpos($output, '/')); + } else if($input == '.' || $input == '..') + { + $input = ''; + } else { + $pos = strpos($input, '/'); + if($pos === 0) $pos = strpos($input, '/', $pos+1); + if($pos === false) $pos = strlen($input); + $output .= substr($input, 0, $pos); + $input = (string) substr($input, $pos); + } + } + + return $output; + } + + /** + * Concentrates path components into a merged path + * + * @param String ...$params Path components + * + * @return String + */ + public static function joinPaths(...$params) + { + $paths = []; + + foreach($params as $param) + { + if($param !== '') + { + $paths[] = $param; + } + } + + return preg_replace('#/+#','/', join(DIRECTORY_SEPARATOR, $paths)); + } + + /** + * Checks if the passed path is above a base directory + * + * $useRealpath resolves the paths using a string-based method + * as opposed to calling `realpath()` directly. + * + * @param String $path The path to check + * @param String $base The base path + * @param Boolean $useRealpath Whether to use realpath + * + * @return String + */ + public static function isAboveCurrent($path, $base, $useRealpath = true) + { + return self::startsWith($useRealpath + ? realpath($path) + : self::removeDotSegments($path), $useRealpath + ? realpath($base) + : self::removeDotSegments($base)); + } + + /** + * Adds a character to both sides of a string + * + * If the string already ends or starts with the given + * string, it will be ignored. + * + * @param String $string String to wrap around + * @param String $char Character to prepend and append + * + * @return String + */ + public static function stringWrap($string, $char) + { + if($string[0] !== $char) + { + $string = ($char . $string); + } + + if(substr($string, -1) !== $char) + { + $string = ($string . $char); + } + + return $string; + } + + /** + * Reads a JSON file and returns the data + * + * @param String $filePath Path of the JSON file + * + * @return String + */ + public function readJson($filePath) + { + if(!file_exists($filePath)) + { + return false; + } + + $json = file_get_contents($filePath); + + if(!$json) + { + return false; + } + + $data = json_decode($json, true); + + if(json_last_error() !== JSON_ERROR_NONE) + { + if($this->debug) + { + echo json_last_error_msg(); + } + + return false; + } + + return $data; + } + + /** + * Merges two sets of metadata arrays + * + * @param Array $source Source array + * @param Array $data Priority array + * + * @return String + */ + public static function mergeMetadata(array $source, array $data) + { + $metadata = []; + + /** Iterate over and store current metadata */ + foreach($source as $item) + { + foreach($item as $key => $value) + { + if($key !== 'content') { + /** Reset object if no content is present, or create on unexisting key */ + if((isset($item['content']) && $item['content'] === false) + || !array_key_exists($key, $metadata)) + { + $metadata[$key] = []; + } + + $metadata[$key][$value] = $item['content'] ?? false; + } + } + } + + /** Iterate over new metadata, overwrite when needed */ + foreach($data as $item) + { + $content = $item['content'] ?? false; + + foreach($item as $key => $value) + { + if($key !== 'content') + { + /** Reset object if no content is present, or create on unexisting key */ + if($content === false || !array_key_exists($key, $metadata)) + { + $metadata[$key] = []; + } + + $metadata[$key][$value] = $content; + } + } + } + + /** Create and return metadata array */ + $result = []; + + foreach($metadata as $property => $values) + { + foreach($values as $key => $content) + { + $item = [$property => $key]; + + if($content) + { + $item['content'] = $content; + } -/* Authentication function */ + $result[] = $item; + } + } + + return $result; + } +} + + /** + * Authenticaticates a user + * + * @param String $users An array of users and their password + * @param String $realm Authenication realm + * + * @return Void + */ function authenticate($users, $realm) { function http_digest_parse($text) { /* Protect against missing data */ - $neededParts = array( + $neededParts = [ 'nonce' => 1, 'nc' => 1, 'cnonce' => 1, @@ -156,12 +496,14 @@ function http_digest_parse($text) 'username' => 1, 'uri' => 1, 'response' => 1 - ); + ]; - $data = array(); + $data = []; $keys = implode('|', array_keys($neededParts)); - preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $text, $matches, PREG_SET_ORDER); + preg_match_all( + '@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $text, $matches, PREG_SET_ORDER + ); foreach($matches as $m) { @@ -209,10 +551,88 @@ function createHeader($realm) } } -/* Call authentication function if authentication is enabled */ -if(isset($config['authentication']) && - $config['authentication'] && - is_array($config['authentication'])) +/** + * Extracts themes from a given path + * + * @param String $basePath The given base path of the script + * @param String $themesPath A themes path relative to the base path + * + * @return Array + */ +function getThemes($basePath, $themesPath) +{ + /* Returnable array */ + $themesPool = []; + + /* Create the absolute path of the directory to scan */ + $absDir = rtrim(Helpers::joinPaths($basePath, $themesPath), DIRECTORY_SEPARATOR); + + if(is_dir($absDir)) + { + /** Iterates over the given path */ + foreach(scandir($absDir, SCANDIR_SORT_NONE) as $item) + { + /** Current iterated item (folder || file) */ + $itemPath = Helpers::joinPaths($absDir, $item); + + if($item[0] !== '.') + { + if(is_dir($itemPath)) + { + /* The current item is assumed to be a theme directory */ + foreach(preg_grep('/^(' . $item . '|index)\.css$/', scandir( + $itemPath, SCANDIR_SORT_NONE) + ) as $theme) + { + if($theme[0] !== '.') + { + $themesPool[strtolower($item)] = [ + 'path' => Helpers::joinPaths($themesPath, $item, $theme) + ]; + + break; + } + } + } else if(preg_match('~\.css$~', $item)) + { + /* The current item is a single .CSS file */ + $themesPool[strtolower(basename($item, '.css'))] = [ + 'path' => Helpers::joinPaths($themesPath, $item) + ]; + } + } + } + + return $themesPool; + } else { + return false; + } +} + +/** + * Attempts to search for a configuration file. + * + * If it exists, the default values will be overwritten. + * Any unset values in the file will take the default values. + */ +if(file_exists(CONFIG_FILE)) +{ + $config = include(CONFIG_FILE); + /* Also check for hidden (.) file */ +} else if(file_exists('.' . CONFIG_FILE)) +{ + $config = include('.' . CONFIG_FILE); +} + +/* Default configuration values. Used if values from the above config are unset */ +$defaults = array('authentication' => false,'single_page' => false,'format' => array('title' => 'Index of %s','date' => array('m/d/y H:i', 'd/m/y'),'sizes' => array(' B', ' KiB', ' MiB', ' GiB', ' TiB')),'icon' => array('path' => '/favicon.png','mime' => 'image/png'),'sorting' => array('enabled' => false,'order' => SORT_ASC,'types' => 0,'sort_by' => 'name','use_mbstring' => false),'gallery' => array('enabled' => true,'reverse_options' => false,'scroll_interval' => 50,'list_alignment' => 0,'fit_content' => true,'image_sharpen' => false),'preview' => array('enabled' => true,'hover_delay' => 75,'cursor_indicator' => true),'extensions' => array('image' => array('jpg', 'jpeg', 'png', 'gif', 'ico', 'svg', 'bmp', 'webp'),'video' => array('webm', 'mp4', 'ogv', 'ogg', 'mov')),'inject' => false,'style' => array('themes' => array('path' => '/<%= indexerPath %>/themes/','default' => false),'css' => array('additional' => false),'compact' => false),'filter' => array('file' => false,'directory' => false),'exclude' => false,'directory_sizes' => array('enabled' => false, 'recursive' => false),'processor' => false,'encode_all' => false,'allow_direct_access' => false,'path_checking' => 'strict','performance' => false,'footer' => array('enabled' => true, 'show_server_name' => true),'credits' => true,'debug' => false); + +/** + * Call authentication function + */ +if(isset($config['authentication']) + && $config['authentication'] + && is_array($config['authentication'])) { /* If `users` key is an array, make way for it and check for restrictions */ if(isset($config['authentication']['users']) && @@ -225,7 +645,7 @@ function createHeader($realm) is_string($config['authentication']['restrict'])) { /* Check if `restrict` filter matches the current requested URI */ - $isRestricted = preg_match($config['authentication']['restrict'], $currentUri); + $isRestricted = preg_match($config['authentication']['restrict'], CURRENT_URI); } /* Restrict content if `restrict` filter matches successfully or it is unset */ @@ -239,8 +659,11 @@ function createHeader($realm) } } -/* Set default configuration values if the config is missing any keys. - * This does not traverse too deep at all */ +/** + * Set default configuration values if the config is missing any keys + * + * This does not traverse too deep at all + */ foreach($defaults as $key => $value) { if(!isset($config[$key])) @@ -259,53 +682,81 @@ function createHeader($realm) } } -$footer = array( - 'enabled' => is_array($config['footer']) ? ($config['footer']['enabled'] ? true : false) : ($config['footer'] ? true : false), - 'show_server_name' => is_array($config['footer']) ? $config['footer']['show_server_name'] : true -); +/* Used to bust the cache (query-strings for js and css files) */ +$bust = md5($config['debug'] ? time() : $version); -/* Set start time for page render calculations */ -if($footer['enabled']) -{ - $render = microtime(true); -} +/* Default stylesheet output */ +$baseStylesheet = sprintf( + '', + $bust +); -/* Enable debugging if enabled */ +/** + * Set debugging + */ if($config['debug'] === true) { ini_set('display_errors', 1); ini_set('display_startup_errors', 1); - error_reporting(E_ALL); } -if($config['style']['themes']['path']) +/** + * Set footer data + */ +$footer = [ + 'enabled' => is_array( + $config['footer']) + ? ($config['footer']['enabled'] ? true : false) + : ($config['footer'] ? true : false), + 'show_server_name' => is_array( + $config['footer']) + ? $config['footer']['show_server_name'] + : true +]; + +/** + * Set start time for page render calculations + */ +if($footer['enabled']) { - if($config['style']['themes']['path'][0] !== '/') - { - $config['style']['themes']['path'] = ('/' . $config['style']['themes']['path']); - } + $render = microtime(true); +} - if(substr($config['style']['themes']['path'], -1) !== '/') - { - $config['style']['themes']['path'] = ($config['style']['themes']['path'] . '/'); - } +if($config['style']['themes']['path']) +{ + $config['style']['themes']['path'] = Helpers::stringWrap( + $config['style']['themes']['path'], '/' + ); } if(!is_array($config['format']['date'])) { - if(is_string($config['format']['date'])) - { - $config['format']['date'] = array($config['format']['date']); - } else { - $config['format']['date'] = array('d/m/y H:i', 'd/m/y'); - } + $config['format']['date'] = [is_string($config['format']['date']) + ? $config['format']['date'] + : 'd/m/y H:i', 'd/m/y' + ]; } -class Indexer +/** + * Indexer Class + */ +class Indexer extends Helpers { public $path; + public $timestamp; + + private $exclude; + + private $client; + + private $format; + + private $filter; + + private $directorySizes; + private $relative; private $pathPrepend; @@ -314,11 +765,15 @@ class Indexer private $types; - private $allow_direct; + private $allowDirectAccess; + + private $encodeAll; - private $encode_all; + private $processor; - function __construct($path, $options = array()) + private $debug; + + function __construct($path, $options = []) { /* Get requested path */ $requested = rawurldecode(strpos($path, '?') !== false ? explode('?', $path)[0] : $path); @@ -333,21 +788,23 @@ function __construct($path, $options = array()) } /* Set encode all options */ - $this->encode_all = $options['encode_all'] ? true : false; + $this->encodeAll = $options['encode_all'] ? true : false; if(isset($options['path']['prepend']) && $options['path']['prepend'] !== NULL && strlen($options['path']['prepend']) >= 1) { - $this->pathPrepend = ltrim(rtrim($options['path']['prepend'], '/'), '/'); + $this->pathPrepend = ltrim( + rtrim($options['path']['prepend'], '/'), '/' + ); } else { $this->pathPrepend = NULL; } /* Declare array for optional processing of data */ - $this->processor = array( + $this->processor = [ 'item' => NULL - ); + ]; /* Check for passed processing functions */ if(isset($options['processor']) && is_array($options['processor'])) @@ -360,10 +817,13 @@ function __construct($path, $options = array()) /* Set remaining options/variables */ $this->client = isset($options['client']) ? $options['client'] : NULL; - $this->allow_direct = isset($options['allow_direct_access']) ? $options['allow_direct_access'] : true; - $this->path = rtrim(self::joinPaths($this->relative, $requested), '/'); + $this->path = rtrim($this->joinPaths($this->relative, $requested), '/'); $this->timestamp = time(); - $this->directory_sizes = $options['directory_sizes']; + $this->debug = $options['debug']; + $this->directorySizes = $options['directory_sizes']; + $this->allowDirectAccess = isset($options['allow_direct_access']) + ? $options['allow_direct_access'] + : true; /* Is requested path a directory? */ if(is_dir($this->path)) @@ -376,27 +836,36 @@ function __construct($path, $options = array()) /* Directory is below the base directory */ if($options['path_checking'] === 'strict' || $options['path_checking'] !== 'weak') { - throw new Exception("requested path (is_dir) is below the public working directory. (mode: {$options['path_checking']})", 1); + throw new Exception( + "requested path (is_dir) is below the public working directory. (mode: {$options['path_checking']})", 1 + ); } else if($options['path_checking'] === 'weak') { - /* If path checking is 'weak' do another test using a 'realpath' alternative instead (string-based approach which doesn't solve links) */ - if(self::isAboveCurrent($this->path, $this->relative, false) || is_link($this->path)) + /** + * If path checking is 'weak' do another test using a 'realpath' alternative + * instead (string-based approach which doesn't solve links) + */ + if(self::isAboveCurrent($this->path, $this->relative, false) + || is_link($this->path)) { $this->requested = $requested; } else { /* Even the 'weak' check failed, throw an exception */ - throw new Exception("requested path (is_dir) is below the public working directory. (mode: {$options['path_checking']})", 2); + throw new Exception( + "requested path (is_dir) is below the public working directory. (mode: {$options['path_checking']})", 2 + ); } } } } else { - /* Is requested path a file (this can only be the indexer as we don't have control over any other files)? */ + /* Is requested path a file (this can only be the Indexer as we don't have control over any other files)? */ if(is_file($this->path)) { /* If direct access is disabled, deny access */ - if($this->allow_direct === false) + if($this->allowDirectAccess === false) { - http_response_code(403); die('Forbidden'); + http_response_code(403); + die('Forbidden'); } else { /* If direct access is allowed, show current directory of script (if it is above base directory) */ $this->path = dirname($this->path); @@ -405,7 +874,9 @@ function __construct($path, $options = array()) { $this->requested = dirname($requested); } else { - throw new Exception('requested path (is_file) is below the public working directory.', 3); + throw new Exception( + 'requested path (is_file) is below the public working directory.', 3 + ); } } } else { @@ -417,14 +888,17 @@ function __construct($path, $options = array()) /* Set extension variables */ if(isset($options['extensions'])) { - $this->types = array(); + $this->types = []; - foreach($options['extensions'] as $type => $value) + foreach($options['extensions'] as $type => $value) + { + foreach($options['extensions'][$type] as $extension) { - foreach($options['extensions'][$type] as $extension) $this->types[strtolower($extension)] = $type; + $this->types[strtolower($extension)] = $type; } + } } else { - $this->types = array( + $this->types = [ 'jpg' => 'image', 'jpeg' => 'image', 'gif' => 'image', @@ -437,7 +911,7 @@ function __construct($path, $options = array()) 'mp4' => 'video', 'ogg' => 'video', 'ogv' => 'video' - ); + ]; } /* Set filter variables */ @@ -445,10 +919,18 @@ function __construct($path, $options = array()) { $this->filter = $options['filter']; } else { - $this->filter = array( + $this->filter = [ 'file' => false, - 'directory' => false - ); + 'directory' => false + ]; + } + + /* Set exclusion variables */ + if(isset($options['exclude']) && is_array($options['exclude'])) + { + $this->exclude = $options['exclude']; + } else { + $this->exclude = false; } /* Set size format variables */ @@ -456,13 +938,20 @@ function __construct($path, $options = array()) { $this->format['sizes'] = $options['format']['sizes']; } else { - $this->format['sizes'] = array(' B', ' KiB', ' MiB', ' GiB', ' TiB', ' PB', ' EB', ' ZB', ' YB'); + $this->format['sizes'] = [' B', ' KiB', ' MiB', ' GiB', ' TiB', ' PB', ' EB', ' ZB', ' YB']; } $this->format['date'] = $options['format']['date']; } - /* Handles pathing by taking any potential prepending into mind */ + /** + * Handles pathing by taking any potential prepending into mind + * + * @param String $path A path + * @param Boolean $isDir Whether the path should be treated as a directory + * + * @return String + */ private function handlePathing($path, $isDir = true) { $path = ltrim(rtrim($path, '/'), '/'); @@ -487,120 +976,289 @@ private function handlePathing($path, $isDir = true) return $path; } - /* Gets file/directory information and constructs the HTML of the table */ - public function buildTable($sorting = false, $sort_items = 0, $sort_type = 'modified', $use_mb = false) + /** + * Handles the construction of the rows for the files + * + * @param Array $files An array of files + * + * @return Array + */ + private function constructRowsFiles($files) { - /* Get client timezone offset */ - - $cookies = array( - 'timezone_offset' => intval(is_array($this->client) ? (isset($this->client['timezone_offset']) ? $this->client['timezone_offset'] : 0) : 0) - ); - - $timezone = array( - 'offset' => $cookies['timezone_offset'] > 0 ? -$cookies['timezone_offset'] * 60 : abs($cookies['timezone_offset']) * 60 - ); + /** Rows (HTML) */ + $rows = []; - /* Gets the filename of this .php file. Used to hide it from the folder */ - $script_name = basename(__FILE__); - /* Gets the current directory */ - $directory = self::getCurrentDirectory(); - /* Gets the files from the current path using 'scandir' */ - $files = self::getFiles(); - /* Is this the base directory (/)?*/ - $is_base = ($directory === '/'); + /** Most recently modified directory */ + $mostRecentTimestamp = 0; - $parentDirectory = dirname($directory); - $parentHref = $this->handlePathing($parentDirectory, true); + /** Total size of all directories */ + $totalSize = 0; - if($this->pathPrepend) + /* Iterate over the files, get and store data */ + foreach($files as $file) { - $prependedCurrent = ltrim(rtrim($this->joinPaths($this->pathPrepend, $directory), '/'), '/'); - $prependedRoot = ltrim(rtrim($this->pathPrepend, '/'), '/'); + /** Deconstruction of array */ + list($fileName, $fileSize, $fileUrl, $fileType, $fileModified) = [ + $file[1], + $file['size'], + $file['url'], + $file['type'], + $file['modified'] + ]; + + /** Append to total size */ + $totalSize = ($totalSize + $fileSize[0]); + + /** Set most recent timestamp if applicable */ + if($mostRecentTimestamp === 0 + || $fileModified[0] > $mostRecentTimestamp) + { + $mostRecentTimestamp = $fileModified[0]; + } - if($prependedCurrent === $prependedRoot) + /** File name anchor attributes */ + $anchorAttributes = [ + 'href' => $this->handlePathing($fileUrl, false) + ]; + + /** If file is an image or video, add preview class */ + if($fileType[0] === 'image' || $fileType[0] === 'video') { - $steppedPath = dirname('/' . $prependedRoot . '/'); - - $parentHref = str_replace( - '\\\\', '\\', $steppedPath . (substr($steppedPath, -1) === '/' ? '' : '/') - ); + $anchorAttributes['class'] = 'preview'; } + + /** Create file name column */ + $tdFileName = parent::createElement('td', [ + 'data-raw' => $fileName + ], parent::createElement( + 'a', $anchorAttributes, $fileName + )); + + /** Create modified column */ + $tdModified = parent::createElement('td', [ + 'data-raw' => $fileModified[0] + ], implode('', [ + parent::createElement( + 'span', [], $fileModified[1] + ) + ])); + + /** Create size column */ + $tdSize = parent::createElement('td', [ + 'data-raw' => $fileSize[0] === -1 ? 0 : $fileSize[0] + ], $fileSize[1]); + + /** Create save anchor */ + $anchorSave = parent::createElement('a', [ + 'href' => $fileUrl, + 'filename' => $fileName, + 'download' => '' + ], implode('', [ + parent::createElement( + 'span', ['data-view' => 'desktop'], '[Download]' + ), + parent::createElement( + 'span', ['data-view' => 'mobile'], '[Save]' + ) + ])); + + /** Create save column */ + $tdSave = parent::createElement('td', [ + 'data-raw' => $fileType[0], + 'class' => 'download' + ], $anchorSave); + + /** Create container and add to rows */ + $rows[] = parent::createElement('tr', [ + 'class' => 'file' + ], implode('', [ + $tdFileName, + $tdModified, + $tdSize, + $tdSave + ])); } - $op = '' . - '[Parent Directory]-'. - '--'; + return [ + 'rows' => $rows, + 'totalSize' => $totalSize, + 'mostRecentTimestamp' => $mostRecentTimestamp + ]; + } - $data = array( - 'files' => array(), - 'directories' => array(), - 'readme' => NULL, - 'recent' => array( - 'file' => 0, - 'directory' => 0 - ), - 'size' => array( - 'total' => 0, - 'readable' => 'N/A' - ) - ); + /** + * Handles the construction of the rows for the directories + * + * @param Array $fildirectorieses An array of directories + * + * @return Array + */ + private function constructRowsDirectory($directories) + { + /** Rows (HTML) */ + $rows = []; - /* Hide directories / files if they match the filter or if they are indexer components */ - foreach($files as $file) + /** Most recently modified directory */ + $mostRecentTimestamp = 0; + + /** Total size of all directories */ + $totalSize = 0; + + /* Iterate over the directories, get and store data */ + foreach($directories as $dir) { - if($file[0] === '.') continue; + /** Directory URL */ + $url = $this->handlePathing($dir['url'], true); - $path = ($this->path . '/' . $file); + /** Directory size */ + $size = $this->directorySizes['enabled'] + ? self::getReadableFileSize($dir['size']) + : '-'; - if(is_dir($path)) + if($this->directorySizes['enabled']) { - if($is_base && $file === 'indexer') - { - continue; - } else if($this->filter['directory'] !== false && !preg_match($this->filter['directory'], $file)) - { - continue; - } + $totalSize = ($totalSize + $dir['size']); + } - array_push($data['directories'], array($path, $file)); continue; - } else if(file_exists($path)) + /** Create directory name column */ + $tdDirectoryName = parent::createElement('td', [ + 'data-raw' => $dir[1] + ], parent::createElement( + 'a', [ + 'href' => $url + ], '[' . $dir[1] . ']' + )); + + /** Create modified column */ + $tdModified = parent::createElement('td', [ + 'data-raw' => $dir['modified'][0] + ], implode('', [ + parent::createElement( + 'span', [], $dir['modified'][1] + ) + ])); + + /** Create size column */ + $tdSize = parent::createElement('td', $this->directorySizes['enabled'] + ? ['data-raw' => $dir['size']] + : [], $size + ); + + $tdType = parent::createElement( + 'td', [], parent::createElement('span', [], '-') + ); + + /** Create container and add to rows */ + $rows[] = parent::createElement('tr', [ + 'class' => 'directory' + ], implode('', [ + $tdDirectoryName, + $tdModified, + $tdSize, + $tdType + ])); + + if(($mostRecentTimestamp === 0) + || $dir['modified'][0] > $mostRecentTimestamp) { - if($file === 'README.md') - { - $data['readme'] = $path; - } - - if($is_base && $file === $script_name) - { - continue; - } else if($this->filter['file'] !== false) - { - $skippable = false; + $mostRecentTimestamp = $dir['modified'][0]; + } + } - if(is_array($this->filter['file'])) - { - foreach($this->filter['file'] as $filter) - { - if(!preg_match($filter, $file)) - { - $skippable = true; break; - } - } - } else if(!$skippable) { - $skippable = !preg_match($this->filter['file'], $file); - } + return [ + 'rows' => $rows, + 'totalSize' => $totalSize, + 'mostRecentTimestamp' => $mostRecentTimestamp + ]; + } - if($skippable) - { - continue; - } - } + /** + * Gets file/directory information and constructs the HTML of the table + * + * @param String $sorting Server-side sorting to use + * @param Integer $sortItems What type of items to sort + * @param String $sortType What to sort by + * @param Boolean $sortType Whether to use mb_* functions for sorting + * + * @return String + */ + public function buildTable($sorting = false, $sortItems = 0, $sortType = 'modified', $useMb = false) + { + /* Get client timezone offset */ + $cookies = [ + 'timezoneOffset' => intval(is_array($this->client) + ? (isset($this->client['timezoneOffset']) + ? $this->client['timezoneOffset'] + : 0) + : 0) + ]; + + $timezone = [ + 'offset' => $cookies['timezoneOffset'] > 0 + ? -$cookies['timezoneOffset'] * 60 + : abs($cookies['timezoneOffset']) * 60 + ]; + + /* Gets the current directory */ + $directory = self::getCurrentDirectory(); + + /* Gets the files from the current path and filter them */ + $files = $this->handleFiles(self::getFiles(), ($directory === '/')); - array_push($data['files'], array($path, $file)); continue; + /** Parent variables */ + $parentDirectory = dirname($directory); + $parentHref = $this->handlePathing($parentDirectory, true); + + if($this->pathPrepend) + { + $prependedCurrent = ltrim( + rtrim($this->joinPaths($this->pathPrepend, $directory), '/'), '/' + ); + + $prependedRoot = ltrim( + rtrim($this->pathPrepend, '/'), '/' + ); + + if($prependedCurrent === $prependedRoot) + { + $steppedPath = dirname('/' . $prependedRoot . '/'); + $parentHref = str_replace( + '\\\\', '\\', $steppedPath . (substr($steppedPath, -1) === '/' ? '' : '/') + ); } } - if($use_mb === true && !function_exists('mb_strtolower')) + /** Construct HTML */ + $HTML = parent::createElement('tr', [ + 'class' => 'parent' + ], implode('', array_merge([ + parent::createElement('td', [], parent::createElement('a', [ + 'href' => $parentHref + ], '[Parent Directory]')) + ], + array_fill(0, 3, parent::createElement('td', [], parent::createElement( + 'span', [], '-' + )))) + )); + + /** Request data */ + $data = [ + 'files' => $files['files'], + 'directories' => $files['directories'], + 'readme' => $files['readme'], + 'dotFile' => $files['dotFile'], + 'recent' => [ + 'file' => 0, + 'directory' => 0 + ], + 'size' => [ + 'total' => 0, + 'readable' => 'N/A' + ] + ]; + + if($useMb === true + && !function_exists('mb_strtolower')) { http_response_code(500); @@ -611,40 +1269,61 @@ public function buildTable($sorting = false, $sort_items = 0, $sort_type = 'modi ); } + /** + * Iterate over the gathered directories and set their data + */ foreach($data['directories'] as $index => $dir) { + /** Deconstruct array */ + list($dirPath, $dirName) = $dir; + $item = &$data['directories'][$index]; /* We only need to set 'name' key if we're sorting by name */ - if($sort_type === 'name') + if($sortType === 'name') { - $item['name'] = $use_mb === true ? mb_strtolower($dir[1], 'UTF-8') : strtolower($dir[1]); + $item['name'] = $useMb === true + ? mb_strtolower($dirName, 'UTF-8') + : strtolower($dirName); } /* Set directory data values */ - $item['modified'] = self::getModified($dir[0], $timezone['offset']); + $item['modified'] = self::getModified($dirPath, $timezone['offset']); $item['type'] = 'directory'; - $item['size'] = $this->directory_sizes['enabled'] ? ($this->directory_sizes['recursive'] ? self::getDirectorySizeRecursively($dir[0]) : self::getDirectorySize($dir[0])) : 0; - $item['url'] = rtrim(self::joinPaths($this->requested, $dir[1]), '/'); + $item['url'] = rtrim($this->joinPaths($this->requested, $dirName), '/'); + $item['size'] = $this->directorySizes['enabled'] + ? ($this->directorySizes['recursive'] + ? self::getDirectorySizeRecursively($dirPath) + : self::getDirectorySize($dirPath)) + : 0; } + /** + * Iterate over the gathered files and set their data + */ foreach($data['files'] as $index => $file) { + /** Deconstruct array */ + list($filePath, $fileName, $fileType) = $file; + $item = &$data['files'][$index]; /* We only need to set 'name' key if we're sorting by name */ - if($sort_type === 'name') + if($sortType === 'name') { - $item['name'] = $use_mb === true ? mb_strtolower($file[1], 'UTF-8') : strtolower($file[1]); + $item['name'] = $useMb === true + ? mb_strtolower($fileName, 'UTF-8') + : strtolower($fileName); } /* Set file data values */ - $item['type'] = self::getFileType($file[1]); - $item['size'] = self::getSize($file[0]); - $item['modified'] = self::getModified($file[0], $timezone['offset']); - $item['url'] = rtrim(self::joinPaths($this->requested, $file[1]), '/'); + $item['type'] = $fileType; + $item['size'] = self::getSize($filePath); + $item['modified'] = self::getModified($filePath, $timezone['offset']); + $item['url'] = rtrim($this->joinPaths($this->requested, $fileName), '/'); - if($this->encode_all) + /** Encode URL if `encode_all` is enabled */ + if($this->encodeAll) { $item['url'] = str_replace('?', '%3F', str_replace('#', '%23', $item['url'])); } @@ -659,213 +1338,156 @@ public function buildTable($sorting = false, $sort_items = 0, $sort_type = 'modi /* Sort items server-side */ if($sorting) { - if($sort_items === 0 || $sort_items === 1) + if($sortItems === 0 || $sortItems === 1) { array_multisort( - array_column($data['files'], $sort_type), + array_column($data['files'], $sortType), $sorting, $data['files'] ); } - if($sort_items === 0 || $sort_items === 2) + if($sortItems === 0 || $sortItems === 2) { array_multisort( - array_column($data['directories'], $sort_type), + array_column($data['directories'], $sortType), $sorting, $data['directories'] ); } } - /* Iterate over the directories, get and store data */ - foreach($data['directories'] as $dir) - { - if($this->directory_sizes['enabled']) - { - $data['size']['total'] = ($data['size']['total'] + $dir['size']); - } - - $op .= sprintf( - '[%s]' . - '%s', - $dir[1], - $this->handlePathing($dir['url'], true), - $dir[1], - $dir['modified'][0], - $dir['modified'][1] - ); - - if($data['recent']['directory'] === 0 || $dir['modified'][0] > $data['recent']['directory']) - { - $data['recent']['directory'] = $dir['modified'][0]; - } - - $op .= sprintf( - '%s', - $this->directory_sizes['enabled'] ? ' data-raw="' . $dir['size'] . '"' : '', - $this->directory_sizes['enabled'] ? self::readableFilesize($dir['size']) : '-' - ); - - $op .= '-'; - } - - /* Iterate over the files, get and store data */ - foreach($data['files'] as $file) - { - $data['size']['total'] = ($data['size']['total'] + $file['size'][0]); + /** Get directory rows data */ + $directoryRows = $this->constructRowsDirectory($data['directories']); - if($data['recent']['file'] === 0 || $file['modified'][0] > $data['recent']['file']) - { - $data['recent']['file'] = $file['modified'][0]; - } - - $op .= sprintf( - '', - $file[1] - ); - - $op .= sprintf( - '%s', - (($file['type'][0] === 'image' || $file['type'][0] === 'video' - ? true - : false) - ? ' class="preview" ' - : ' '), - $this->handlePathing($file['url'], false), - $file[1] - ); - - $op .= sprintf( - '%s', - $file['modified'][0], $file['modified'][1] - ); - - $op .= sprintf( - '%s', - $file['size'][0] === -1 ? 0 : $file['size'][0], $file['size'][1] - ); - - $op .= sprintf( - '%s', - $file['type'][0], $file['url'], $file[1], ('[Save][Download]') - ); - } + /** Get file rows data */ + $fileRows = $this->constructRowsFiles($data['files']); - $data['size']['readable'] = self::readableFilesize($data['size']['total']); + /** Implode directory and files rows */ + $HTML .= (implode(PHP_EOL, $directoryRows['rows']) . implode(PHP_EOL, $fileRows['rows'])); + + /** Set request data */ + $data['size']['total'] = ($directoryRows['totalSize'] + $fileRows['totalSize']); + $data['recent']['directory'] = $directoryRows['mostRecentTimestamp']; + $data['recent']['file'] = $fileRows['mostRecentTimestamp']; - $this->data = $data; + /** Get readable size */ + $data['size']['readable'] = self::getReadableFileSize($data['size']['total']); - return $op; + return [ + 'contents' => $HTML, + 'data' => $data + ]; } - /* Gets the current files from set path */ + /** + * Gets the current files from set path + */ private function getFiles() { return scandir($this->path, SCANDIR_SORT_NONE); } - /* A 'realpath' alternative, doesn't resolve links, relies purely on strings instead. - * Used with 'weak' path checking */ - private function removeDotSegments($input) - { - $output = ''; - - while($input !== '') - { - if(($prefix = substr($input, 0, 3)) == '../' || ($prefix = substr($input, 0, 2)) == './') - { - $input = substr($input, strlen($prefix)); - } else if(($prefix = substr($input, 0, 3)) == '/./' || ($prefix = $input) == '/.') - { - $input = '/' . substr($input, strlen($prefix)); - } else if (($prefix = substr($input, 0, 4)) == '/../' || ($prefix = $input) == '/..') - { - $input = '/' . substr($input, strlen($prefix)); - $output = substr($output, 0, strrpos($output, '/')); - } else if($input == '.' || $input == '..') - { - $input = ''; - } else - { - $pos = strpos($input, '/'); - if($pos === 0) $pos = strpos($input, '/', $pos+1); - if($pos === false) $pos = strlen($input); - $output .= substr($input, 0, $pos); - $input = (string) substr($input, $pos); - } - } - - return $output; - } - - /* Checks if $path is above $base. Reverse path traversal is bad? */ - private function isAboveCurrent($path, $base, $use_realpath = true) - { - return self::startsWith($use_realpath ? realpath($path) : self::removeDotSegments($path), $use_realpath ? realpath($base) : self::removeDotSegments($base)); - } - - /* Some data is stored in $this->data, this retrieves that */ - public function getLastData() - { - return isset($this->data) ? $this->data : false; - } - - /* Gets the current directory */ + /** + * Gets the currently requested directory + */ public function getCurrentDirectory() { $requested = trim($this->requested); - if($requested === '/' || $requested === '\\' || empty($requested)) + if($requested === '/' + || $requested === '\\' + || empty($requested)) { return '/'; } else { - return preg_replace('#/+#','/', $requested[strlen($requested) - 1] === '/' ? rtrim($requested, '/') . '/' : rtrim($requested, '/')); + return preg_replace( + '#/+#','/', + $requested[strlen($requested) - 1] === '/' + ? rtrim($requested, '/') . '/' + : rtrim($requested, '/') + ); } } - /* Identifies file type by matching it against the extension arrays */ + /** + * Identifies file type by matching it against the extension arrays + * + * @param String $filename Filename + * + * @return Array + */ private function getFileType($filename) { $extension = strtolower(ltrim(pathinfo($filename, PATHINFO_EXTENSION), '.')); - return array(isset($this->types[$extension]) ? $this->types[$extension] : 'other', $extension); + return [isset($this->types[$extension]) + ? $this->types[$extension] + : 'other', $extension + ]; } - /* Converts the current path into clickable a[href] links */ + /** + * Converts the current path into clickable anchors + * + * @param String $path URI public path + * + * @return Array + */ public function makePathClickable($path) { - $path = $this->handlePathing($path, true); + $output = parent::createElement('a', [ + 'href' => '/' + ], '/'); - $paths = explode('/', ltrim($path, '/')); + $path = $this->handlePathing($path, true); - $output = ('/'); + $items = explode('/', ltrim($path, '/')); - foreach($paths as $i => $p) + foreach($items as $i => $p) { $i++; $text = (($i !== 1 ? '/' : '') . $p); - if($text === '/') continue; - - if($i === count($paths) - 1) + if($text === '/') { - $text = rtrim($text, '/') . '/'; + continue; } - $anchor = implode('/', array_slice($paths, 0, $i)); - $output .= sprintf('%s', $anchor, $text); + $output .= Helpers::createElement('a', [ + 'href' => sprintf('/%s', implode( + '/', array_slice($items, 0, $i) + )) + ], ($i === (count($items) - 1)) + ? rtrim($text, '/') . '/' + : $text + ); } return $output; } - /* Formats a unix timestamp */ + /** + * Formats a unix timestamp + * + * @param String $format String formatting + * @param Integer $stamp Timestamp + * @param Integer $modifier An integer that gets added to the timestamp + * + * @return String + */ private function formatDate($format, $stamp, $modifier = 0) { return gmdate($format, $stamp + $modifier); } - /* Gets the last modified date of a file */ + /** + * Gets the last modified date of a file + * + * @param String $path File path + * @param Integer $modifier An integer that gets added to the timestamp + * + * @return Array + */ private function getModified($path, $modifier = 0) { $stamp = filemtime($path); @@ -880,33 +1502,307 @@ private function getModified($path, $modifier = 0) $this->format['date'][$i], $stamp, $modifier ); - $formatted .= sprintf( - "%s", $i === 0 ? 'desktop' : 'mobile', $format - ); + $formatted .= parent::createElement('span', [ + 'data-view' => $i === 0 ? 'desktop' : 'mobile' + ], $format); } } else { - $formatted = self::formatDate($this->format['date'][0], $stamp, $modifier); + $formatted = self::formatDate( + $this->format['date'][0], $stamp, $modifier + ); + } + + return [$stamp, $formatted]; + } + + /** + * Reads and returns potential filters from a dotfile + * + * @param Array $file Dotfile array + * + * @return Array + */ + private function getDotFileFilters($file) + { + /** Regular expressions */ + $expDirs = []; $expFiles = []; + + /** Handles ignored files */ + if(isset($file['ignore']) && is_array($file['ignore'])) + { + $ignored = []; + + foreach($file['ignore'] as $expression) + { + if(!$expression || empty($expression)) + { + continue; + } + + /** Escape string and convert it to a wildcard expression */ + $regex = str_replace( + '\*', '.*', preg_quote($expression, '/') + ) . '$'; + + array_push($ignored, $regex); + } + + if(count($ignored) > 0) + { + /** Create group expression */ + array_push($expDirs, '/^(?!' . ( + implode('|', $ignored) + ) . ').*$/'); + + /** Create group expression */ + array_push($expFiles, '/^(?!' . ( + implode('|', $ignored) + ) . ').*$/'); + } + } + + /** Handles exluded extensions */ + if(isset($file['exclude']) && is_array($file['exclude'])) + { + foreach($file['exclude'] as $extension) + { + if(!$extension || empty($extension)) + { + continue; + } + + /** + * It may be better to not use regular expressions when excluding + * certain extensions, however, with the current setup, streamlining + * the process is easier since we are already doing the same thing + * with the `ignore` feature. + * + * In the future, this can be changed to use a simple `endsWith` check or + * incorporated into the actual extension matching used when doing exclusion + * through the config. + */ + array_push( + $expFiles, '/^(?!' . ('.*\.' . $extension) . '$).*$/' + ); + } + } + + return [ + 'ignore' => [ + 'file' => $expFiles, + 'directory' => $expDirs + ] + ]; + } + + /** + * Filters a set of gathered files + * + * @param Array $files Array of files + * @param Boolean $isBase Whether or not the current path is the base path + * + * @return Array + */ + private function handleFiles($files, $isBase) + { + /* Gets the filename of this script */ + $scriptName = basename(__FILE__); + + $data = array( + 'files' => array(), + 'directories' => array(), + 'readme' => NULL, + 'dotFile' => NULL + ); + + /** + * [Check for dotfile presence] + * + * It may contain filters for the current directory, so it's + * convenient to check for its existence before filtering. + * + * array_flip+isset is used because it's the most consistent when + * it comes to performance over a wide range of directory lenghts. + * + * @see https://gist.github.com/ksimka/21a6ff74b41451c430e8 + */ + if(isset(array_flip($files)[DOTFILE_NAME])) + { + /** Read file as JSON */ + $data['dotFile'] = $this->readJson( + $this->joinPaths($this->path, DOTFILE_NAME) + ); + } + + /** Set used filters */ + $usedFilters = [ + 'directory' => $this->filter['directory'] ?? [], + 'file' => $this->filter['file'] ?? [] + ]; + + /** Get extra filters */ + $dotFilters = $data['dotFile'] + ? $this->getDotFileFilters($data['dotFile']) + : []; + + if(isset($dotFilters['ignore'])) + { + /** Add any extra filters from dotfile */ + foreach(['directory', 'file'] as $filterType) + { + if(!$usedFilters[$filterType]) + { + $usedFilters[$filterType] = []; + } else if(!is_array($usedFilters[$filterType])) + { + $usedFilters[$filterType] = is_string($usedFilters[$filterType]) + ? [$usedFilters[$filterType]] + : []; + } + } + + /** Push file filters */ + array_push( + $usedFilters['file'], + ...$dotFilters['ignore']['file'] + ); + + /** Push directory filters */ + array_push( + $usedFilters['directory'], + ...$dotFilters['ignore']['directory'] + ); + } + + foreach($files as $file) + { + /** Skip hidden files */ + if($file[0] === '.') + { + continue; + } + + $filePath = ($this->path . '/' . $file); + $skipItem = false; + + if(is_dir($filePath)) + { + if($isBase && $file === 'indexer') + { + /** Ignore `indexer` directory */ + continue; + } else if($usedFilters['directory'] !== false) + { + if(is_array($usedFilters['directory'])) + { + foreach($usedFilters['directory'] as $filter) + { + if(!preg_match($filter, $file . '/')) + { + $skipItem = true; + break; + } + } + } else if(!preg_match($usedFilters['directory'], $file . '/')) + { + /** Ignore directories matching any potential filter */ + continue; + } + } + + if(!$skipItem) + { + array_push( + $data['directories'], array($filePath, $file) + ); + } + } else if(file_exists($filePath)) + { + if($file === 'README.md') + { + /** Set README data */ + $data['readme'] = $filePath; + } + + if($isBase && $file === $scriptName) + { + continue; + } else if($usedFilters['file'] !== false) + { + if(is_array($usedFilters['file'])) + { + foreach($usedFilters['file'] as $filter) + { + if(!preg_match($filter, $file)) + { + $skipItem = true; + break; + } + } + } else if(!$skipItem) + { + $skipItem = !preg_match($usedFilters['file'], $file); + } + } + + $fileType = $this->getFileType($file); + + if(($this->exclude && is_array($this->exclude)) + && in_array($fileType[1], $this->exclude)) + { + $skipItem = true; + } + + if($skipItem) + { + continue; + } + + array_push( + $data['files'], + array($filePath, $file, $fileType) + ); + } } - return array($stamp, $formatted); + return $data; } - /* Gets a client cookie key (if it exists) */ + /** + * Gets a client cookie key + * + * @param String $path File path + * @param Integer $modifier An integer that gets added to the timestamp + * + * @return Array + */ private function getCookie($key, $default = NULL) { return isset($_COOKIE[$key]) ? $_COOKIE[$key] : $default; } - /* Gets the size of a file */ + /** + * Gets the size of a file + * + * @param String $path File path + * + * @return Array + */ private function getSize($path) { $fs = filesize($path); $size = ($fs < 0 ? -1 : $fs); - return array($size, self::readableFilesize($size)); + return array($size, self::getReadableFileSize($size)); } - /* Gets the size of a directory */ + /** + * Gets the size of a directory + * + * @param String $path File path + * + * @return Integer + */ private function getDirectorySize($path) { $size = 0; @@ -919,7 +1815,7 @@ private function getDirectorySize($path) { continue; } else { - $filesize = filesize(self::joinPaths($path, $file)); + $filesize = filesize($this->joinPaths($path, $file)); if($filesize && $filesize > 0) { @@ -935,10 +1831,17 @@ private function getDirectorySize($path) return $size; } - /* Gets the full size of a director using */ + /** + * Gets the full size of a director using recursive scanning + * + * @param String $path File path + * + * @return Integer + */ private function getDirectorySizeRecursively($path) { $size = 0; + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); try @@ -960,8 +1863,15 @@ private function getDirectorySizeRecursively($path) return $size; } - /* Converts bytes to a readable file size */ - private function readableFilesize($bytes, $decimals = 1) + /** + * Converts bytes to a readable file size + * + * @param Integer $bytes File size in bytes + * @param Integer $decimals # of decimals in the readable output + * + * @return String + */ + private function getReadableFileSize($bytes, $decimals = 1) { if($bytes === 0) { @@ -979,51 +1889,32 @@ private function readableFilesize($bytes, $decimals = 1) return round($value, $decimals) . $this->format['sizes'][$floored]; } - - /* Checks if a string starts with a string */ - private function startsWith($haystack, $needle) - { - return $needle === '' || strrpos($haystack, $needle, - strlen($haystack)) !== false; - } - - /* Concentrates path components into a merged path */ - public function joinPaths(...$params) - { - $paths = array(); - - foreach($params as $param) - { - if($param !== '') - { - $paths[] = $param; - } - } - - return preg_replace('#/+#','/', join('/', $paths)); - } -} - -/* Is cookie set? */ -$client = isset($_COOKIE['ei-client']) ? $_COOKIE['ei-client'] : NULL; - -/* If client cookie is set, parse it */ -if($client) -{ - $client = json_decode($client, true); } -/* Validate that the cookie is a valid array */ -$validate = is_array($client); - +/** Define (and get) cookie array */ $cookies = array( + 'readme' => array( + 'toggled' => isset($client['readme']['toggled']) + ? $client['readme']['toggled'] + : true + ), 'sorting' => array( - 'row' => $validate ? (isset($client['sort']['row']) ? $client['sort']['row'] : NULL) : NULL, - 'ascending' => $validate ? (isset($client['sort']['ascending']) ? $client['sort']['ascending'] : NULL) : NULL + 'row' => $validate + ? (isset($client['sort']['row']) + ? $client['sort']['row'] + : NULL) + : NULL, + 'ascending' => $validate + ? (isset($client['sort']['ascending']) + ? $client['sort']['ascending'] + : NULL) + : NULL ) ); /* Override the config value if the cookie value is set */ -if($validate && isset($client['style']['compact']) && $client['style']['compact']) +if($validate && isset($client['style']['compact']) + && $client['style']['compact']) { $config['style']['compact'] = $client['style']['compact']; } @@ -1049,22 +1940,19 @@ public function joinPaths(...$params) if($cookies['sorting']['ascending'] !== NULL) { - $sorting['order'] = (boolval($cookies['sorting']['ascending']) === true ? SORT_ASC : SORT_DESC); + $sorting['order'] = (boolval($cookies['sorting']['ascending']) === true + ? SORT_ASC + : SORT_DESC + ); } -if($cookies['sorting']['ascending'] !== NULL || $cookies['sorting']['row'] !== NULL) +/** Enable client-side sorting if it's set */ +if($cookies['sorting']['ascending'] !== NULL + || $cookies['sorting']['row'] !== NULL) { $sorting['enabled'] = true; } -/* Get `INDEXER_BASE_PATH` if set */ -if(isset($_SERVER['INDEXER_BASE_PATH'])) -{ - $basePath = $_SERVER['INDEXER_BASE_PATH']; -} else { - $basePath = dirname(__FILE__); -} - /* Get `INDEXER_PREPEND_PATH` if set */ if(isset($_SERVER['INDEXER_PREPEND_PATH'])) { @@ -1076,55 +1964,79 @@ public function joinPaths(...$params) $prependPath = ''; } - try { /* Call class with options set */ $indexer = new Indexer( - $currentUri, - array( - 'path' => array( - 'relative' => $basePath, + CURRENT_URI, + [ + 'path' => [ + 'relative' => BASE_PATH, 'prepend' => $prependPath - ), - 'format' => array( - 'date' => isset($config['format']['date']) ? $config['format']['date'] : NULL, - 'sizes' => isset($config['format']['sizes']) ? $config['format']['sizes'] : NULL - ), + ], + 'format' => [ + 'date' => isset($config['format']['date']) + ? $config['format']['date'] + : NULL, + 'sizes' => isset($config['format']['sizes']) + ? $config['format']['sizes'] + : NULL + ], 'directory_sizes' => $config['directory_sizes'], 'client' => $client, 'filter' => $config['filter'], + 'exclude' => $config['exclude'], 'extensions' => $config['extensions'], 'path_checking' => strtolower($config['path_checking']), 'processor' => $config['processor'], 'encode_all' => $config['encode_all'], + 'debug' => $config['debug'], 'allow_direct_access' => $config['allow_direct_access'] - ) + ] ); } catch (Exception $e) { http_response_code(500); - echo "

Error:

{$e} ({$e->getCode()})

"; + /** Get error code */ + $eCode = $e->getCode(); + + echo implode('', [ + Helpers::createElement('h3', [], 'Error:'), + Helpers::createElement('p', [], $e . '({' . $eCode . '})') + ]); - if($e->getCode() === 1 || $e->getCode() === 2) + if($eCode === 1 || $eCode === 2) { - echo '

This error occurs when the requested directory is below the directory of the PHP file.'. - ($e->getCode() === 1 ? '
You can try setting path_checking to weak if you are working with symbolic links etc.' : '') . '

'; + echo Helpers::createElement( + 'p', [], sprintf( + 'This error occurs when the requested directory is below the directory of the PHP file. %s', + $eCode === 1 + ? ( + '
You can try setting path_checking to weak ' . + 'if you are working with symbolic links etc.' + ) + : '' + ) + ); } - exit('

Fatal error - Exiting.

'); + exit(Helpers::createElement( + 'p', [], 'Fatal error - Exiting.') + ); } -/* Call 'buildTable', get content */ -$contents = $indexer->buildTable( +/* Get directory data */ +$table = $indexer->buildTable( $sorting['enabled'] ? $sorting['order'] : false, $sorting['enabled'] ? $sorting['types'] : 0, $sorting['enabled'] ? strtolower($sorting['sort_by']) : 'modified', $sorting['enabled'] ? $config['sorting']['use_mbstring'] : false ); -$data = $indexer->getLastData(); +/** Get the fetched data from the request */ +$data = $table['data']; +/** Calculate total items */ $itemsTotal = (count($data['files']) + count($data['directories'])); /* Check if performance mode depends on item count */ @@ -1141,68 +2053,52 @@ public function joinPaths(...$params) } /* Set some data like file count etc */ -$counts = array( +$counts = [ 'files' => count($data['files']), 'directories' => count($data['directories']) -); +]; -$themes = array(); - -/* Are themes enabled? */ if($config['style']['themes']['path']) { - /* Trim the string of set directory path */ - $directory = rtrim($indexer->joinPaths($basePath, $config['style']['themes']['path']), '/'); + $themesPool = getThemes(BASE_PATH, $config['style']['themes']['path']); - /* If set theme path is valid directory, scan it for .css files and add them to the theme pool */ - if(is_dir($directory)) + if($themesPool + && is_array($themesPool) + && count($themesPool) > 0) { - foreach(preg_grep('~\.css$~', scandir($directory, SCANDIR_SORT_NONE)) as $theme) - { - if($theme[0] !== '.') array_push($themes, substr($theme, 0, strrpos($theme, '.'))); - } + $themes = array_merge($themes, $themesPool); } - - /* Prepend default theme to the beginning of the array */ - if(count($themes) > 0) array_unshift($themes, 'default'); } -$currentTheme = NULL; - if(count($themes) > 0) { - /* Check if a theme is already set */ - if(is_array($client) && isset($client['style']['theme'])) + /* Check if client has a custom theme already set */ + if(is_array($client) + && isset($client['style']['theme'])) { - $currentTheme = in_array($client['style']['theme'], $themes) ? $client['style']['theme'] : NULL; - } elseif(isset($config['style']['themes']['default']) && in_array($config['style']['themes']['default'], $themes)) + $currentTheme = $client['style']['theme'] ? $client['style']['theme'] : NULL; + /* Check for a default theme */ + } else if(isset($config['style']['themes']['default'])) { - $currentTheme = $config['style']['themes']['default']; + $defaultTheme = strtolower($config['style']['themes']['default']); + + if($defaultTheme && isset($themes[$defaultTheme])) + { + $currentTheme = $defaultTheme; + } } } -$compact = NULL; - /* Apply compact mode if that is set */ -if(is_array($client) && isset($client['style']['compact'])) -{ - $compact = $client['style']['compact']; -} else { - $compact = $config['style']['compact']; -} - -/* Used to bust the cache (query-strings for js and css files) */ -$bust = md5($config['debug'] ? time() : $version); - -/* Set any additional CSS */ -$additionalCss = "<%= additonalCss ? additonalCss.join('') : null %>"; +$compact = (is_array($client) && isset($client['style']['compact'])) + ? $client['style']['compact'] + : $config['style']['compact']; if(is_array($config['style']['css']['additional'])) { foreach($config['style']['css']['additional'] as $key => $value) { - $selector = $key; - $values = (string) NULL; + $selector = $key; $values = ''; foreach($value as $key => $value) { @@ -1213,12 +2109,11 @@ public function joinPaths(...$params) } } else if(is_string($config['style']['css']['additional'])) { - $additionalCss .= str_replace('"', '\"', $config['style']['css']['additional']); + $additionalCss .= str_replace( + '"', '\"', $config['style']['css']['additional'] + ); } -/* Default stylesheet output */ -$baseStylesheet = ''; - /* Alternative stylesheet output for when single-page is enabled */ if($config['single_page']) { @@ -1229,7 +2124,7 @@ public function joinPaths(...$params) /* Set a header to identify the response on the client side */ header('navigate-type: dynamic'); - $stylePath = $indexer->joinPaths($basePath, '<%= indexerPath %>', '/css/style.css'); + $stylePath = $indexer->joinPaths(BASE_PATH, '<%= indexerPath %>', '/css/style.css'); if(file_exists($stylePath)) { @@ -1242,14 +2137,13 @@ public function joinPaths(...$params) $additionalCss = ''; } - $baseStylesheet = sprintf('' . PHP_EOL, $styleData); + $baseStylesheet = Helpers::createElement('style', [ + 'type' => 'text/css' + ], $styleData); } } } -/* Passed to any inject functions that are called from config */ -$injectPassableData = array(); - if($config['inject']) { /* Current path */ @@ -1262,54 +2156,366 @@ public function joinPaths(...$params) $injectPassableData['config'] = $config; } -/* Gets the inject options */ +/** + * Gets the inject options + * + * @param String $key Inject key (`head`, `body` or `footer`) + * + * @return String + */ $getInjectable = function($key) use ($config, $injectPassableData) { if($config['inject'] && array_key_exists($key, $config['inject'])) { if($config['inject'][$key]) { - if(is_string($config['inject'][$key])) - { - return $config['inject'][$key] . PHP_EOL; - } else if(is_callable($config['inject'][$key])) - { - return $config['inject'][$key]($injectPassableData) . PHP_EOL; - } + return is_string($config['inject'][$key]) + ? $config['inject'][$key] + : (is_callable($config['inject'][$key]) + ? $config['inject'][$key]($injectPassableData) + : ''); } - return PHP_EOL; - } else { - return PHP_EOL; } + + return ''; +}; + +/** + * Builds the header for the page + * + * @param Array $config Configuration values + * @param Indexer $indexer Indexer class + * @param String $baseStylesheet Base stylesheet + * @param String $currentTheme Selected theme + * @param Array $themes Themes array + * @param Array $metadata Metadata array + * @param String $bust Cache-busting string + * @param String $additionalCss String of additional CSS + * @param function $getInjectable Function to get injectable values + * + * @return Array + */ +function buildHeader( + $config, + $indexer, + $baseStylesheet, + $currentTheme, + $themes, + $metadata, + $bust, + $additionalCss, + $getInjectable +) +{ + /** Create header array and construct title */ + $header = [Helpers::createElement( + 'title', [], sprintf( + $config['format']['title'], + $indexer->getCurrentDirectory() + ) + )]; + + /** Construct metadata */ + foreach($metadata as &$meta) + { + $header[] = Helpers::createElement('meta', $meta); + } + + /** Construct header icon */ + $header[] = Helpers::createElement('link', [ + 'rel' => 'shortcut icon', + 'href' => $config['icon']['path'], + 'type' => $config['icon']['mime'] + ], NULL); + + /** Add base stylesheet link */ + $header[] = $baseStylesheet; + + /** Add current theme stylesheet */ + if($currentTheme && strtolower($currentTheme) !== 'default' + && isset($themes[$currentTheme])) + { + $header[] = Helpers::createElement('link', [ + 'rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => sprintf( + '%s?bust=%s', $themes[$currentTheme]['path'], $bust + ) + ]); + } + + /** Construct script linking */ + $header[] = Helpers::createElement('script', [ + 'type' => 'text/javascript', + 'defer' => NULL, + 'src' => sprintf('<%= indexerPath %>main.js?bust=%s', $bust) + ]); + + /** Additional stylesheets */ + if(!empty($additionalCss)) + { + $header[] = Helpers::createElement('style', [ + 'type' => 'text/css' + ], $additionalCss); + } + + /** Injectable headers */ + $additionalHeaders = $getInjectable('head'); + + if($additionalHeaders) + { + $header[] = $additionalHeaders; + } + + return $header; +} + +/** Server name constructor */ +function constructServerNameNotice() +{ + return !empty($_SERVER['SERVER_NAME']) ? sprintf( + ' @ %s', Helpers::createElement('a', [ + 'href' => '/' + ], $_SERVER['SERVER_NAME']) + ) : ''; +} + +/** + * Builds the footer for the page + * + * @param Float $renderTime Render time + * @param String $currentDirectory Current directory + * @param Array $config Configuration values + * @param String $version Current version + * + * @return String + */ +function constructFooter($renderTime, $currentDirectory, $config, $version) +{ + $footerHtml = [ + Helpers::createElement('div', [ + 'class' => 'currentPageInfo' + ], sprintf('Page generated in %s', Helpers::createElement('span', [ + 'class' => 'generationTime' + ], sprintf("%.6f", $renderTime) . 's'))) + ]; + + $footerHtml[] = Helpers::createElement('div', [], sprintf( + 'Browsing %s%s', Helpers::createElement( + 'span', [], $currentDirectory + ), constructServerNameNotice() + )); + + if($config['credits'] !== false) + { + $footerHtml[] = Helpers::createElement('div', [ + 'class' => 'referenceGit' + ], implode('', [ + Helpers::createElement('a', [ + 'target' => '_blank', + 'href' => 'https://git.five.sh/ivfi/' + ], 'IVFi'), + Helpers::createElement('span', [], $version) + ])); + } + + return sprintf( + '
%s
', implode('', $footerHtml) + ); +} + +/** + * Creates the top-bar file/directory counts + * + * @param Integer $modified Most recently modified item + * @param Integer $count Item count + * @param String $sString Singular string + * @param String $pString Plural string + * + * @return String + */ +function generateCountDiv($modified, $count, $sString, $pString) +{ + $attributes = ['data-count' => $pString]; + + if($modified) + { + $attributes['data-raw'] = $modified; + } + + return Helpers::createElement('div', $attributes, sprintf( + '%s %s', $count, ($count === 1 ? $sString : $pString) + )); +} + +/** + * Creates the JS config object + * + * @param Array $config Configuration values + * @param Array $sorting Sorting settings + * @param Integer $timestamp Timestamp + * @param String $bust Cache-busting string + * @param Array $theme An array containg a pool and a selected theme + * + * @return String + */ +function constructJsConfig($config, $sorting, $timestamp, $bust, $theme) +{ + /** + * [Extract themes and options values] + * + * Using list deconstruction here would be better, but + * it's not supported in PHP 7.0, and dropping support + * for a single feature isn't really worth it. + * + * If for some reason we drop support for it in the + * future, this should then be changed to use list deconstruction: + * + * @see https://www.php.net/manual/en/function.list.php#refsect1-function.list-changelog + */ + + $preview = $config['preview']; + $gallery = $config['gallery']; + $extensions = $config['extensions']; + + $themePool = $theme['pool']; + $themeCurrent = $theme['current']; + + /** Construct JS configuration */ + $jsConfig = [ + 'bust' => $bust, + 'singlePage' => $config['single_page'], + 'preview' => [ + 'enabled' => $preview['enabled'], + 'hoverDelay' => $preview['hover_delay'], + 'cursorIndicator' => $preview['cursor_indicator'], + ], + 'sorting' => [ + 'enabled' => $sorting['enabled'], + 'types' => $sorting['types'], + 'sortBy' => strtolower($sorting['sort_by']), + 'order' => $sorting['order'] === SORT_ASC ? 'asc' : 'desc', + 'directorySizes' => $config['directory_sizes']['enabled'] + ], + 'gallery' => [ + 'enabled' => $gallery['enabled'], + 'reverseOptions' => $gallery['reverse_options'], + 'scrollInterval' => $gallery['scroll_interval'], + 'listAlignment' => $gallery['list_alignment'], + 'fitContent' => $gallery['fit_content'], + 'imageSharpen' => $gallery['image_sharpen'] + ], + 'extensions' => [ + 'image' => $extensions['image'], + 'video' => $extensions['video'] + ], + 'style' => [ + 'themes' => [ + 'path' => $config['style']['themes']['path'], + 'pool' => $themePool, + 'set' => $themeCurrent ? $themeCurrent : 'default' + ], + 'compact' => $config['style']['compact'] + ], + 'format' => array_intersect_key( + $config['format'], array_flip(['sizes', 'date', 'title']) + ), + 'encodeAll' => $config['encode_all'], + 'performance' => $config['performance'], + 'timestamp' => $timestamp, + 'debug' => $config['debug'], + 'mobile' => false + ]; + + /** Return JSON-encoded configuration */ + return json_encode($jsConfig); +} + +/** Set metadata behavior */ +$metadataBehavior = $data['dotFile']['metadataBehavior'] ?? 'overwrite'; +$metadataBehavior = is_string($metadataBehavior) + && $metadataBehavior === 'replace' ? 'replace' : 'overwrite'; + +/** Merge metadata using config and potential dotfile contents */ +if($metadataBehavior === 'replace' + && isset($data['dotFile']['metadata']) + && is_array($data['dotFile']['metadata'])) +{ + /** Replace metadata */ + $metadata = $data['dotFile']['metadata']; +} else { + /** Overwrite metadata */ + $metadata = Helpers::mergeMetadata( + isset($config['metadata']) + && is_array($config['metadata']) + ? $config['metadata'] + : [], + isset($data['dotFile']['metadata']) + && is_array($data['dotFile']['metadata']) + ? $data['dotFile']['metadata'] + : [] + ); +} + +/** + * Set default metadata values + * + * These can be overwritten by the dotfile or the config + * + * Order of priorty: Dotfile > Config > Default + */ +if($metadataBehavior === 'overwrite') +{ + $metadata = Helpers::mergeMetadata([ + [ + 'charset' => 'utf-8' + ], + [ + 'name' => 'viewport', + 'content' => 'width=device-width, initial-scale=1' + ] + ], $metadata); } + +/** Build header */ +$header = buildHeader( + $config, + $indexer, + $baseStylesheet, + $currentTheme, + $themes, + $metadata, + $bust, + $additionalCss, + $getInjectable +); + +/** Create JS configuration */ +$jsConfig = constructJsConfig( + $config, $sorting, $indexer->timestamp, $bust, [ + 'pool' => $themes, + 'current' => $currentTheme + ] +); ?> - - - - <?=sprintf($config['format']['title'], $indexer->getCurrentDirectory());?> - - - - - ' . PHP_EOL : ''?> - - - %s' . PHP_EOL, $additionalCss) : PHP_EOL?> - + root> -
-
data-count="files">
-
data-count="directories">
+ +
@@ -1322,92 +2528,46 @@ public function joinPaths(...$params) - - - - + + + + + + + - +
FilenameModifiedSizeType + Filename + + + Modified + + + Size + + + Type + +
-'; - - echo sprintf( - '
Page generated in %f seconds
Browsing %s%s
', - 'currentPageInfo', - 'generationTime', - microtime(true) - $render, - $indexer->getCurrentDirectory(), - $footer['show_server_name'] && !empty($_SERVER['SERVER_NAME']) ? sprintf(' @ %s', $_SERVER['SERVER_NAME']) : '' - ); - echo ($config['credits'] !== false) ? sprintf( - '
- eyy-indexer%s -
', $version - ) : ''; + getCurrentDirectory(), $config, $version) + ) : '';?> - echo ''; -} -?> + - + - + - - - - - + + + \ No newline at end of file diff --git a/themes/Amethyst.css b/themes/Amethyst.css deleted file mode 100755 index 955eb68..0000000 --- a/themes/Amethyst.css +++ /dev/null @@ -1,44 +0,0 @@ -body > div.tableContainer > table tr.file a.preview, body > div.tableContainer > table tr.file a.preview:hover { - color: #8d6ec8!important; -} - -body > div.tableContainer > table tr.file a.preview:visited { - color: #443660!important; -} - -body > div.tableContainer > table tr.directory a { - color: #f86f8d!important; -} - -body > div.tableContainer > table tr.parent a { - color: #ffc561!important; -} - -body > div.tableContainer > table tr.file td.download a { - color: #8d6ec8!important; -} - -body > div.tableContainer > table tr.file td.download a:visited { - color: #3e2b38!important; -} - -body.compact > div.path { - text-align: center!important; -} - -body > div.path a { - color: #ab8fdf!important; - font-weight: normal!important; -} - -.rootGallery div.galleryContent .list table tr.selected td { - background-color: #342d4294!important; -} - -.rootGallery div.galleryContent .list table tr.selected td:hover { - background-color: #342d4294!important; -} - -.rootGallery div.galleryContent .list table tr:not(.selected) td:hover { - background-color: #342d422e!important; -} \ No newline at end of file diff --git a/themes/Coral.css b/themes/Coral.css deleted file mode 100755 index 04668b3..0000000 --- a/themes/Coral.css +++ /dev/null @@ -1,48 +0,0 @@ -body > div.tableContainer > table tr.file a.preview, body > div.tableContainer > table tr.file a.preview:hover { - color: #4bd2ff!important; -} - -body > div.tableContainer > table tr.file a.preview:visited { - color: #3f7486!important; -} - -body > div.tableContainer > table tr.directory a { - color: #75f7b4!important; -} - -body > div.tableContainer > table tr.parent a { - color: #f9f871!important; -} - -body > div.tableContainer > table tr.file td.download a { - color: #4792ac!important; -} - -body > div.tableContainer > table tr.file td.download a:visited { - color: #406e7d!important; -} - -body.compact > div.path { - text-align: center!important; -} - -body > div.path a { - color: #7efff1!important; - font-weight: normal!important; -} - -.rootGallery div.galleryContent .list table tr.selected td { - background-color: #2d2d2d4f!important; -} - -.rootGallery div.galleryContent .list table tr.selected td:hover { - background-color: #3737374f!important; -} - -.rootGallery div.galleryBar .galleryBar__right a.download { - color: #4792ac!important; -} - -.rootGallery div.galleryContent .list table tr:not(.selected) td:hover { - background-color: rgba(34, 34, 34, 0.2)!important; -} \ No newline at end of file diff --git a/themes/README.md b/themes/README.md deleted file mode 100755 index f3b0f36..0000000 --- a/themes/README.md +++ /dev/null @@ -1,28 +0,0 @@ -

Themes

- -

This folder contains some official optional themes for the Indexer.

-

See CONFIG.md to see how themes can be used.

- -## White - -_A simple white theme._ - ---- - -## Amethyst - -_A dark, purple theme based on the colors of the amethyst._ - ---- - -## Coral - -_A turquoise and greenish theme._ - - -
- -

Got anything to add?

- -

If you've created a nice theme, feel free to contact me and I'll add it to the list if it's suitable!

-

PS: Themes can be created by simply modifying any existing CSS values.

\ No newline at end of file diff --git a/themes/White.css b/themes/White.css deleted file mode 100755 index 0efecd5..0000000 --- a/themes/White.css +++ /dev/null @@ -1,308 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); - -body, html { - background-color: #f7f7f7!important; - color: #1c1c1c!important; - font-family: 'Roboto', Arial, Helvetica, sans-serif!important; -} - -body > div.tableContainer > table { - background-color: #aeaeae!important; -} - -body:not([is-loading]) > div.tableContainer > table tbody tr:not(.parent):hover { - background-color: rgba(238, 238, 238, 0.55)!important; -} - -body[optimize] > div.tableContainer > table tbody > tr { - background-color: #fff!important; -} - -html > body > div.tableContainer > table > thead > tr { - background-color: #fff; - border-bottom: 2px solid #f4f4f4; - } - -body.compact > div.path { - margin: 20px 8px 16px 8px!important; - padding: 6px 10px 7px 10px!important; - background-color: #fff!important; - color: #6f6f6f!important; - border-radius: 6px!important; - border: 1px solid #e1e1e1!important; - text-align: center!important; -} - -body > div.path a { - color: #76b2ff!important; - font-weight: normal!important; -} - -.topBar { - background-color: rgba(255, 255, 255, 0.40)!important; - border-bottom: 1px solid hsla(0, 0%, 81.6%, 0.52)!important; - box-shadow: 0 0 3px #cecece!important; -} - -.topBar > div.extend:hover { - background-color: hsla(0, 0%, 0%, 0.03)!important; - color: #222!important; -} - -.topBar > .directoryInfo > div:not(.quickPath) { - border-right: 2px solid hsla(0, 0%, 91%, 0.52)!important; -} - -.topBar > div.extend { - border-left: 2px solid hsla(0, 0%, 91%, 0.52)!important; -} - -div.topBar > div.directoryInfo > div.quickPath a { - color: #5ab2ff!important; -} - -body > div.tableContainer > table thead tr > th { - color: #404040!important; -} - -body > div.tableContainer { - border-top: 1px solid #dfdfdf!important; - background-color: #fff!important; -} - -body > div.tableContainer > table { - background-color: #fff!important; -} - -body > div.bottom { - border-top: 1px solid #dfdfdf!important; -} - -body > div.tableContainer > table tr.file a.preview, body > div.tableContainer > table tr.file a.preview:hover { - color: #418bff!important; -} - -body > div.tableContainer > table tr.file td.download a { - color: #7dadf9!important; -} - -body > div.tableContainer > table tr.file td.download a:visited { - color: #9da8d2!important; -} - -body > div.tableContainer > table tr.parent a { - color: #b03cff!important; -} - -body > div.tableContainer > table tr.directory a { - color: #ce5858!important; -} - -.preview-container video, .preview-container img { - -webkit-box-shadow: 0px 0px 3px 0px #00000042!important; - box-shadow: 0px 0px 3px 0px #00000042!important; - overflow: hidden!important; -} - -.menu { - border: 1px solid #fff!important; - background-color: rgba(255, 255, 255, 0.15)!important; - color: #484848!important; - -webkit-box-shadow: 0px 0px 3px 0px #00000042!important; - box-shadow: 0px 0px 3px 0px #00000042!important; -} - -.menu > div:hover { - background-color: #e0f3ff!important; - border-left: 3px solid #c8d3ff!important; - color: #000!important; -} - -.settingsContainer { - background-color: rgba(255, 255, 255, 0.94)!important; - border: 1px solid #f0f0f0!important; - border-radius: 3px!important; - box-shadow: 0px 0 5px #0d0d0d52!important; -} - -.settingsContainer > .wrapper > div.section > div.header { - background-color: #fff!important; - border-bottom: 1px solid #e1e1e1!important; - border-top: 1px solid #e1e1e1!important; - box-shadow: none!important; - color: #9d9d9d!important; -} - -select:not(.default) { - background-color: #fff!important; - border: 1px solid #cecece!important; - box-shadow: 0 1px 0 1px rgba(0,0,0,.1)!important; - color: #4c4c4c!important; -} - -select:not(.default):focus, select:not(.default):hover { - background-color: #e6f1ff!important; - border: 1px solid #a8a8a8!important; -} - -.settingsContainer > div.bottom { - background-color: #fff!important; - border-top: 1px solid #c3c3c3!important; -} - -.settingsContainer > div.bottom > div:not(:last-child) { - border-right: 1px solid #dbdbdb!important; -} - -.settingsContainer > div.bottom > div:hover { - background-color: #e3f7ff!important; -} - -::-webkit-scrollbar { - background-color:#fbfbfb!important; - width:10px -} -::-webkit-scrollbar-thumb { - background-color:#d1d1d1!important; -} -::-webkit-scrollbar-thumb:hover { - background-color:#b8b8b8!important; -} - -.rootGallery { - background-color: rgba(165, 183, 255, 0.30)!important; -} - -.rootGallery div.galleryContent .list { - background-color: rgba(255, 255, 255, 0.94)!important; -} - -.rootGallery div.galleryContent .list table tr { - color: #313131!important; -} - -.rootGallery div.galleryContent .list, html { - scrollbar-color: #d1d1d1 #fff!important; -} - -.rootGallery div.galleryContent .list table tr:not(.selected) td:hover { - background-color: rgba(140, 171, 255, 0.2)!important; -} - -.rootGallery div.galleryContent .list table tr.selected td:hover { - background-color: #ceefff!important; -} - -.rootGallery div.galleryContent .list table tr.selected td { - background-color: #d8f2ff!important; - border-left: 5px solid #a0e4ff!important; - color: #373737!important; -} - -.rootGallery div.galleryContent .list > div.drag { - border-left: 2px solid hsla(211.2, 100%, 85.3%, 0.55)!important; -} - -.rootGallery div.galleryContent .media > div.item-info-static { - background-color: #ffffff69!important; -} - -.rootGallery div.galleryBar { - background-color: rgba(255, 255, 255, 0.84)!important; - border-bottom: 2px solid hsla(211.2, 100%, 85.3%, 0.55)!important; - color: #404040!important; -} - -.rootGallery div.galleryBar .galleryBar__left a { - color: #71bcff!important; -} - -.rootGallery div.galleryContent .media .wrapper .cover .reverse a { - background-color: #ffffffb3!important; - color: #404040!important; - transition: none!important; - -moz-transition: none!important; - -webkit-transition: none!important; -} - -.rootGallery div.galleryContent .list { - border-top: 1px solid hsla(0, 0%, 76.9%, 0.72)!important; -} - -.rootGallery div.galleryContent .media .wrapper .cover .reverse a:not(:last-child) { - border-right: 1px solid #d0d0d0!important; -} - -.rootGallery div.galleryContent .media .wrapper .cover .reverse a:hover { - background-color: rgba(255, 255, 255, 1)!important; - color: #393939!important; -} - -.filterContainer { - box-shadow: none!important; -} - -.filterContainer > input[type="text"] { - background-color: rgba(255, 255, 255, 0.94)!important; - border-top: 2px solid hsla(211.2, 100%, 85.3%, 0.55)!important; - color: #151515!important; -} - -@media only screen and (max-width:640px) { - body > div.tableContainer > table tr.directory a, - body > div.tableContainer > table tr.parent a, - body > div.tableContainer > table tr.file td.download a, - body > div.tableContainer > table tr > td:nth-child(2), body > div.tableContainer > table tr > td:nth-child(3), - body > div.tableContainer > table tr.file a.preview, body > div.tableContainer > table tr.file a.preview:hover { - font-size: 12px!important; - } - - .rootGallery div.galleryContent .screen-navigate { - background-color: rgba(255, 255, 255, 0.12)!important; - } - - .rootGallery div.galleryContent .screen-navigate.left > span::after, - .rootGallery div.galleryContent .screen-navigate.right > span::after { - color: white!important; - } -} - -@media only screen and (min-width: 640px) { - body.compact { - - border: 1px solid #eaeaea!important; - } - - body > div.tableContainer > table tr.directory a, - body > div.tableContainer > table tr.parent a, - body > div.tableContainer > table tr.file td.download a, - body > div.tableContainer > table tr > td:nth-child(2), body > div.tableContainer > table tr > td:nth-child(3), - body > div.tableContainer > table tr.file a.preview, body > div.tableContainer > table tr.file a.preview:hover { - font-size: 14px!important; - } - - .topBar > .directoryInfo { - font-size: 13px!important; - } -} - -.readmeContainer { - overflow: hidden!important; - margin: 0px 10px 12px 10px!important; - border-radius: 9px!important; - background-color: #fbfbfb!important; - color: #484848!important; - border-top: none!important; - border-bottom: none!important; - - -webkit-box-shadow: 0px 0px 3px 0px #00000042!important; - box-shadow: 0px 0px 3px 0px #00000042!important; -} - -.readmeContainer::before { - border-radius: 9px 9px 0px 0px!important; - background-color: #fff!important; - padding: 6px 10px 6px 6px!important; - color: #939498!important; - border-bottom: 1px solid #dfdfdf!important; -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9a7d4f9..8c3d955 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,5 +5,8 @@ "allowSyntheticDefaultImports": true, "esModuleInterop": true, "resolveJsonModule": true + }, + "ts-node": { + "esm": true } } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index cc83ac8..ee4640d 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,14 +1,26 @@ -const package = require('./package.json'); -const build = require('./build.helpers.js'); +/** Package information */ +import pck from './package.json' assert { + type: 'json' +}; + +/** Build helpers */ +import build from './build.helpers.js'; -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const TerserPlugin = require('terser-webpack-plugin'); +/** Webpack plugins */ +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +/** Developer imports */ +import webpack from 'webpack'; +import moment from 'moment'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; -const webpack = require('webpack'); -const moment = require('moment'); +/** Directory name and file name constants */ +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); /** * Read build.options.js if it exists @@ -31,7 +43,7 @@ assetDir = assetDir.replace(/([^:]\/)\/+/g, '$1').replace(/^\/|\/$/g, ''); let templateParameters = { buildInject: {}, additonalCss: [], - version: package.version, + version: pck.version, indexerPath : `/${assetDir}/` }; @@ -105,12 +117,12 @@ const banner = () => @preserved - ## eyy-indexer - ${package.description} ## + ## IVFi-PHP - ${pck.description} ## - [Version: ${package.version}] + [Version: ${pck.version}] - [Git: https://github.com/sixem/eyy-indexer] + [Git: https://github.com/sixem/ivfi-php] [Build: [fullhash] @ ${moment().format('dddd, MMMM Do YYYY')}] @@ -123,7 +135,7 @@ const banner = () => \n`; }; -module.exports = (env, argv) => { +const config = (env, argv) => { const isProduction = argv.mode === 'production'; return { @@ -205,4 +217,6 @@ module.exports = (env, argv) => { ] } } -}; \ No newline at end of file +}; + +export default config; \ No newline at end of file