Переглянути джерело

Added new manga reader design

Toby Chui 1 день тому
батько
коміт
947675c73b

+ 0 - 144
web/Manga Cafe/index.html

@@ -1,144 +0,0 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        <meta name="apple-mobile-web-app-capable" content="yes" />
-        <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
-        <meta charset="UTF-8">
-        <meta name="theme-color" content="#fcfcfc">
-        <link rel="stylesheet" href="../script/semantic/semantic.min.css">
-        <script src="../script/jquery.min.js"></script>
-        <script src="../script/ao_module.js"></script>
-        <script src="../script/semantic/semantic.min.js"></script>
-        <link rel="shortcut icon" type="image/png" href="img/small_icon.png"/>
-        <link rel="manifest" crossorigin="use-credentials" href="manifest.json">
-        <title>Manga Cafe</title>
-        <style>
-            body{
-                background-color:#f0f0f0;
-            }
-
-            .manga{
-                margin-left: 8px !important;
-            }
-
-            .mangaTitle{
-                width: 100%;
-            }
-        </style>
-    </head>
-    <body>
-        <div class="ui container">
-            <div class="ui secondary pointing menu">
-                <a class="item active" href="">
-                    Manga Cafe
-                </a>
-                <!--
-                <div class="right menu">
-                    <a class="ui item">
-                        <i class="setting icon"></i>
-                    </a>
-                </div>
-                -->
-                </div>
-                <div class="ui basic segment" id="mangalist">
-                   
-                </div>
-        </div>
-        <script>
-            var isMobile = false; //initiate as false
-            // device detection
-            if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 
-                || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 
-                isMobile = true;
-            }
-
-            //List all the scanned titles
-            ao_module_agirun("Manga Cafe/backend/listTitles.js", {},function(data){
-                $("#mangalist").html("");
-                data.forEach(manga => {
-                    var title = manga[0].split("/").pop();
-                    var preview = `/media?file=${manga[2]}`;
-                    if (manga[2] == null || manga[2] == ""){
-                        preview = "./img/notitle.png";
-                    }
-
-                    var defaultWidth = "200px";
-                    var defaultImageHeight = "300px";
-                    if (isMobile){
-                        defaultWidth = "150px";
-                        defaultImageHeight = "230px";
-                    }
-                    $("#mangalist").append(`<div class="ui card manga" style="width: ${defaultWidth}; display: inline-block;">
-                        <div class="content" style="border-bottom: 1px solid #bfbfbf;">
-                            <div class="right floated meta">Ch#: ${manga[1]}</div>
-                            ${title}
-                        </div>
-                        <a class="image" href="viewComic.html#${manga[3]}" style="text-align:center;">
-                            <img class="mangaTitle" src="${preview}" style="height: ${defaultImageHeight};"/>
-                        </a> 
-                    </div>`);
-                });
-
-                if (data.length == 0){
-                    //No Manga
-                    $("#mangalist").append(`<div class="ui message">
-                        <h4 class="ui header">
-                            <i class="question icon"></i>
-                            <div class="content">
-                                No Manga Found
-                                <div class="sub header">Try upload something to the <a href="#" onclick="openUploadFolder();">user:/Photo/Manga</a> folder?<br>
-                                    You got no manga? Have you heard of <a href="https://github.com/abc9070410/JComicDownloader" target="_blank">JComicDownloader</a>?</div>
-                            
-                            <br>
-                        </h4>
-                    </div>
-                    
-                    <div class="ui divider"></div>
-                    <p>Example Folder Structure</p>
-                    <div class="ui list">
-                        <div class="item">
-                            <i class="folder icon"></i>
-                            <div class="content">
-                                <div class="header">[vRoot[:/Photo/Manga/</div>
-                                <div class="description">The root folder where the Manga Cafe will scan for mangas.</div>
-                                <div class="list">
-                                    <div class="item">
-                                    <i class="folder icon"></i>
-                                    <div class="content">
-                                        <div class="header">Manga Titles/</div>
-                                        <div class="description">The title of this particular manga</div>
-                                        <div class="list">
-                                            <div class="item">
-                                            <i class="folder icon"></i>
-                                            <div class="content">
-                                                <div class="header">Chapter Name/</div>
-                                                <div class="description">The Chapter Name of this group of images</div>
-                                                <div class="list">
-                                                    <div class="item">
-                                                    <i class="image file icon"></i>
-                                                    <div class="content">
-                                                        <div class="header">Pages</div>
-                                                        <div class="description">The manga pages in jpg format</div>
-                                                    </div>
-                                                    </div>
-                                                </div>
-                                            </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                        </div>
-                    `);
-                }
-            });
-
-
-        function openUploadFolder(){
-            ao_module_openPath("user:/Photo/Manga");
-        }
-        </script>
-    </body>
-</html>

+ 78 - 78
web/Manga Cafe/backend/getMangaInfo.js → web/Manga/backend/getMangaInfo.js

@@ -1,79 +1,79 @@
-/*
-    Manga Cafe - Get Manga Information
-
-    This script get the information of the reading manga title
-    Require paramter: folder
-*/
-
-requirelib("filelib");
-requirelib("imagelib");
-
-var targetFolder = decodeURIComponent(folder) + "/";
-var tmp = decodeURIComponent(folder).split("/"); 
-var chapterName = tmp.pop();
-var parentFolder = tmp.join("/");
-var title = [tmp.pop(), chapterName];
-
-//Scan the manga content, process the image if nessary
-var pages = filelib.aglob(targetFolder + "*", "smart");
-var validPages = [];
-for (var i = 0; i < pages.length; i++){
-    var thisPage = pages[i];
-    var basename = thisPage.split("/").pop().split("."); basename.pop(); basename = basename.join(".");
-    var ext = thisPage.split(".").pop();
-    if (!filelib.isDir(thisPage) && (ext == "png" || ext == "jpg") && !(basename.indexOf("-left") > 0 || basename.indexOf("-right") > 0 )){
-        //Check if it is 2 connected page. If yes, split it into two page, left and write
-        var dimension = imagelib.getImageDimension(thisPage);
-        var width = dimension[0];
-        var height = dimension[1];
-        if (width > height){
-            //Check if cached split image exists
-            var pathdata = thisPage.split("/");
-            var filename = pathdata.pop();
-            var dirname = pathdata.join("/");
-            var basename = filename.split(".");
-            var ext = basename.pop();
-            basename = basename.join(".");
-
-            var targetLeft = dirname + "/" + basename + "-left." + ext;
-            var targetRight = dirname + "/" + basename + "-right." + ext;
-
-            if (filelib.fileExists(targetLeft) && filelib.fileExists(targetRight)){
-                //Serve the previous cropped files
-                
-            }else{
-                //Cut and serve
-                imagelib.cropImage(thisPage, targetLeft,0,0,width/2,height)
-                imagelib.cropImage(thisPage, targetRight,width/2,0,width/2,height)
-            }
-           
-            validPages.push(targetRight);
-            validPages.push(targetLeft);
-        }else{
-            //This is a valid page. Serve it
-            validPages.push(thisPage);
-        }
-        
-        
-    }
-}
-
-//Search for other chapter links
-var otherChapterCandidate = filelib.aglob(parentFolder + "/*", "smart");
-var otherChapters = [];
-for (var i =0; i < otherChapterCandidate.length; i++){
-    var basename = otherChapterCandidate[i].split('/').pop();
-    if (filelib.isDir(otherChapterCandidate[i]) && basename.substring(0,1) != "."){
-        otherChapters.push(otherChapterCandidate[i]);
-    }
-}
-
-var info = {
-    title: title,
-    pages: validPages,
-    dir: targetFolder,
-    otherChapterDir: otherChapters,
-}
-
-//Process the image file.
+/*
+    Manga Cafe - Get Manga Information
+
+    This script get the information of the reading manga title
+    Require paramter: folder
+*/
+
+requirelib("filelib");
+requirelib("imagelib");
+
+var targetFolder = decodeURIComponent(folder) + "/";
+var tmp = decodeURIComponent(folder).split("/"); 
+var chapterName = tmp.pop();
+var parentFolder = tmp.join("/");
+var title = [tmp.pop(), chapterName];
+
+//Scan the manga content, process the image if nessary
+var pages = filelib.aglob(targetFolder + "*", "smart");
+var validPages = [];
+for (var i = 0; i < pages.length; i++){
+    var thisPage = pages[i];
+    var basename = thisPage.split("/").pop().split("."); basename.pop(); basename = basename.join(".");
+    var ext = thisPage.split(".").pop();
+    if (!filelib.isDir(thisPage) && (ext == "png" || ext == "jpg") && !(basename.indexOf("-left") > 0 || basename.indexOf("-right") > 0 )){
+        //Check if it is 2 connected page. If yes, split it into two page, left and write
+        var dimension = imagelib.getImageDimension(thisPage);
+        var width = dimension[0];
+        var height = dimension[1];
+        if (width > height){
+            //Check if cached split image exists
+            var pathdata = thisPage.split("/");
+            var filename = pathdata.pop();
+            var dirname = pathdata.join("/");
+            var basename = filename.split(".");
+            var ext = basename.pop();
+            basename = basename.join(".");
+
+            var targetLeft = dirname + "/" + basename + "-left." + ext;
+            var targetRight = dirname + "/" + basename + "-right." + ext;
+
+            if (filelib.fileExists(targetLeft) && filelib.fileExists(targetRight)){
+                //Serve the previous cropped files
+                
+            }else{
+                //Cut and serve
+                imagelib.cropImage(thisPage, targetLeft,0,0,width/2,height)
+                imagelib.cropImage(thisPage, targetRight,width/2,0,width/2,height)
+            }
+           
+            validPages.push(targetRight);
+            validPages.push(targetLeft);
+        }else{
+            //This is a valid page. Serve it
+            validPages.push(thisPage);
+        }
+        
+        
+    }
+}
+
+//Search for other chapter links
+var otherChapterCandidate = filelib.aglob(parentFolder + "/*", "smart");
+var otherChapters = [];
+for (var i =0; i < otherChapterCandidate.length; i++){
+    var basename = otherChapterCandidate[i].split('/').pop();
+    if (filelib.isDir(otherChapterCandidate[i]) && basename.substring(0,1) != "."){
+        otherChapters.push(otherChapterCandidate[i]);
+    }
+}
+
+var info = {
+    title: title,
+    pages: validPages,
+    dir: targetFolder,
+    otherChapterDir: otherChapters,
+}
+
+//Process the image file.
 sendJSONResp(JSON.stringify(info));

+ 7 - 4
web/Manga Cafe/backend/listTitles.js → web/Manga/backend/listTitles.js

@@ -60,11 +60,14 @@ for (var i =0; i < rootList.length; i++){
                     }
                 }
 
-                //Get the starting chapter
-                var startChapter = foldersInTitle[0];
-
+                //Get the chapter names
+                var chapterNames = [];
+                for (var i = 0; i < foldersInTitle.length; i++) {
+                    chapterNames.push(foldersInTitle[i].split("/").pop());
+                }
+               
                 //Prase the return output, src folder, chapter count and title image path
-                scannedTitles.push([thisFileObject, chapterCount, titleImagePath, startChapter]);
+                scannedTitles.push([thisFileObject, chapterCount, titleImagePath, chapterNames]);
             }
         }
     }

BIN
web/Manga/img/banner.png


BIN
web/Manga/img/banner.psd


+ 0 - 0
web/Manga Cafe/img/desktop_icon.png → web/Manga/img/desktop_icon.png


+ 0 - 0
web/Manga Cafe/img/desktop_icon.psd → web/Manga/img/desktop_icon.psd


+ 0 - 0
web/Manga Cafe/img/notitle.png → web/Manga/img/notitle.png


+ 0 - 0
web/Manga Cafe/img/notitle.psd → web/Manga/img/notitle.psd


+ 0 - 0
web/Manga Cafe/img/pwa/128.png → web/Manga/img/pwa/128.png


+ 0 - 0
web/Manga Cafe/img/pwa/192.png → web/Manga/img/pwa/192.png


+ 0 - 0
web/Manga Cafe/img/pwa/256.png → web/Manga/img/pwa/256.png


+ 0 - 0
web/Manga Cafe/img/pwa/256.psd → web/Manga/img/pwa/256.psd


+ 0 - 0
web/Manga Cafe/img/pwa/512.png → web/Manga/img/pwa/512.png


+ 0 - 0
web/Manga Cafe/img/pwa/512.psd → web/Manga/img/pwa/512.psd


+ 0 - 0
web/Manga Cafe/img/small_icon.png → web/Manga/img/small_icon.png


+ 0 - 0
web/Manga Cafe/img/small_icon.psd → web/Manga/img/small_icon.psd


+ 246 - 0
web/Manga/index.html

