├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── Scripts └── initialize.php ├── app ├── Connectors │ ├── Connector.php │ ├── Jikan.php │ └── MangaCovers.php ├── Controllers │ ├── Config.php │ ├── DisplayLibrary.php │ ├── DisplaySeries.php │ ├── Home.php │ ├── Login.php │ └── Reader.php ├── Core │ ├── AJAXProcessor.php │ ├── Database.php │ ├── Debug.php │ ├── LazyLoader.php │ ├── MetadataManager.php │ ├── SessionManager.php │ └── ZipManager.php ├── Exception │ ├── DatabaseInitException.php │ └── DatabaseQueryException.php ├── External │ ├── FontAwesome │ │ ├── css │ │ │ └── fontawesome-5.10.2.min.css │ │ └── webfonts │ │ │ ├── fa-brands-400.eot │ │ │ ├── fa-brands-400.svg │ │ │ ├── fa-brands-400.ttf │ │ │ ├── fa-brands-400.woff │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.eot │ │ │ ├── fa-regular-400.svg │ │ │ ├── fa-regular-400.ttf │ │ │ ├── fa-regular-400.woff │ │ │ ├── fa-regular-400.woff2 │ │ │ ├── fa-solid-900.eot │ │ │ ├── fa-solid-900.svg │ │ │ ├── fa-solid-900.ttf │ │ │ ├── fa-solid-900.woff │ │ │ └── fa-solid-900.woff2 │ └── Javascript │ │ └── jquery-3.3.1.js ├── Services │ └── View │ │ ├── Controller.php │ │ ├── Data │ │ ├── Config.php │ │ ├── DisplayLibrary.php │ │ ├── DisplaySeries.php │ │ ├── Home.php │ │ ├── IViewData.php │ │ ├── Master.php │ │ ├── Page.php │ │ ├── ReaderPage.php │ │ ├── ReaderStrip.php │ │ ├── ViewData.php │ │ └── ViewItem.php │ │ ├── Factory.php │ │ ├── Provider.php │ │ └── Service.php ├── ViewItems │ ├── CSS │ │ ├── Config.css │ │ ├── DisplayLibrary.css │ │ ├── DisplaySeries.css │ │ ├── Home.css │ │ ├── Login.css │ │ ├── ReaderPage.css │ │ ├── ReaderStrip.css │ │ └── UIFrame.css │ ├── HTML │ │ ├── Config.php │ │ ├── DisplayLibrary.php │ │ ├── DisplaySeries.php │ │ ├── Home.php │ │ ├── Login.php │ │ ├── Master.php │ │ ├── Page.php │ │ ├── ReaderPage.php │ │ └── ReaderStrip.php │ ├── JS │ │ ├── Config.js │ │ ├── DisplayLibrary.js │ │ ├── DisplaySeries.js │ │ ├── LazyLoader.js │ │ ├── LazyLoaderEvents.js │ │ ├── Login.js │ │ ├── ReaderPage.js │ │ ├── ReaderStrip.js │ │ ├── dropdown.js │ │ └── logout.js │ └── PageViews │ │ └── DisplayLibraryBookcaseView.php ├── database.db ├── index.php ├── resources │ └── icons │ │ ├── loading-3s-200px.svg │ │ ├── magnifying-glass.svg │ │ ├── placeholder.svg │ │ └── upload.svg └── server.ini ├── composer.json ├── composer.lock └── tests ├── DataProviders.php ├── Services └── View │ ├── ControllerTest.php │ └── Data │ └── ViewItemTest.php └── TestBootstrap.php /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # PHP CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-php/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | phpunit: 8 | docker: 9 | - image: circleci/php:7.3.8 10 | steps: 11 | - checkout 12 | - run: 13 | name: Composer Install 14 | command: composer install 15 | - run: 16 | name: Run PHPUint 17 | command: vendor/bin/phpunit --bootstrap tests/TestBootstrap.php tests 18 | workflows: 19 | version: 2 20 | build: 21 | jobs: 22 | - phpunit 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{php,js,css,html}] 8 | charset = utf-8 9 | indent_style = tab 10 | tab_width = 4 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - Browser [e.g. chrome, safari] 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - Browser [e.g. stock browser, safari] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | 52 | /vendor/* 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wufflums 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MangaBango 2 | [![CodeFactor](https://www.codefactor.io/repository/github/mluzarow/mangobango/badge)](https://www.codefactor.io/repository/github/mluzarow/mangobango) 3 | [![CircleCI](https://circleci.com/gh/mluzarow/MangoBango/tree/master.svg?style=svg)](https://circleci.com/gh/mluzarow/MangoBango/tree/master) 4 | 5 | A manga server for manga things. 6 | 7 | Also a makes a local front end I guess. 8 | -------------------------------------------------------------------------------- /Scripts/initialize.php: -------------------------------------------------------------------------------- 1 | query ($q); 72 | 73 | if ($r === false) { 74 | $table_failed = true; 75 | echo "!! Failed to create table: metadata_series.\n"; 76 | } 77 | 78 | $q = 79 | 'CREATE TABLE IF NOT EXISTS directories_series ( 80 | series_id INTEGER NOT NULL PRIMARY KEY, 81 | folder_name TEXT NOT NULL 82 | )'; 83 | $r = $db->query ($q); 84 | 85 | if ($r === false) { 86 | $table_failed = true; 87 | echo "!! Failed to create table: directories_series.\n"; 88 | } 89 | 90 | $q = 91 | 'CREATE TABLE IF NOT EXISTS images_series ( 92 | series_id INTEGER NOT NULL PRIMARY KEY, 93 | cover_ext TEXT NULL DEFAULT NULL 94 | )'; 95 | $r = $db->query ($q); 96 | 97 | if ($r === false) { 98 | $table_failed = true; 99 | echo "!! Failed to create table: images_series.\n"; 100 | } 101 | 102 | $q = 103 | 'CREATE TABLE IF NOT EXISTS metadata_chapters ( 104 | chapter_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 105 | global_sort INTEGER NOT NULL, 106 | chapter_name text NULL DEFAULT NULL 107 | )'; 108 | $r = $db->query ($q); 109 | 110 | if ($r === false) { 111 | $table_failed = true; 112 | echo "!! Failed to create table: metadata_chapters.\n"; 113 | } 114 | 115 | $q = 116 | 'CREATE TABLE IF NOT EXISTS directories_chapters ( 117 | chapter_id INTEGER NOT NULL PRIMARY KEY, 118 | folder_name TEXT NOT NULL, 119 | is_archive INTEGER NOT NULL 120 | )'; 121 | $r = $db->query ($q); 122 | 123 | if ($r === false) { 124 | $table_failed = true; 125 | echo "!! Failed to create table: directories_chapters.\n"; 126 | } 127 | 128 | $q = 129 | 'CREATE TABLE IF NOT EXISTS metadata_volumes ( 130 | volume_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 131 | sort INTEGER NOT NULL, 132 | volume_name TEXT NULL DEFAULT NULL 133 | )'; 134 | $r = $db->query ($q); 135 | 136 | if ($r === false) { 137 | $table_failed = true; 138 | echo "!! Failed to create table: metadata_volumes.\n"; 139 | } 140 | 141 | $q = 142 | 'CREATE TABLE IF NOT EXISTS images_volumes ( 143 | volume_id INTEGER NOT NULL PRIMARY KEY, 144 | cover_ext TEXT NULL DEFAULT NULL, 145 | index_ext TEXT NULL DEFAULT NULL, 146 | spine_ext TEXT NULL DEFAULT NULL 147 | )'; 148 | $r = $db->query ($q); 149 | 150 | if ($r === false) { 151 | $table_failed = true; 152 | echo "!! Failed to create table: images_volumes.\n"; 153 | } 154 | 155 | $q = 156 | 'CREATE TABLE IF NOT EXISTS connections_series ( 157 | chapter_id INTEGER NOT NULL PRIMARY KEY, 158 | series_id INTEGER NOT NULL 159 | )'; 160 | $r = $db->query ($q); 161 | 162 | if ($r === false) { 163 | $table_failed = true; 164 | echo "!! Failed to create table: connections_series.\n"; 165 | } 166 | 167 | $q = 168 | 'CREATE TABLE IF NOT EXISTS connections_volumes ( 169 | volume_id INTEGER NOT NULL PRIMARY KEY, 170 | series_id INTEGER NOT NULL 171 | )'; 172 | $r = $db->query ($q); 173 | 174 | if ($r === false) { 175 | $table_failed = true; 176 | echo "!! Failed to create table: connections_volumes.\n"; 177 | } 178 | 179 | $q = 180 | 'CREATE TABLE IF NOT EXISTS connections_chapters ( 181 | chapter_id INTEGER NOT NULL PRIMARY KEY, 182 | volume_id INTEGER NULL 183 | )'; 184 | $r = $db->query ($q); 185 | 186 | if ($r === false) { 187 | $table_failed = true; 188 | echo "!! Failed to create table: connections_chapters.\n"; 189 | } 190 | 191 | $q = 192 | 'CREATE TABLE IF NOT EXISTS server_configs ( 193 | config_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 194 | config_name TEXT NOT NULL UNIQUE, 195 | config_value TEXT NULL DEFAULT NULL 196 | )'; 197 | $r = $db->query ($q); 198 | 199 | if ($r === false) { 200 | $table_failed = true; 201 | echo "!! Failed to create table: server_configs.\n"; 202 | } 203 | 204 | $q = 205 | 'CREATE TABLE IF NOT EXISTS statistics ( 206 | stat_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 207 | name TEXT NOT NULL, 208 | value TEXT NOT NULL 209 | )'; 210 | $r = $db->query ($q); 211 | 212 | if ($r === false) { 213 | $table_failed = true; 214 | echo "!! Failed to create table: statistics.\n"; 215 | } 216 | 217 | $q = 218 | 'CREATE TABLE IF NOT EXISTS users ( 219 | user_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 220 | username TEXT NOT NULL UNIQUE, 221 | password TEXT NOT NULL, 222 | type TEXT NOT NULL 223 | )'; 224 | $r = $db->query ($q); 225 | 226 | if ($r === false) { 227 | $table_failed = true; 228 | echo "!! Failed to create table: users.\n"; 229 | } 230 | 231 | $q = 232 | 'CREATE TABLE IF NOT EXISTS user_types ( 233 | type_name TEXT NOT NULL PRIMARY KEY, 234 | permissions TEXT NULL DEFAULT NULL 235 | )'; 236 | $r = $db->query ($q); 237 | 238 | if ($r === false) { 239 | $table_failed = true; 240 | echo "!! Failed to create table: user_types.\n"; 241 | } 242 | 243 | if ($table_failed === true) { 244 | echo "✗ Failed to create some tables.\n"; 245 | return; 246 | } 247 | 248 | echo "✔ Successfully created all tables.\n"; 249 | 250 | // Create default configs 251 | $q = 252 | 'INSERT INTO server_configs 253 | (config_name, config_value) 254 | VALUES 255 | ("assets_directory", ""), 256 | ("directory_structure", ""), 257 | ("reader_display_style", 2), 258 | ("manga_directory", ""), 259 | ("library_view_type", 1)'; 260 | $r = $db->query ($q); 261 | 262 | if ($r === false) { 263 | echo "✗ Failed to set up default configs.\n"; 264 | return; 265 | } 266 | 267 | echo "✔ Successfully created default configs.\n"; 268 | 269 | // Create default user 270 | $q = ' 271 | INSERT INTO `users` 272 | (`username`, `password`, `type`) 273 | VALUES 274 | ("admin", "'.password_hash ('racecar', PASSWORD_DEFAULT).'", "admin")'; 275 | $r = $db->query ($q); 276 | 277 | if ($r === false) { 278 | echo "✗ Failed to set up default user.\n"; 279 | return; 280 | } 281 | 282 | echo "✔ Successfully created default user. Username: admin, Password: racecar\n"; 283 | 284 | echo "✔ Success! We did it!\n"; 285 | -------------------------------------------------------------------------------- /app/Connectors/Connector.php: -------------------------------------------------------------------------------- 1 | jikan_base_url.'manga/'.$mal_id.'/'; 31 | 32 | $result = json_decode ($this->requestGET ($url), true); 33 | 34 | return ($result); 35 | } 36 | 37 | /** 38 | * Searches for manga by the given query string. 39 | * 40 | * @param string $query search query string 41 | * 42 | * @return array response from Jikan API 43 | */ 44 | public function searchByQueryString (string $query) { 45 | $query = trim ($query); 46 | 47 | if (empty ($query)) { 48 | throw new \InvalidArgumentException ('Argument (Query) of searchByQueryString() cannot be empty.'); 49 | } else if (strlen ($query) < 2) { 50 | throw new \InvalidArgumentException ('Argument (Query) of searchByQueryString() must be a minimum of 3 letters.'); 51 | } 52 | 53 | $query = urlencode ($query); 54 | 55 | $url = $this->jikan_base_url.'search/manga?q='.$query.'&page=1'; 56 | 57 | $result = json_decode ($this->requestGET ($url), true); 58 | 59 | return ($result); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Connectors/MangaCovers.php: -------------------------------------------------------------------------------- 1 | base_url}/series/{$mu_id}/"; 31 | 32 | return json_decode ($this->requestGET ($url), true); 33 | } 34 | 35 | /** 36 | * Search for manga metadata using the manga series name. 37 | * 38 | * @param string $title series name string 39 | * 40 | * @return array dictionary of series title matches and their MU IDs 41 | * 42 | * @throws TypeError on non-string or non-array return 43 | */ 44 | public function searchByTitle (string $title) : array { 45 | $url = "{$this->base_url}/search/"; 46 | 47 | $args = [ 48 | 'Title' => urlencode ($title) 49 | ]; 50 | 51 | return json_decode ($this->requestPOST ($url, $args), true); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Controllers/Config.php: -------------------------------------------------------------------------------- 1 | db = \Core\Database::getInstance (); 17 | } 18 | 19 | /** 20 | * AJAX method for updating the manga library. 21 | */ 22 | public function ajaxRescanLibrary () { 23 | $manga = $this->scanLibrary (); 24 | $this->saveNewManga ($manga); 25 | 26 | return (true); 27 | } 28 | 29 | /** 30 | * AJAX method for saving updated configs to the DB. 31 | */ 32 | public function ajaxUpdateConfigs () { 33 | $configs = json_decode ($_POST['config'], true); 34 | 35 | if (!empty ($configs['assets_directory'])) { 36 | $q = 37 | 'UPDATE server_configs 38 | SET config_value = :v 39 | WHERE config_name = "assets_directory"'; 40 | $r = $this->db->execute ($q, ['v' => $configs['assets_directory']]); 41 | } 42 | 43 | if (!empty ($configs['directory_structure'])) { 44 | $q = 45 | 'UPDATE server_configs 46 | SET config_value = :v 47 | WHERE config_name = "directory_structure"'; 48 | $r = $this->db->execute ($q, ['v' => $configs['directory_structure']]); 49 | } 50 | 51 | if (!empty ($configs['reader_display_style'])) { 52 | $q = 53 | 'UPDATE server_configs 54 | SET config_value = :v 55 | WHERE config_name = "reader_display_style"'; 56 | $r = $this->db->execute ($q, ['v' => $configs['reader_display_style']]); 57 | } 58 | 59 | if (!empty ($configs['manga_directory'])) { 60 | $q = 61 | 'UPDATE server_configs 62 | SET config_value = :v 63 | WHERE config_name = "manga_directory"'; 64 | $r = $this->db->execute ($q, ['v' => $configs['manga_directory']]); 65 | } 66 | 67 | if (!empty ($configs['library_view_type'])) { 68 | $q = 69 | 'UPDATE server_configs 70 | SET config_value = :v 71 | WHERE config_name = "library_view_type"'; 72 | $r = $this->db->execute ($q, ['v' => $configs['library_view_type']]); 73 | } 74 | } 75 | 76 | /** 77 | * Runs page process. 78 | */ 79 | public function begin () { 80 | $q = 81 | 'SELECT config_name, config_value 82 | FROM server_configs'; 83 | $r = $this->db->query ($q); 84 | 85 | $configs_dict = []; 86 | foreach ($r as $row) { 87 | $configs_dict[$row['config_name']] = $row['config_value']; 88 | } 89 | 90 | $view_parameters = []; 91 | $view_parameters['assets_directory'] = $configs_dict['assets_directory']; 92 | $view_parameters['reader_display_style'] = (int) $configs_dict['reader_display_style']; 93 | $view_parameters['manga_directory'] = $configs_dict['manga_directory']; 94 | $view_parameters['library_view_type'] = (int) $configs_dict['library_view_type']; 95 | $view_parameters['directory_structure'] = $configs_dict['directory_structure']; 96 | 97 | return (new \Services\View\Controller ())-> 98 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 99 | buildView ( 100 | [ 101 | 'name' => 'Config', 102 | 'CSS' => ['Config'], 103 | 'HTML' => 'Config', 104 | 'JS' => ['Config'] 105 | ], 106 | $view_parameters 107 | ); 108 | } 109 | 110 | /** 111 | * Create an array reflecting the directory structure at a given location. 112 | * 113 | * @param string $dir file path to folder 114 | * 115 | * @return array directory structure 116 | */ 117 | private function dirToArray ($dir) { 118 | $result = array(); 119 | 120 | $cdir = scandir($dir); 121 | foreach ($cdir as $key => $value) { 122 | if (!in_array ($value, array ('.', '..'))) { 123 | if (is_dir ($dir . DIRECTORY_SEPARATOR . $value)) { 124 | $result[$value] = $this->dirToArray ($dir . DIRECTORY_SEPARATOR . $value); 125 | } else { 126 | $result[] = $value; 127 | } 128 | } 129 | } 130 | 131 | return ($result); 132 | } 133 | 134 | /** 135 | * Scans the manga library for any new series. 136 | * 137 | * @return array dictionary of new series and all its files in order 138 | */ 139 | private function scanLibrary () { 140 | $q = 141 | 'SELECT config_name, config_value 142 | FROM server_configs 143 | WHERE config_name IN ("directory_structure", "manga_directory")'; 144 | $r = $this->db->query ($q); 145 | 146 | $configs = []; 147 | foreach ($r as $row) { 148 | $configs[$row['config_name']] = $row['config_value']; 149 | } 150 | 151 | $directory_tree = $this->dirToArray ($configs['manga_directory']); 152 | 153 | $new_content = []; 154 | foreach ($directory_tree as $series_name => $series_contents) { 155 | // Check if this folder is already bound to a series 156 | $q = 157 | 'SELECT series_id FROM directories_series 158 | WHERE folder_name = :v'; 159 | $r = $this->db->execute ($q, ['v' => $series_name]); 160 | 161 | if (!empty ($r)) { 162 | continue; 163 | } 164 | 165 | // Try and load the metadata file 166 | $metadata = $configs['manga_directory'].DIRECTORY_SEPARATOR.$series_name.DIRECTORY_SEPARATOR.'info.json'; 167 | $f = fopen ($metadata, 'r'); 168 | 169 | if (!empty($f)) { 170 | $blob = fread ($f, filesize ($metadata)); 171 | fclose ($f); 172 | } else { 173 | $blob = '[]'; 174 | } 175 | 176 | $new_manga = [ 177 | 'folder_name' => $series_name, 178 | 'metadata' => json_decode ($blob, true), 179 | 'chapters' => [] 180 | ]; 181 | 182 | foreach ($series_contents as $chapter_name => $chapter_contents) { 183 | $is_archive = 0; 184 | 185 | if (is_string ($chapter_contents)) { 186 | // Is a file 187 | $file_segs = explode ('.', $chapter_contents); 188 | $ext = end ($file_segs); 189 | 190 | $chap_folder_name = $chapter_contents; 191 | 192 | if ($ext === 'zip') { 193 | // Is archive 194 | $is_archive = 1; 195 | } else { 196 | continue; 197 | } 198 | } else { 199 | $chap_folder_name = $chapter_name; 200 | } 201 | 202 | // Try and get the chapter number 203 | preg_match ('/((?:[0-9]+)(?:\.(?:[0-9]+))?)/', $chap_folder_name, $matches); 204 | $chap_number = $matches[0]; 205 | 206 | $new_manga['chapters'][$chap_number] = [ 207 | 'folder_name' => $chap_folder_name, 208 | 'is_archive' => $is_archive 209 | ]; 210 | } 211 | 212 | ksort ($new_manga['chapters']); 213 | 214 | $new_content[] = $new_manga; 215 | } 216 | 217 | return $new_content; 218 | } 219 | 220 | /** 221 | * Saves new manga into the database. 222 | * 223 | * @param array $manga_list dictionary of new manga 224 | */ 225 | private function saveNewManga ($manga_list) { 226 | foreach ($manga_list as $manga) { 227 | $q = 228 | 'INSERT INTO metadata_series 229 | (name, name_original, summary, genres) 230 | VALUES 231 | (:name, :name_original, :summary, :genres)'; 232 | $r = $this->db->execute ( 233 | $q, 234 | [ 235 | 'name' => empty($manga['metadata']['manga_info']['title']) ? 236 | '' : $manga['metadata']['manga_info']['title'], 237 | 'name_original' => empty($manga['metadata']['manga_info']['original_title']) ? 238 | '' : $manga['metadata']['manga_info']['original_title'], 239 | 'summary' => empty($manga['metadata']['manga_info']['description']) ? 240 | '' : $manga['metadata']['manga_info']['description'], 241 | 'genres' => empty($manga['metadata']['manga_info']['tags']) ? 242 | '[]' : json_encode ($manga['metadata']['manga_info']['tags']) 243 | ] 244 | ); 245 | 246 | $new_id_series = $this->db->getLastIndex (); 247 | 248 | $q = 249 | "INSERT INTO directories_series 250 | (series_id, folder_name) 251 | VALUES 252 | ({$new_id_series}, \"{$manga['folder_name']}\")"; 253 | $r = $this->db->query ($q); 254 | 255 | $q = 256 | "INSERT INTO images_series 257 | (series_id, cover_ext) 258 | VALUES 259 | ({$new_id_series}, \"\")"; 260 | $r = $this->db->query ($q); 261 | 262 | $chap_sort = 1; 263 | foreach ($manga['chapters'] as $chapter) { 264 | $q = 265 | "INSERT INTO metadata_chapters 266 | (global_sort, chapter_name) 267 | VALUES 268 | ({$chap_sort}, \"\")"; 269 | $r = $this->db->query ($q); 270 | 271 | $new_id_chapter = $this->db->getLastIndex (); 272 | 273 | $q = 274 | "INSERT INTO directories_chapters 275 | (chapter_id, folder_name, is_archive) 276 | VALUES 277 | ( 278 | {$new_id_chapter}, 279 | \"{$chapter['folder_name']}\", 280 | {$chapter['is_archive']} 281 | )"; 282 | $r = $this->db->query ($q); 283 | 284 | $q = 285 | "INSERT INTO connections_series 286 | (chapter_id, series_id) 287 | VALUES 288 | ({$new_id_chapter}, {$new_id_series})"; 289 | $r = $this->db->query ($q); 290 | 291 | $chap_sort++; 292 | } 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /app/Controllers/DisplayLibrary.php: -------------------------------------------------------------------------------- 1 | db = \Core\Database::getInstance (); 12 | } 13 | 14 | /** 15 | * Runs page process. 16 | */ 17 | public function begin () { 18 | // Get library configs 19 | $q = ' 20 | SELECT `config_name`, `config_value` 21 | FROM `server_configs` 22 | WHERE `config_name` IN ("manga_directory", "library_view_type")'; 23 | $r = $this->db->query ($q); 24 | 25 | $configs = []; 26 | foreach ($r as $row) { 27 | $configs[$row['config_name']] = $row['config_value']; 28 | } 29 | 30 | $configs['library_view_type'] = (int) $configs['library_view_type']; 31 | 32 | if ($configs['library_view_type'] === 1) { 33 | // Display as series of covers 34 | $view_parameters['manga_data'] = $this->getImagesCovers ( 35 | $configs['manga_directory'] 36 | ); 37 | 38 | return (new \Services\View\Controller ())-> 39 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 40 | buildView ( 41 | [ 42 | 'name' => 'DisplayLibrary', 43 | 'CSS' => ['DisplayLibrary'], 44 | 'HTML' => 'DisplayLibrary', 45 | 'JS' => ['LazyLoader', 'LazyLoaderEvents', 'DisplayLibrary'] 46 | ], 47 | $view_parameters 48 | ); 49 | } else if ($configs['library_view_type'] === 2) { 50 | // Display as bookcase 51 | $view_parameters['manga_data'] = $this->getImagesSpines ($directory_tree); 52 | $view = new DisplayLibraryBookcaseView ($view_parameters); 53 | } 54 | } 55 | 56 | /** 57 | * Collects series cover image locations. 58 | * 59 | * @param string $manga_directory manga directory 60 | * 61 | * @return array dictionary of series data in the following structure: 62 | * [manga ID] int manga ID 63 | * ├── ['link'] string link to the series page for this manga 64 | * ├── ['folder_name'] string path to cover image 65 | * └── ['title'] string meta name of series 66 | */ 67 | private function getImagesCovers ($manga_directory) { 68 | $q = ' 69 | SELECT `m`.`series_id`, `m`.`name`, `d`.`folder_name`, `i`.`cover_ext` 70 | FROM `metadata_series` AS `m` 71 | JOIN `directories_series` AS `d` 72 | ON `m`.`series_id` = `d`.`series_id` 73 | JOIN `images_series` AS `i` 74 | ON `m`.`series_id` = `i`.`series_id`'; 75 | $r = $this->db->query ($q); 76 | 77 | if ($r === false) { 78 | return ([]); 79 | } 80 | 81 | $series_data = []; 82 | foreach ($r as $series) { 83 | if (empty ($series['cover_ext'])) { 84 | $path = ''; 85 | } else { 86 | $path = "{$manga_directory}\\{$series['folder_name']}\\series_cover.{$series['cover_ext']}"; 87 | } 88 | 89 | $series_data[$series['name']] = [ 90 | 'link' => "/displaySeries?s={$series['series_id']}", 91 | 'path' => $path, 92 | 'title' => $series['name'] 93 | ]; 94 | } 95 | 96 | ksort ($series_data); 97 | 98 | return ($series_data); 99 | } 100 | 101 | /** 102 | * Collects volume spine image locations. 103 | * 104 | * @param string $manga_directory manga directory 105 | * 106 | * @return array dictionary of series data in the following structure: 107 | * [manga ID] int manga ID 108 | * ├── ['basepath'] string path to this manga's folder 109 | * ├── ['link'] string link to the series page for this manga 110 | * ├── ['name'] string meta name of series 111 | * └── ['paths'] array list of spine paths keyed by volume sort order 112 | * ├── [0] string path to this volume's spine image (if exists) 113 | * | . 114 | * └── [n] 115 | */ 116 | private function getImagesSpines ($manga_directory) { 117 | $q = ' 118 | SELECT `s`.`folder_name`, `m`.`manga_id`, `m`.`name` 119 | FROM `manga_directories_series` AS `s` 120 | JOIN `manga_metadata` AS `m` 121 | ON `s`.`manga_id` = `m`.`manga_id`'; 122 | $r = $this->db->query ($q); 123 | 124 | if ($r === false) { 125 | return ([]); 126 | } 127 | 128 | $manga_data = []; 129 | foreach ($r as $row) { 130 | $manga_data[$row['manga_id']] = [ 131 | 'basepath' => $row['folder_name'], 132 | 'link' => "/displaySeries?s={$row['manga_id']}", 133 | 'name' => $row['name'], 134 | 'paths' => [] 135 | ]; 136 | } 137 | 138 | $q = ' 139 | SELECT `sort`, `manga_id`, `folder_name`, `spine` 140 | FROM `manga_directories_volumes`'; 141 | $r = $this->db->query ($q); 142 | 143 | if ($r === false) { 144 | return ([]); 145 | } 146 | 147 | foreach ($r as $row) { 148 | if (empty ($manga_data[$row['manga_id']])) { 149 | continue; 150 | } 151 | 152 | if (empty ($row['spine'])) { 153 | $manga_data[$row['manga_id']]['paths'][$row['sort']] = ''; 154 | } else { 155 | $manga_data[$row['manga_id']]['paths'][$row['sort']] = 156 | $manga_directory.'\\'. 157 | $manga_data[$row['manga_id']]['basepath'].'\\'. 158 | $row['filename'].'\\'. 159 | 'spine.'.$row['spine']; 160 | } 161 | } 162 | 163 | return ($manga_data); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/Controllers/DisplaySeries.php: -------------------------------------------------------------------------------- 1 | db = \Core\Database::getInstance (); 17 | } 18 | 19 | /** 20 | * Runs page process. 21 | */ 22 | public function begin () { 23 | // Fetch manga directory 24 | $q = ' 25 | SELECT `config_value` FROM `server_configs` 26 | WHERE `config_name` = "manga_directory"'; 27 | $r = $this->db->query ($q); 28 | 29 | $manga_directory = $r[0]['config_value']; 30 | 31 | // Fetch manga info by ID 32 | $q = ' 33 | SELECT `ds`.`series_id`, `ds`.`folder_name`, `cv`.`volume_id`, `iv`.`cover_ext` 34 | FROM `directories_series` AS `ds` 35 | JOIN `connections_volumes` AS `cv` 36 | ON `ds`.`series_id` = `cv`.`series_id` 37 | JOIN `images_volumes` AS `iv` 38 | ON `cv`.`volume_id` = `iv`.`volume_id` 39 | WHERE `ds`.`series_id` = '.$_GET['s']; 40 | $r = $this->db->query ($q); 41 | 42 | if ($r === false) { 43 | return; 44 | } 45 | 46 | $view_parameters = []; 47 | $view_parameters['volumes'] = []; 48 | 49 | foreach ($r as $v) { 50 | if (empty($v['cover_ext'])) { 51 | $path = ''; 52 | } else { 53 | $path = "{$manga_directory}\\{$v['folder_name']}\\cover.{$v['cover_ext']}"; 54 | } 55 | 56 | $view_parameters['volumes'][] = [ 57 | 'link' => "/reader?sid={$_GET['s']}&cid=1", 58 | 'source' => $path 59 | ]; 60 | } 61 | 62 | $q = ' 63 | SELECT `mc`.`chapter_id`, `mc`.`global_sort` 64 | FROM `metadata_chapters` AS `mc` 65 | JOIN `connections_series` AS `cs` 66 | ON `mc`.`chapter_id` = `cs`.`chapter_id` 67 | WHERE `cs`.`series_id` = '.$_GET['s']; 68 | $r = $this->db->query ($q); 69 | 70 | if ($r === false) { 71 | return; 72 | } 73 | 74 | $view_parameters['chapters'] = []; 75 | foreach ($r as $row) { 76 | $view_parameters['chapters'][$row['global_sort']] = [ 77 | 'title' => "Chapter {$row['global_sort']}", 78 | 'link' => "\\reader?sid={$_GET['s']}&cid={$row['chapter_id']}" 79 | ]; 80 | } 81 | 82 | ksort ($view_parameters['chapters']); 83 | 84 | $q = ' 85 | SELECT `name`, `summary`, `genres` 86 | FROM `metadata_series` 87 | WHERE `series_id` = '.$_GET['s']; 88 | $r = $this->db->query ($q); 89 | 90 | $view_parameters['summary'] = ''; 91 | $view_parameters['genres'] = []; 92 | $view_parameters['title'] = ''; 93 | 94 | if ($r !== false) { 95 | $row = current ($r); 96 | 97 | $view_parameters['summary'] = empty ($row['summary']) ? '' : $row['summary']; 98 | $view_parameters['genres'] = empty($row['genres']) ? [] : json_decode ($row['genres'], true); 99 | $view_parameters['title'] = empty ($row['name']) ? '' : $row['name']; 100 | } 101 | 102 | return (new \Services\View\Controller ())-> 103 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 104 | buildView ( 105 | [ 106 | 'name' => 'DisplaySeries', 107 | 'CSS' => ['DisplaySeries'], 108 | 'HTML' => 'DisplaySeries', 109 | 'JS' => ['LazyLoader', 'LazyLoaderEvents', 'DisplaySeries'] 110 | ], 111 | $view_parameters 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/Controllers/Home.php: -------------------------------------------------------------------------------- 1 | query ($q); 20 | 21 | $view_parameters['box_contents'] = [ 22 | [ 23 | 'title' => 'Number of series', 24 | 'value' => $r[0]['series'] 25 | ], 26 | [ 27 | 'title' => 'Number of volumes', 28 | 'value' => $r[0]['volumes'] 29 | ], 30 | [ 31 | 'title' => 'Number of chapters', 32 | 'value' => $r[0]['chapters'] 33 | ] 34 | ]; 35 | 36 | return (new \Services\View\Controller ())-> 37 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 38 | buildView ( 39 | [ 40 | 'name' => 'Home', 41 | 'CSS' => ['Home'], 42 | 'HTML' => 'Home', 43 | 'JS' => [] 44 | ], 45 | $view_parameters 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Controllers/Login.php: -------------------------------------------------------------------------------- 1 | isLoggedIn () === true) { 17 | // Redirect to home page 18 | header ('Location: /', true, 301); 19 | exit; 20 | } 21 | 22 | return (new \Services\View\Controller ())-> 23 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 24 | buildView ( 25 | [ 26 | 'name' => '', 27 | 'CSS' => ['Login'], 28 | 'HTML' => 'Login', 29 | 'JS' => ['Login'] 30 | ], 31 | [] 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Controllers/Reader.php: -------------------------------------------------------------------------------- 1 | query ($q); 25 | 26 | $manga_dir = $r[0]['config_value']; 27 | 28 | $manga_info = []; 29 | 30 | // Fetch manga info by ID 31 | $q = ' 32 | SELECT 33 | `mc`.`chapter_id`, 34 | `mc`.`global_sort`, 35 | `ds`.`folder_name` AS `series_folder`, 36 | `dc`.`folder_name` AS `chapter_folder`, 37 | `dc`.`is_archive` 38 | FROM `metadata_chapters` AS `mc` 39 | JOIN `directories_chapters` AS `dc` 40 | ON `mc`.`chapter_id` = `dc`.`chapter_id` 41 | JOIN `connections_series` AS `cs` 42 | ON `dc`.`chapter_id` = `cs`.`chapter_id` 43 | JOIN `directories_series` AS `ds` 44 | ON `cs`.`series_id` = `ds`.`series_id` 45 | WHERE `cs`.`series_id` = '.$_GET['sid'].' 46 | AND `mc`.`global_sort` IN ( 47 | ( 48 | SELECT `global_sort` 49 | FROM `metadata_chapters` 50 | WHERE `chapter_id` = '.$_GET['cid'].' 51 | ), 52 | ( 53 | SELECT `global_sort` 54 | FROM `metadata_chapters` 55 | WHERE `chapter_id` = '.$_GET['cid'].' 56 | ) + 1 57 | )'; 58 | $r = $db->query ($q); 59 | 60 | if ($r === false) 61 | return ['file_paths' => []]; 62 | 63 | $next_chapter = count($r) > 1 ? end ($r)['chapter_id'] : null; 64 | 65 | // Get the first row (which should be the sort we want to display) 66 | $r = reset ($r); 67 | 68 | $path = "{$manga_dir}\\{$r['series_folder']}\\{$r['chapter_folder']}"; 69 | 70 | $file_paths = []; 71 | if ($r['is_archive'] === '1') { 72 | $files = array_keys (\Core\ZipManager::readFiles ($path)); 73 | 74 | foreach ($files as $file) { 75 | if (substr ($file, -1) === '/') { 76 | continue; 77 | } 78 | 79 | $file_paths[] = "{$path}#{$file}"; 80 | } 81 | } else { 82 | // Reading list of images from a directory 83 | $files = array_values ($this->dirToArray($path)); 84 | 85 | foreach ($files as $file) { 86 | $file_paths[] = "{$path}\\{$file}"; 87 | } 88 | } 89 | 90 | $view_parameters = []; 91 | $view_parameters['file_paths'] = $file_paths; 92 | 93 | if ($next_chapter !== null) { 94 | $view_parameters['next_chapter_link'] = "\\reader?sid={$_GET['sid']}&cid={$next_chapter}"; 95 | } else { 96 | $view_parameters['next_chapter_link'] = ''; 97 | } 98 | 99 | // Get the reader view style 100 | $q = ' 101 | SELECT `config_value` FROM `server_configs` 102 | WHERE `config_name` = "reader_display_style"'; 103 | $r = $db->query ($q); 104 | 105 | $reader_display_style = (int) $r[0]['config_value']; 106 | 107 | if ($reader_display_style === 2) { 108 | // Display as a strip 109 | return (new \Services\View\Controller ())-> 110 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 111 | buildView ( 112 | [ 113 | 'name' => 'ReaderStrip', 114 | 'CSS' => ['ReaderStrip'], 115 | 'HTML' => 'ReaderStrip', 116 | 'JS' => ['LazyLoader', 'LazyLoaderEvents', 'ReaderStrip'] 117 | ], 118 | $view_parameters 119 | ); 120 | } else if ($reader_display_style === 1) { 121 | // Display as a single page with left and right arrows 122 | return (new \Services\View\Controller ())-> 123 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 124 | buildView ( 125 | [ 126 | 'name' => 'ReaderPage', 127 | 'CSS' => ['ReaderPage'], 128 | 'HTML' => 'ReaderPage', 129 | 'JS' => ['LazyLoader', 'LazyLoaderEvents', 'ReaderPage'] 130 | ], 131 | $view_parameters 132 | ); 133 | } 134 | } 135 | 136 | /** 137 | * Create an array reflecting the directory structure at a given location. 138 | * 139 | * @param string $dir file path to folder 140 | * 141 | * @return array directory structure 142 | */ 143 | private function dirToArray ($dir) { 144 | $result = array(); 145 | 146 | $cdir = scandir($dir); 147 | foreach ($cdir as $key => $value) { 148 | if (!in_array ($value, array ('.', '..'))) { 149 | if (is_dir ($dir . DIRECTORY_SEPARATOR . $value)) { 150 | $result[$value] = $this->dirToArray ($dir . DIRECTORY_SEPARATOR . $value); 151 | } else { 152 | $result[] = $value; 153 | } 154 | } 155 | } 156 | 157 | return ($result); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/Core/AJAXProcessor.php: -------------------------------------------------------------------------------- 1 | setURLSegments ($segments); 24 | } 25 | 26 | /** 27 | * Constructs the class and method call with the path given by the segments. 28 | * 29 | * @return mixed return value of the called AJAX method 30 | */ 31 | public function fireTargetMethod () { 32 | // Construct the method call. 33 | $namespace = '\\'; 34 | for ($i = 0; $i < (count ($this->getURLSegments ()) - 1); $i++) { 35 | $namespace .= $this->getURLSegments ()[$i] . '\\'; 36 | } 37 | $namespace = rtrim ($namespace, '\\'); 38 | 39 | $method = $this->getURLSegments ()[$i]; 40 | 41 | $result = (new $namespace)->$method (); 42 | 43 | return ($result); 44 | } 45 | 46 | /** 47 | * Getter for URL segments. 48 | * 49 | * @return array list of URL segments from the request 50 | */ 51 | private function getURLSegments () { 52 | return ($this->url_segments); 53 | } 54 | 55 | /** 56 | * Setting for URL segments. 57 | * 58 | * @param array $segments list of URL segments from the request 59 | * 60 | * @throws TypeError on non-array URL segments 61 | */ 62 | private function setURLSegments (array $segments) { 63 | $this->url_segments = $segments; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Core/Database.php: -------------------------------------------------------------------------------- 1 | initialize (); 39 | 40 | } 41 | 42 | return self::$instance; 43 | } 44 | 45 | /** 46 | * Gets the database connection information. 47 | * 48 | * @return array connection information 49 | */ 50 | public function getConnectionData () { 51 | return [ 52 | 'client_info' => $this->connection->client_info, 53 | 'client_version' => $this->connection->client_version, 54 | 'host_info' => $this->connection->host_info, 55 | 'stat' => $this->connection->stat, 56 | 'server_info' => $this->connection->server_info, 57 | 'server_version' => $this->connection->server_version 58 | ]; 59 | } 60 | 61 | /** 62 | * Query via prepared statement q with parameters defined in params. 63 | * 64 | * @param string $q query string 65 | * @param array $params dictionary of query parameters 66 | * 67 | * @return array|true list of returned rows or success flag 68 | * 69 | * @throws DatabaseQueryException on failed query 70 | */ 71 | public function execute (string $q, array $params) { 72 | $s = $this->connection->prepare ($q); 73 | 74 | if ($s === false) { 75 | throw new DatabaseQueryException ( 76 | 'Failed to prepare query. Error message: '. 77 | $this->connection->error 78 | ); 79 | } 80 | 81 | foreach ($params as $var_name => $value) { 82 | if (is_int ($value)) { 83 | $type = SQLITE3_INTEGER; 84 | } elseif (is_null($value)) { 85 | $type = SQLITE3_NULL; 86 | } else { 87 | $type = SQLITE3_TEXT; 88 | } 89 | 90 | $p = $s->bindParam (':'.$var_name, $value, $type); 91 | 92 | if ($p === false) { 93 | throw new DatabaseQueryException ( 94 | "Failed to bind parameter {$var_name}. Error message: ". 95 | $this->connection->error 96 | ); 97 | } 98 | } 99 | 100 | $r = $s->execute (); 101 | 102 | if ($r === false) { 103 | throw new DatabaseQueryException ( 104 | 'Database query failed. Error message: '.$this->connection->error 105 | ); 106 | } 107 | 108 | if ($r === true) { 109 | return true; 110 | } 111 | 112 | $data = []; 113 | while ($row = $r->fetchArray (SQLITE3_ASSOC)) { 114 | $data[] = $row; 115 | } 116 | 117 | return $data; 118 | } 119 | 120 | /** 121 | * Queries the database with the given MySQL string. 122 | * 123 | * @param string $q MySQL query string 124 | * 125 | * @return array|true list of returned rows or success flag 126 | * 127 | * @throws DatabaseQueryException on failed query 128 | */ 129 | public function query (string $q) { 130 | $r = $this->connection->query ($q); 131 | 132 | if ($r === false) 133 | throw new DatabaseQueryException ( 134 | 'Database query failed. Error message: '.$this->connection->error 135 | ); 136 | 137 | if ($r === true) 138 | return true; 139 | 140 | $data = []; 141 | while ($row = $r->fetchArray (SQLITE3_ASSOC)) { 142 | $data[] = $row; 143 | } 144 | 145 | return $data; 146 | } 147 | 148 | public function getLastIndex () { 149 | return $this->connection->lastInsertRowid (); 150 | } 151 | 152 | /** 153 | * Constructor for database controller. 154 | * 155 | * @throws \IOException on missing server ini file 156 | */ 157 | private function __construct () { 158 | $config_data = parse_ini_file (self::DB_INI); 159 | 160 | if (empty($config_data)) 161 | throw new \IOException ('Missing server ini file at '.DB_INI); 162 | 163 | $this->db_path = empty($config_data['path']) ? 164 | APP_PATH : $config_data['path']; 165 | } 166 | 167 | /** 168 | * Initialized database connection. 169 | * 170 | * @throws DatabaseInitException on DB initialization failure 171 | */ 172 | private function initialize () { 173 | if (empty($this->db_path)) 174 | throw new DatabaseInitException ( 175 | 'Path to database file not provided.' 176 | ); 177 | 178 | $full_path = rtrim ($this->db_path, DIRECTORY_SEPARATOR). 179 | DIRECTORY_SEPARATOR.'database.db'; 180 | 181 | try { 182 | $this->connection = new \SQLite3 ($full_path); 183 | } catch (\Exception $e) { 184 | throw new DatabaseInitException ( 185 | "Failed to open database file at {$full_path}. ". 186 | 'SQLite3 message: '.$e->getMessage() 187 | ); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /app/Core/Debug.php: -------------------------------------------------------------------------------- 1 | Debug::prettyPrint on line {$backtrace['line']} at {$backtrace['file']}\n"; 20 | 21 | if (is_array($var)) { 22 | $output .= print_r($var, true).''; 23 | } else { 24 | $output .= var_export ($var, true).''; 25 | } 26 | 27 | echo $output; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Core/LazyLoader.php: -------------------------------------------------------------------------------- 1 | fetchImageSrc ($path); 37 | } 38 | 39 | return json_encode ($image_srcs); 40 | } 41 | 42 | /** 43 | * Fetches image source data based on image path. 44 | * 45 | * @param string $path path of image 46 | * 47 | * @return string base 64 encoded requested image file or empty string if no 48 | * file could be found or loaded 49 | * 50 | * @throws TypeError on invalid parameter or return type 51 | */ 52 | private function fetchImageSrc (string $path) : string { 53 | $image_segs = explode ('#', $path); 54 | 55 | if (count ($image_segs) === 1) { 56 | return $this->loadLooseImage ($path); 57 | } 58 | 59 | return $image_data = $this->loadArchiveImage ( 60 | $image_segs[0], 61 | $image_segs[1] 62 | ); 63 | } 64 | 65 | /** 66 | * Loads an image from from an archive. 67 | * 68 | * @param string $archive_path path to the archive 69 | * @param string $image_path path of image inside archive 70 | * 71 | * @return string base 64 encoded requested image file or empty string if no 72 | * file could be found or loaded 73 | * 74 | * @throws TypeError on invalid parameter or return type 75 | */ 76 | private function loadArchiveImage ( 77 | string $archive_path, 78 | string $image_path 79 | ) : string { 80 | // Open the archive 81 | $file_list = \Core\ZipManager::readFiles ($archive_path); 82 | 83 | // No image could be loaded 84 | if (empty ($file_list[$image_path])) 85 | return ''; 86 | 87 | $blob = $file_list[$image_path]; 88 | 89 | $file_segs = explode ('.', $image_path); 90 | $ext = end ($file_segs); 91 | 92 | return "data:image/{$ext};base64,".$blob; 93 | } 94 | 95 | /** 96 | * Loads an image file. 97 | * 98 | * @param string $image_path path to image 99 | * 100 | * @return string base 64 encoded requested image file or empty string if no 101 | * file could be found or loaded 102 | * 103 | * @throws TypeError on invalid parameter or return type 104 | */ 105 | private function loadLooseImage (string $image_path) : string { 106 | $f = fopen ($image_path, 'r'); 107 | 108 | if ($f === false) { 109 | // No image could be loaded 110 | $image_data = ''; 111 | } else { 112 | $blob = fread ($f, filesize ($image_path)); 113 | 114 | $file_segs = explode ('.', $image_path); 115 | $ext = end ($file_segs); 116 | 117 | $image_data = "data:image/{$ext};base64,".base64_encode ($blob); 118 | } 119 | 120 | fclose ($f); 121 | 122 | return $image_data; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/Core/MetadataManager.php: -------------------------------------------------------------------------------- 1 | jikan = new Jikan (); 25 | $this->manga_covers = new MangaCovers (); 26 | } 27 | 28 | /** 29 | * Fetches manga info by the given query string. 30 | * 31 | * @param string $manga_name name of the manga to look for 32 | * 33 | * @return array fetched manga information 34 | * 35 | * @throws TypeError on non-string parameter & non-array return 36 | */ 37 | public function fetchMangaInfo (string $manga_name) : array { 38 | $manga_result = $this->jikan->searchByQueryString ($manga_name); 39 | 40 | if (empty($manga_result['result'])) { 41 | return ([]); 42 | } 43 | 44 | $manga_info = $this->jikan->searchByMangaID ( 45 | (int) $manga_result['result'][0]['mal_id'] 46 | ); 47 | 48 | if (empty($manga_info)) { 49 | return ([]); 50 | } 51 | 52 | $final_info = [ 53 | 'title' => $manga_info['title'], 54 | 'title_original' => $manga_info['title_japanese'], 55 | 'summary' => $manga_info['synopsis'], 56 | 'genres' => [] 57 | ]; 58 | 59 | foreach ($manga_info['genre'] as $genre) { 60 | $final_info['genres'][] = $genre['name']; 61 | } 62 | 63 | return ($final_info); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Core/SessionManager.php: -------------------------------------------------------------------------------- 1 | createUser ($_POST['username'], $_POST['password'], 'admin'); 27 | 28 | if ($status === true) { 29 | return (1); 30 | } else { 31 | // Failed to create user 32 | return (0); 33 | } 34 | } 35 | 36 | /** 37 | * AJAX method validates the given username / password given via the login 38 | * form. 39 | * 40 | * @return int login success status 41 | */ 42 | public function ajaxValidateLogin () { 43 | if ( 44 | empty ($_POST['username']) || 45 | empty ($_POST['password']) 46 | ) { 47 | // Missing POST data 48 | return (0); 49 | } 50 | 51 | $db = \Core\Database::getInstance (); 52 | 53 | // Get saved value 54 | $q = 55 | 'SELECT username, password FROM users 56 | WHERE username = :username'; 57 | $r = $db->execute ($q, ['username' => $_POST['username']]); 58 | 59 | if (empty ($r)) { 60 | // No matching username found 61 | return 0; 62 | } 63 | 64 | $pass_valid = password_verify($_POST['password'], $r[0]['password']); 65 | 66 | if ($pass_valid === true) { 67 | $this->setSessionItem ('username', $_POST['username']); 68 | 69 | return (1); 70 | } else { 71 | return (0); 72 | } 73 | } 74 | 75 | /** 76 | * Creates a new user and updated the database with said user. 77 | * 78 | * @param string $username new user's username 79 | * @param string $password new user's plaintext password 80 | * @param string $type new user's user type 81 | * 82 | * @return bool creation success flag 83 | * 84 | * @throws TypeError on: 85 | * - Non-string username 86 | * - Non-string password 87 | * - Non-string user type 88 | * - Non-bool return success flag 89 | */ 90 | public function createUser (string $username, string $password, string $type) : bool { 91 | $db = \Core\Database::getInstance (); 92 | 93 | $pass_hash = password_hash ($password, PASSWORD_DEFAULT); 94 | 95 | $q = ' 96 | INSERT INTO `users` 97 | (`username`, `password`, `type`) 98 | VALUES 99 | ("'.$username.'", "'.$pass_hash.'", "'.$type.'")'; 100 | $r = $db->query ($q); 101 | 102 | if ($r === false) { 103 | return (false); 104 | } else { 105 | return (true); 106 | } 107 | } 108 | 109 | /** 110 | * Gets all session data for the current user's session. 111 | * 112 | * @return array user session data 113 | */ 114 | public function getSessionData () { 115 | return ($_SESSION); 116 | } 117 | 118 | /** 119 | * Gets session value for the given key. 120 | * 121 | * @param string $key session dictionary key 122 | * 123 | * @return string session dictionary value for given key 124 | * 125 | * @throws TypeError on non-string parameter & non-string return 126 | */ 127 | public function getSessionItem (string $key) : string { 128 | if (array_key_exists ($key, $_SESSION)) { 129 | $value = $_SESSION[$key]; 130 | } else { 131 | $value = ''; 132 | } 133 | 134 | return ($value); 135 | } 136 | 137 | /** 138 | * Checks if user is logged in. 139 | * 140 | * @return bool user logged in flag 141 | */ 142 | public function isLoggedIn () { 143 | if (!empty ($_SESSION['username'])) { 144 | return (true); 145 | } else { 146 | return (false); 147 | } 148 | } 149 | 150 | /** 151 | * Loads a user's session. 152 | */ 153 | public function loadSession () { 154 | session_start (); 155 | } 156 | 157 | /** 158 | * Updates user session data with give key value pair. 159 | * 160 | * @param string $key session dictionary key 161 | * @param string $value session dictionary value for given key 162 | * 163 | * @throws TypeError on non-string parameters 164 | */ 165 | public function setSessionItem (string $key, string $value) { 166 | $_SESSION[$key] = $value; 167 | } 168 | 169 | /** 170 | * Unload a user's session. 171 | */ 172 | public function unloadSession () { 173 | session_unset (); 174 | session_destroy (); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/Core/ZipManager.php: -------------------------------------------------------------------------------- 1 | buildViewFactory (), 22 | $this->buildViewProvider ($root_path) 23 | ); 24 | } 25 | 26 | /** 27 | * Builds the view item factory. 28 | * 29 | * @return Factory view item factory 30 | * 31 | * @throws TypeError on non-Factory return 32 | */ 33 | private function buildViewFactory () : Factory { 34 | return new Factory (); 35 | } 36 | 37 | /** 38 | * Builds the view provider. 39 | * 40 | * @param string $root_path path to MangoBango root (index) 41 | * 42 | * @return Provider view provider 43 | * 44 | * @throws TypeError on invalid parameter or return type 45 | */ 46 | private function buildViewProvider (string $root_path) : Provider { 47 | return new Provider ($root_path); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Services/View/Data/Config.php: -------------------------------------------------------------------------------- 1 | setAssetsDirectory ($assets_directory); 42 | $this->setDirectoryStructure ($directory_structure); 43 | $this->setLibraryViewType ($library_view_type); 44 | $this->setMangaDirectory ($manga_directory); 45 | $this->setReaderDisplayStyle ($reader_display_style); 46 | } 47 | 48 | /** 49 | * Gets the assets directory setting. 50 | * 51 | * @return string assets directory setting 52 | * 53 | * @throws TypeError on non-string return 54 | */ 55 | public function getAssetsDirectory () : string { 56 | return $this->assets_directory; 57 | } 58 | 59 | /** 60 | * Gets the manga file directory structure setting. 61 | * 62 | * @return string structure of files in directory 63 | * 64 | * @throws TypeError on non-string return 65 | */ 66 | public function getDirectoryStructure () : string { 67 | return $this->directory_structure; 68 | } 69 | 70 | /** 71 | * Gets the library view type. 72 | * 73 | * @return int library view type 74 | * 75 | * @throws TypeError on non-int return 76 | */ 77 | public function getLibraryViewType () : int { 78 | return $this->library_view_type; 79 | } 80 | 81 | /** 82 | * Gets the manga directory setting. 83 | * 84 | * @return string manga directory setting 85 | * 86 | * @throws TypeError on non-string return 87 | */ 88 | public function getMangaDirectory () : string { 89 | return $this->manga_directory; 90 | } 91 | 92 | /** 93 | * Gets the reader display style setting. 94 | * 95 | * @return int reader display style setting 96 | * 97 | * @throws TypeError on non-int return 98 | */ 99 | public function getReaderDisplayStyle () : int { 100 | return $this->reader_display_style; 101 | } 102 | 103 | /** 104 | * Gets the view name to which the data is tied (the controller's name). 105 | * 106 | * @return string page controller name 107 | * 108 | * @throws TypeError on non-string return 109 | */ 110 | public function getViewName () : string { 111 | return 'Config'; 112 | } 113 | 114 | /** 115 | * Sets the assets directory setting. 116 | * 117 | * @param string $assets_directory assets directory setting 118 | * 119 | * @throws TypeError on non-string config value 120 | */ 121 | private function setAssetsDirectory (string $assets_directory) { 122 | $this->assets_directory = $assets_directory; 123 | } 124 | 125 | /** 126 | * Sets the manga directory structure setting. 127 | * 128 | * @param string $directory_structure structure of files in directory 129 | * 130 | * @throws TypeError on non-string config value 131 | */ 132 | private function setDirectoryStructure (string $directory_structure) { 133 | $this->directory_structure = $directory_structure; 134 | } 135 | 136 | /** 137 | * Sets the library view type. 138 | * 139 | * @param int $config library view type 140 | * 141 | * @throws TypeError on non-int config value 142 | * @throws InvalidArgumentException on config value out of bounds 143 | */ 144 | private function setLibraryViewType (int $config) { 145 | if (!in_array($config, [1, 2])) { 146 | throw new \InvalidArgumentException ( 147 | 'Argument (Library View Type) value must in set [1, 2]; '. 148 | "{$config} given." 149 | ); 150 | } 151 | 152 | $this->library_view_type = $config; 153 | } 154 | 155 | /** 156 | * Sets the manga directory setting. 157 | * 158 | * @param string $config manga directory setting 159 | * 160 | * @throws TypeError on non-string config value 161 | */ 162 | private function setMangaDirectory (string $config) { 163 | $this->manga_directory = trim ($config); 164 | } 165 | 166 | /** 167 | * Sets the reader display style setting. 168 | * 169 | * @param int $config reader display style setting 170 | * 171 | * @throws TypeError on non-int config value 172 | * @throws InvalidArgumentException on config value out of bounds 173 | */ 174 | private function setReaderDisplayStyle (int $config) { 175 | if (!in_array($config, [1, 2])) { 176 | throw new \InvalidArgumentException ( 177 | 'Argument (Reader Display Style) value must in set [1, 2]; '. 178 | "{$config} given."); 179 | } 180 | 181 | $this->reader_display_style = $config; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/Services/View/Data/DisplayLibrary.php: -------------------------------------------------------------------------------- 1 | setMangaData ($manga_data); 24 | } 25 | 26 | /** 27 | * Gets display data for each series. 28 | * 29 | * @return array dictionary of manga data 30 | * 31 | * @throws TypeError on non-array return 32 | */ 33 | public function getMangaData () : array { 34 | return $this->manga_data; 35 | } 36 | 37 | /** 38 | * Gets the view name to which the data is tied (the controller's name). 39 | * 40 | * @return string page controller name 41 | * 42 | * @throws TypeError on non-string return 43 | */ 44 | public function getViewName () : string { 45 | return 'DisplayLibrary'; 46 | } 47 | 48 | /** 49 | * Sets display data for each series. 50 | * 51 | * @param array $manga_data dictionary of manga data in the following structure: 52 | * [manga ID] int manga ID 53 | * ├── ['link'] string link to the series page for this manga 54 | * ├── ['path'] string path to cover image 55 | * └── ['title'] string meta name of series 56 | * 57 | * @throws TypeError on non-array parameter 58 | * @throws InvalidArgumentException on: 59 | * - missing array item keys 60 | * - non string array items 61 | * - empty array items 62 | */ 63 | private function setMangaData (array $manga_data) { 64 | foreach ($manga_data as $i => $manga) { 65 | foreach (['link', 'path', 'title'] as $key) { 66 | if (!array_key_exists ($key, $manga)) { 67 | throw new \InvalidArgumentException ( 68 | "Parameter (Manga Data > {$i}) must have key \"{$key}\"." 69 | ); 70 | } 71 | 72 | if (!is_string ($manga[$key])) { 73 | throw new \InvalidArgumentException ( 74 | "Parameter (Manga Data > {$i} > {$key}) must be of ". 75 | 'type string; '.gettype ($manga[$key]).' given.' 76 | ); 77 | } 78 | 79 | if ($key !== 'path' && empty ($manga[$key])) { 80 | throw new \InvalidArgumentException ( 81 | "Parameter (Manga Data > {$i} > {$key}) cannot be empty." 82 | ); 83 | } 84 | } 85 | } 86 | 87 | $this->manga_data = $manga_data; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/Services/View/Data/DisplaySeries.php: -------------------------------------------------------------------------------- 1 | setChapters ($chapters); 42 | $this->setGenres ($genres); 43 | $this->setSummary ($summary); 44 | $this->setTitle ($title); 45 | $this->setVolumes ($volumes); 46 | } 47 | 48 | /** 49 | * Gets the view name to which the data is tied (the controller's name). 50 | * 51 | * @return string page controller name 52 | * 53 | * @throws TypeError on non-string return 54 | */ 55 | public function getViewName () : string { 56 | return 'DisplaySeries'; 57 | } 58 | 59 | /** 60 | * Gets chapter anchor data list. 61 | * 62 | * @return array chapter data list 63 | * 64 | * @throws TypeError on non-array return 65 | */ 66 | public function getChapters () : array { 67 | return $this->chapters; 68 | } 69 | 70 | /** 71 | * Gets list of series genres. 72 | * 73 | * @return array list of series genres 74 | * 75 | * @throws TypeError on non-array return 76 | */ 77 | public function getGenres () : array { 78 | return $this->genres; 79 | } 80 | 81 | /** 82 | * Gets series summary. 83 | * 84 | * @return string series summary 85 | * 86 | * @throws TypeError on non-string return 87 | */ 88 | public function getSummary () : string { 89 | if (empty ($this->summary)) { 90 | return 'No summary available.'; 91 | } else { 92 | return $this->summary; 93 | } 94 | } 95 | 96 | /** 97 | * Gets manga title. 98 | * 99 | * @return string manga title 100 | * 101 | * @throws TypeError on non-string return 102 | */ 103 | public function getTitle () : string { 104 | if (empty ($this->title)) { 105 | return 'No Title'; 106 | } else { 107 | return $this->title; 108 | } 109 | } 110 | 111 | /** 112 | * Gets display data for each volume. 113 | * 114 | * @return array display data for each volume 115 | * 116 | * @throws TypeError on non-array return 117 | */ 118 | public function getVolumes () : array { 119 | return $this->volumes; 120 | } 121 | 122 | /** 123 | * Sets chapter anchor data list. 124 | * 125 | * Uses the following array structure: 126 | * array 127 | * ├── [0] 128 | * │ ├── ['link'] string anchor href to chapter in reader 129 | * │ └── ['title'] string chapter title 130 | * │ . 131 | * │ . 132 | * └── [n] 133 | * 134 | * @param array $chapters chapter data list 135 | * 136 | * @throws TypeError on non-array parameter 137 | * @throws InvalidArgumentException on: 138 | * - Non-array chapter data 139 | * - Missing chapter data keys (title|link) 140 | * - Non-string chapter data title or link 141 | * - Empty chapter data title or lin 142 | */ 143 | private function setChapters (array $chapters) { 144 | foreach ($chapters as $i => &$chapter) { 145 | if (!is_array ($chapter)) { 146 | throw new \InvalidArgumentException ( 147 | "Parameter (Chapters > {$i}) must be of type array; ". 148 | gettype ($chapter).' given.' 149 | ); 150 | } 151 | 152 | foreach (['title', 'link'] as $key) { 153 | if (!array_key_exists ($key, $chapter)) { 154 | throw new \InvalidArgumentException ( 155 | "Parameter (Chapters > {$i}) must have key \"{$key}\"." 156 | ); 157 | } 158 | 159 | if (!is_string ($chapter[$key])) { 160 | throw new \InvalidArgumentException ( 161 | "Parameter (Chapters > {$i} > {$key}) must be of type ". 162 | 'string; '.gettype ($chapter[$key]).' given.' 163 | ); 164 | } 165 | 166 | $chapter[$key] = trim ($chapter[$key]); 167 | 168 | if (empty ($chapter[$key])) { 169 | throw new \InvalidArgumentException ( 170 | "Parameter (Chapters > {$i} > {$key}) cannot be empty." 171 | ); 172 | } 173 | 174 | $this->chapters = $chapters; 175 | } 176 | } 177 | } 178 | 179 | /** 180 | * Sets list of series genre tags. 181 | * 182 | * @param array $genres list of series genres 183 | * 184 | * @throws TypeError on non-string parameter 185 | * @throws InvalidArgumentException on non-string or empty array items 186 | */ 187 | private function setGenres (array $genres) { 188 | foreach ($genres as $i => $genre) { 189 | if (!is_string ($genre)) { 190 | throw new \InvalidArgumentException ( 191 | "Parameter (Genres > {$i}) must be of type string; ". 192 | gettype ($genre).' given.' 193 | ); 194 | } 195 | 196 | if (empty ($genre)) { 197 | throw new \InvalidArgumentException ( 198 | "Parameter (Genres > {$i}) cannot be empty." 199 | ); 200 | } 201 | } 202 | 203 | $this->genres = $genres; 204 | } 205 | 206 | /** 207 | * Sets series summary. 208 | * 209 | * @var string series summary 210 | * 211 | * @throws TypeError on non-string parameter 212 | */ 213 | private function setSummary (string $summary) { 214 | $this->summary = $summary; 215 | } 216 | 217 | /** 218 | * Sets manga title. 219 | * 220 | * @param string $title manga title 221 | * 222 | * @throws TypeError on non-string parameter 223 | */ 224 | private function setTitle (string $title) { 225 | $this->title = $title; 226 | } 227 | 228 | /** 229 | * Sets display data for each volume. 230 | * 231 | * Uses the following array structure: 232 | * array 233 | * ├── [0] 234 | * │ ├── ['link'] string href reader link for the manga volume 235 | * │ └── ['source'] string image source for the volume cover 236 | * │ . 237 | * │ . 238 | * └── [n] 239 | * 240 | * @param array $volumes display data for each volume 241 | * 242 | * @throws TypeError on non-array parameter 243 | * @throws InvalidArgumentException on: 244 | * - non-array items 245 | * - missing array item keys 246 | * - non string array items 247 | * - empty array items 248 | */ 249 | private function setVolumes (array $volumes) { 250 | foreach ($volumes as $i => $volume) { 251 | if (!is_array ($volume)) { 252 | throw new InvalidArgumentException ( 253 | "Parameter (Volumes > {$i}) must be of type array; ". 254 | gettype ($volume).' given.' 255 | ); 256 | } 257 | 258 | foreach (['link', 'source'] as $key) { 259 | if (!array_key_exists ($key, $volume)) { 260 | throw new InvalidArgumentException ( 261 | "Paramater (Volumes > {$i}) must have key \"{$key}\"." 262 | ); 263 | } 264 | 265 | if (!is_string ($volume[$key])) { 266 | throw new InvalidArgumentException ( 267 | "Paramater (Volumes > {$i} > {$key}) must be of type ". 268 | 'string; '.gettype ($volume[$key]).' given.' 269 | ); 270 | } 271 | 272 | if ($key === 'link' && empty($volume[$key])) { 273 | throw new \InvalidArgumentException ( 274 | "Paramater (Volumes > {$i} > {$key}) cannot be empty." 275 | ); 276 | } 277 | } 278 | } 279 | 280 | $this->volumes = $volumes; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /app/Services/View/Data/Home.php: -------------------------------------------------------------------------------- 1 | setBoxContents ($box_contents); 24 | } 25 | 26 | /** 27 | * Gets the box contents list. 28 | * 29 | * @return array box contents list 30 | * 31 | * @throws TypeError on non-array return 32 | */ 33 | public function getBoxContents () : array { 34 | return $this->box_contents; 35 | } 36 | 37 | /** 38 | * Gets the view name to which the data is tied (the controller's name). 39 | * 40 | * @return string page controller name 41 | * 42 | * @throws TypeError on non-string return 43 | */ 44 | public function getViewName () : string { 45 | return 'Home'; 46 | } 47 | 48 | /** 49 | * Set the box contents list. 50 | * 51 | * @param array $box_contents box contents list 52 | * 53 | * @throws TypeError on non-array parameter 54 | * @throws InvalidArgumentException on non-array items or missing item keys 55 | */ 56 | private function setBoxContents (array $box_contents) { 57 | foreach ($box_contents as $i => $box) { 58 | if (!is_array($box)) { 59 | throw new \InvalidArgumentException( 60 | "Parameter (Box Contents > {$i}) must be of type array; ". 61 | gettype ($box).' given.' 62 | ); 63 | } 64 | 65 | if (!array_key_exists ('title', $box)) { 66 | throw new \InvalidArgumentException( 67 | "Parameter (Box Contents > {$i}) must have key \"title\"." 68 | ); 69 | } 70 | if (!array_key_exists ('value', $box)) { 71 | throw new \InvalidArgumentException( 72 | "Parameter (Box Contents > {$i}) must have key \"value\"." 73 | ); 74 | } 75 | } 76 | 77 | $this->box_contents = $box_contents; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/Services/View/Data/IViewData.php: -------------------------------------------------------------------------------- 1 | setUsername ($username); 27 | $this->setViewContent ($view_content); 28 | } 29 | 30 | /** 31 | * Gets username. 32 | * 33 | * @return string username 34 | * 35 | * @throws TypeError on non-string return 36 | */ 37 | public function getUsername () : string { 38 | return $this->username; 39 | } 40 | 41 | /** 42 | * Gets view content. 43 | * 44 | * @return ViewItem view content 45 | * 46 | * @throws TypeError on non-ViewItem return 47 | */ 48 | public function getViewContent () : ViewItem { 49 | return $this->view_content; 50 | } 51 | 52 | /** 53 | * Gets the view name to which the data is tied (the controller's name). 54 | * 55 | * @return string page controller name 56 | * 57 | * @throws TypeError on non-string return 58 | */ 59 | public function getViewName () : string { 60 | return 'Master'; 61 | } 62 | 63 | /** 64 | * Sets username. 65 | * 66 | * @param string $username username 67 | * 68 | * @throws TypeError on non-string parameter 69 | */ 70 | private function setUsername (string $username) { 71 | $this->username = $username; 72 | } 73 | 74 | /** 75 | * Sets view content. 76 | * 77 | * @param string $view_content view content 78 | * 79 | * @throws TypeError on non-ViewItem parameter 80 | */ 81 | private function setViewContent (ViewItem $view_content) { 82 | $this->view_content = $view_content; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/Services/View/Data/Page.php: -------------------------------------------------------------------------------- 1 | setTitle ($title); 27 | $this->setViewContent ($view_content); 28 | } 29 | 30 | /** 31 | * Gets page title. 32 | * 33 | * @return string page title 34 | * 35 | * @throws TypeError on non-string return 36 | */ 37 | public function getTitle () : string { 38 | return $this->title; 39 | } 40 | 41 | /** 42 | * Gets view content. 43 | * 44 | * @return ViewItem view content 45 | * 46 | * @throws TypeError on non-ViewItem return 47 | */ 48 | public function getViewContent () : ViewItem { 49 | return $this->view_content; 50 | } 51 | 52 | /** 53 | * Gets the view name to which the data is tied (the controller's name). 54 | * 55 | * @return string page controller name 56 | * 57 | * @throws TypeError on non-string return 58 | */ 59 | public function getViewName () : string { 60 | return 'Page'; 61 | } 62 | 63 | /** 64 | * Sets page title. 65 | * 66 | * @param string $title page title 67 | * 68 | * @throws TypeError on non-string parameter 69 | */ 70 | private function setTitle (string $title) { 71 | $this->title = $title; 72 | } 73 | 74 | /** 75 | * Sets view content. 76 | * 77 | * @param string $view_content view content 78 | * 79 | * @throws TypeError on non-ViewItem parameter 80 | */ 81 | private function setViewContent (ViewItem $view_content) { 82 | $this->view_content = $view_content; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/Services/View/Data/ReaderPage.php: -------------------------------------------------------------------------------- 1 | setFilePaths ($file_paths); 27 | $this->setNextChapterLink ($next_chapter_link); 28 | } 29 | 30 | /** 31 | * Gets the view name to which the data is tied (the controller's name). 32 | * 33 | * @return string page controller name 34 | * 35 | * @throws TypeError on non-string return 36 | */ 37 | public function getViewName () : string { 38 | return 'ReaderPage'; 39 | } 40 | 41 | /** 42 | * Gets list of image paths. 43 | * 44 | * @return array list of image paths 45 | * 46 | * @throws TypeError on non-array return 47 | */ 48 | public function getFilePaths () : array { 49 | return $this->file_paths; 50 | } 51 | 52 | /** 53 | * Gets next chapter anchor link. 54 | * 55 | * @return string next chapter anchor link or empty if no next chapter 56 | * 57 | * @throws TypeError on non-string return 58 | */ 59 | public function getNextChapterLink () : string { 60 | return $this->next_chapter_link; 61 | } 62 | 63 | /** 64 | * Sets list of image paths. 65 | * 66 | * @param array $file_paths list of image paths 67 | * 68 | * @throws InvalidArgumentException on non-string or empty items 69 | * @throws TypeError on non-array parameter 70 | */ 71 | private function setFilePaths (array $file_paths) { 72 | foreach ($file_paths as $i => $path) { 73 | if (!is_string ($path)) { 74 | throw new \InvalidArgumentException ( 75 | "Parameter (File Paths > {$i}) must be of type string; ". 76 | gettype ($path).' given.' 77 | ); 78 | } 79 | 80 | if (empty (trim ($path))) { 81 | throw new \InvalidArgumentException ( 82 | "Parameter (File Paths > {$i}) can not be empty." 83 | ); 84 | } 85 | } 86 | 87 | $this->file_paths = $file_paths; 88 | } 89 | 90 | /** 91 | * Sets next chapter anchor link. 92 | * 93 | * @param string $next_chapter_link next chapter anchor link or empty if no 94 | * next chapter 95 | * 96 | * @throws TypeError on non-string parameter 97 | */ 98 | private function setNextChapterLink (string $next_chapter_link) { 99 | $this->next_chapter_link = $next_chapter_link; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/Services/View/Data/ReaderStrip.php: -------------------------------------------------------------------------------- 1 | setFilePaths ($file_paths); 27 | $this->setNextChapterLink ($next_chapter_link); 28 | } 29 | 30 | /** 31 | * Gets list of image paths. 32 | * 33 | * @return array list of image paths 34 | * 35 | * @throws TypeError on non-array return 36 | */ 37 | public function getFilePaths () : array { 38 | return $this->file_paths; 39 | } 40 | 41 | /** 42 | * Gets next chapter anchor link. 43 | * 44 | * @return string next chapter anchor link or empty if no next chapter 45 | * 46 | * @throws TypeError on non-string return 47 | */ 48 | public function getNextChapterLink () : string { 49 | return $this->next_chapter_link; 50 | } 51 | 52 | /** 53 | * Gets the view name to which the data is tied (the controller's name). 54 | * 55 | * @return string page controller name 56 | * 57 | * @throws TypeError on non-string return 58 | */ 59 | public function getViewName () : string { 60 | return 'ReaderStrip'; 61 | } 62 | 63 | /** 64 | * Sets list of image paths. 65 | * 66 | * @param array $file_paths list of image paths 67 | * 68 | * @throws InvalidArgumentException on non-string or empty items 69 | * @throws TypeError on non-array parameter 70 | */ 71 | private function setFilePaths (array $file_paths) { 72 | foreach ($file_paths as $i => $path) { 73 | if (!is_string ($path)) { 74 | throw new \InvalidArgumentException ( 75 | "Parameter (File Paths > {$i}) must be of type string; ". 76 | gettype ($path).' given.' 77 | ); 78 | } 79 | 80 | if (empty (trim ($path))) { 81 | throw new \InvalidArgumentException ( 82 | "Parameter (File Paths > {$i}) can not be empty." 83 | ); 84 | } 85 | } 86 | 87 | $this->file_paths = $file_paths; 88 | } 89 | 90 | /** 91 | * Sets next chapter anchor link. 92 | * 93 | * @param string $next_chapter_link next chapter anchor link or empty if no 94 | * next chapter 95 | * 96 | * @throws TypeError on non-string parameter 97 | */ 98 | private function setNextChapterLink (string $next_chapter_link) { 99 | $this->next_chapter_link = $next_chapter_link; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/Services/View/Data/ViewData.php: -------------------------------------------------------------------------------- 1 | view_data = $view_data; 17 | } 18 | 19 | /** 20 | * Gets view data item by key. 21 | * 22 | * @param string $key view data key 23 | * 24 | * @return mixed requested view data or null if not found 25 | */ 26 | public function getViewData (string $key) { 27 | return isset($this->view_data[$key]) ? $this->view_data[$key] : null; 28 | } 29 | 30 | /** 31 | * Required method. 32 | */ 33 | public function getViewName () : string 34 | { 35 | return ''; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Services/View/Data/ViewItem.php: -------------------------------------------------------------------------------- 1 | setCSSTags ($css); 21 | $this->setHTML ($html); 22 | $this->setJSTags ($js); 23 | } 24 | 25 | /** 26 | * Gets CSS output. 27 | * 28 | * @return string CSS output 29 | * 30 | * @throws TypeError on non-string return 31 | */ 32 | public function getCSS () : string { 33 | return implode ("\n", $this->css); 34 | } 35 | 36 | /** 37 | * Gets list of CSS tags. 38 | * 39 | * @return array list of CSS tags 40 | * 41 | * @throws TypeError on non-array return 42 | */ 43 | public function getCSSTags () : array { 44 | return $this->css; 45 | } 46 | 47 | /** 48 | * Gets HTML output. 49 | * 50 | * @return string HTML output 51 | * 52 | * @throws TypeError on non-string return 53 | */ 54 | public function getHTML () : string { 55 | return $this->html; 56 | } 57 | 58 | /** 59 | * Gets JS output. 60 | * 61 | * @return string JS output 62 | * 63 | * @throws TypeError on non-string return 64 | */ 65 | public function getJS () : string { 66 | return implode ("\n", $this->js); 67 | } 68 | 69 | /** 70 | * Gets list of JS tags. 71 | * 72 | * @return array list of JS tags 73 | * 74 | * @throws TypeError on non-array return 75 | */ 76 | public function getJSTags () : array { 77 | return $this->js; 78 | } 79 | 80 | /** 81 | * Sets CSS tag list. 82 | * 83 | * @param array $css CSS tag list 84 | * 85 | * @throws InvalidArgumentException on non-string items 86 | * @throws TypeError on non-array parameter 87 | */ 88 | private function setCSSTags (array $css) { 89 | foreach ($css as $item) { 90 | if (!is_string ($item)) 91 | throw new \InvalidArgumentException ( 92 | 'Parameter (CSS > n) must be of type string; '. 93 | gettype ($css).' given.' 94 | ); 95 | } 96 | 97 | $this->css = $css; 98 | } 99 | 100 | /** 101 | * Sets HTML. 102 | * 103 | * @param string $html HTML output 104 | * 105 | * @throws TypeError on non-string parameter 106 | */ 107 | private function setHTML (string $html) { 108 | $this->html = $html; 109 | } 110 | 111 | /** 112 | * Sets JS tag list. 113 | * 114 | * @param array $js JS tag list 115 | * 116 | * @throws InvalidArgumentException on non-string items 117 | * @throws TypeError on non-array parameter 118 | */ 119 | private function setJSTags (array $js) { 120 | foreach ($js as $item) { 121 | if (!is_string ($item)) 122 | throw new \InvalidArgumentException ( 123 | 'Parameter (JS > n) must be of type string; '. 124 | gettype ($js).' given.' 125 | ); 126 | } 127 | 128 | $this->js = $js; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/Services/View/Factory.php: -------------------------------------------------------------------------------- 1 | setRootPath ($root_path); 21 | } 22 | 23 | /** 24 | * Fetches HTML file contents. 25 | * 26 | * @param string $filename name of HTML file to open. 27 | * @param IViewData $view data object for the view 28 | * 29 | * @return string file contents or empty if open failed 30 | * 31 | * @throws TypeError on invalid parameter or return type 32 | */ 33 | public function fetchHTMLFile (string $filename, ?IViewData $view) : string { 34 | $root = $this->getRootPath (); 35 | 36 | ob_start (); 37 | require_once ("{$root}\\ViewItems\\HTML\\{$filename}.php"); 38 | 39 | return ob_get_clean (); 40 | } 41 | 42 | /** 43 | * Validates that the requested files exist. 44 | * 45 | * @param array $config dictionary of view file names to load 46 | * 47 | * @throws InvalidArgumentException on unloadable file 48 | * @throws TypeError on non-array parameter 49 | */ 50 | public function validateFiles (array $config) { 51 | $root = $this->getRootPath (); 52 | 53 | foreach ($config as $name => $file_list) { 54 | if ($name === 'name' || empty ($file_list)) 55 | continue; 56 | 57 | switch ($name) { 58 | case 'CSS': 59 | $ext = 'css'; 60 | break; 61 | case 'HTML': 62 | $ext = 'php'; 63 | break; 64 | case 'JS': 65 | $ext = 'js'; 66 | break; 67 | } 68 | 69 | if ($name === 'HTML') { 70 | $filepath = "{$root}\\ViewItems\\{$name}\\{$file_list}.{$ext}"; 71 | 72 | if (!file_exists ($filepath)) 73 | throw new \InvalidArgumentException ( 74 | "File at {$filepath} could not be loaded." 75 | ); 76 | 77 | continue; 78 | } 79 | 80 | foreach ($file_list as $file) { 81 | $filepath = "{$root}\\ViewItems\\{$name}\\{$file}.{$ext}"; 82 | 83 | if (!file_exists ($filepath)) 84 | throw new \InvalidArgumentException ( 85 | "File at {$filepath} could not be loaded." 86 | ); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Gets root path. 93 | * 94 | * @return string root path 95 | * 96 | * @throws TypeError on non-string return 97 | */ 98 | private function getRootPath () : string { 99 | return $this->root_path; 100 | } 101 | 102 | /** 103 | * Sets root path. 104 | * 105 | * @param string $root_path root path 106 | * 107 | * @throws TypeError on non-string parameter 108 | */ 109 | private function setRootPath (string $root_path) { 110 | $this->root_path = $root_path; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/Services/View/Service.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 23 | $this->provider = $provider; 24 | } 25 | 26 | /** 27 | * Finalizes the page with the view content given. 28 | * 29 | * @param string $title title of the page 30 | * @param ViewItem $view_content view content object 31 | * 32 | * @return string page HTML (with CSS / JS tags) 33 | * 34 | * @throws TypeError on invalid parameter or return type 35 | */ 36 | public function buildPage (string $title, ViewItem $view_content) : string { 37 | $view_data = $this->factory->buildViewData ( 38 | 'Page', 39 | [ 40 | 'title' => $title, 41 | 'view_content' => $view_content 42 | ] 43 | ); 44 | 45 | return $this->provider->fetchHTMLFile ('Page', $view_data); 46 | } 47 | 48 | /** 49 | * Builds the requested view. 50 | * 51 | * @param array $config dictionary of view file names to load 52 | * @param array $parameters dictionary of view variables 53 | * 54 | * @return ViewItem processed view structure 55 | * 56 | * @throws TypeError on non-array parameters or non-ViewItem return 57 | */ 58 | public function buildView (array $config, array $parameters) : ViewItem { 59 | // Validate types and array structure 60 | $this->validateConfigStructure ($config); 61 | 62 | // Validate file targets 63 | $this->provider->validateFiles ($config); 64 | 65 | // Build view parameters object 66 | $view_data = null; 67 | if (!empty ($config['name'])) 68 | $view_data = $this->factory->buildViewData ( 69 | $config['name'], 70 | $parameters 71 | ); 72 | 73 | // Get CSS/JS tags 74 | $css = []; 75 | if (!empty ($config['CSS'])) 76 | $css = $this->getCSSTags ($config['CSS']); 77 | 78 | $js = []; 79 | if (!empty ($config['JS'])) 80 | $js = $this->getJSTags ($config['JS']); 81 | 82 | foreach ($parameters as $param) { 83 | if ($param instanceof ViewItem) { 84 | $css = array_merge ($css, $param->getCSSTags ()); 85 | $js = array_merge ($js, $param->getJSTags ()); 86 | } 87 | } 88 | 89 | // Get & process HTML 90 | $html = ''; 91 | if (!empty ($config['HTML'])) 92 | $html = $this->provider->fetchHTMLFile ($config['HTML'], $view_data); 93 | 94 | return $this->factory->buildViewItem ($css, $html, $js); 95 | } 96 | 97 | /** 98 | * Gets CSS tags for the requested CSS files. 99 | * 100 | * @param array $css_files list of CSS file names to load 101 | * 102 | * @return array list of CSS tags 103 | * 104 | * @throws TypeError on invalid parameter or return types 105 | */ 106 | private function getCSSTags (array $css_files) : array { 107 | return array_map ( 108 | function (string $file) : string { 109 | return " 110 | "; 115 | }, 116 | $css_files 117 | ); 118 | } 119 | 120 | /** 121 | * Gets JS tags for the requested JS files. 122 | * 123 | * @param array $js_files list of JS file names to load 124 | * 125 | * @return array list of JS tags 126 | * 127 | * @throws TypeError on invalid parameter or return types 128 | */ 129 | private function getJSTags (array $js_files) : array { 130 | return array_map ( 131 | function (string $file) : string { 132 | return " 133 | "; 138 | }, 139 | $js_files 140 | ); 141 | } 142 | 143 | /** 144 | * Validates view config dictionary structure and data types. 145 | * 146 | * @param array $config dictionary of view file names to load 147 | * 148 | * @throws InvalidArgumentException on invalid dictionary structure or types 149 | * @throws TypeError on non-array parameter 150 | */ 151 | private function validateConfigStructure (array $config) { 152 | foreach (['name', 'CSS', 'HTML', 'JS'] as $req_key) 153 | if (!array_key_exists ($req_key, $config)) 154 | throw new \InvalidArgumentException ( 155 | "Parameter (Config > {$req_key}) must exist." 156 | ); 157 | 158 | foreach (['CSS', 'JS'] as $key) { 159 | if (!is_array ($config[$key])) 160 | throw new \InvalidArgumentException ( 161 | "Parameter (Config > {$key}) must be of type 162 | array; ".gettype ($key).' given.' 163 | ); 164 | 165 | foreach ($config[$key] as $i => $item) { 166 | if (!is_string ($item)) 167 | throw new \InvalidArgumentException ( 168 | "Parameter (Config > {$key} > {$i}) must be of type 169 | string; ".gettype ($item).' given.' 170 | ); 171 | 172 | if (empty ($item)) 173 | throw new \InvalidArgumentException ( 174 | "Parameter (Config > {$key} > {$i}) must not be empty 175 | if included." 176 | ); 177 | } 178 | } 179 | 180 | foreach (['name', 'HTML'] as $key) 181 | if (!is_string ($config[$key])) 182 | throw new \InvalidArgumentException ( 183 | "Parameter (Config > {$key}) must be of type string; ". 184 | gettype ($config[$key]).' given.' 185 | ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /app/ViewItems/CSS/Config.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for ConfigView page view. 3 | */ 4 | .config_list_wrap { 5 | font-family: Consolas; 6 | } 7 | 8 | .config_list_wrap .title { 9 | margin-bottom: 0; 10 | text-align: center; 11 | font-size: 2em; 12 | } 13 | 14 | .config_list_wrap .config_list { 15 | padding: 30px; 16 | } 17 | 18 | .config_list_wrap .config_list .config_section { 19 | margin-bottom: 30px; 20 | background-color: #2b2b2b; 21 | box-shadow: 0 0 4px 4px rgba(0, 0, 0, 0.4); 22 | box-sizing: border-box; 23 | color: #828282; 24 | } 25 | 26 | .config_list_wrap .config_list .config_section .section_header { 27 | padding: 3px 15px; 28 | background-color: var(--hightlight_bg); 29 | color: #000; 30 | font-size: 1.7em; 31 | } 32 | 33 | .config_list_wrap .config_list .config_section .config_wrap { 34 | padding: 10px; 35 | font-size: 1.5em; 36 | } 37 | 38 | .config_list_wrap .config_list .config_section .config_wrap .config_item { 39 | padding: 10px; 40 | } 41 | 42 | .config_list_wrap .config_list .config_section .config_wrap select, 43 | .config_list_wrap .config_list .config_section .config_wrap input { 44 | margin-left: 15px; 45 | font-size: 0.9em; 46 | } 47 | 48 | .config_list_wrap .config_list .config_section .config_wrap input { 49 | width: 550px; 50 | } 51 | 52 | .config_list_wrap .config_list .config_section .config_wrap #rescan_library_btn { 53 | padding: 10px; 54 | display: inline-block; 55 | background-color: var(--hightlight_bg); 56 | color: black; 57 | cursor: pointer; 58 | } 59 | -------------------------------------------------------------------------------- /app/ViewItems/CSS/DisplayLibrary.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for the DisplayLibraryView page view. 3 | */ 4 | .library_display_container { 5 | padding: 10px; 6 | box-sizing: border-box; 7 | overflow-x: hidden; 8 | } 9 | 10 | .library_display_container .manga_series_wrap { 11 | margin: 5px; 12 | padding: 10px; 13 | width: 300px; 14 | display: inline-block; 15 | background-color: #2b2b2b; 16 | vertical-align: top; 17 | } 18 | 19 | .library_display_container .manga_series_wrap:hover { 20 | background-color: var(--hightlight_bg); 21 | } 22 | 23 | .library_display_container .manga_series_wrap .title { 24 | position: relative; 25 | margin: 0; 26 | height: 3.06em; 27 | display: block; 28 | color: #b9b9b9; 29 | overflow: hidden; 30 | text-align: center; 31 | font-family: Arial; 32 | font-size: 1.63em; 33 | white-space: nowrap; 34 | } 35 | 36 | .library_display_container .manga_series_wrap .title .elipsis { 37 | display: inline-block; 38 | } 39 | 40 | .library_display_container .manga_series_wrap .title .title_expand { 41 | position: absolute; 42 | top: 0; 43 | right: 0; 44 | left: 0; 45 | min-height: 100%; 46 | display: none; 47 | background-color: var(--hightlight_bg); 48 | color: #5f3d00; 49 | white-space: normal; 50 | } 51 | 52 | .library_display_container .manga_series_wrap .title::before { 53 | width: 0; 54 | height: 100%; 55 | display: inline-block; 56 | vertical-align: middle; 57 | content: ''; 58 | } 59 | 60 | .library_display_container .manga_series_wrap:hover .title { 61 | color: #5f3d00; 62 | } 63 | 64 | .library_display_container .manga_series_wrap:hover .title .title_expand { 65 | display: block; 66 | } 67 | 68 | .library_display_container .manga_series_wrap a { 69 | display: block; 70 | text-decoration: none; 71 | } 72 | 73 | .library_display_container .manga_series_wrap a img { 74 | width: 100%; 75 | height: 425px; 76 | vertical-align: top; 77 | } 78 | 79 | .library_display_container .manga_series_wrap a .placeholder { 80 | height: 425px; 81 | border: 2px dashed #b28c00; 82 | text-align: center; 83 | } 84 | 85 | .library_display_container .manga_series_wrap a .placeholder::before { 86 | width: 0; 87 | height: 100%; 88 | display: inline-block; 89 | vertical-align: middle; 90 | content: ''; 91 | } 92 | 93 | .library_display_container .manga_series_wrap a .placeholder img { 94 | width: 65%; 95 | height: auto; 96 | display: inline-block; 97 | vertical-align: middle; 98 | } 99 | 100 | .nothing_to_show { 101 | position: absolute; 102 | top: calc(49px + 50% - 2em); 103 | right: 0; 104 | bottom: 0; 105 | left: 0; 106 | padding: 0 20px; 107 | color: #676767; 108 | text-align: center; 109 | font-family: Consolas; 110 | font-size: 4em; 111 | } 112 | 113 | @media all and (max-width: 2000px) { 114 | .library_display_container { 115 | display: grid; 116 | grid-column-gap: 10px; 117 | grid-row-gap: 10px; 118 | grid-template-columns: calc((100% / 6) - 8.33px) calc((100% / 6) - 8.33px) calc((100% / 6) - 8.33px) calc((100% / 6) - 8.33px) calc((100% / 6) - 8.33px) calc((100% / 6) - 8.33px); 119 | } 120 | 121 | .library_display_container .manga_series_wrap { 122 | margin: 0; 123 | width: auto; 124 | } 125 | 126 | .library_display_container .manga_series_wrap .title { 127 | height: 4vw; 128 | font-size: 1.3vw; 129 | } 130 | 131 | .library_display_container .manga_series_wrap a img, 132 | .library_display_container .manga_series_wrap a .placeholder { 133 | height: 20vw; 134 | } 135 | } 136 | 137 | @media all and (max-width: 1600px) { 138 | .library_display_container { 139 | grid-template-columns: calc((100% / 5) - 8px) calc((100% / 5) - 8px) calc((100% / 5) - 8px) calc((100% / 5) - 8px) calc((100% / 5) - 8px); 140 | } 141 | 142 | .library_display_container .manga_series_wrap a img, 143 | .library_display_container .manga_series_wrap a .placeholder { 144 | height: 24.2vw; 145 | } 146 | } 147 | 148 | @media all and (max-width: 1100px) { 149 | .library_display_container { 150 | grid-template-columns: calc((100% / 4) - 7.5px) calc((100% / 4) - 7.5px) calc((100% / 4) - 7.5px) calc((100% / 4) - 7.5px); 151 | } 152 | 153 | .library_display_container .manga_series_wrap .title { 154 | font-size: 1.5vw; 155 | } 156 | 157 | .library_display_container .manga_series_wrap a img, 158 | .library_display_container .manga_series_wrap a .placeholder { 159 | height: 30.34vw; 160 | } 161 | } 162 | 163 | @media all and (max-width: 768px) { 164 | .library_display_container { 165 | grid-template-columns: calc((100% / 3) - 6.7px) calc((100% / 3) - 6.7px) calc((100% / 3) - 6.7px); 166 | } 167 | 168 | .library_display_container .manga_series_wrap .title { 169 | height: 5.5vw; 170 | font-size: 1.9vw; 171 | } 172 | 173 | .library_display_container .manga_series_wrap a img, 174 | .library_display_container .manga_series_wrap a .placeholder { 175 | height: 37.66vw; 176 | } 177 | 178 | .nothing_to_show { 179 | top: calc(50% - 1.5em); 180 | font-size: 3em; 181 | } 182 | } 183 | 184 | @media all and (max-width: 500px) { 185 | .library_display_container { 186 | grid-template-columns: calc((100% / 2) - 5px) calc((100% / 2) - 5px); 187 | } 188 | 189 | .library_display_container .manga_series_wrap .title { 190 | height: 6.5vw; 191 | font-size: 2.6vw; 192 | } 193 | 194 | .library_display_container .manga_series_wrap a img, 195 | .library_display_container .manga_series_wrap a .placeholder { 196 | height: 53.24vw; 197 | } 198 | 199 | .nothing_to_show { 200 | top: calc(50% - 1em); 201 | font-size: 2em; 202 | } 203 | } 204 | 205 | @media all and (max-width: 320px) { 206 | .library_display_container { 207 | grid-template-columns: 100%; 208 | } 209 | 210 | .library_display_container .manga_series_wrap .title { 211 | height: 13vw; 212 | font-size: 5vw; 213 | } 214 | 215 | .library_display_container .manga_series_wrap a img, 216 | .library_display_container .manga_series_wrap a .placeholder { 217 | height: 113.64vw; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /app/ViewItems/CSS/DisplaySeries.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for DisplaySeriesView page view. 3 | */ 4 | .library_metadata { 5 | position: relative; 6 | z-index: 2; 7 | padding-bottom: 10px; 8 | display: block; 9 | background-color: #2b2b2b; 10 | box-shadow: 0 0 4px 4px rgba(0, 0, 0,0.6); 11 | color: #fff; 12 | font-family: Consolas; 13 | } 14 | 15 | .library_metadata .section_left { 16 | width: 25%; 17 | display: block; 18 | float: left; 19 | } 20 | 21 | .library_metadata .section_middle { 22 | width: 50%; 23 | display: block; 24 | float: left; 25 | } 26 | 27 | .library_metadata .section_right { 28 | width: 25%; 29 | display: block; 30 | float: left; 31 | } 32 | 33 | .library_metadata .series_title { 34 | padding-top: 10px; 35 | padding-bottom: 5px; 36 | display: block; 37 | text-align: center; 38 | font-size: 2em; 39 | } 40 | 41 | .library_metadata .section_header { 42 | padding-top: 20px; 43 | padding-left: 20px; 44 | display: block; 45 | font-size: 1.4em; 46 | } 47 | 48 | .library_metadata .section_block { 49 | padding: 10px 20px; 50 | display: block; 51 | } 52 | 53 | .library_metadata .section_block .tag_wrap { 54 | margin-bottom: 10px; 55 | padding: 2px 8px; 56 | display: inline-block; 57 | background-color: #007ab3; 58 | border-radius: 4px; 59 | } 60 | 61 | .left_wrap { 62 | width: 25%; 63 | float: left; 64 | } 65 | 66 | .sticky_glue { 67 | position: relative; 68 | z-index: 3; 69 | margin-top: 15px; 70 | margin-left: 3%; 71 | width: 97%; 72 | height: 8px; 73 | background-color: var(--hightlight_bg); 74 | } 75 | 76 | .chapter_container { 77 | position: relative; 78 | z-index: 1; 79 | margin-left: 3%; 80 | width: 97%; 81 | background-color: #2b2b2b; 82 | box-shadow: 0 0 4px 4px rgba(0, 0, 0,0.5); 83 | box-sizing: border-box; 84 | font-family: Consolas; 85 | } 86 | 87 | .chapter_container .header { 88 | padding-bottom: 6px; 89 | color: #000; 90 | background-color: var(--hightlight_bg); 91 | text-align: center; 92 | font-size: 1.5em; 93 | } 94 | 95 | .chapter_container a { 96 | padding: 5px; 97 | display: block; 98 | background-color: #373737; 99 | color: #fff; 100 | text-align: center; 101 | text-decoration: none; 102 | } 103 | 104 | .chapter_container a:nth-child(2n) { 105 | background-color: #2b2b2b; 106 | } 107 | 108 | .chapter_container a:hover, 109 | .chapter_container a:nth-child(2n):hover { 110 | background-color: var(--hightlight_bg); 111 | color: #000; 112 | } 113 | 114 | .library_display_container { 115 | padding: 8px 10px 10px; 116 | width: 75%; 117 | float: right; 118 | box-sizing: border-box; 119 | } 120 | 121 | .library_display_container .manga_volume_wrap { 122 | margin: 5px; 123 | padding: 10px; 124 | width: 300px; 125 | display: inline-block; 126 | background-color: #2b2b2b; 127 | vertical-align: top; 128 | } 129 | 130 | .library_display_container .manga_volume_wrap:hover { 131 | background-color: var(--hightlight_bg); 132 | } 133 | 134 | .library_display_container .manga_volume_wrap a { 135 | display: block; 136 | text-decoration: none; 137 | } 138 | 139 | .library_display_container .manga_volume_wrap a img { 140 | width: 100%; 141 | height: 425px; 142 | vertical-align: top; 143 | } 144 | 145 | .library_display_container .manga_volume_wrap a .placeholder { 146 | text-align: center; 147 | } 148 | 149 | .library_display_container .manga_volume_wrap a .placeholder::before { 150 | width: 0; 151 | height: 100%; 152 | display: inline-block; 153 | vertical-align: middle; 154 | content: ''; 155 | } 156 | 157 | .library_display_container .manga_volume_wrap a .placeholder img { 158 | width: 65%; 159 | display: inline-block; 160 | vertical-align: middle; 161 | } 162 | 163 | @media all and (max-width: 1000px) { 164 | .library_metadata .section_left { 165 | width: 100%; 166 | float: none; 167 | } 168 | 169 | .library_metadata .section_right { 170 | width: 50%; 171 | } 172 | 173 | .left_wrap { 174 | width: 40%; 175 | } 176 | 177 | .library_display_container { 178 | width: 60%; 179 | } 180 | 181 | .library_display_container .manga_volume_wrap { 182 | width: 40%; 183 | } 184 | 185 | .library_display_container .manga_volume_wrap a img { 186 | height: 31.5vw; 187 | } 188 | } 189 | 190 | @media all and (max-width: 600px) { 191 | .library_metadata .section_middle { 192 | width: 100%; 193 | float: none; 194 | } 195 | 196 | .library_metadata .section_right { 197 | width: 100%; 198 | float: none; 199 | } 200 | 201 | .left_wrap { 202 | width: 100%; 203 | float: none; 204 | } 205 | 206 | .sticky_glue { 207 | margin-left: 0; 208 | width: 100%; 209 | } 210 | 211 | .chapter_container { 212 | margin-left: 0; 213 | width: 100%; 214 | /* This resets Dan's scripted height requirements */ 215 | height: auto !important; 216 | } 217 | 218 | .library_display_container { 219 | width: 100%; 220 | } 221 | 222 | .library_display_container .manga_volume_wrap { 223 | width: calc(100% - 30px); 224 | } 225 | 226 | .library_display_container .manga_volume_wrap a img { 227 | height: auto; 228 | } 229 | 230 | .library_display_container .manga_volume_wrap a .placeholder { 231 | height: 109.6vw; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /app/ViewItems/CSS/Home.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for HomeView page view. 3 | */ 4 | .home_header { 5 | margin: 0.5em 0; 6 | padding-bottom: 0.5em; 7 | color: #c19356; 8 | text-align: center; 9 | font-family: Consolas; 10 | font-size: 2em; 11 | } 12 | 13 | .home_header::after { 14 | position: relative; 15 | top: 16px; 16 | margin: 0 auto; 17 | width: 500px; 18 | display: block; 19 | border-bottom: 1px solid #d68100; 20 | content: ""; 21 | } 22 | 23 | .statbox_grid_display { 24 | padding: 50px; 25 | display: grid; 26 | grid-column-gap: 50px; 27 | grid-row-gap: 50px; 28 | grid-template-columns: auto auto auto; 29 | box-sizing: border-box; 30 | } 31 | 32 | .statbox_grid_display .statbox_wrap { 33 | padding: 10px; 34 | background-color: #aaa78c; 35 | box-shadow: 0 0 9px 4px rgba(0, 0, 0,0.2); 36 | box-sizing: border-box; 37 | font-family: Consolas; 38 | } 39 | 40 | .statbox_grid_display .statbox_wrap .title { 41 | padding-bottom: 10px; 42 | display: block; 43 | text-align: center; 44 | font-size: 2em; 45 | } 46 | 47 | .statbox_grid_display .statbox_wrap .statbox_inner_wrap span { 48 | display: block; 49 | text-align: center; 50 | font-size: 3em; 51 | } 52 | 53 | @media all and (max-width: 768px) { 54 | .statbox_grid_display { 55 | grid-template-columns: auto auto; 56 | } 57 | } 58 | 59 | @media all and (max-width: 500px) { 60 | .statbox_grid_display { 61 | padding: 25px; 62 | grid-row-gap: 25px; 63 | grid-template-columns: auto; 64 | } 65 | 66 | .statbox_grid_display .statbox_wrap .title { 67 | font-size: 1.6em; 68 | } 69 | 70 | .statbox_grid_display .statbox_wrap .statbox_inner_wrap span { 71 | font-size: 2.3em; 72 | } 73 | } -------------------------------------------------------------------------------- /app/ViewItems/CSS/Login.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for log in page. 3 | */ 4 | :root { 5 | --hightlight_bg: #ffc800; 6 | } 7 | 8 | html, 9 | body { 10 | margin: 0 auto; 11 | height: 100%; 12 | background-color: #2b2b2b; 13 | } 14 | 15 | .login_wrap { 16 | height: 100%; 17 | text-align: center; 18 | } 19 | 20 | .login_wrap::before { 21 | width: 0; 22 | height: 100%; 23 | display: inline-block; 24 | vertical-align: middle; 25 | content: ''; 26 | } 27 | 28 | .login_wrap .login_box { 29 | width: 600px; 30 | display: inline-block; 31 | background-color: #3a3a3a; 32 | box-shadow: 0 0 4px 4px rgba(0, 0, 0, 0.3); 33 | vertical-align: middle; 34 | font-family: Consolas; 35 | } 36 | 37 | .login_wrap .login_box .title_strip { 38 | padding: 10px; 39 | background-color: var(--hightlight_bg); 40 | font-size: 2.5em; 41 | } 42 | 43 | .login_wrap .login_box .login_box_inner { 44 | padding: 30px; 45 | } 46 | 47 | .login_wrap .login_box .login_box_inner .warning { 48 | margin-bottom: 10px; 49 | padding: 10px; 50 | display: none; 51 | border: 1px solid #ff1d1d; 52 | color: #ff1d1d; 53 | } 54 | 55 | .login_wrap .login_box .login_box_inner .input_wrap { 56 | margin-bottom: 20px; 57 | padding: 10px; 58 | display: block; 59 | background-color: #d9d9d9; 60 | } 61 | 62 | .login_wrap .login_box .login_box_inner .input_wrap input { 63 | width: 100%; 64 | background-color: transparent; 65 | border: none; 66 | font-size: 1.5em; 67 | } 68 | 69 | .login_wrap .login_box .login_box_inner #login_btn { 70 | padding: 10px 40px; 71 | background-color: var(--hightlight_bg); 72 | border: none; 73 | cursor: pointer; 74 | font-size: 1.5em; 75 | } 76 | 77 | .login_wrap .login_box .login_box_inner #login_btn:hover { 78 | background-color: #fff576; 79 | } 80 | 81 | @media all and (max-width: 768px) { 82 | .login_wrap .login_box { 83 | position: relative; 84 | left: -2.3px; 85 | width: calc(100% - 4.6px); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/ViewItems/CSS/ReaderPage.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for ReaderPageView page view. 3 | */ 4 | .reader_wrap { 5 | margin: 0 auto; 6 | width: 1200px; 7 | max-width: 100%; 8 | background-color: #fff; 9 | box-shadow: 0 0 25px 20px rgba(0, 0, 0, 0.3); 10 | } 11 | 12 | .reader_wrap .img_wrap { 13 | cursor: pointer; 14 | } 15 | 16 | .reader_wrap .img_wrap img { 17 | margin: 0 auto; 18 | max-width: 100%; 19 | display: none; 20 | } 21 | 22 | .reader_wrap .img_wrap .placeholder { 23 | width: 100%; 24 | height: 100vh; 25 | display: none; 26 | text-align: center; 27 | } 28 | 29 | .reader_wrap .img_wrap .placeholder::before { 30 | width: 0; 31 | height: 100%; 32 | display: inline-block; 33 | vertical-align: middle; 34 | content: ''; 35 | } 36 | 37 | .reader_wrap .img_wrap .placeholder img { 38 | display: inline-block; 39 | vertical-align: middle; 40 | } 41 | 42 | .reader_wrap .img_wrap img.selected_image, 43 | .reader_wrap .img_wrap .placeholder.selected_image { 44 | display: block; 45 | } 46 | 47 | 48 | 49 | .reader_wrap .img_wrap .next_chapter { 50 | display: none; 51 | } 52 | -------------------------------------------------------------------------------- /app/ViewItems/CSS/ReaderStrip.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for ReaderStripView page view. 3 | */ 4 | .strip_wrap { 5 | margin: 0 auto; 6 | width: 1200px; 7 | max-width: 100%; 8 | background-color: #fff; 9 | box-shadow: 0 0 25px 20px rgba(0, 0, 0, 0.3); 10 | } 11 | 12 | .strip_wrap img { 13 | margin: 0 auto; 14 | max-width: 100%; 15 | display: block; 16 | } 17 | 18 | .strip_wrap .placeholder { 19 | height: 100vh; 20 | text-align: center; 21 | } 22 | 23 | .strip_wrap .placeholder::before { 24 | width: 0; 25 | height: 100%; 26 | display: inline-block; 27 | vertical-align: middle; 28 | content: ''; 29 | } 30 | 31 | .strip_wrap .placeholder img { 32 | display: inline-block; 33 | vertical-align: middle; 34 | } 35 | 36 | .strip_wrap .continue_btn { 37 | display: block; 38 | background-color: #d68100; 39 | } 40 | 41 | .strip_wrap .continue_btn:hover { 42 | background-color: #ffbb54; 43 | } 44 | 45 | .strip_wrap .continue_btn a { 46 | padding: 20px; 47 | display: block; 48 | color: #000; 49 | text-align: center; 50 | text-decoration: none; 51 | font-family: Arial; 52 | font-size: 2em; 53 | } 54 | -------------------------------------------------------------------------------- /app/ViewItems/CSS/UIFrame.css: -------------------------------------------------------------------------------- 1 | /* 2 | This is a core CSS file that will always be available on every page of the 3 | system. Color definitions reside here. 4 | */ 5 | 6 | :root { 7 | --hightlight_bg: #ffc800; 8 | --backlight: #414141; 9 | --icon_hover: #000; 10 | } 11 | 12 | html, 13 | body { 14 | margin: 0 auto; 15 | height: 100%; 16 | background-color: #414141; 17 | } 18 | 19 | .topbar { 20 | position: fixed; 21 | padding: 5px; 22 | width: 100%; 23 | background-color: var(--hightlight_bg); 24 | box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.3); 25 | box-sizing: border-box; 26 | z-index: 4; 27 | } 28 | 29 | .topbar .logo { 30 | padding-left: 10px; 31 | float: left; 32 | font-family: Consolas; 33 | font-size: 2em; 34 | } 35 | 36 | .topbar .logo a { 37 | color: #000; 38 | text-decoration: none; 39 | } 40 | 41 | .topbar .icons_wrap { 42 | float: right; 43 | } 44 | 45 | .topbar .icons_wrap .button { 46 | padding-right: 25px; 47 | display: inline-block; 48 | font-size: 2.3em; 49 | } 50 | 51 | .topbar .icons_wrap .button i { 52 | color: var(--backlight); 53 | } 54 | 55 | .topbar .icons_wrap .button i:hover { 56 | color: var(--icon_hover); 57 | } 58 | 59 | .topbar .icons_wrap .button.btn_burger { 60 | display: none; 61 | cursor: pointer; 62 | } 63 | 64 | .topbar .icons_wrap .button a { 65 | width: 100%; 66 | } 67 | 68 | .topbar .icons_wrap .button img { 69 | width: 100%; 70 | } 71 | 72 | .topbar .icons_wrap .logout_btn { 73 | background-color: green; 74 | cursor: pointer; 75 | font-family: Consolas; 76 | text-align: center; 77 | } 78 | 79 | .topbar .icons_wrap .logout_btn:hover { 80 | background-color: lightgreen; 81 | } 82 | 83 | .topbar .login_wrap { 84 | min-width: 140px; 85 | height: 39px; 86 | display: inline-block; 87 | vertical-align: top; 88 | font-family: Arial; 89 | } 90 | 91 | .topbar .login_wrap .login_text { 92 | width: 85%; 93 | height: 39px; 94 | float: left; 95 | display: block; 96 | } 97 | 98 | .topbar .login_wrap .login_text span { 99 | display: block; 100 | } 101 | 102 | .topbar .login_wrap .login_text .login_name { 103 | font-weight: bold; 104 | } 105 | 106 | .topbar .login_wrap .expand_arrow { 107 | padding: 8px 0; 108 | width: 15%; 109 | float: right; 110 | display: block; 111 | cursor: pointer; 112 | text-align: center; 113 | } 114 | 115 | .topbar .login_wrap .dropdown_menu { 116 | position: absolute; 117 | top: 100%; 118 | right: 0; 119 | width: 145px; 120 | } 121 | 122 | .topbar .icons_wrap .burger_dropdown { 123 | position: absolute; 124 | top: 50px; 125 | right: 0; 126 | background-color: #ffdc5c; 127 | } 128 | 129 | .dropdown_menu { 130 | position: relative; 131 | width: 100%; 132 | display: none; 133 | overflow: hidden; 134 | } 135 | 136 | .topbar .icons_wrap .burger_dropdown .menu_item { 137 | display: block; 138 | text-align: center; 139 | } 140 | 141 | .topbar .icons_wrap .burger_dropdown .menu_item:hover { 142 | background-color: #ffef94; 143 | } 144 | 145 | .topbar .icons_wrap .burger_dropdown .menu_item.login_wrap_mobile:hover { 146 | background-color: transparent; 147 | } 148 | 149 | .topbar .icons_wrap .burger_dropdown .login_wrap_mobile { 150 | padding: 11px 0; 151 | font-family: Arial; 152 | } 153 | 154 | .topbar .icons_wrap .burger_dropdown .login_wrap_mobile .login_text { 155 | display: inline-block; 156 | } 157 | 158 | .topbar .icons_wrap .burger_dropdown .login_wrap_mobile .login_text .login_name { 159 | font-weight: bold; 160 | } 161 | 162 | .topbar .icons_wrap .burger_dropdown .login_wrap_mobile .logout_btn { 163 | padding: 6px; 164 | display: inline-block; 165 | } 166 | 167 | .topbar .icons_wrap .burger_dropdown .menu_item a { 168 | padding: 10px; 169 | display: block; 170 | box-sizing: border-box; 171 | text-decoration: none; 172 | } 173 | 174 | .topbar .icons_wrap .burger_dropdown .menu_item a img { 175 | width: 100%; 176 | max-width: 34px; 177 | display: inline-block; 178 | vertical-align: bottom; 179 | } 180 | 181 | .topbar .icons_wrap .burger_dropdown .menu_item a span { 182 | padding: 0 0 5px 20px; 183 | display: inline-block; 184 | color: #000; 185 | font-family: Consolas; 186 | font-size: 1.3em; 187 | } 188 | 189 | .topbar .icons_wrap .flyout { 190 | padding: 10px; 191 | display: none; 192 | } 193 | 194 | .topbar .icons_wrap .flyout.library .search_wrap { 195 | padding: 3px 5px; 196 | display: block; 197 | float: right; 198 | background-color: aliceblue; 199 | } 200 | 201 | .topbar .icons_wrap .flyout.library .search_wrap .search_box { 202 | width: 300px; 203 | border: none; 204 | background-color: transparent; 205 | font-size: 1.5em; 206 | } 207 | 208 | .display_container { 209 | padding-top: 49px; 210 | box-sizing: border-box; 211 | } 212 | 213 | @media all and (max-width: 768px) { 214 | .topbar .login_wrap, 215 | .topbar .icons_wrap .button { 216 | display: none; 217 | } 218 | 219 | .topbar .icons_wrap .button.btn_burger { 220 | display: inline-block; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/Config.php: -------------------------------------------------------------------------------- 1 |
2 |