@@ -0,0 +1,246 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta name="apple-mobile-web-app-capable" content="yes" />
+        <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
+        <meta charset="UTF-8">
+        <meta name="theme-color" content="#fcfcfc">
+        <link rel="stylesheet" href="../script/semantic/semantic.min.css">
+        <script src="../script/jquery.min.js"></script>
+        <script src="../script/ao_module.js"></script>
+        <script src="../script/semantic/semantic.min.js"></script>
+        <link rel="shortcut icon" type="image/png" href="img/small_icon.png"/>
+        <link rel="manifest" crossorigin="use-credentials" href="manifest.json">
+        <title>Manga</title>
+        <style>
+            body{
+                background-color:#f0f0f0;
+            }
+
+            .manga{
+                margin-left: 8px !important;
+            }
+
+            .mangaTitle{
+                width: 100%;
+            }
+
+            #no_manga_info{
+                margin-top: 20px;
+                display:none;
+            }
+
+            .chapterBtn{
+                margin-bottom: 5px !important;
+            }
+
+            .chapterSelector{
+                overflow-y: auto;
+                max-height: 300px;
+            }
+
+            #banner{
+                pointer-events: none;
+                user-select: none;
+            }
+        </style>
+    </head>
+    <body>
+        <div class="ui pointing menu" style="align-items:center;">
+            <!-- Left: logo/icon -->
+            <div class="ui medium image" id="banenr">
+                <img src="img/banner.png" alt="Manga" >
+            </div>
+
+            <!-- Center: search (flexible and centered) -->
+            <div class="item" style="flex:1; display:flex; justify-content:center;">
+                <div class="ui icon input" style="width:60%; max-width:600px;">
+                    <input type="text" id="mangaSearch" placeholder="Search manga...">
+                    <i class="search link icon" aria-hidden="true"></i>
+                </div>
+            </div>
+
+            <!-- Right: folder button -->
+            <div class="right menu">
+                <a class="item" onclick="openUploadFolder();" title="Open upload folder" role="button">
+                    <i class="folder open outline large icon" aria-hidden="true"></i>
+                </a>
+            </div>
+        </div>
+        <div class="ui container">
+            <div class="ui basic segment" id="mangalist">
+                
+            </div>
+            <div id="no_manga_info">
+                <div class="ui message">
+                    <h4 class="ui header">
+                        <i class="question icon"></i>
+                        <div class="content">
+                            No Manga Found
+                            <div class="sub header">Try upload something to the <a href="#" onclick="openUploadFolder();">user:/Photo/Manga</a> folder?<br>
+                                You got no manga? Have you heard of <a href="https://github.com/abc9070410/JComicDownloader" target="_blank">JComicDownloader</a>?</div>
+                        <br>
+                    </h4>
+                </div>
+            
+                <div class="ui divider"></div>
+                <p>Example Folder Structure</p>
+                <div class="ui list">
+                    <div class="item">
+                        <i class="folder icon"></i>
+                        <div class="content">
+                            <div class="header">[vRoot]:/Photo/Manga/</div>
+                            <div class="description">The root folder where the Manga will scan for mangas.</div>
+                            <div class="list">
+                                <div class="item">
+                                <i class="folder icon"></i>
+                                <div class="content">
+                                    <div class="header">Manga Titles/</div>
+                                    <div class="description">The title of this particular manga</div>
+                                    <div class="list">
+                                        <div class="item">
+                                        <i class="folder icon"></i>
+                                        <div class="content">
+                                            <div class="header">Chapter Name/</div>
+                                            <div class="description">The Chapter Name of this group of images</div>
+                                            <div class="list">
+                                                <div class="item">
+                                                <i class="image file icon"></i>
+                                                <div class="content">
+                                                    <div class="header">Pages</div>
+                                                    <div class="description">The manga pages in jpg format</div>
+                                                </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        </div>
+                                    </div>
+                                </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div id="no_search_results" style="display:none;">
+            <div class="ui container">
+                <div class="ui message">
+                    <h4 class="ui header">
+                        <i class="search icon"></i>
+                        <div class="content">
+                            No Search Results
+                            <div class="sub header">No manga matches your search term.</div>
+                        </div>
+                    </h4>
+                    <br>
+                    <div class="ui basic button" onclick="clearSearch();">Clear Search</div>
+                </div>
+            </div>
+        </div>
+        <script>
+            var isMobile = false; //initiate as false
+            // device detection
+            if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 
+                || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 
+                isMobile = true;
+            }
+
+            var allMangaData = [];
+
+            //List all the scanned titles
+            ao_module_agirun("Manga/backend/listTitles.js", {},function(data){
+                allMangaData = data;
+                renderMangaList(data);
+            });
+
+            function renderMangaList(mangaList){
+                $("#mangalist").html("");
+                mangaList.forEach(manga => {
+                    var mangaFolderFullPath = manga[0];
+                    var chapterCount = manga[1];
+                    var titleImagePath = manga[2];
+                    var chapterNames = manga[3];
+                    console.log(manga);
+                    var title = manga[0].split("/").pop();
+                    var preview = `/media?file=${manga[2]}`;
+                    if (manga[2] == null || manga[2] == ""){
+                        preview = "./img/notitle.png";
+                    }
+
+                    let firstChapterURL = mangaFolderFullPath + "/" + chapterNames[0];
+                    let chapterSelectionButtons = ``;
+                    chapterNames.forEach(chapterName => {
+                        chapterSelectionButtons += `<a href="viewComic.html#${mangaFolderFullPath}/${chapterName}" class="ui mini basic button chapterBtn manga">${chapterName}</a>`;
+                    });
+
+                    $("#mangalist").append(`<div class="ui basic message" style="width: 100%; display: inline-block; background-color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); padding: 1em 1.5em; margin-bottom: 1em;">
+                        <div class="content">
+                            <div class="ui grid stackable">
+                                <div class="four wide column" style="padding-right:0;">
+                                    <a href="viewComic.html#${firstChapterURL}" class="ui medium image" style="display:block; text-align:center;">
+                                        <img src="${preview}" />
+                                    </a>
+                                </div>
+                                <div class="twelve wide column" style="display:flex; flex-direction:column; justify-content:center;">
+                                    <div class="header" style="word-break:break-word;">${title}</div>
+                                    <div class="meta" style="margin-top:0.35em;">Ch#: ${manga[1]}</div>
+                                    <div class="ui segment chapterSelector">
+                                        ${chapterSelectionButtons}
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>`);
+                });
+
+                if (mangaList.length == 0){
+                    if($("#mangaSearch").val().trim() !== ""){
+                        //No search results
+                        $("#no_search_results").show();
+                        $("#no_manga_info").hide();
+                    }else{
+                        //No Manga
+                        $("#no_manga_info").show();
+                        $("#no_search_results").hide();
+                    }
+                }else{
+                     $("#no_manga_info").hide();
+                     $("#no_search_results").hide();
+                }
+            }
+
+            function filterManga(){
+                var term = $("#mangaSearch").val().toLowerCase().trim();
+                if(term === ""){
+                    renderMangaList(allMangaData);
+                }else{
+                    var filtered = allMangaData.filter(function(manga){
+                        var title = manga[0].split("/").pop().toLowerCase();
+                        if(title.includes(term)) return true;
+                        var chapters = manga[3];
+                        for(var i=0; i<chapters.length; i++){
+                            if(chapters[i].toLowerCase().includes(term)) return true;
+                        }
+                        return false;
+                    });
+                    renderMangaList(filtered);
+                }
+            }
+
+            function clearSearch(){
+                $("#mangaSearch").val('');
+                filterManga();
+            }
+
+            //Add event listener to search input
+            $("#mangaSearch").on('input', function(){
+                filterManga();
+            });
+
+            function openUploadFolder(){
+                ao_module_openPath("user:/Photo/Manga");
+            }
+        </script>
+    </body>
+</html>

+ 18 - 18
web/Manga Cafe/init.agi → web/Manga/init.agi

@@ -1,19 +1,19 @@
-/*
-	Manga Cafe 
-	Author: tobychui
-
-	The grestest tool to read Manga on your arozos
-*/
-
-
-//Define the launchInfo for the module
-var moduleLaunchInfo = {
-    Name: "Manga Cafe",
-	Group: "Media",
-	IconPath: "Manga Cafe/img/small_icon.png",
-	Version: "1.1",
-	StartDir: "Manga Cafe/index.html"
-}
-
-//Register the module
+/*
+	Manga Cafe 
+	Author: tobychui
+
+	The grestest tool to read Manga on your arozos
+*/
+
+
+//Define the launchInfo for the module
+var moduleLaunchInfo = {
+    Name: "Manga",
+	Group: "Media",
+	IconPath: "Manga/img/small_icon.png",
+	Version: "1.1",
+	StartDir: "Manga/index.html"
+}
+
+//Register the module
 registerModule(JSON.stringify(moduleLaunchInfo));

+ 25 - 25
web/Manga Cafe/manifest.json → web/Manga/manifest.json

@@ -1,26 +1,26 @@
-{
-  "name": "Manga Cafe",
-  "short_name": "Manga Cafe",
-  "icons": [{
-    "src": "img/pwa/128.png",
-      "sizes": "128x128",
-      "type": "image/png"
-    },{
-      "src": "img/pwa/192.png",
-      "sizes": "192x192",
-      "type": "image/png"
-    }, {
-      "src": "img/pwa/256.png",
-      "sizes": "256x256",
-      "type": "image/png"
-    }, {
-      "src": "img/pwa/512.png",
-      "sizes": "512x512",
-      "type": "image/png"
-    }],
-  "start_url": "index.html",
-  "display": "fullscreen",
-  "scope": "./",
-  "background_color": "#fcfcfc",
-  "theme_color": "#fcfcfc"
+{
+  "name": "Manga Cafe",
+  "short_name": "Manga Cafe",
+  "icons": [{
+    "src": "img/pwa/128.png",
+      "sizes": "128x128",
+      "type": "image/png"
+    },{
+      "src": "img/pwa/192.png",
+      "sizes": "192x192",
+      "type": "image/png"
+    }, {
+      "src": "img/pwa/256.png",
+      "sizes": "256x256",
+      "type": "image/png"
+    }, {
+      "src": "img/pwa/512.png",
+      "sizes": "512x512",
+      "type": "image/png"
+    }],
+  "start_url": "index.html",
+  "display": "fullscreen",
+  "scope": "./",
+  "background_color": "#fcfcfc",
+  "theme_color": "#fcfcfc"
 }

+ 598 - 565
web/Manga Cafe/viewComic.html → web/Manga/viewComic.html

@@ -1,565 +1,598 @@
-<!DOCTYPE html>
-<meta name="apple-mobile-web-app-capable" content="yes" />
-<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
-<html>
-<head>
-	<meta charset="UTF-8">
-	<script src="../script/jquery.min.js"></script>
-	<link rel="stylesheet" href="../script/semantic/semantic.min.css">
-	<script type='text/javascript' src="../script/semantic/semantic.min.js"></script>
-	<script type='text/javascript' src="../script/ao_module.js"></script>
-	<style>
-	body, html {
-		height: 100%;
-		margin: 0;
-		background-color:#2b2b2b;
-	}
-
-	#settingMenu .ui.toggle.checkbox input:checked ~ .box,#settingMenu  .ui.toggle.checkbox input:checked ~ label{
-		color: rgb(189, 189, 189) !important;
-	}
-	</style>
-	<script>
-		//Scrolling Controller
-		var keys = {37: 1, 38: 1, 39: 1, 40: 1};
-
-		function preventDefault(e) {
-		e = e || window.event;
-		if (e.preventDefault)
-			e.preventDefault();
-		e.returnValue = false;  
-		}
-
-		function preventDefaultForScrollKeys(e) {
-			if (keys[e.keyCode]) {
-				preventDefault(e);
-				return false;
-			}
-		}
-
-		function disableScroll() {
-		if (window.addEventListener) // older FF
-			window.addEventListener('DOMMouseScroll', preventDefault, false);
-			window.onwheel = preventDefault; // modern standard
-			window.onmousewheel = document.onmousewheel = preventDefault; // older browsers, IE
-			window.ontouchmove  = preventDefault; // mobile
-			document.onkeydown  = preventDefaultForScrollKeys;
-		}
-
-		function enableScroll() {
-			if (window.removeEventListener)
-				window.removeEventListener('DOMMouseScroll', preventDefault, false);
-				window.onmousewheel = document.onmousewheel = null; 
-				window.onwheel = null; 
-				window.ontouchmove = null;  
-				document.onkeydown = null;  
-			}
-		disableScroll();
-	</script>
-</head>
-<body>
-<div id="loading" style="position: fixed;top: 0;left: 0;background:rgba(0,0,0,0.5);color:white;width:100%;height:100%;z-index:100">
-	<div class="ui active dimmer">
-        <div class="ui text loader">Loading</div>
-    </div>
-</div>
-<div class="ui attached inverted message" style="margin-top: 0em;" onClick="ToggleChapters();">
-    <div class="header"><i class="bookmark icon"></i>You are already reached the top of this manga.</div>
-    <p>Click here to select more chapters.</p>
-</div>
-
-<div id="MangaList" class="ui list">
-	<div class="item" style="width: 100%;display: flex;align-items: flex-start;"><img src="" style="width: 100%;height: auto;"></img></div>
-</div>
-
-<!-- Click Detect DIV -->
-<div id="clickInterface" style="opacity:0;
-    filter: alpha(opacity = 0);
-    position:fixed;
-    top:25%; bottom:25%; left:0; right:0;
-	width:100%;
-	height:50%;
-    display:block;
-    z-index:998;
-    background:transparent;" onClick="">
-</div>
-
-<!-- Setting Config DIV -->
-<div id="SettingInterface" style="
-    position:fixed;
-    top:0; bottom:0; left:0; right:0;
-	width:100%;
-	height:100%;
-    display:block;
-    z-index:1501;
-	display:none;" onClick="">
-	<div id="settingMenu" class="ui very narrow container" style="top:15%;z-index:1502;">
-		<div class="ui container" style="color:white;">
-		<div class="ui inverted segment" style="background:rgba(61,61,61,0.8);">
-			<p style="font-size:120%;"><i class='setting icon'></i>Settings</p>
-			<div class="ui toggle checkbox">
-				<input type="checkbox" id="lefthand">
-				<label for="lefthand" style="color:white;"><i class="hand paper icon" style="transform: scaleX(-1);-moz-transform: scaleX(-1);-webkit-transform: scaleX(-1);-ms-transform: scaleX(-1);"></i>Left hand mode <br> Click the left section of the view for downward auto scrolling instead of right section.</label>
-			</div><br><br>
-			<div class="ui toggle checkbox">
-				<input type="checkbox" id="progress">
-				<label for="progress" style="color:white;"><i class="save icon"></i>Save progress <br> Save the current progress and return to that page automatically in the future.</label>
-			</div><br><br>
-			<div class="ui toggle checkbox">
-				<input type="checkbox" id="pcm">
-				<label for="pcm" style="color:white;"><i class="save icon"></i>PC Mode <br> Scale down the manga a bit to make you feel better on a big PC screen.</label>
-			</div>
-		</div>
-		</div>
-	</div>
-</div>
-
-<!-- Page Info DIV-->
-<div align="right" style="position: fixed;bottom: 0;right: 0;background:rgba(0,0,0,0.5);color:white;z-index:1000;">
-<div id="PageInfo" class="ui fluid text container">
-Loading Page Data...
-</div>
-</div>
-<!-- Next Chapter Message-->
-<div class="ui inverted message" onclick="NextChapter();" style="cursor: pointer;">
-    <div class="header"><i class="bookmark icon"></i>You have finished this chapter.</div>
-    <p id="reminder">Click this box to proceed to next chapter.</p>
-</div>
-
-<!-- Start of Chapter Selector-->
-<div id="ChapterSelector" class="ui inverted message" style="color:white;display:none;">
-<div class="ui stackable grid" id="chapterlist">
-    
-</div>
-</div>
-<!-- END of Chapter Selector-->
-<div align="center" style="width:100%">
-<div class="ui buttons" >
-    <button class="ui inverted basic button" onClick="window.location.href='index.html'" style="color:white !important;"><i class="chevron left icon"></i>Back</button>
-    <button class="ui inverted basic button" onClick="ToggleChapters();" style="color:white !important;"><i class="book icon"></i>Chapters</button>
-</div>
-</div>
-
-<br>
-<br>
-<script type="text/javascript">
-    var pages = [];
-	var titles = "";
-	var chapterPaths = [];
-	var nextChapterID = 0;
-	var maxChapterID = 0;
-	var pagesNum = pages.length; //This page number is the number of files in the directory, not the actual page number
-	var positions = [];
-	var currentPage = 0;
-	var chapterShown = false;
-	var lastpage = 0;
-	var floatWindow = !(!parent.isFunctionBar); //Updated support on floatWindow System
-	//Auto save and left hand mode
-	var autoSave = false;
-	var leftHandMode = false;
-	var isMobile = false; //initiate as false
-	// device detection
-	if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 
-		|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 
-		isMobile = true;
-	}
-	var preRenderComplete = false;
-	var pcm = false; //PC Mode
-	//Screen Width and height data
-	var screenWidth = $(window).width();
-	var screenHeight = $(window).height();
-	//Debug Interface
-	console.log("This Chapter ID: ",nextChapterID);
-	console.log("Max Chapter ID: ",maxChapterID);
-	console.log("Chapter Paths: ",chapterPaths);
-
-	if(floatWindow){
-		$("#PageInfo").parent().css("bottom","0px");
-		$("body").css("padding-bottom","20px");
-	}
-
-	//Load the content of this manga chapter
-	var mangaFolder = window.location.hash.substr(1);
-	ao_module_agirun("Manga Cafe/backend/getMangaInfo.js", {folder: mangaFolder}, function(data){
-		console.log(data);
-		pages = data.pages;
-		titles = data.title,
-		chapterPaths = data.otherChapterDir;
-		nextChapterID = 0;
-		maxChapterID = chapterPaths.length;
-
-		//Render manga to ui
-		$("#MangaList").html("");
-		data.pages.forEach(page => {
-			$("#MangaList").append(`<div class="item" style="width: 100%;display: flex;align-items: flex-start;"><img src="../media?file=${page}" style="width: 100%;height: auto;"></img></div>`)
-		});
-
-		//Render next chapter button
-		$("#chapterlist").html("");
-		var counter = 0;
-		var nextOneIsPreRender = false;
-		var prerender = "";
-		data.otherChapterDir.forEach(chapter => {
-			var chapterName = chapter.split("/").pop();
-			var chapterWide = "four";
-			if (isMobile){
-				chapterWide = "sixteen";
-			}
-			if (chapterName == data.title[1]){
-				$("#chapterlist").append(`<div class="${chapterWide} wide column compact" style="color:white; padding: 0.3em !important;"><a class="ui fluid inverted basic disabled button" style="color:white;">${chapterName}</a></div>`);
-				nextChapterID = counter;
-				nextOneIsPreRender = true;
-			}else{
-				if (nextOneIsPreRender){
-					nextOneIsPreRender = false;
-					prerender = chapter;
-				}
-				$("#chapterlist").append(`<div class="${chapterWide} wide column compact" style="color:white; padding: 0.3em !important;"><a class="ui fluid inverted basic button" style="color:white;" onClick="ChapterRedirect('${chapter}');">${chapterName}</a></div>`);
-			}
-			counter++;
-		});
-
-		//Start waiting for all images to load
-		var imagesLoaded = 0;
-		var totalImages = $('img').length;
-		$('img').on('load', function(event) {
-			imagesLoaded++;
-			if (imagesLoaded == totalImages) {
-				init();
-
-				if (nextOneIsPreRender == true){
-					//There is no next chapter
-					preRenderComplete = true;
-				}else{
-					//Start next chapter pre-render
-					ao_module_agirun("Manga Cafe/backend/getMangaInfo.js",{folder: prerender}, function(data){
-						//Pre-render complete
-						console.log("Next chapter pre-render completed")
-						preRenderComplete = true;
-					});
-				}
-			
-			}
-		});
-		
-	});
-	
-	//Window onload replace document.ready due to the measurement of each div have to be done after the page loaded
-	function init(){
-		$('#loading').hide(); 
-		enableScroll();
-		positions = [];
-		var count = 0;
-		$('.item').each(function(i, obj) {
-			var offsets = $(this).offset();
-			var top = offsets.top;
-			//console.log(top);
-			positions.push(top);
-			count += 1;
-		});
-		if (nextChapterID + 1 > maxChapterID - 1){
-				$('#reminder').html('There is no more chapters.');
-		}
-		//alert(count);
-		//Check if PCB Mode
-		if (localStorage.pcm !== null){
-			pcm = (localStorage.pcm == 'true');
-			console.log('PC Mode: ' + pcm);
-			$('#pcm').prop("checked",pcm);
-		}
-		if (pcm){
-			//If PC mode is enabled, re-allign all image
-			setTimeout(function(){ PCMode(0); }, 100);
-		}
-		//AutoSave Function
-		if (localStorage.autoSave !== null){
-			autoSave = (localStorage.autoSave == 'true');
-			console.log('AutoSave: ' + autoSave);
-			$('#progress').prop("checked",autoSave);
-			if (localStorage.getItem(titles[0] + "-" + titles[1]) !== null && autoSave){
-				var page = localStorage.getItem(titles[0] + "-" + titles[1]);
-				console.log('Loading progress: ' + page);
-				if (page != count){
-					//Not yet finished
-					console.log(count,page)
-					setTimeout(function(){ GoToPage(page); },100);
-				}
-			}
-		}
-		if(localStorage.getItem(titles[0] + "-" + titles[1]) !== null && (localStorage.autoSave === null || localStorage.autoSave == 'false')){
-				localStorage.removeItem(titles[0] + "-" + titles[1]);
-				console.log("Old autosave record removed.");
-			}
-			
-		//Left hand Mode function
-		if (localStorage.leftHandMode !== null){
-			leftHandMode = (localStorage.leftHandMode == 'true');
-			console.log('Left Hand Mode: ' + leftHandMode);
-			$('#lefthand').prop("checked",leftHandMode);
-		}
-		
-		UpdatePageInfo();
-	}
-	
-	//Check if settings changed
-	$('#lefthand').change(function(){
-	  if($(this).is(':checked')){
-		localStorage.leftHandMode = true;
-		leftHandMode = true;
-		console.log('Left Hand Mode Changed to ' + leftHandMode);
-	  } else {
-		localStorage.leftHandMode = false;
-		leftHandMode = false;
-		console.log('Left Hand Mode Changed to ' + leftHandMode);
-	  }
-	});
-
-	$('#progress').change(function(){
-	  if($(this).is(':checked')){
-		localStorage.autoSave = true;
-		autoSave = true;
-		console.log('Auto Save Changed to ' + autoSave);
-		localStorage.setItem(titles[0] + "-" + titles[1], currentPage);
-	  } else {
-		localStorage.autoSave = false;
-		autoSave = false;
-		console.log('Auto Save Changed to ' + autoSave);
-	  }
-	});
-	
-	$('#pcm').change(function(){
-	  if($(this).is(':checked')){
-		localStorage.pcm = true;
-		pcm = true;
-		console.log('PC Mode Changed to ' + pcm);
-		PCMode();
-	  } else {
-		localStorage.pcm = false;
-		pcm = false;
-		console.log('PC Mode Changed to ' + pcm);
-		MobileMode();
-	  }
-	});
-	//End of setting change section
-	
-	//Recalculate page number accodring to new image position
-	window.onresize = function() {
-		positions = [];
-		$('.item').each(function(i, obj) {
-			var offsets = $(this).offset();
-			var top = offsets.top;
-			//console.log(top);
-			positions.push(top);
-			if (nextChapterID + 1 > maxChapterID - 1){
-				$('#reminder').html('There is no more chapters.');
-			}
-			screenWidth = $(window).width();
-			screenHeight = $(window).height();
-		});
-	}
-	
-	//Click Page Scroll functions
-	var ControlPanel = document.getElementById('clickInterface');
-	ControlPanel.addEventListener("click", AutoPageScroll, false);
-	
-	function AutoPageScroll(e){
-		var clickX = e.clientX;
-		var clickY = e.clientY;
-		var split = screenWidth / 2;
-		if (leftHandMode == true){
-			//Left side is next page, right side is previous page
-			if (clickX < split){
-				//left
-				scrollDown();
-			}else{
-				//right
-				scrollUp();
-			}
-		}else{
-			if (clickX > split){
-				//right
-				scrollDown();
-			}else{
-				//left
-				scrollUp();
-			}
-		}
-	}
-	
-	function scrollDown(){
-		$("html, body").animate({ scrollTop: $(window).scrollTop() + screenHeight * 0.8 }, 1000);
-	}
-	function scrollUp(){
-		$("html, body").animate({ scrollTop: $(window).scrollTop() - screenHeight * 0.8 }, 1000);
-	}
-	//End of auto scrolling
-	
-	//Resize all image to PC mode
-	function PCMode(mode){
-		var tmpPage = currentPage;
-		$('.item').each(function(i, obj) {
-			$(this).css("width","50%");
-		});
-		$('#MangaList').css("width","100%");
-		$('#MangaList').prop("align","center");
-		setTimeout(function(){ CalculatePageOffsets(); }, 1000);
-		if (mode != 0){
-			setTimeout(function(){ GoToPage(tmpPage); }, 100);
-		}
-	}
-	function MobileMode(){
-		var tmpPage = currentPage;
-		$('.item').each(function(i, obj) {
-			$(this).css("width","100%");
-		});
-		$('#MangaList').css("width","");
-		$('#MangaList').prop("align","");
-		setTimeout(function(){ CalculatePageOffsets(); }, 1000);
-		setTimeout(function(){ GoToPage(tmpPage); }, 100);
-	}
-	function CalculatePageOffsets(){
-		console.log('Calculating Page Offsets');
-		positions = [];
-		$('.item').each(function(i, obj) {
-			var offsets = $(this).offset();
-			var top = offsets.top;
-			//console.log(top);
-			positions.push(top);
-			if (nextChapterID + 1 > maxChapterID - 1){
-				$('#reminder').html('There is no more chapters.');
-			}
-		});
-		UpdatePageInfo();
-		 
-	}
-	//Update page info on window scroll
-	$( window ).scroll(function() {
-	  //console.log($(window).scrollTop());
-	  currentPage = getPageNum($(window).scrollTop(),positions);
-	  
-	  if (autoSave == true && currentPage != 0 && lastpage != currentPage){
-		  //Save Page number as: "title-chapter:page"
-		  localStorage.setItem(titles[0] + "-" + titles[1], currentPage);
-		  lastpage = currentPage;
-		  console.log(localStorage.getItem(titles[0] + "-" + titles[1]));
-	  }
-	  var shortentitle = titles[1];
-	  //console.log(currentPage);
-	  if (shortentitle.length > 12){
-		  shortentitle = shortentitle.substring(0, 12) + "...";
-	  }
-	  	var prerenderFinishIcon = "<i class='loading spinner icon'></i>";
-		if (preRenderComplete){
-			prerenderFinishIcon = "<i class='checkmark icon' style='color: green;'></i>"
-		}
-
-	  $('#PageInfo').html("  <i class='chevron left icon' onClick='back();'></i>  <i class='setting icon' onClick='ShowSetting();'></i>" + shortentitle  + " " + prerenderFinishIcon + " " +  currentPage.toString() + "/" +  positions.length.toString() + "    " + titles[0]  +  "  ");
-	  
-	  //if the page reached the end, hide the auto scroll div
-	  if($(window).scrollTop() + $(window).height() == $(document).height()) {
-		  $('#clickInterface').hide();
-	  }else if ( $('#clickInterface').is(':visible') !== true  && chapterShown !== true){
-		  $('#clickInterface').show();
-	  }
-	});
-	
-	//Update Page Info (same as window.scroll function)
-	function UpdatePageInfo(){
-		currentPage = getPageNum($(window).scrollTop(),positions);
-		if (autoSave == true && currentPage != 0){
-			//Save Page number as: "title-chapter:page"
-			localStorage.setItem(titles[0] + "-" + titles[1], currentPage);
-		}
-		//console.log(currentPage);
-		var shortentitle = titles[1];
-		//console.log(currentPage);
-		if (shortentitle.length > 12){
-			shortentitle = shortentitle.substring(0, 12) + "...";
-		}
-
-		var prerenderFinishIcon = "";
-		if (preRenderComplete){
-			prerenderFinishIcon = "<i class='checkmark icon'></i>"
-		}
-
-		$('#PageInfo').html("  <i class='chevron left icon' onClick='back();'></i>  <i class='setting icon' onClick='ShowSetting();'></i>" + shortentitle + " " +  prerenderFinishIcon + " " +  currentPage.toString() + "/" +  positions.length.toString() + "    " + titles[0]  + "  ");
-		
-	}
-	
-	function back(){
-		window.location.href='index.html';
-	}
-	//Setting Interface controlling
-	function ShowSetting(){
-		$('#SettingInterface').fadeIn('slow');
-	}
-	var ExitPanel = document.getElementById('SettingInterface');
-	ExitPanel.addEventListener("click", HideSetting, false);
-	function HideSetting(e){
-		//console.log(e.clientX);
-		//console.log(e.clientY);
-		pleft = $('#settingMenu').offset().left;
-		pright = pleft + $('#settingMenu').width();
-		ptop = $('#settingMenu').offset().top;
-		pbottom = ptop + $('#settingMenu').height();
-		//console.log(e.clientX,e.clientY,pleft,pright,ptop,pbottom);
-		if (e.clientX < pleft || e.clientX > pright || e.clientY + $(window).scrollTop() < ptop || e.clientY + $(window).scrollTop()> pbottom){
-			$('#SettingInterface').fadeOut('slow');
-		}
-	}
-	
-	//Next chapter controlling algorithm
-	function NextChapter(){
-		var id = nextChapterID + 1;
-		if (id > maxChapterID - 1){
-			
-		}else{
-			ChapterRedirect(chapterPaths[id]);
-		}
-		
-		
-	}
-	
-	function ChapterRedirect(path){
-		window.location.href= "viewComic.html#" + path;
-		window.location.reload();
-	}
-	
-	
-	//Show all chapters of this manga
-	function ToggleChapters(){
-		if (chapterShown == true){
-			$('#ChapterSelector').fadeOut('slow');
-			//$("html, body").animate({ scrollTop: $("#ChapterSelector").offset().top }, 1000);
-			$("html, body").animate({ scrollTop: $(document).height() }, 1000);
-			chapterShown = false;
-			$('#clickInterface').show();
-		}else{
-			$('#ChapterSelector').fadeIn('slow');
-			$("html, body").animate({ scrollTop: $("#ChapterSelector").offset().top }, 1000);
-			//$("html, body").animate({ scrollTop: $(document).height() }, 1000);
-			chapterShown = true;
-			$('#clickInterface').hide();
-		}
-	}
-	
-	//Convert current view top to page number
-	function getPageNum (num, arr) {
-                for (var i = 0;i<arr.length;i++){
-					if (arr[i] > num){
-						return i;
-					}
-				}
-				return arr.length;
-            }
-	
-	function GoToPage(num){
-		//console.log('Going to page: ' + num);
-		CalculatePageOffsets();
-		console.log("Going to page number:" + num);
-		var targetTop = positions[num - 1];
-		$('html, body').animate({scrollTop: targetTop + 5}, 1000);
-	}
-</script>
-</body>
-</html>
+<!DOCTYPE html>
+<meta name="apple-mobile-web-app-capable" content="yes" />
+<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
+<html>
+<head>
+	<meta charset="UTF-8">
+	<script src="../script/jquery.min.js"></script>
+	<link rel="stylesheet" href="../script/semantic/semantic.min.css">
+	<script type='text/javascript' src="../script/semantic/semantic.min.js"></script>
+	<script type='text/javascript' src="../script/ao_module.js"></script>
+	<title></title>
+	<style>
+	body, html {
+		height: 100%;
+		margin: 0;
+		background-color:#2b2b2b;
+	}
+
+	#settingMenu .ui.toggle.checkbox input:checked ~ .box,#settingMenu  .ui.toggle.checkbox input:checked ~ label{
+		color: rgb(189, 189, 189) !important;
+	}
+
+	#SettingInterface{
+		position:fixed;
+		top:0;
+		left:0;
+		right:0;
+		bottom:0;
+		width:100%;
+		height:100%;
+		display:none;
+	}
+
+	#SettingInterface .dimmer {
+		position: fixed;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		z-index: 1500 !important;
+		background: rgba(0,0,0,0.35);
+		backdrop-filter: blur(3px);
+	}
+	
+	#SettingInterface #settingMenu {
+		position: relative;
+		left: 50%;
+		top: 50%;
+		transform: translate(-50%, -50%);
+		margin: 0 !important;
+		z-index:1502;
+	}
+	
+	</style>
+	<script>
+		//Scrolling Controller
+		var keys = {37: 1, 38: 1, 39: 1, 40: 1};
+
+		function preventDefault(e) {
+		e = e || window.event;
+		if (e.preventDefault)
+			e.preventDefault();
+		e.returnValue = false;  
+		}
+
+		function preventDefaultForScrollKeys(e) {
+			if (keys[e.keyCode]) {
+				preventDefault(e);
+				return false;
+			}
+		}
+
+		function disableScroll() {
+		if (window.addEventListener) // older FF
+			window.addEventListener('DOMMouseScroll', preventDefault, false);
+			window.onwheel = preventDefault; // modern standard
+			window.onmousewheel = document.onmousewheel = preventDefault; // older browsers, IE
+			window.ontouchmove  = preventDefault; // mobile
+			document.onkeydown  = preventDefaultForScrollKeys;
+		}
+
+		function enableScroll() {
+			if (window.removeEventListener)
+				window.removeEventListener('DOMMouseScroll', preventDefault, false);
+				window.onmousewheel = document.onmousewheel = null; 
+				window.onwheel = null; 
+				window.ontouchmove = null;  
+				document.onkeydown = null;  
+			}
+		disableScroll();
+	</script>
+</head>
+<body>
+<div id="loading" style="position: fixed;top: 0;left: 0;background:rgba(0,0,0,0.5);color:white;width:100%;height:100%;z-index:100">
+	<div class="ui active dimmer">
+        <div class="ui text loader">Loading</div>
+    </div>
+</div>
+<div class="ui attached inverted message" style="margin-top: 0em;" onClick="ToggleChapters();">
+    <div class="header"><i class="bookmark icon"></i>You are already reached the top of this manga.</div>
+    <p>Click here to select more chapters.</p>
+</div>
+
+<div id="MangaList" class="ui list">
+	<div class="item" style="width: 100%;display: flex;align-items: flex-start;"><img src="" style="width: 100%;height: auto;"></img></div>
+</div>
+
+<!-- Click Detect DIV -->
+<div id="clickInterface" style="opacity:0;
+    filter: alpha(opacity = 0);
+    position:fixed;
+    top:25%; bottom:25%; left:0; right:0;
+	width:100%;
+	height:50%;
+    display:block;
+    z-index:998;
+    background:transparent;" onClick="">
+</div>
+
+<!-- Setting Config DIV -->
+<div id="SettingInterface" onClick="">
+	<div class="dimmer" onclick="HideSetting(event);"></div>
+	<div id="settingMenu" class="ui very narrow container" style="top:15%;z-index:1502;">
+		<div class="ui container" style="color:white;">
+		<div class="ui inverted segment" style="background:rgba(61,61,61,0.8); ">
+			<p style="font-size:120%;"><i class='setting icon'></i>Settings</p>
+			<div class="ui toggle checkbox">
+				<input type="checkbox" id="lefthand">
+				<label for="lefthand" style="color:white;"><i class="hand paper icon" style="transform: scaleX(-1);-moz-transform: scaleX(-1);-webkit-transform: scaleX(-1);-ms-transform: scaleX(-1);"></i>Left hand mode <br> Click the left section of the view for downward auto scrolling instead of right section.</label>
+			</div><br><br>
+			<div class="ui toggle checkbox">
+				<input type="checkbox" id="progress">
+				<label for="progress" style="color:white;"><i class="save icon"></i>Save progress <br> Save the current progress and return to that page automatically in the future.</label>
+			</div><br><br>
+			<div class="ui toggle checkbox">
+				<input type="checkbox" id="pcm">
+				<label for="pcm" style="color:white;"><i class="save icon"></i>PC Mode <br> Scale down the manga a bit to make you feel better on a big PC screen.</label>
+			</div>
+		</div>
+		</div>
+	</div>
+</div>
+
+<!-- Page Info DIV-->
+<div align="right" style="position: fixed;bottom: 0;right: 0;background:rgba(0,0,0,0.5);color:white;z-index:1000;">
+<div id="PageInfo" class="ui fluid text container">
+Loading Page Data...
+</div>
+</div>
+<!-- Next Chapter Message-->
+<div class="ui inverted message" onclick="NextChapter();" style="cursor: pointer;">
+    <div class="header"><i class="bookmark icon"></i>You have finished this chapter.</div>
+    <p id="reminder">Click this box to proceed to next chapter.</p>
+</div>
+
+<!-- Start of Chapter Selector-->
+<div id="ChapterSelector" class="ui inverted message" style="color:white;display:none;">
+<div class="ui stackable grid" id="chapterlist">
+    
+</div>
+</div>
+<!-- END of Chapter Selector-->
+<div align="center" style="width:100%">
+<div class="ui buttons" >
+    <button class="ui inverted basic button" onClick="window.location.href='index.html'" style="color:white !important;"><i class="chevron left icon"></i>Back</button>
+    <button class="ui inverted basic button" onClick="ToggleChapters();" style="color:white !important;"><i class="book icon"></i>Chapters</button>
+</div>
+</div>
+
+<br>
+<br>
+<script type="text/javascript">
+    var pages = [];
+	var titles = "";
+	var chapterPaths = [];
+	var nextChapterID = 0;
+	var maxChapterID = 0;
+	var pagesNum = pages.length; //This page number is the number of files in the directory, not the actual page number
+	var positions = [];
+	var currentPage = 0;
+	var chapterShown = false;
+	var lastpage = 0;
+	var floatWindow = !(!parent.isFunctionBar); //Updated support on floatWindow System
+	//Auto save and left hand mode
+	var autoSave = false;
+	var leftHandMode = false;
+	var isMobile = false; //initiate as false
+	// device detection
+	if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 
+		|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 
+		isMobile = true;
+	}
+	var preRenderComplete = false;
+	var pcm = false; //PC Mode
+	//Screen Width and height data
+	var screenWidth = $(window).width();
+	var screenHeight = $(window).height();
+	//Debug Interface
+	console.log("This Chapter ID: ",nextChapterID);
+	console.log("Max Chapter ID: ",maxChapterID);
+	console.log("Chapter Paths: ",chapterPaths);
+
+	if(floatWindow){
+		$("#PageInfo").parent().css("bottom","0px");
+		$("body").css("padding-bottom","20px");
+	}else{
+		// Ensure PageInfo sits at bottom for non-floating mode and set page title once manga data is available
+		$("#PageInfo").parent().css("position","fixed");
+		$("#PageInfo").parent().css("bottom","0");
+		$("body").css("padding-bottom","20px");
+
+		(function waitAndSetTitle(){
+			var t = setInterval(function(){
+				if (Array.isArray(titles) && titles.length >= 2 && titles[0] && titles[1]){
+					document.title = titles[0] + " " + titles[1];
+					clearInterval(t);
+				}
+			}, 100);
+		})();
+	}
+
+	//Load the content of this manga chapter
+	var mangaFolder = window.location.hash.substr(1);
+	ao_module_agirun("Manga/backend/getMangaInfo.js", {folder: mangaFolder}, function(data){
+		console.log(data);
+		pages = data.pages;
+		titles = data.title,
+		chapterPaths = data.otherChapterDir;
+		nextChapterID = 0;
+		maxChapterID = chapterPaths.length;
+
+		//Render manga to ui
+		$("#MangaList").html("");
+		data.pages.forEach(page => {
+			$("#MangaList").append(`<div class="item" style="width: 100%;display: flex;align-items: flex-start;"><img src="../media?file=${page}" style="width: 100%;height: auto;"></img></div>`)
+		});
+
+		//Render next chapter button
+		$("#chapterlist").html("");
+		var counter = 0;
+		var nextOneIsPreRender = false;
+		var prerender = "";
+		data.otherChapterDir.forEach(chapter => {
+			var chapterName = chapter.split("/").pop();
+			var chapterWide = "four";
+			if (isMobile){
+				chapterWide = "sixteen";
+			}
+			if (chapterName == data.title[1]){
+				$("#chapterlist").append(`<div class="${chapterWide} wide column compact" style="color:white; padding: 0.3em !important;"><a class="ui fluid inverted basic disabled button" style="color:white;">${chapterName}</a></div>`);
+				nextChapterID = counter;
+				nextOneIsPreRender = true;
+			}else{
+				if (nextOneIsPreRender){
+					nextOneIsPreRender = false;
+					prerender = chapter;
+				}
+				$("#chapterlist").append(`<div class="${chapterWide} wide column compact" style="color:white; padding: 0.3em !important;"><a class="ui fluid inverted basic button" style="color:white;" onClick="ChapterRedirect('${chapter}');">${chapterName}</a></div>`);
+			}
+			counter++;
+		});
+
+		//Start waiting for all images to load
+		var imagesLoaded = 0;
+		var totalImages = $('img').length;
+		$('img').on('load', function(event) {
+			imagesLoaded++;
+			if (imagesLoaded == totalImages) {
+				init();
+
+				if (nextOneIsPreRender == true){
+					//There is no next chapter
+					preRenderComplete = true;
+				}else{
+					//Start next chapter pre-render
+					ao_module_agirun("Manga/backend/getMangaInfo.js",{folder: prerender}, function(data){
+						//Pre-render complete
+						console.log("Next chapter pre-render completed")
+						preRenderComplete = true;
+					});
+				}
+			
+			}
+		});
+		
+	});
+	
+	//Window onload replace document.ready due to the measurement of each div have to be done after the page loaded
+	function init(){
+		$('#loading').hide(); 
+		enableScroll();
+		positions = [];
+		var count = 0;
+		$('.item').each(function(i, obj) {
+			var offsets = $(this).offset();
+			var top = offsets.top;
+			//console.log(top);
+			positions.push(top);
+			count += 1;
+		});
+		if (nextChapterID + 1 > maxChapterID - 1){
+				$('#reminder').html('There is no more chapters.');
+		}
+		//alert(count);
+		//Check if PCB Mode
+		if (localStorage.pcm !== null){
+			pcm = (localStorage.pcm == 'true');
+			console.log('PC Mode: ' + pcm);
+			$('#pcm').prop("checked",pcm);
+		}
+		if (pcm){
+			//If PC mode is enabled, re-allign all image
+			setTimeout(function(){ PCMode(0); }, 100);
+		}
+		//AutoSave Function
+		if (localStorage.autoSave !== null){
+			autoSave = (localStorage.autoSave == 'true');
+			console.log('AutoSave: ' + autoSave);
+			$('#progress').prop("checked",autoSave);
+			if (localStorage.getItem(titles[0] + "-" + titles[1]) !== null && autoSave){
+				var page = localStorage.getItem(titles[0] + "-" + titles[1]);
+				console.log('Loading progress: ' + page);
+				if (page != count){
+					//Not yet finished
+					console.log(count,page)
+					setTimeout(function(){ GoToPage(page); },100);
+				}
+			}
+		}
+		if(localStorage.getItem(titles[0] + "-" + titles[1]) !== null && (localStorage.autoSave === null || localStorage.autoSave == 'false')){
+				localStorage.removeItem(titles[0] + "-" + titles[1]);
+				console.log("Old autosave record removed.");
+			}
+			
+		//Left hand Mode function
+		if (localStorage.leftHandMode !== null){
+			leftHandMode = (localStorage.leftHandMode == 'true');
+			console.log('Left Hand Mode: ' + leftHandMode);
+			$('#lefthand').prop("checked",leftHandMode);
+		}
+		
+		UpdatePageInfo();
+	}
+	
+	//Check if settings changed
+	$('#lefthand').change(function(){
+	  if($(this).is(':checked')){
+		localStorage.leftHandMode = true;
+		leftHandMode = true;
+		console.log('Left Hand Mode Changed to ' + leftHandMode);
+	  } else {
+		localStorage.leftHandMode = false;
+		leftHandMode = false;
+		console.log('Left Hand Mode Changed to ' + leftHandMode);
+	  }
+	});
+
+	$('#progress').change(function(){
+	  if($(this).is(':checked')){
+		localStorage.autoSave = true;
+		autoSave = true;
+		console.log('Auto Save Changed to ' + autoSave);
+		localStorage.setItem(titles[0] + "-" + titles[1], currentPage);
+	  } else {
+		localStorage.autoSave = false;
+		autoSave = false;
+		console.log('Auto Save Changed to ' + autoSave);
+	  }
+	});
+	
+	$('#pcm').change(function(){
+	  if($(this).is(':checked')){
+		localStorage.pcm = true;
+		pcm = true;
+		console.log('PC Mode Changed to ' + pcm);
+		PCMode();
+	  } else {
+		localStorage.pcm = false;
+		pcm = false;
+		console.log('PC Mode Changed to ' + pcm);
+		MobileMode();
+	  }
+	});
+	//End of setting change section
+	
+	//Recalculate page number accodring to new image position
+	window.onresize = function() {
+		positions = [];
+		$('.item').each(function(i, obj) {
+			var offsets = $(this).offset();
+			var top = offsets.top;
+			//console.log(top);
+			positions.push(top);
+			if (nextChapterID + 1 > maxChapterID - 1){
+				$('#reminder').html('There is no more chapters.');
+			}
+			screenWidth = $(window).width();
+			screenHeight = $(window).height();
+		});
+	}
+	
+	//Click Page Scroll functions
+	var ControlPanel = document.getElementById('clickInterface');
+	ControlPanel.addEventListener("click", AutoPageScroll, false);
+	
+	function AutoPageScroll(e){
+		var clickX = e.clientX;
+		var clickY = e.clientY;
+		var split = screenWidth / 2;
+		if (leftHandMode == true){
+			//Left side is next page, right side is previous page
+			if (clickX < split){
+				//left
+				scrollDown();
+			}else{
+				//right
+				scrollUp();
+			}
+		}else{
+			if (clickX > split){
+				//right
+				scrollDown();
+			}else{
+				//left
+				scrollUp();
+			}
+		}
+	}
+	
+	function scrollDown(){
+		$("html, body").animate({ scrollTop: $(window).scrollTop() + screenHeight * 0.8 }, 1000);
+	}
+	function scrollUp(){
+		$("html, body").animate({ scrollTop: $(window).scrollTop() - screenHeight * 0.8 }, 1000);
+	}
+	//End of auto scrolling
+	
+	//Resize all image to PC mode
+	function PCMode(mode){
+		var tmpPage = currentPage;
+		$('.item').each(function(i, obj) {
+			$(this).css("width","50%");
+		});
+		$('#MangaList').css("width","100%");
+		$('#MangaList').prop("align","center");
+		setTimeout(function(){ CalculatePageOffsets(); }, 1000);
+		if (mode != 0){
+			setTimeout(function(){ GoToPage(tmpPage); }, 100);
+		}
+	}
+	function MobileMode(){
+		var tmpPage = currentPage;
+		$('.item').each(function(i, obj) {
+			$(this).css("width","100%");
+		});
+		$('#MangaList').css("width","");
+		$('#MangaList').prop("align","");
+		setTimeout(function(){ CalculatePageOffsets(); }, 1000);
+		setTimeout(function(){ GoToPage(tmpPage); }, 100);
+	}
+	function CalculatePageOffsets(){
+		console.log('Calculating Page Offsets');
+		positions = [];
+		$('.item').each(function(i, obj) {
+			var offsets = $(this).offset();
+			var top = offsets.top;
+			//console.log(top);
+			positions.push(top);
+			if (nextChapterID + 1 > maxChapterID - 1){
+				$('#reminder').html('There is no more chapters.');
+			}
+		});
+		UpdatePageInfo();
+		 
+	}
+	//Update page info on window scroll
+	$( window ).scroll(function() {
+	  //console.log($(window).scrollTop());
+	  currentPage = getPageNum($(window).scrollTop(),positions);
+	  
+	  if (autoSave == true && currentPage != 0 && lastpage != currentPage){
+		  //Save Page number as: "title-chapter:page"
+		  localStorage.setItem(titles[0] + "-" + titles[1], currentPage);
+		  lastpage = currentPage;
+		  console.log(localStorage.getItem(titles[0] + "-" + titles[1]));
+	  }
+	  var shortentitle = titles[1];
+	  //console.log(currentPage);
+	  if (shortentitle.length > 12){
+		  shortentitle = shortentitle.substring(0, 12) + "...";
+	  }
+	  	var prerenderFinishIcon = "<i class='loading spinner icon'></i>";
+		if (preRenderComplete){
+			prerenderFinishIcon = "<i class='checkmark icon' style='color: green;'></i>"
+		}
+
+	  $('#PageInfo').html("  <i class='chevron left icon' onClick='back();'></i>  <i class='setting icon' onClick='ShowSetting();'></i>" + shortentitle  + " " + prerenderFinishIcon + " " +  currentPage.toString() + "/" +  positions.length.toString() + "    " + titles[0]  +  "  ");
+	  
+	  //if the page reached the end, hide the auto scroll div
+	  if($(window).scrollTop() + $(window).height() == $(document).height()) {
+		  $('#clickInterface').hide();
+	  }else if ( $('#clickInterface').is(':visible') !== true  && chapterShown !== true){
+		  $('#clickInterface').show();
+	  }
+	});
+	
+	//Update Page Info (same as window.scroll function)
+	function UpdatePageInfo(){
+		currentPage = getPageNum($(window).scrollTop(),positions);
+		if (autoSave == true && currentPage != 0){
+			//Save Page number as: "title-chapter:page"
+			localStorage.setItem(titles[0] + "-" + titles[1], currentPage);
+		}
+		//console.log(currentPage);
+		var shortentitle = titles[1];
+		//console.log(currentPage);
+		if (shortentitle.length > 12){
+			shortentitle = shortentitle.substring(0, 12) + "...";
+		}
+
+		var prerenderFinishIcon = "";
+		if (preRenderComplete){
+			prerenderFinishIcon = "<i class='checkmark icon'></i>"
+		}
+
+		$('#PageInfo').html("  <i class='chevron left icon' onClick='back();'></i>  <i class='setting icon' onClick='ShowSetting();'></i>" + shortentitle + " " +  prerenderFinishIcon + " " +  currentPage.toString() + "/" +  positions.length.toString() + "    " + titles[0]  + "  ");
+		
+	}
+	
+	function back(){
+		window.location.href='index.html';
+	}
+	//Setting Interface controlling
+	function ShowSetting(){
+		$('#SettingInterface').fadeIn('fast');
+	}
+	
+	function HideSetting(e){
+		e.preventDefault();
+		e.stopImmediatePropagation();
+		$('#SettingInterface').fadeOut('fast');
+	}
+	
+	//Next chapter controlling algorithm
+	function NextChapter(){
+		var id = nextChapterID + 1;
+		if (id > maxChapterID - 1){
+			
+		}else{
+			ChapterRedirect(chapterPaths[id]);
+		}
+		
+		
+	}
+	
+	function ChapterRedirect(path){
+		window.location.href= "viewComic.html#" + path;
+		window.location.reload();
+	}
+	
+	
+	//Show all chapters of this manga
+	function ToggleChapters(){
+		if (chapterShown == true){
+			$('#ChapterSelector').fadeOut('slow');
+			//$("html, body").animate({ scrollTop: $("#ChapterSelector").offset().top }, 1000);
+			$("html, body").animate({ scrollTop: $(document).height() }, 1000);
+			chapterShown = false;
+			$('#clickInterface').show();
+		}else{
+			$('#ChapterSelector').fadeIn('slow');
+			$("html, body").animate({ scrollTop: $("#ChapterSelector").offset().top }, 1000);
+			//$("html, body").animate({ scrollTop: $(document).height() }, 1000);
+			chapterShown = true;
+			$('#clickInterface').hide();
+		}
+	}
+	
+	//Convert current view top to page number
+	function getPageNum (num, arr) {
+                for (var i = 0;i<arr.length;i++){
+					if (arr[i] > num){
+						return i;
+					}
+				}
+				return arr.length;
+            }
+	
+	function GoToPage(num){
+		//console.log('Going to page: ' + num);
+		CalculatePageOffsets();
+		console.log("Going to page number:" + num);
+		var targetTop = positions[num - 1];
+		$('html, body').animate({scrollTop: targetTop + 5}, 1000);
+	}
+</script>
+</body>
+</html>