Configuration

3 |
4 |
5 |
Library Settings
6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | Rescan Manga Library 22 |
23 |
24 |
25 |
26 |
27 |
Reader Settings
28 |
29 |
30 | 31 | 39 |
40 |
41 | 42 | 50 |
51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/DisplayLibrary.php: -------------------------------------------------------------------------------- 1 |
2 | getMangaData () as $manga) { ?> 4 |
5 |

6 | 7 |
8 | 9 |
10 |
11 |
12 | getMangaData ())) { ?> 16 |
17 | No manga in your library :< 18 |
19 | 21 |
22 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/DisplaySeries.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | getTitle (); ?> 5 |
6 |
7 |
8 |
Summary
9 |
10 | getSummary (); ?> 11 |
12 |
13 |
14 |
Tags
15 |
16 | getGenres () as $genre) { ?> 18 |
19 | 20 |
21 | getGenres ())) { ?> 25 | No tags available 26 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Chapter List
36 | getChapters () as $chapter) { ?> 38 | 39 | 40 | 41 | 43 |
44 |
45 |
46 | getVolumes () as $volume) { ?> 48 |
49 | 50 |
51 | 52 |
53 |
54 |
55 | 57 |
58 |
59 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/Home.php: -------------------------------------------------------------------------------- 1 |

Library Statistics

2 |
3 | getBoxContents() as $box) { ?> 5 |
6 |
7 |
8 | 9 |
10 |
11 | 13 |
14 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/Login.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | MangoBango 5 |
6 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/Master.php: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 9 | 30 |
31 | 32 | 33 | 34 |
36 |
37 | 38 |
39 |
41 | 42 | 43 | 44 |
45 | 57 |
58 |
59 |
60 | getViewContent ()->getHTML (); ?> 61 |
62 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/Page.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <?php echo $view->getTitle (); ?> 5 | 6 | getViewContent ()->getCSS (); ?> 7 | 8 | getViewContent ()->getJS (); ?> 9 | 10 | 11 | getViewContent ()->getHTML (); ?> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/ReaderPage.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | getFilePaths() as $path) { ?> 6 |
7 | 8 |
9 | 10 | getNextChapterLink ())) { ?> 15 | 16 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /app/ViewItems/HTML/ReaderStrip.php: -------------------------------------------------------------------------------- 1 |
2 | getFilePaths () as $path) { ?> 4 |
5 | 6 |
7 | getNextChapterLink ())) { ?> 11 |
12 | 13 | Continue to next chaper. 14 | 15 |
16 | 18 |
19 | -------------------------------------------------------------------------------- /app/ViewItems/JS/Config.js: -------------------------------------------------------------------------------- 1 | $(window).ready (function () { 2 | /** 3 | * Calls the AJAX update method of the config page controller in order to 4 | * save changes. 5 | * 6 | * @param {Object} config dictionary of updated config(s) 7 | * @param {Node} $this control being used to update this config 8 | */ 9 | function ajaxUpdateConfigs (config, $this) { 10 | $($this).attr ("disabled", true); 11 | 12 | $.ajax ({ 13 | url: "ajax/Controllers/Config/ajaxUpdateConfigs", 14 | method: "POST", 15 | data: { 16 | config: JSON.stringify (config) 17 | } 18 | }).done (function () { 19 | $($this).attr ("disabled", false); 20 | }); 21 | } 22 | 23 | $("#directory_structure").change (function () { 24 | var config = { 25 | "directory_structure" : $("#directory_structure").val () 26 | }; 27 | 28 | ajaxUpdateConfigs (config, $(this)); 29 | }); 30 | 31 | $("#reader_display_style").change (function () { 32 | var config = { 33 | "reader_display_style" : $("#reader_display_style").val () 34 | }; 35 | 36 | ajaxUpdateConfigs (config, $(this)); 37 | }); 38 | 39 | $("#manga_directory").focusout (function () { 40 | var config = { 41 | "manga_directory" : $("#manga_directory").val () 42 | }; 43 | 44 | ajaxUpdateConfigs (config, $(this)); 45 | }); 46 | 47 | $("#assets_directory").focusout (function () { 48 | var config = { 49 | "assets_directory" : $("#assets_directory").val () 50 | }; 51 | 52 | ajaxUpdateConfigs (config, $(this)); 53 | }); 54 | 55 | $("#library_view_type").change (function () { 56 | var config = { 57 | "library_view_type" : $("#library_view_type").val () 58 | }; 59 | 60 | ajaxUpdateConfigs (config, $(this)); 61 | }); 62 | 63 | $("#rescan_library_btn").click (function () { 64 | var $btn = $(this); 65 | $btn.html ("LOADING"); 66 | 67 | $.ajax ({ 68 | url: "ajax/Controllers/Config/ajaxRescanLibrary", 69 | success: function () { 70 | $btn.html ("DONE!"); 71 | } 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /app/ViewItems/JS/DisplayLibrary.js: -------------------------------------------------------------------------------- 1 | /* 2 | Page JS for view DisplayLibraryView. 3 | */ 4 | $(window).ready (function () { 5 | $(".title").each (function () { 6 | if ($(this).html ().length > 20) { 7 | let displayText = $(this).html ().substring (0, 20); 8 | let fullText = $(this).html (); 9 | 10 | $(this).html (displayText); 11 | $(this).append ( 12 | $("
") 13 | .addClass ("elipsis") 14 | .html ("...") 15 | ); 16 | $(this).append ( 17 | $("
") 18 | .addClass ("title_expand") 19 | .html (fullText) 20 | ); 21 | } 22 | }); 23 | }); 24 | 25 | // Start lazy loader 26 | lazyLoadByScroll (); 27 | -------------------------------------------------------------------------------- /app/ViewItems/JS/DisplaySeries.js: -------------------------------------------------------------------------------- 1 | /* 2 | Page JS for view DisplaySeriesView. 3 | */ 4 | // Fix for height of chapter list to always reach the bottom of 5 | // the viewport no matter what 6 | $(window).ready (function () { 7 | let chapterTop = $(".chapter_container").offset ().top; 8 | let chapterHeight = $(".chapter_container").height (); 9 | let viewportHeight = $(window).height (); 10 | let volumeHeight = $(".library_display_container").height (); 11 | 12 | if (viewportHeight > (chapterTop + chapterHeight)) { 13 | $(".chapter_container").height (viewportHeight - chapterTop); 14 | } else if (volumeHeight > chapterHeight) { 15 | $(".chapter_container").height (volumeHeight); 16 | } 17 | }); 18 | 19 | // Start lazy loader 20 | lazyLoadByScroll (); 21 | -------------------------------------------------------------------------------- /app/ViewItems/JS/LazyLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy image loader class. Loads images by looking for placeholder divs (.placeholder), 3 | * replacing the HTML content with the image requested by the data-origin attribute 4 | * on said placeholder div. 5 | */ 6 | class LazyLoader { 7 | /** 8 | * Finds all the placeholders on the page. 9 | * 10 | * @return {Array} list of jQuery nodes of placeholders 11 | */ 12 | findPlaceholdersAll () { 13 | return $(".placeholder").toArray ().filter ( 14 | node => !$(node).hasClass ("processing") && !$(node).hasClass ("done") 15 | ); 16 | } 17 | 18 | /** 19 | * Finds all the placeholders that fit on the section of the page currently 20 | * visible to the user. 21 | * 22 | * @return {Array} List of nodes within the current viewport 23 | */ 24 | findPlaceholdersViewport () { 25 | let nodes = $(".placeholder").toArray ().filter ( 26 | node => !$(node).hasClass ("processing") && !$(node).hasClass ("done") 27 | ); 28 | 29 | if (nodes.length === 0) 30 | return []; 31 | 32 | let vpTop = $(window).scrollTop (); 33 | let vpBottom = vpTop + $(window).height (); 34 | 35 | return nodes.filter (node => { 36 | let eTop = $(node).offset ().top; 37 | let eBottom = eTop + $(node).outerHeight (); 38 | 39 | return eBottom > vpTop && eTop < vpBottom; 40 | }); 41 | } 42 | 43 | /** 44 | * Request images for all placeholders on the page. 45 | * 46 | * @param {Array} placeholders list of placeholder nodes 47 | */ 48 | replacePlaceholders (placeholders) { 49 | // Mark all these nodes as being actively updated. 50 | $.each (placeholders, (i, v) => $(v).addClass ("processing")); 51 | 52 | // Cut the images up into batches of 5 53 | let chunks = []; 54 | let tChunk = []; 55 | 56 | for (let i = 0; i < placeholders.length; i++) { 57 | if (tChunk.length === 5) { 58 | chunks.push (tChunk); 59 | tChunk = []; 60 | } 61 | 62 | tChunk.push (placeholders[i]); 63 | } 64 | 65 | if (tChunk.length > 0) { 66 | chunks.push (tChunk); 67 | } 68 | 69 | $.each (chunks, (i, v) => this._requestImages (v)); 70 | } 71 | 72 | /** 73 | * Requests a batch of images from the LazyLoader controller using the 74 | * data-origin attribute of the given jQuery nodes & replaces the given nodes 75 | * with the received images. 76 | * 77 | * @param {Array} placeholders list of jQuery nodes of placeholders 78 | */ 79 | _requestImages (placeholders) { 80 | var filePaths = []; 81 | 82 | $.each (placeholders, (i, $v) => filePaths.push ($($v).attr ("data-origin"))); 83 | 84 | $.ajax({ 85 | url: "ajax/Core/LazyLoader/ajaxRequestImages", 86 | method: "POST", 87 | data: { 88 | filepaths: JSON.stringify (filePaths) 89 | }, 90 | dataType: "json", 91 | timeout: 10000, 92 | error: function (placeholders) { 93 | // No image was found, so add a placeholder img 94 | $.each (placeholders, (i, $v) => { 95 | $v.find ("img").attr ( 96 | "src", 97 | "/resources/icons/placeholder.svg" 98 | ); 99 | }); 100 | }.bind (this, placeholders), 101 | success: function (placeholders, imageSrc) { 102 | $.each (placeholders, (i, $v) => { 103 | if (imageSrc[i].length < 1) { 104 | // No image was found, so add a placeholder img 105 | $($v).find ("img").attr ( 106 | "src", 107 | "/resources/icons/placeholder.svg" 108 | ); 109 | 110 | return; 111 | } 112 | 113 | let newNode = $("") 114 | .attr ("src", imageSrc[i]) 115 | .addClass ($($v)[0].classList.value) 116 | .removeClass ("placeholder") 117 | .removeClass ("processing"); 118 | 119 | $v.replaceWith (newNode[0]); 120 | }); 121 | }.bind (this, placeholders) 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/ViewItems/JS/LazyLoaderEvents.js: -------------------------------------------------------------------------------- 1 | /* 2 | Events for lazy loading using LazyLoader.js 3 | */ 4 | function lazyLoadByScroll () { 5 | $(window).on ("load", () => { 6 | var lazyLoader = new LazyLoader (); 7 | 8 | // Request first batch of images on load once 9 | var scrollTimeout = true; 10 | 11 | lazyLoader.replacePlaceholders ( 12 | lazyLoader.findPlaceholdersViewport () 13 | ); 14 | 15 | setTimeout (() => scrollTimeout = false, 1000); 16 | 17 | $(window).scroll (() => { 18 | if (scrollTimeout === false) 19 | scrollTimeout = true; 20 | 21 | lazyLoader.replacePlaceholders ( 22 | lazyLoader.findPlaceholdersViewport () 23 | ); 24 | 25 | setTimeout (() => scrollTimeout = false, 1000); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /app/ViewItems/JS/Login.js: -------------------------------------------------------------------------------- 1 | $(window).ready (function () { 2 | $("#login_btn").click (function () { 3 | $.ajax ({ 4 | url: "ajax/Core/SessionManager/ajaxValidateLogin", 5 | method: "POST", 6 | data: { 7 | username: $("#username_field").val (), 8 | password: $("#password_field").val () 9 | } 10 | }).done (function (response) { 11 | if (response === "1") { 12 | window.location = "/"; 13 | } else { 14 | $(".login_box .warning").toggle (true); 15 | } 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/ViewItems/JS/ReaderPage.js: -------------------------------------------------------------------------------- 1 | $(window).ready (function () { 2 | $(".img_wrap").click (function (e) { 3 | let x = e.clientX - $(this).offset ().left; 4 | 5 | let $selected = $(this).find (".selected_image"); 6 | 7 | if (x < $(this).width () / 2) { 8 | // Go back 9 | let $prevImage = $selected.prev (); 10 | 11 | if ($prevImage.length > 0) { 12 | $selected.removeClass ("selected_image"); 13 | $prevImage.addClass ("selected_image"); 14 | } 15 | } else { 16 | // Go forwards 17 | let $nextImage = $selected.next (); 18 | 19 | if ($nextImage.length > 0) { 20 | $selected.removeClass ("selected_image"); 21 | $nextImage.addClass ("selected_image"); 22 | } else { 23 | // If there is no next image BUT there is a next chapter, reload 24 | // page with new chapter 25 | let nextChapter = $(".next_chapter"); 26 | 27 | if (nextChapter.length > 0) { 28 | window.location = nextChapter.attr ("href"); 29 | } 30 | } 31 | } 32 | 33 | $(window).scrollTop (0); 34 | }); 35 | }); 36 | 37 | // Start lazy loader 38 | lazyLoadByScroll (); 39 | -------------------------------------------------------------------------------- /app/ViewItems/JS/ReaderStrip.js: -------------------------------------------------------------------------------- 1 | /* 2 | Page JS for view ReaderStripView. 3 | */ 4 | // Start lazy loader 5 | lazyLoadByScroll (); 6 | -------------------------------------------------------------------------------- /app/ViewItems/JS/dropdown.js: -------------------------------------------------------------------------------- 1 | $(window).ready (function () { 2 | $(".dropdown_menu_button").click (function () { 3 | $(this).next (".dropdown_menu").slideToggle (); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /app/ViewItems/JS/logout.js: -------------------------------------------------------------------------------- 1 | $(window).ready (function () { 2 | $(".logout_btn").click (function () { 3 | $(this).attr ("disabled", true); 4 | 5 | $.ajax ({ 6 | url: "ajax/Core/SessionManager/unloadSession", 7 | method: "GET" 8 | }).done (function () { 9 | window.location = "/login"; 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /app/ViewItems/PageViews/DisplayLibraryBookcaseView.php: -------------------------------------------------------------------------------- 1 | 17 | .spine_anchor { 18 | margin-right: 4px; 19 | display: inline-block; 20 | } 21 | 22 | .spine_anchor img { 23 | max-height: 480px; 24 | } 25 | '; 26 | 27 | return ($output); 28 | } 29 | 30 | /** 31 | * Constructs the HTML using the available properties. 32 | */ 33 | protected function constructHTML () { 34 | $output = ''; 35 | 36 | foreach ($this->getSpines () as $series => $volumes) { 37 | $link = $this->getSeriesLinks ()[$series]; 38 | 39 | foreach ($volumes as $spine) { 40 | $output .= 41 | ' 42 | 43 | '; 44 | } 45 | } 46 | 47 | return ($output); 48 | } 49 | 50 | /** 51 | * Constructs the javascript using the available properties. 52 | */ 53 | protected function constructJavascript () { 54 | $output = 55 | ''; 58 | 59 | return ($output); 60 | } 61 | 62 | protected function setSpines (array $spines) { 63 | $this->spines = $spines; 64 | } 65 | 66 | protected function setSeriesLinks (array $series_links) { 67 | $this->series_links = $series_links; 68 | } 69 | 70 | protected function getSpines () { 71 | return ($this->spines); 72 | } 73 | 74 | protected function getSeriesLinks () { 75 | return ($this->series_links); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mluzarow/MangoBango/b5e222f97632c414920a7db99cae9e3bd8b8584b/app/database.db -------------------------------------------------------------------------------- /app/index.php: -------------------------------------------------------------------------------- 1 | prependHandler(new \Whoops\Handler\PrettyPageHandler); 31 | $whoops->register(); 32 | 33 | /** 34 | * Gets the master view object. 35 | * 36 | * @param string $username username 37 | * @param ViewItem $view child view object 38 | * 39 | * @return ViewItem master view object 40 | * 41 | * @throws TypeError on invalid parameter or return type 42 | */ 43 | function getMasterView (string $username, ViewItem $view) : ViewItem { 44 | return (new \Services\View\Controller ())-> 45 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 46 | buildView ( 47 | [ 48 | 'name' => 'Master', 49 | 'CSS' => ['UIFrame'], 50 | 'HTML' => 'Master', 51 | 'JS' => ['dropdown', 'logout'] 52 | ], 53 | [ 54 | 'username' => $username, 55 | 'view_content' => $view 56 | ] 57 | ); 58 | } 59 | 60 | /** 61 | * Gets page view HTML. 62 | * 63 | * @param string $title page title 64 | * @param ViewItem $view child view object 65 | * 66 | * @return string view HTML 67 | * 68 | * @throws TypeError on invalid parameters or return type 69 | */ 70 | function getPageView (string $title, ViewItem $view) : string { 71 | return (new \Services\View\Controller ())-> 72 | buildViewService ($_SERVER['DOCUMENT_ROOT'])-> 73 | buildPage ( 74 | $title, 75 | $view 76 | ); 77 | } 78 | 79 | $db = \Core\Database::getInstance (); 80 | 81 | // Load user session 82 | $user_session = new \Core\SessionManager (); 83 | $user_session->loadSession (); 84 | 85 | // Parse the URL here 86 | $url_split = explode ('?', $_SERVER['REQUEST_URI']); 87 | 88 | $current_segs = $url_split[0]; 89 | $current_segs = trim ($current_segs, '/'); 90 | $current_segs = explode ('/', $current_segs); 91 | 92 | if (count ($current_segs) === 1) { 93 | if (empty ($current_segs[0])) { 94 | unset ($current_segs[0]); 95 | $current_segs = array_values ($current_segs); 96 | } 97 | } 98 | 99 | if ((new \Core\SessionManager ())->isLoggedIn () === false) { 100 | // Not logged in; should only be able to ajax request 101 | // SessionManager::ajaxValidateLogin and Controllers/Login 102 | $uri = strtolower (implode ('/', $current_segs)); 103 | 104 | if ( 105 | $uri === 'ajax/core/sessionmanager/ajaxvalidatelogin' || 106 | $uri === 'login' 107 | ) { 108 | if ($current_segs[0] === 'ajax') { 109 | unset ($current_segs[0]); 110 | $current_segs = array_values ($current_segs); 111 | 112 | $ajax = new AJAXProcessor ($current_segs); 113 | $result = $ajax->fireTargetMethod (); 114 | 115 | echo $result; 116 | } else { 117 | echo getPageView ( 118 | 'Login', 119 | (new \Controllers\Login ())->begin () 120 | ); 121 | } 122 | } else { 123 | // Redirect to login page 124 | header ('Location: /login', true, 301); 125 | exit; 126 | } 127 | 128 | return; 129 | } 130 | 131 | if (!empty($current_segs)) { 132 | if ($current_segs[0] === 'ajax') { 133 | // If the first segment is ajax, pass the rest of the data to the ajax 134 | // controller so it can decide what methods to run. 135 | unset ($current_segs[0]); 136 | $current_segs = array_values ($current_segs); 137 | 138 | $ajax = new AJAXProcessor ($current_segs); 139 | $result = $ajax->fireTargetMethod (); 140 | 141 | echo $result; 142 | return; 143 | } else { 144 | $namespace = '\Controllers'; 145 | 146 | for ($i = 0; $i < count ($current_segs); $i++) { 147 | $namespace .= '\\'.$current_segs[$i]; 148 | } 149 | 150 | try { 151 | echo getPageView ( 152 | preg_replace ('/(?getSessionItem ('username'), 155 | (new $namespace ())->begin () 156 | ) 157 | ); 158 | } catch (Error $e) { 159 | echo $e->getMessage (); 160 | } 161 | } 162 | } else { 163 | // Empty so its just the home page. 164 | echo getPageView ( 165 | 'Home Page', 166 | getMasterView ( 167 | $user_session->getSessionItem ('username'), 168 | (new \Controllers\Home ())->begin () 169 | ) 170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /app/resources/icons/loading-3s-200px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/resources/icons/magnifying-glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/resources/icons/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/resources/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/server.ini: -------------------------------------------------------------------------------- 1 | [database] 2 | ; User-specified database path. By default, the database file is located in 3 | ; the application (app) folder. If that is desired, leave blank, otherwise 4 | ; provider a direct file path to the database file. 5 | path = 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "mluzarow/MangoBango", 3 | "description" : "Manga Server", 4 | "require" : { 5 | "php": ">=7.3" 6 | }, 7 | "require-dev" : { 8 | "phpunit/phpunit" : "8.*", 9 | "filp/whoops": "^2.5" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/DataProviders.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf (Controller::class, $this->instance ()); 27 | } 28 | 29 | /** 30 | * Testing valid root path paramater on method buildViewService yields 31 | * valid Service instance. 32 | * 33 | * @dataProvider validRootPathProvider 34 | * 35 | * @param string $valid valid root path 36 | */ 37 | public function testBuildViewServiceValidRootPath ($valid) { 38 | $this->assertInstanceOf ( 39 | Service::class, 40 | $this->instance ()->buildViewService ($valid) 41 | ); 42 | } 43 | 44 | /** 45 | * Data provider for testBuildViewServiceValidRootPath(). 46 | * 47 | * @return array valid root path data 48 | */ 49 | public function validRootPathProvider () { 50 | return [ 51 | ['~\\www\\MangoBango\\app\\'], 52 | ['C:\\Users\\Mark\\MangoBango\\app\\'], 53 | [''] 54 | ]; 55 | } 56 | 57 | /** 58 | * Testing invalid root path type paramater on method buildViewService yields 59 | * TypeError. 60 | * 61 | * @dataProvider Tests\DataProviders::nonStringProvider 62 | * 63 | * @param mixed $invalid non-string type 64 | */ 65 | public function testBuildViewServiceInvalidRootPathType ($invalid) { 66 | $this->expectException('\TypeError'); 67 | 68 | $this->instance ()->buildViewService ($invalid); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Services/View/Data/ViewItemTest.php: -------------------------------------------------------------------------------- 1 | css_tags = [ 17 | '', 18 | '' 19 | ]; 20 | 21 | $this->html = ' 22 |
23 |
24 |

090 Eko to Issho

25 | 26 | 27 | 28 |
29 |
'; 30 | 31 | $this->js_tags = [ 32 | '', 33 | '', 34 | '', 35 | '', 36 | '', 37 | '' 38 | ]; 39 | } 40 | 41 | /** 42 | * Instances the ViewItem class with the current parameters. 43 | * 44 | * @return ViewItem instance of ViewItem 45 | */ 46 | public function instance () { 47 | return new ViewItem ( 48 | $this->css_tags, 49 | $this->html, 50 | $this->js_tags 51 | ); 52 | } 53 | 54 | /** 55 | * Testing valid parameters yield valid instance. 56 | */ 57 | public function testConstructor () { 58 | $this->assertInstanceOf (ViewItem::class, $this->instance ()); 59 | } 60 | 61 | /** 62 | * Testing valid CSS tags parameter yields no exception. 63 | * 64 | * @dataProvider validCSSTagsProvider 65 | * 66 | * @param array $valid valid CSS tags 67 | */ 68 | public function testValidCSSTags ($valid) { 69 | $this->css_tags = $valid; 70 | $this->assertInstanceOf (ViewItem::class, $this->instance ()); 71 | } 72 | 73 | /** 74 | * Data provider for testValidCSSTags(). 75 | * 76 | * @return array valid CSS tags data 77 | */ 78 | public function validCSSTagsProvider () { 79 | return [ 80 | // Empty 81 | [[]], 82 | // One Item 83 | [['']], 84 | // Multiple Items 85 | [[ 86 | '', 87 | '' 88 | ]] 89 | ]; 90 | } 91 | 92 | /** 93 | * Testing invalid CSS tags parameter yeilds TypeError. 94 | * 95 | * @dataProvider Tests\DataProviders::nonArrayProvider 96 | * 97 | * @param mixed $invalid non-array type 98 | */ 99 | public function testInvalidCSSTagsType ($invalid) { 100 | $this->expectException('\TypeError'); 101 | 102 | $this->css_tags = $invalid; 103 | $this->instance (); 104 | } 105 | 106 | /** 107 | * Testing invalid CSS tags items type yields InvalidArgumentException. 108 | * 109 | * @dataProvider Tests\DataProviders::nonStringProvider 110 | * 111 | * @param mixed $invalid non-string type 112 | */ 113 | public function testInvalidCSSTagsItemType ($invalid) { 114 | $this->expectException('\InvalidArgumentException'); 115 | 116 | $this->css_tags = [$invalid]; 117 | $this->instance (); 118 | } 119 | 120 | /** 121 | * Testing valid HTML parameter yields no exception. 122 | * 123 | * @dataProvider validHTMLProvider 124 | * 125 | * @param array $valid valid HTML 126 | */ 127 | public function testValidHTML ($valid) { 128 | $this->html = $valid; 129 | $this->assertInstanceOf (ViewItem::class, $this->instance ()); 130 | } 131 | 132 | /** 133 | * Data provider for testValidHTML(). 134 | * 135 | * @return array valid HTML data 136 | */ 137 | public function validHTMLProvider () { 138 | return [ 139 | // Empty 140 | [''], 141 | // Single tag 142 | ['
'], 143 | // Hefty HTML 144 | [ 145 | '
146 |
147 |

090 Eko to Issho

148 | 149 | 150 | 151 |
152 |
' 153 | ] 154 | ]; 155 | } 156 | 157 | /** 158 | * Testing invalid HTML parameter yeilds TypeError. 159 | * 160 | * @dataProvider Tests\DataProviders::nonStringProvider 161 | * 162 | * @param mixed $invalid non-string type 163 | */ 164 | public function testInvalidHTMLType ($invalid) { 165 | $this->expectException('\TypeError'); 166 | 167 | $this->html = $invalid; 168 | $this->instance (); 169 | } 170 | 171 | /** 172 | * Testing valid JS tags parameter yields no exception. 173 | * 174 | * @dataProvider validJSTagsProvider 175 | * 176 | * @param array $valid valid JS tags 177 | */ 178 | public function testValidJSTags ($valid) { 179 | $this->js_tags = $valid; 180 | $this->assertInstanceOf (ViewItem::class, $this->instance ()); 181 | } 182 | 183 | /** 184 | * Data provider for testValidJSTags(). 185 | * 186 | * @return array valid JS tags data 187 | */ 188 | public function validJSTagsProvider () { 189 | return [ 190 | // Empty 191 | [[]], 192 | // One Item 193 | [['']], 194 | // Multiple Items 195 | [[ 196 | '', 197 | '' 198 | ]] 199 | ]; 200 | } 201 | 202 | /** 203 | * Testing invalid JS tags parameter yeilds TypeError. 204 | * 205 | * @dataProvider Tests\DataProviders::nonArrayProvider 206 | * 207 | * @param mixed $invalid non-array type 208 | */ 209 | public function testInvalidJSTagsType ($invalid) { 210 | $this->expectException('\TypeError'); 211 | 212 | $this->js_tags = $invalid; 213 | $this->instance (); 214 | } 215 | 216 | /** 217 | * Testing invalid JS tags items type yields InvalidArgumentException. 218 | * 219 | * @dataProvider Tests\DataProviders::nonStringProvider 220 | * 221 | * @param mixed $invalid non-string type 222 | */ 223 | public function testInvalidJSTagsItemType ($invalid) { 224 | $this->expectException('\InvalidArgumentException'); 225 | 226 | $this->js_tags = [$invalid]; 227 | $this->instance (); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /tests/TestBootstrap.php: -------------------------------------------------------------------------------- 1 |