+ 2 - 0
web/SystemAO/info/gomod-license.csv

@@ -57,6 +57,8 @@ github.com/satori/go.uuid,https://github.com/satori/go.uuid/blob/v1.2.0/LICENSE,
 github.com/sergi/go-diff/diffmatchpatch,https://github.com/sergi/go-diff/blob/v1.4.0/LICENSE,MIT
 github.com/skeema/knownhosts,https://github.com/skeema/knownhosts/blob/v1.3.2/LICENSE,Apache-2.0
 github.com/spf13/afero,https://github.com/spf13/afero/blob/v1.15.0/LICENSE.txt,Apache-2.0
+github.com/srwiley/oksvg,https://github.com/srwiley/oksvg/blob/be6e8873101c/LICENSE,BSD-3-Clause
+github.com/srwiley/rasterx,https://github.com/srwiley/rasterx/blob/2ab79fcdd4ef/LICENSE,BSD-3-Clause
 github.com/studio-b12/gowebdav,https://github.com/studio-b12/gowebdav/blob/v0.11.0/LICENSE,BSD-3-Clause
 github.com/ulikunitz/xz,https://github.com/ulikunitz/xz/blob/v0.5.15/LICENSE,BSD-3-Clause
 github.com/xanzy/ssh-agent,https://github.com/xanzy/ssh-agent/blob/v0.3.3/LICENSE,Apache-2.0

+ 594 - 467
web/login.system

@@ -1,467 +1,594 @@
-<!DOCTYPE HTML>
-<html>
-    <head>
-    <meta charset="UTF-8">
-    <meta name="robots" content="noindex" />
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <link rel="author" href="humans.txt"/>
-    <title>ArozOS - Login</title>
-    <link rel="stylesheet" href="script/semantic/semantic.min.css">
-    <link rel="stylesheet" href="script/ao.css">
-    <script type="application/javascript" src="script/jquery.min.js"></script>
-    <script type="application/javascript" src="script/semantic/semantic.min.js"></script>
-    
-    <style>
-    @media only screen and (max-height: 1000px) {
-        .leftPictureFrame {
-        height:auto !important;
-        }
-    }
-
-    :root{
-        --main_theme_color: #5baf6d;
-        --secondary_theme_color: #4b7952;
-        --dimmer_theme_color: #125e1b;
-        --inverse_theme_color: #e0f1b3;
-    }
-
-    .leftPictureFrame{
-        position:fixed;
-        top:0px;
-        left:0px;
-        min-width:calc(100% - 500px);
-        min-height:100%;
-        background-color:#faf7eb;
-        background-image:url("./system/info/wallpaper.jpg");
-        -webkit-background-size: cover;
-        -moz-background-size: cover;
-        -o-background-size: cover;
-        background-size: cover;
-        background-repeat: no-repeat, no-repeat;
-        background-position:bottom left;
-    }
-
-    .rightLoginFrame{
-        position:fixed;
-        top:0;
-        right:0;
-        height:100%;
-        width:500px;
-        background:white;
-        z-index:100%;
-        padding-left: 30px;
-        padding-right: 20px;
-    }
-
-    @media (max-width: 600px) {
-        .rightLoginFrame{
-            width: 100%;
-        }
-    }
-    
-    @media (min-width: 600px) {
-        @supports (backdrop-filter: blur(10px)) {
-        /* This browser support backdrop filter */
-            .rightLoginFrame {
-                margin-top: 5em;
-                margin-right: 5em;
-                margin-left: 5em;
-                height:calc(100% - 10em);
-                border-radius: 1em;
-            }
-
-            .leftPictureFrame{
-                min-width:calc(100%);
-            }
-        }
-    }
-
-    .fullHeightImage{
-        height:100% !important;
-        position:relative;
-        left:-20px;
-        
-    }
-
-    .bottombar{
-        position:absolute;
-        bottom:1em;
-        left:0;
-        padding-left: 20px;
-        width:100%;
-    }
-
-    #animationFrame{
-        position:absolute;
-        bottom:0px;
-        width:100%;
-    }
-
-    .textbox{
-        margin-bottom:15px;
-    }
-
-    .themecolor{
-        background-color: var(--main_theme_color) !important;
-        transition: background-color 0.1s;
-    }
-
-    .themecolor:hover{
-        background-color: var(--secondary_theme_color) !important;
-    }
-
-    .subthemecolor{
-        background-color: var(--secondary_theme_color) !important;
-        transition: background-color 0.1s;
-    }
-
-    .subthemecolor:hover{
-        background-color: var(--dimmer_theme_color) !important;
-    }
-
-    .inversehighlight{
-        padding: 0.4em;
-        padding-top: 0.2em;
-        padding-bottom: 0.3em;
-        background-color: var(--dimmer_theme_color);
-        color: white;
-        border-radius: 0.4em;
-        margin-top: 0.4em;
-    }
-
-    .loginbtn{
-        color:white !important;
-        margin-top:4em;
-    }
-
-    .oauthbtn{
-        color:white !important;
-        margin-top:1em;
-    }
-
-    .alternativeAccount:not(.disabled){
-        cursor: pointer;
-    }
-
-    .alternativeAccount:not(.disabled):hover{
-        background-color: rgb(245, 245, 245);
-    }
-
-    .alternativeAccount:disabled{
-        opacity: 0.6;
-        pointer-events: none !important;
-        user-select: none;
-        cursor: not-allowed;
-    }
-
-    .loginLogo{
-        margin-top: 4em;
-    }
-
-    @media (orientation: landscape) and (max-height: 765px) {
-        .rightLoginFrame{
-            width: 500px !important;
-            margin-left: auto;
-            margin-right: auto;
-            margin-top: 0;
-            height: 100%;
-            overflow-y: auto;
-            border-radius: 0;
-        }
-        
-        .loginLogo{
-            margin-top: 1.2em;
-        }
-    }
-
-    </style>
-    </head>
-    <body>
-        <div class="leftPictureFrame">
-            
-        </div>
-        <div id="loginInterface" class="rightLoginFrame">
-            <img class="ui medium image loginLogo" src="data:image/png;base64, {{service_logo}}">
-
-            <div class="ui borderless basic segment">
-                <p>Sign in to <span class="hostname">ArozOS</span> with your username and password</p>
-                
-               
-                
-                <div class="ui fluid input textbox">
-                    <input id="username" type="text" placeholder="Username">
-                </div>
-                <div class="ui fluid input textbox">
-                    <input id="magic" type="password" placeholder="Password">
-                </div>
-
-                <div class="ui checkbox">
-                    <input id="rmbme" type="checkbox">
-                    <label for="rmbme">Remember Me</label>
-                </div>
-                
-                <br><br>
-                <button id="loginbtn" class="ui button loginbtn themecolor" style="display:inline-block;">Sign In</button>
-                <div class="oauthonly" style="display:inline-block;">
-                    <a class="ui button oauthbtn subthemecolor" href="system/auth/oauth/login">Sign In via OAuth 2.0</a><br>
-                </div>
-                <div class="ldaponly" style="display:inline-block;">
-                    <a class="ui button oauthbtn subthemecolor" href="ldapLogin.system">Sign In via LDAP</a><br>
-                </div>
-                <div class="resumableOnly" style="display:none;">
-                    <br>
-                    <div class="ui clickable segment alternativeAccount" style="margin-bottom: 0px; padding-bottom: 8px; width: 100%; padding-top: 0px;">
-                        <div style="margin-top: 0.6em;">
-                            <div class="ui header">
-                                <img class="usericon ui circular image" src="img/public/user.svg">
-                                <div class="content" style="font-size: 95% !important;">
-                                    <span class="username"><i class="ui loading spinner icon"></i></span> 
-                                    <div class="sub header usergroup"><i class="ui green check circle icon" style="margin-right: 0px;"></i> Resumable Session</div>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                    <br>
-                    <button class="ui subthemecolor newResumableSession button" style="color: white; display:none;"><i class="ui add icon"></i> Create New Session</button>
-                </div>
-
-                <br>
-                <div class="ui breadcrumb" style="margin-top:12px;">
-                    <a class="section signup" style="cursor:pointer; display:none;" href="public/register/register.system">Sign Up</a>
-                    <div class="divider signup"> / </div>
-                    <a  id="forgetpw" class="section" style="cursor:pointer" href="reset.system">Forgot Password</a>
-                </div>
-                <p style="margin-top:18px;color:#ff7a70; display:none;font-size:1.2em;"><i class="remove icon"></i><span id="errmsg">Error. Incorrect username or password.</span></p>
-               
-            </div>
-           
-            <div class="bottombar">
-                © <a href="https://arozos.com">ArozOS</a> 2017 - <span class="thisyear"></span><br>
-                <small class="inversehighlight" style="font-size: 80%">Request Timestamp: <span id="requestTime"></span> | <span id="requestHostCommonName"></span></small>
-            </div>
-        </div>
-        
-    <script>
-        var redirectionAddress = "{{redirection_addr}}";
-        var loginAddress = "{{login_addr}}";
-        var systemUserCount = "{{usercount}}" - 0; //Magic way to convert string to int :)
-        var autoRedirectTimer;
-        var isMobile = false; //initiate as false
-        // device detection
-        if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 
-            || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 
-            isMobile = true;
-        }  
-
-
-
-        if (isMobile){
-            //Full screen the login panel
-            $("#loginInterface").css("width","100%");
-        }
-
-        if (systemUserCount == 0){
-            //There are no user in this system yet. Rediect to user setup
-            window.location.href = "/user.system";
-        }
-
-        //Hide elements by default
-        $(".ldaponly").hide();
-        $(".oauthonly").hide();
-        
-        $(document).ready(function(){
-            function zeroPad(num) {
-                return num < 10 ? '0' + num : num;
-            }
-
-            var currentdate = new Date(); 
-            var datetime = zeroPad(currentdate.getDate()) + "/"
-                + zeroPad(currentdate.getMonth() + 1) + "/" 
-                + currentdate.getFullYear() + " "  
-                + zeroPad(currentdate.getHours()) + ":"  
-                + zeroPad(currentdate.getMinutes()) + ":" 
-                + zeroPad(currentdate.getSeconds());
-            $("#requestTime").text(datetime);
-            $("#requestHostCommonName").text(location.hostname);
-            $(".ui.checkbox").checkbox();
-            //Check if the user already logged in
-            $.get("system/auth/checkLogin",function(data){
-                try{
-                    if (data === true || data.trim() == "true"){
-                    //User already logged in. Redirect to target page.
-                    if (redirectionAddress == ""){
-                        //Redirect back to index
-                        window.location.href = "/";
-                    }else{
-                        console.log(data);
-                        //window.location.href = redirectionAddress;
-                    }
-                }
-                }catch(ex){
-                    //Assume not logged in
-                    console.log(data);
-                }
-                
-            });
-
-            //Check if the system is open for registry
-            $.get("public/register/checkPublicRegister",function(data){
-                if (data == true){
-                    $(".signup").show();
-                }else{
-                    $(".signup").remove();
-                }
-            });
-
-            //OAuth related code, check if system is open for ext login
-            $.getJSON("system/auth/oauth/checkoauth",function(data){
-                if (data.enabled == true){
-                    $(".oauthonly").show();
-                }else{
-                    $(".oauthonly").hide();
-                }
-                //if auto redirect is on
-                if(data.auto_redirect == true) {
-                    //checking if they come from desktop.system or mobile.system
-                    //if they come from that two pages, usually mean they are just logged out.
-                    if(document.referrer != ''){
-                        var path = new URL(document.referrer);
-                    } else {
-                        var path = new URL('http://0.0.0.0');
-
-                    }
-                    if(document.referrer != window.location.origin + "/desktop.system" && document.referrer != window.location.origin + "/mobile.system" && path.origin + path.pathname !=  window.location.origin + "/system/auth/oauth/authorize"){
-                        $(".ts.borderless.basic.segment").attr("style","display: none;");
-                        $(".ts.borderless.basic.segment").attr("id","aoLogin");
-                        $(".ts.borderless.basic.segment").after('<div id="autoRedirectSegment" class="ui borderless basic segment"><p><i class="key icon"></i>Redirecting to organization sign-in page in 5 seconds...</p><br><a style="cursor: pointer;" onclick="stopAutoRedirect()">Cancel</a></div>');
-                        autoRedirectTimer = setTimeout(function(){
-                            window.location.href = "system/auth/oauth/login?redirect=" + redirectionAddress;
-                        }, 3000);
-                    }
-                }
-            });
-
-            //LDAP related code, check if system is open for ext login
-            $.getJSON("system/auth/ldap/checkldap",function(data){
-                if (data.enabled == true && window.location.pathname.toLowerCase() != "/ldaplogin.system"){
-                    $(".ldaponly").show();
-                }else{
-                    $(".ldaponly").hide();
-                }
-            });
-
-            //Switchable accounts related code, check if the user has a session to continue
-            $.getJSON("system/auth/u/p/list",function(data){
-                if (data.Username != ""){
-                    //There is a session to resume
-                    let resumeableAccountUsername = data.Username;
-                    $(".resumableOnly").show();
-                    $(".resumableOnly").find(".username").text(data.Username);
-                    console.log(data.ProfileImage, $('.resumableOnly').find(".usericon"));
-                    if (data.ProfileImage != ""){
-                        $('.resumableOnly').find(".usericon").attr("src", data.ProfileImage);
-                    }
-
-                    $(".alternativeAccount").on("click", function(event){
-                        $("#username").val(resumeableAccountUsername);
-                        $("#username").parent().addClass("disabled");
-                        $(".alternativeAccount").addClass("disabled");
-                        $(".newResumableSession").show();
-                    });
-
-                    $(".newResumableSession").on("click", function(event){
-                        $("#username").val("");
-                        $("#username").parent().removeClass("disabled");
-                        $(".alternativeAccount").removeClass("disabled");
-                        $(".newResumableSession").hide();
-                    })
-                }
-            });
-
-            if(get('redirect') != undefined){
-                $(".section.signin").attr("href","system/auth/oauth/login?redirect=" + redirectionAddress);
-            }
-
-            //Get the system hostname and replace the hostname fields
-            $.get("system/info/getArOZInfo", function(data){
-                document.title = data.HostName + " - Sign In"
-                $(".hostname").text(data.HostName);
-            });
-        });
-
-        //Event handlers for buttons
-        $("#loginbtn").on("click",function(){
-            login();
-        });
-
-        $("input").on("keydown",function(event){
-            if (event.keyCode === 13) {
-                event.preventDefault();
-                if ($(this).attr("id") == "magic"){
-                    login();
-                }else{
-                    //Fuocus to password field
-                    $("#magic").focus();
-                }
-                
-            }
-        });
-
-        //Login system with the given username and password
-        function login(){
-            var username = $("#username").val();
-            var magic = $("#magic").val();
-            var rmbme = document.getElementById("rmbme").checked;
-            $("input").addClass('disabled');
-            $.post(loginAddress, {"username": username, "password": magic, "rmbme": rmbme}).done(function(data){
-                if (data.error !== undefined){
-                    //Something went wrong during the login
-                    $("#errmsg").text(data.error);
-                    $("#errmsg").parent().stop().finish().slideDown('fast').delay(5000).slideUp('fast');
-                }else if(data.redirect !== undefined){
-                    //LDAP Related Code
-                    window.location.href = data.redirect;
-                }else{
-                    //Login succeed
-                    if (redirectionAddress == "" || redirectionAddress == "/"){
-                        //Redirect back to index
-                        window.location.href = "./";
-                    }else{
-                        if (window.location.hash.length > 0){
-                            redirectionAddress += window.location.hash
-                        }
-                        window.location.href = redirectionAddress;
-                    }
-                }
-                $("input").removeClass('disabled');
-            });
-
-        }
-
-        function get(name){
-            if(name=(new RegExp('[?&]'+encodeURIComponent(name)+'=([^&]*)')).exec(location.search))
-                return decodeURIComponent(name[1]);
-        }
-
-        function stopAutoRedirect(){
-            clearTimeout(autoRedirectTimer);
-            $("#aoLogin").removeAttr("style");
-            $("#autoRedirectSegment").attr("style", "display:none");
-        }
-
-        $(".thisyear").text(new Date().getFullYear());
-
-        function updateRenderElements(){
-            if (window.innerHeight < 520){
-                $(".bottombar").hide();
-            }else{
-                $(".bottombar").show();
-            }
-        }
-        updateRenderElements();
-        $(window).on("resize", function(){
-            updateRenderElements();
-        });
-    </script>
-    </body>
-</html>
+<!DOCTYPE HTML>
+<html>
+    <head>
+    <meta charset="UTF-8">
+    <meta name="robots" content="noindex" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <link rel="author" href="humans.txt"/>
+    <title locale="page/title">ArozOS - Login</title>
+    <link rel="stylesheet" href="script/semantic/semantic.min.css">
+    <link rel="stylesheet" href="script/ao.css">
+    <script type="application/javascript" src="script/jquery.min.js"></script>
+    <script type="application/javascript" src="script/semantic/semantic.min.js"></script>
+    <script type="text/javascript" src="script/locale/login.js"></script>
+    
+    <style>
+    @media only screen and (max-height: 1000px) {
+        .leftPictureFrame {
+        height:auto !important;
+        }
+    }
+
+    :root{
+        --main_theme_color: #5baf6d;
+        --secondary_theme_color: #4b7952;
+        --dimmer_theme_color: #125e1b;
+        --inverse_theme_color: #e0f1b3;
+    }
+
+    .leftPictureFrame{
+        position:fixed;
+        top:0px;
+        left:0px;
+        min-width:calc(100% - 500px);
+        min-height:100%;
+        background-color:#faf7eb;
+        background-image:url("./system/info/wallpaper.jpg");
+        -webkit-background-size: cover;
+        -moz-background-size: cover;
+        -o-background-size: cover;
+        background-size: cover;
+        background-repeat: no-repeat, no-repeat;
+        background-position:bottom left;
+    }
+
+    .rightLoginFrame{
+        position:fixed;
+        top:0;
+        right:0;
+        height:100%;
+        width:500px;
+        background:white;
+        z-index:100%;
+        padding-left: 30px;
+        padding-right: 20px;
+    }
+
+    @media (max-width: 600px) {
+        .rightLoginFrame{
+            width: 100%;
+        }
+    }
+    
+    @media (min-width: 600px) {
+        @supports (backdrop-filter: blur(10px)) {
+        /* This browser support backdrop filter */
+            .rightLoginFrame {
+                margin-top: 5em;
+                margin-right: 5em;
+                margin-left: 5em;
+                height:calc(100% - 10em);
+                border-radius: 1em;
+            }
+
+            .leftPictureFrame{
+                min-width:calc(100%);
+            }
+        }
+    }
+
+    .fullHeightImage{
+        height:100% !important;
+        position:relative;
+        left:-20px;
+        
+    }
+
+    .bottombar{
+        position:absolute;
+        bottom:1em;
+        left:0;
+        padding-left: 20px;
+        width:100%;
+    }
+
+    #animationFrame{
+        position:absolute;
+        bottom:0px;
+        width:100%;
+    }
+
+    .textbox{
+        margin-bottom:15px;
+    }
+
+    .themecolor{
+        background-color: var(--main_theme_color) !important;
+        transition: background-color 0.1s;
+    }
+
+    .themecolor:hover{
+        background-color: var(--secondary_theme_color) !important;
+    }
+
+    .subthemecolor{
+        background-color: var(--secondary_theme_color) !important;
+        transition: background-color 0.1s;
+    }
+
+    .subthemecolor:hover{
+        background-color: var(--dimmer_theme_color) !important;
+    }
+
+    .inversehighlight{
+        padding: 0.4em;
+        padding-top: 0.2em;
+        padding-bottom: 0.3em;
+        background-color: var(--dimmer_theme_color);
+        color: white;
+        border-radius: 0.4em;
+        margin-top: 0.4em;
+    }
+
+    .loginbtn{
+        color:white !important;
+        margin-top:4em;
+    }
+
+    .oauthbtn{
+        color:white !important;
+        margin-top:1em;
+    }
+
+    .alternativeAccount:not(.disabled){
+        cursor: pointer;
+    }
+
+    .alternativeAccount:not(.disabled):hover{
+        background-color: rgb(245, 245, 245);
+    }
+
+    .alternativeAccount:disabled{
+        opacity: 0.6;
+        pointer-events: none !important;
+        user-select: none;
+        cursor: not-allowed;
+    }
+
+    .loginLogo{
+        margin-top: 4em;
+    }
+
+    .languageSelector{
+        position: absolute;
+        top: 1em;
+        right: 1em;
+        z-index: 1000;
+    }
+
+    .languageSelector .ui.dropdown{
+        min-width: 120px;
+        background: rgba(255, 255, 255, 0.9);
+        border: 1px solid rgba(0, 0, 0, 0.1);
+        border-radius: 0.3em;
+    }
+
+    .languageSelector .ui.dropdown .menu{
+        min-width: 140px;
+    }
+
+    @media (max-width: 600px) {
+        .languageSelector {
+            position: fixed;
+            top: 0.5em;
+            right: 0.5em;
+        }
+    }
+
+    @media (orientation: landscape) and (max-height: 765px) {
+        .rightLoginFrame{
+            width: 500px !important;
+            margin-left: auto;
+            margin-right: auto;
+            margin-top: 0;
+            height: 100%;
+            overflow-y: auto;
+            border-radius: 0;
+        }
+        
+        .loginLogo{
+            margin-top: 1.2em;
+        }
+    }
+
+    </style>
+    </head>
+    <body>
+        <div class="leftPictureFrame">
+            
+        </div>
+        
+        <!-- Language Selector -->
+        <div class="languageSelector">
+            <div class="ui selection dropdown">
+                <input id="language" type="hidden" name="language">
+                <i class="dropdown icon"></i>
+                <div class="default text">Language</div>
+                <div class="menu" id="langlist">
+                    <div class="item" data-value="default" locale="locale/browser-default">Browser Default</div>
+                </div>
+            </div>
+        </div>
+        <div id="loginInterface" class="rightLoginFrame">
+            <img class="ui medium image loginLogo" src="data:image/png;base64, {{service_logo}}">
+
+            <div class="ui borderless basic segment">
+                <p><span locale="login/introPrefix">Sign in to </span><span class="hostname">ArozOS</span><span locale="login/introSuffix"> </span><span locale="login/description">with your username and password</span></p>
+                
+               
+                
+                <div class="ui fluid input textbox">
+                    <input id="username" type="text" placeholder="Username" locale="username">
+                </div>
+                <div class="ui fluid input textbox">
+                    <input id="magic" type="password" placeholder="Password" locale="password">
+                </div>
+
+                <div class="ui checkbox">
+                    <input id="rmbme" type="checkbox">
+                    <label for="rmbme" locale="login/rememberMe">Remember Me</label>
+                </div>
+                
+                <br><br>
+                <button id="loginbtn" class="ui button loginbtn themecolor" style="display:inline-block;" locale="login/signinButton">Sign In</button>
+                <div class="oauthonly" style="display:inline-block;">
+                    <a class="ui button oauthbtn subthemecolor" href="system/auth/oauth/login" locale="login/oauthButton">Sign In via OAuth 2.0</a><br>
+                </div>
+                <div class="ldaponly" style="display:inline-block;">
+                    <a class="ui button oauthbtn subthemecolor" href="ldapLogin.system" locale="login/ldapButton">Sign In via LDAP</a><br>
+                </div>
+                <div class="resumableOnly" style="display:none;">
+                    <br>
+                    <div class="ui clickable segment alternativeAccount" style="margin-bottom: 0px; padding-bottom: 8px; width: 100%; padding-top: 0px;">
+                        <div style="margin-top: 0.6em;">
+                            <div class="ui header">
+                                <img class="usericon ui circular image" src="img/public/user.svg">
+                                <div class="content" style="font-size: 95% !important;">
+                                    <span class="username"><i class="ui loading spinner icon"></i></span> 
+                                    <div class="sub header usergroup"><i class="ui green check circle icon" style="margin-right: 0px;"></i> <span locale="login/resumableSession">Resumable Session</span></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <br>
+                    <button class="ui subthemecolor newResumableSession button" style="color: white; display:none;"><i class="ui add icon"></i> <span locale="login/createNewSession">Create New Session</span></button>
+                </div>
+
+                <br>
+                <div class="ui breadcrumb" style="margin-top:12px;">
+                    <a class="section signup" style="cursor:pointer; display:none;" href="public/register/register.system" locale="login/signUp">Sign Up</a>
+                    <div class="divider signup"> / </div>
+                    <a  id="forgetpw" class="section" style="cursor:pointer" href="reset.system" locale="login/forgotPassword">Forgot Password</a>
+                </div>
+                <p style="margin-top:18px;color:#ff7a70; display:none;font-size:1.2em;"><i class="remove icon"></i><span id="errmsg" locale="login/errorIncorrect">Error. Incorrect username or password.</span></p>
+               
+            </div>
+           
+            <div class="bottombar">
+                © <a href="https://arozos.com">ArozOS</a> 2017 - <span class="thisyear"></span><br>
+                <small class="inversehighlight" style="font-size: 80%"><span locale="login/requestTimestamp">Request Timestamp</span>: <span id="requestTime"></span> | <span id="requestHostCommonName"></span></small>
+            </div>
+        </div>
+        
+    <script>
+        var localeManager = window.LoginLocale || null;
+        if (localeManager && typeof localeManager.init === "function") {
+            localeManager.init();
+        }
+
+        function localeGetString(key, fallback) {
+            if (localeManager && typeof localeManager.getString === "function") {
+                return localeManager.getString(key, fallback);
+            }
+            return fallback;
+        }
+
+        var redirectionAddress = "{{redirection_addr}}";
+        var loginAddress = "{{login_addr}}";
+        var systemUserCount = "{{usercount}}" - 0; //Magic way to convert string to int :)
+        var autoRedirectTimer;
+        var isMobile = false; //initiate as false
+        // device detection
+        if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 
+            || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 
+            isMobile = true;
+        }  
+
+
+
+        if (isMobile){
+            //Full screen the login panel
+            $("#loginInterface").css("width","100%");
+        }
+
+        if (systemUserCount == 0){
+            //There are no user in this system yet. Rediect to user setup
+            window.location.href = "/user.system";
+        }
+
+        //Hide elements by default
+        $(".ldaponly").hide();
+        $(".oauthonly").hide();
+        
+        $(document).ready(function(){
+            function zeroPad(num) {
+                return num < 10 ? '0' + num : num;
+            }
+
+            var currentdate = new Date(); 
+            var datetime = zeroPad(currentdate.getDate()) + "/"
+                + zeroPad(currentdate.getMonth() + 1) + "/" 
+                + currentdate.getFullYear() + " "  
+                + zeroPad(currentdate.getHours()) + ":"  
+                + zeroPad(currentdate.getMinutes()) + ":" 
+                + zeroPad(currentdate.getSeconds());
+            $("#requestTime").text(datetime);
+            $("#requestHostCommonName").text(location.hostname);
+            $(".ui.checkbox").checkbox();
+            
+            // Initialize language dropdown using settings method
+            var storedLanguage = localStorage.getItem('global_language') || 'default';
+            $("#language").val(storedLanguage);
+            $('.selection.dropdown').dropdown({
+                onChange: function(value) {
+                    localStorage.setItem('global_language', value);
+                    if (localeManager && typeof localeManager.setLanguage === 'function') {
+                        localeManager.setLanguage(value);
+                    }
+                }
+            });
+
+            function populateLanguageOptions() {
+                if (!localeManager || typeof localeManager.getAvailableLocales !== 'function') {
+                    return;
+                }
+                var locales = localeManager.getAvailableLocales();
+                for (var key in locales) {
+                    if (!Object.prototype.hasOwnProperty.call(locales, key)) {
+                        continue;
+                    }
+                    if (!$("#langlist .item[data-value='" + key + "']").length) {
+                        var value = locales[key];
+                        var langName = (value && value.name) ? value.name : key;
+                        $("#langlist").append('<div class="item" data-value="' + key + '">' + langName + '</div>');
+                    }
+                }
+                var currentLang = localStorage.getItem('global_language') || 'default';
+                $("#language").val(currentLang);
+                $('.selection.dropdown').dropdown('refresh');
+                $('.selection.dropdown').dropdown('set selected', currentLang);
+            }
+
+            function updateHostnameAndTitle() {
+                $.get("system/info/getArOZInfo", function(data){
+                    if (data && data.HostName){
+                        $(".hostname").text(data.HostName);
+                        var titleSuffix = localeGetString('page/titleSuffix', 'Sign In');
+                        document.title = data.HostName + " - " + titleSuffix;
+                    }
+                });
+            }
+
+            window.updateHostnameAndTitle = updateHostnameAndTitle;
+
+            window.updateDropdownText = function() {
+                var localeManager = window.LoginLocale;
+                if (!localeManager) return;
+                var currentLang = localeManager.getCurrentLanguage();
+                var text = 'Language';
+                if (currentLang === 'default') {
+                    text = localeGetString('locale/browser-default', 'Browser Default');
+                } else {
+                    var locales = localeManager.getAvailableLocales();
+                    var langData = locales[currentLang];
+                    if (langData && langData.name) {
+                        text = langData.name;
+                    }
+                }
+                $('.selection.dropdown .text').text(text);
+            };
+
+            var runLocaleEnhancements = function () {
+                populateLanguageOptions();
+                updateHostnameAndTitle();
+                updateDropdownText();
+            };
+
+            if (localeManager && typeof localeManager.onReady === 'function') {
+                localeManager.onReady(runLocaleEnhancements);
+            } else {
+                runLocaleEnhancements();
+            }
+            
+            //Check if the user already logged in
+            $.get("system/auth/checkLogin",function(data){
+                try{
+                    if (data === true || data.trim() == "true"){
+                    //User already logged in. Redirect to target page.
+                    if (redirectionAddress == ""){
+                        //Redirect back to index
+                        window.location.href = "/";
+                    }else{
+                        console.log(data);
+                        //window.location.href = redirectionAddress;
+                    }
+                }
+                }catch(ex){
+                    //Assume not logged in
+                    console.log(data);
+                }
+                
+            });
+
+            //Check if the system is open for registry
+            $.get("public/register/checkPublicRegister",function(data){
+                if (data == true){
+                    $(".signup").show();
+                }else{
+                    $(".signup").remove();
+                }
+            });
+
+            //OAuth related code, check if system is open for ext login
+            $.getJSON("system/auth/oauth/checkoauth",function(data){
+                if (data.enabled == true){
+                    $(".oauthonly").show();
+                }else{
+                    $(".oauthonly").hide();
+                }
+                //if auto redirect is on
+                if(data.auto_redirect == true) {
+                    //checking if they come from desktop.system or mobile.system
+                    //if they come from that two pages, usually mean they are just logged out.
+                    if(document.referrer != ''){
+                        var path = new URL(document.referrer);
+                    } else {
+                        var path = new URL('http://0.0.0.0');
+
+                    }
+                    if(document.referrer != window.location.origin + "/desktop.system" && document.referrer != window.location.origin + "/mobile.system" && path.origin + path.pathname !=  window.location.origin + "/system/auth/oauth/authorize"){
+                        $(".ts.borderless.basic.segment").attr("style","display: none;");
+                        $(".ts.borderless.basic.segment").attr("id","aoLogin");
+                        var redirectMessage = localeGetString('login/redirectMessage', 'Redirecting to organization sign-in page in 5 seconds...');
+                        var cancelText = localeGetString('login/cancel', 'Cancel');
+                        $(".ts.borderless.basic.segment").after('<div id="autoRedirectSegment" class="ui borderless basic segment"><p><i class="key icon"></i>' + redirectMessage + '</p><br><a style="cursor: pointer;" onclick="stopAutoRedirect()">' + cancelText + '</a></div>');
+                        autoRedirectTimer = setTimeout(function(){
+                            window.location.href = "system/auth/oauth/login?redirect=" + redirectionAddress;
+                        }, 3000);
+                    }
+                }
+            });
+
+            //LDAP related code, check if system is open for ext login
+            $.getJSON("system/auth/ldap/checkldap",function(data){
+                if (data.enabled == true && window.location.pathname.toLowerCase() != "/ldaplogin.system"){
+                    $(".ldaponly").show();
+                }else{
+                    $(".ldaponly").hide();
+                }
+            });
+
+            //Switchable accounts related code, check if the user has a session to continue
+            $.getJSON("system/auth/u/p/list",function(data){
+                if (data.Username != ""){
+                    //There is a session to resume
+                    let resumeableAccountUsername = data.Username;
+                    $(".resumableOnly").show();
+                    $(".resumableOnly").find(".username").text(data.Username);
+                    console.log(data.ProfileImage, $('.resumableOnly').find(".usericon"));
+                    if (data.ProfileImage != ""){
+                        $('.resumableOnly').find(".usericon").attr("src", data.ProfileImage);
+                    }
+
+                    $(".alternativeAccount").on("click", function(event){
+                        $("#username").val(resumeableAccountUsername);
+                        $("#username").parent().addClass("disabled");
+                        $(".alternativeAccount").addClass("disabled");
+                        $(".newResumableSession").show();
+                    });
+
+                    $(".newResumableSession").on("click", function(event){
+                        $("#username").val("");
+                        $("#username").parent().removeClass("disabled");
+                        $(".alternativeAccount").removeClass("disabled");
+                        $(".newResumableSession").hide();
+                    })
+                }
+            });
+
+            if(get('redirect') != undefined){
+                $(".section.signin").attr("href","system/auth/oauth/login?redirect=" + redirectionAddress);
+            }
+
+        });
+
+        //Event handlers for buttons
+        $("#loginbtn").on("click",function(){
+            login();
+        });
+
+        $("input").on("keydown",function(event){
+            if (event.keyCode === 13) {
+                event.preventDefault();
+                if ($(this).attr("id") == "magic"){
+                    login();
+                }else{
+                    //Fuocus to password field
+                    $("#magic").focus();
+                }
+                
+            }
+        });
+
+        //Login system with the given username and password
+        function login(){
+            var username = $("#username").val();
+            var magic = $("#magic").val();
+            var rmbme = document.getElementById("rmbme").checked;
+            $("input").addClass('disabled');
+            $.post(loginAddress, {"username": username, "password": magic, "rmbme": rmbme}).done(function(data){
+                if (data.error !== undefined){
+                    //Something went wrong during the login  
+                    var errorMsg = data.error;
+                    if (data.error === "Error. Incorrect username or password.") {
+                        errorMsg = localeGetString('login/errorIncorrect', data.error);
+                    }
+                    $("#errmsg").text(errorMsg);
+                    $("#errmsg").parent().stop().finish().slideDown('fast').delay(5000).slideUp('fast');
+                }else if(data.redirect !== undefined){
+                    //LDAP Related Code
+                    window.location.href = data.redirect;
+                }else{
+                    //Login succeed
+                    if (redirectionAddress == "" || redirectionAddress == "/"){
+                        //Redirect back to index
+                        window.location.href = "./";
+                    }else{
+                        if (window.location.hash.length > 0){
+                            redirectionAddress += window.location.hash
+                        }
+                        window.location.href = redirectionAddress;
+                    }
+                }
+                $("input").removeClass('disabled');
+            });
+
+        }
+
+        function get(name){
+            if(name=(new RegExp('[?&]'+encodeURIComponent(name)+'=([^&]*)')).exec(location.search))
+                return decodeURIComponent(name[1]);
+        }
+
+        function stopAutoRedirect(){
+            clearTimeout(autoRedirectTimer);
+            $("#aoLogin").removeAttr("style");
+            $("#autoRedirectSegment").attr("style", "display:none");
+        }
+
+        $(".thisyear").text(new Date().getFullYear());
+
+        function updateRenderElements(){
+            if (window.innerHeight < 520){
+                $(".bottombar").hide();
+            }else{
+                $(".bottombar").show();
+            }
+        }
+        updateRenderElements();
+        $(window).on("resize", function(){
+            updateRenderElements();
+        });
+    </script>
+    </body>
+</html>

+ 461 - 0
web/script/locale/login.js

@@ -0,0 +1,461 @@
+
+window.LOGIN_LOCALE_DATA = {
+    "zh-tw": {
+        "name": "繁體中文(台灣)",
+        "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+        "strings": {
+            "page/title": "ArozOS - 登入",
+            "login/description": "",
+            "login/signinButton": "登入",
+            "login/oauthButton": "透過 OAuth 2.0 登入",
+            "login/ldapButton": "透過 LDAP 登入",
+            "login/rememberMe": "記住我",
+            "login/signUp": "註冊",
+            "login/forgotPassword": "忘記密碼",
+            "login/createNewSession": "建立新工作階段",
+            "login/resumableSession": "可恢復的工作階段",
+            "login/errorIncorrect": "錯誤。使用者名稱或密碼不正確。",
+            "login/requestTimestamp": "請求時間戳記",
+            "locale/language-default-text": "語言",
+            "locale/browser-default": "Default",
+            "login/introPrefix": "使用您的使用者名稱及密碼登入 ",
+            "login/introSuffix": "",
+            "login/redirectMessage": "5 秒後導向組織登入頁面...",
+            "login/cancel": "取消",
+            "page/titleSuffix": "登入"
+        },
+        "placeholder": {
+            "Username": "使用者名稱",
+            "Password": "密碼"
+        }
+    },
+    "zh-hk": {
+        "name": "繁體中文(香港)",
+        "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+        "strings": {
+            "page/title": "ArozOS - 登入",
+            "login/description": "",
+            "login/signinButton": "登入",
+            "login/oauthButton": "透過 OAuth 2.0 登入",
+            "login/ldapButton": "透過 LDAP 登入",
+            "login/rememberMe": "記住我",
+            "login/signUp": "註冊",
+            "login/forgotPassword": "忘記密碼",
+            "login/createNewSession": "建立新工作階段",
+            "login/resumableSession": "可恢復的工作階段",
+            "login/errorIncorrect": "錯誤。使用者名稱或密碼不正確。",
+            "login/requestTimestamp": "請求時間戳記",
+            "locale/language-default-text": "語言",
+            "locale/browser-default": "Default",
+            "login/introPrefix": "使用您的使用者名稱及密碼登入 ",
+            "login/introSuffix": "",
+            "login/redirectMessage": "5 秒後重新導向至組織登入頁面...",
+            "login/cancel": "取消",
+            "page/titleSuffix": "登入"
+        },
+        "placeholder": {
+            "Username": "使用者名稱",
+            "Password": "密碼"
+        }
+    },
+    "zh-cn": {
+        "name": "简体中文",
+        "fontFamily": "\"Microsoft YaHei\", \"SimHei\", \"STHeiti\"",
+        "strings": {
+            "page/title": "ArozOS - 登录",
+            "login/description": "",
+            "login/signinButton": "登录",
+            "login/oauthButton": "通过 OAuth 2.0 登录",
+            "login/ldapButton": "通过 LDAP 登录",
+            "login/rememberMe": "记住我",
+            "login/signUp": "注册",
+            "login/forgotPassword": "忘记密码",
+            "login/createNewSession": "创建新会话",
+            "login/resumableSession": "可恢复的会话",
+            "login/errorIncorrect": "错误。用户名或密码不正确。",
+            "login/requestTimestamp": "请求时间戳",
+            "locale/language-default-text": "语言",
+            "locale/browser-default": "Default",
+            "login/introPrefix": "使用您的用户名和密码登录 ",
+            "login/introSuffix": "",
+            "login/redirectMessage": "5 秒后重定向到组织登录页面...",
+            "login/cancel": "取消",
+            "page/titleSuffix": "登录"
+        },
+        "placeholder": {
+            "Username": "用户名",
+            "Password": "密码"
+        }
+    },
+    "en-us": {
+        "name": "English (US)",
+        "fontFamily": "\"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif",
+        "strings": {
+            "page/title": "ArozOS - Login",
+            "login/description": "with your username and password",
+            "login/signinButton": "Sign In",
+            "login/oauthButton": "Sign In via OAuth 2.0",
+            "login/ldapButton": "Sign In via LDAP",
+            "login/rememberMe": "Remember Me",
+            "login/signUp": "Sign Up",
+            "login/forgotPassword": "Forgot Password",
+            "login/createNewSession": "Create New Session",
+            "login/resumableSession": "Resumable Session",
+            "login/errorIncorrect": "Error. Incorrect username or password.",
+            "login/requestTimestamp": "Request Timestamp",
+            "locale/language-default-text": "Language",
+            "locale/browser-default": "Default",
+            "login/introPrefix": "Sign in to ",
+            "login/introSuffix": " ",
+            "login/redirectMessage": "Redirecting to organization sign-in page in 5 seconds...",
+            "login/cancel": "Cancel",
+            "page/titleSuffix": "Sign In"
+        },
+        "placeholder": {
+            "Username": "Username",
+            "Password": "Password"
+        }
+    },
+    "ja-jp": {
+        "name": "日本語",
+        "fontFamily": "\"Hiragino Sans\", \"Hiragino Kaku Gothic ProN\", \"Noto Sans JP\", sans-serif",
+        "strings": {
+            "page/title": "ArozOS - ログイン",
+            "login/description": "",
+            "login/signinButton": "ログイン",
+            "login/oauthButton": "OAuth 2.0でログイン",
+            "login/ldapButton": "LDAPでログイン",
+            "login/rememberMe": "ログイン状態を保持する",
+            "login/signUp": "新規登録",
+            "login/forgotPassword": "パスワードをお忘れですか?",
+            "login/createNewSession": "新しいセッションを作成",
+            "login/resumableSession": "復元可能なセッション",
+            "login/errorIncorrect": "エラー。ユーザー名またはパスワードが正しくありません。",
+            "login/requestTimestamp": "リクエストタイムスタンプ",
+            "locale/language-default-text": "言語",
+            "locale/browser-default": "Default",
+            "login/introPrefix": "ユーザー名とパスワードでサインイン ",
+            "login/introSuffix": "",
+            "login/redirectMessage": "5 秒後に組織のサインインページへリダイレクトします...",
+            "login/cancel": "キャンセル",
+            "page/titleSuffix": "ログイン"
+        },
+        "placeholder": {
+            "Username": "ユーザー名",
+            "Password": "パスワード"
+        }
+    },
+    "ko-kr": {
+        "name": "한국어",
+        "fontFamily": "\"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif",
+        "strings": {
+            "page/title": "ArozOS - 로그인",
+            "login/description": "",
+            "login/signinButton": "로그인",
+            "login/oauthButton": "OAuth 2.0으로 로그인",
+            "login/ldapButton": "LDAP으로 로그인",
+            "login/rememberMe": "로그인 상태 유지",
+            "login/signUp": "회원가입",
+            "login/forgotPassword": "비밀번호 찾기",
+            "login/createNewSession": "새 세션 만들기",
+            "login/resumableSession": "복원 가능한 세션",
+            "login/errorIncorrect": "오류. 사용자 이름 또는 비밀번호가 올바르지 않습니다.",
+            "login/requestTimestamp": "요청 타임스탬프",
+            "locale/language-default-text": "언어",
+            "locale/browser-default": "Default",
+            "login/introPrefix": "사용자 이름과 비밀번호로 로그인 ",
+            "login/introSuffix": "",
+            "login/redirectMessage": "5초 후 조직 로그인 페이지로 이동합니다...",
+            "login/cancel": "취소",
+            "page/titleSuffix": "로그인"
+        },
+        "placeholder": {
+            "Username": "사용자 이름",
+            "Password": "비밀번호"
+        }
+    }
+};
+
+(function () {
+    var rawData = window.LOGIN_LOCALE_DATA || null;
+    var FALLBACK_LANG = 'en-us';
+    var STORAGE_KEY = 'global_language';
+    var readyQueue = [];
+    var currentLanguage = null;
+    var currentDataset = null;
+
+    function getAvailableLocales() {
+        return rawData || {};
+    }
+
+    function normalizeLanguageCandidate(candidate) {
+        if (!candidate) {
+            return null;
+        }
+        return candidate.replace(/_/g, '-').toLowerCase();
+    }
+
+    function matchLanguageCandidate(candidate) {
+        if (!candidate || candidate === 'default') {
+            return null;
+        }
+        var normalized = normalizeLanguageCandidate(candidate);
+        if (!normalized) {
+            return null;
+        }
+        var availableKeys = Object.keys(getAvailableLocales());
+        for (var i = 0; i < availableKeys.length; i++) {
+            if (availableKeys[i] === normalized) {
+                return availableKeys[i];
+            }
+        }
+        var primary = normalized.split('-')[0];
+        for (var j = 0; j < availableKeys.length; j++) {
+            if (availableKeys[j].split('-')[0] === primary) {
+                return availableKeys[j];
+            }
+        }
+        return null;
+    }
+
+    function getBrowserLanguage() {
+        var browserLangs = [];
+        if (navigator.languages && navigator.languages.length) {
+            browserLangs = navigator.languages;
+        } else if (navigator.language) {
+            browserLangs = [navigator.language];
+        }
+        for (var i = 0; i < browserLangs.length; i++) {
+            var match = matchLanguageCandidate(browserLangs[i]);
+            if (match) {
+                return match;
+            }
+        }
+        return null;
+    }
+
+    function getQueryParam(name) {
+        var regex = new RegExp('[?&]' + name + '=([^&]*)');
+        var results = regex.exec(window.location.search);
+        if (!results || results.length < 2) {
+            return null;
+        }
+        try {
+            return decodeURIComponent(results[1].replace(/\+/g, ' '));
+        } catch (err) {
+            return null;
+        }
+    }
+
+    function getUrlLanguage() {
+        var value = getQueryParam('lang');
+        return value ? value.toLowerCase() : null;
+    }
+
+    function getStoredLanguage() {
+        try {
+            if (window.localStorage) {
+                var stored = localStorage.getItem(STORAGE_KEY);
+                return stored ? stored.toLowerCase() : null;
+            }
+        } catch (err) {
+            // Ignore storage errors
+        }
+        return null;
+    }
+
+    function resolveLanguage() {
+        var availableKeys = Object.keys(getAvailableLocales());
+        if (!availableKeys.length) {
+            return FALLBACK_LANG;
+        }
+
+        var overrideLang = getUrlLanguage();
+        var overrideMatch = matchLanguageCandidate(overrideLang);
+        if (overrideMatch) {
+            return overrideMatch;
+        }
+
+        var storedLang = getStoredLanguage();
+        if (storedLang && storedLang !== 'default') {
+            var storedMatch = matchLanguageCandidate(storedLang);
+            if (storedMatch) {
+                return storedMatch;
+            }
+        }
+
+        var browserLang = getBrowserLanguage();
+        if (browserLang) {
+            return browserLang;
+        }
+
+        for (var i = 0; i < availableKeys.length; i++) {
+            if (availableKeys[i] === FALLBACK_LANG) {
+                return FALLBACK_LANG;
+            }
+        }
+        return availableKeys[0];
+    }
+
+    function ensureDataset() {
+        var locales = getAvailableLocales();
+        currentDataset = locales[currentLanguage] || null;
+        if (!currentDataset) {
+            var keys = Object.keys(locales);
+            if (keys.length) {
+                currentLanguage = keys[0];
+                currentDataset = locales[currentLanguage];
+            }
+        }
+    }
+
+    function forEachNode(list, handler) {
+        if (!list || !handler) {
+            return;
+        }
+        for (var i = 0; i < list.length; i++) {
+            handler(list[i]);
+        }
+    }
+
+    function translateDocument() {
+        if (!currentDataset) {
+            return;
+        }
+        var strings = currentDataset.strings || {};
+        var placeholders = currentDataset.placeholder || {};
+        var titles = currentDataset.titles || {};
+
+        forEachNode(document.querySelectorAll('[locale]'), function (el) {
+            var key = el.getAttribute('locale');
+            if (!key || strings[key] === undefined) {
+                return;
+            }
+            if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
+                el.value = strings[key];
+            } else {
+                el.innerHTML = strings[key];
+            }
+        });
+
+        forEachNode(document.querySelectorAll('[placeholder]'), function (el) {
+            var placeholderKey = el.getAttribute('placeholder');
+            if (placeholderKey && placeholders[placeholderKey] !== undefined) {
+                el.setAttribute('placeholder', placeholders[placeholderKey]);
+            }
+        });
+
+        forEachNode(document.querySelectorAll('[title]'), function (el) {
+            var titleKey = el.getAttribute('title');
+            if (titleKey && titles[titleKey] !== undefined) {
+                el.setAttribute('title', titles[titleKey]);
+            }
+        });
+
+        if (currentDataset.fontFamily && document.body) {
+            document.body.style.fontFamily = currentDataset.fontFamily;
+        }
+        if (document.documentElement) {
+            document.documentElement.setAttribute('lang', currentLanguage || 'en');
+        }
+        if (strings['page/title']) {
+            document.title = strings['page/title'];
+        }
+    }
+
+    function flushReadyQueue() {
+        while (readyQueue.length) {
+            var callback = readyQueue.shift();
+            try {
+                callback(currentDataset || {});
+            } catch (err) {
+                console.error('[LoginLocale] callback error', err);
+            }
+        }
+    }
+
+    function init() {
+        if (currentDataset || !rawData) {
+            if (!currentDataset) {
+                flushReadyQueue();
+            }
+            return currentDataset;
+        }
+        currentLanguage = resolveLanguage();
+        ensureDataset();
+
+        var run = function () {
+            translateDocument();
+            flushReadyQueue();
+            updateDropdownText();
+        };
+
+        if (document.readyState === 'loading') {
+            var onDomReady = function () {
+                document.removeEventListener('DOMContentLoaded', onDomReady);
+                run();
+            };
+            document.addEventListener('DOMContentLoaded', onDomReady);
+        } else {
+            run();
+        }
+        return currentDataset;
+    }
+
+    function onReady(callback) {
+        if (typeof callback !== 'function') {
+            return;
+        }
+        if (currentDataset) {
+            callback(currentDataset);
+        } else {
+            readyQueue.push(callback);
+        }
+    }
+
+    function getString(key, fallback) {
+        if (!currentDataset || !currentDataset.strings || currentDataset.strings[key] === undefined) {
+            return fallback;
+        }
+        return currentDataset.strings[key];
+    }
+
+    function setLanguage(lang) {
+        currentLanguage = lang;
+        localStorage.setItem(STORAGE_KEY, lang);
+        ensureDataset();
+        translateDocument();
+        updateDropdownText();
+        if (window.updateHostnameAndTitle) {
+            window.updateHostnameAndTitle();
+        }
+    }
+
+    function updateDropdownText() {
+        var text = 'Language';
+        if (currentLanguage === 'default') {
+            text = 'Browser Default';
+        } else if (currentDataset && currentDataset.name) {
+            text = currentDataset.name;
+        }
+        $('.selection.dropdown .text').text(text);
+    }
+
+    function getCurrentLanguage() {
+        return currentLanguage;
+    }
+
+    function getAvailable() {
+        return getAvailableLocales();
+    }
+
+    window.LoginLocale = {
+        init: init,
+        translateDocument: translateDocument,
+        getString: getString,
+        getCurrentLanguage: getCurrentLanguage,
+        getAvailableLocales: getAvailable,
+        onReady: onReady,
+        setLanguage: setLanguage
+    };
+})();