Przeglądaj źródła

Integrate versions into properties dialog

Moves file version history management into the File Properties window by replacing the old “Other” tab with a new “Versions” tab, including inline create/download/restore/delete actions and delete-all support. It also reorganizes Basic tab rows into grouped sections for readability, adds related styling and polling behavior, and removes the now-obsolete standalone file_versions page and locale file.
Toby Chui 1 miesiąc temu
rodzic
commit
ec25b2cd33

+ 6 - 0
src/web/SystemAO/file_system/dialog.css

@@ -271,6 +271,12 @@ select.dlgField{
     color: var(--dlg_text_dim);
 }
 
+/* Rows of one kind sit together; the gap alone separates the groups, so no
+   rule is drawn to pull the eye away from the values themselves */
+.dlgPropGroup + .dlgPropGroup{
+    margin-top: 14px;
+}
+
 /* Labelled rows used by the permission editor */
 .dlgFormRow{
     display: flex;

+ 376 - 58
src/web/SystemAO/file_system/file_properties.html

@@ -16,7 +16,7 @@
             <div class="dlgTabs">
                 <div class="dlgTab active" tab="basic" onclick="switchTab('basic');" locale="tab/basic">Basic</div>
                 <div class="dlgTab" tab="permission" onclick="switchTab('permission');" locale="tab/permission">Permissions</div>
-                <div class="dlgTab" tab="other" onclick="switchTab('other');" locale="tab/other">Other</div>
+                <div class="dlgTab" tab="versions" onclick="switchTab('versions');" locale="tab/versions">Versions</div>
             </div>
             <div class="dialogBody">
                 <!-- Basic -->
@@ -100,21 +100,40 @@
                     </div>
                 </div>
 
-                <!-- Other -->
-                <div class="dlgPage" id="page_other">
-                    <div id="otherProperties"></div>
+                <!-- Versions -->
+                <div class="dlgPage" id="page_versions">
+                    <div id="versionsUnavailable" class="dlgEmpty" style="display:none;">
+                        <i class="history icon"></i>
+                        <div locale="message/noVersions">Version history is only kept for files</div>
+                    </div>
+                    <div id="versionsManager" style="display:none;">
+                        <div class="dlgNotice">
+                            <i class="info circle icon"></i>
+                            <span locale="versions/desc">Any file version that is more than 30 days old is removed from the system automatically.</span>
+                        </div>
+                        <button class="dlgBtn" onclick="createSnapshot();">
+                            <i class="plus icon"></i> <span locale="versions/newver">Create New Version</span>
+                        </button>
+                        <div class="dlgSectionLabel" locale="versions/history">Version History</div>
+                        <div class="dlgList" id="versionList"></div>
+                    </div>
                 </div>
             </div>
 
             <div class="dialogFooter">
-                <button class="dlgBtn footerBasic footerOther" onclick="copyPath();"><i class="copy outline icon"></i> <span locale="button/copyPath">Copy Path</span></button>
+                <button class="dlgBtn footerBasic" onclick="copyPath();"><i class="copy outline icon"></i> <span locale="button/copyPath">Copy Path</span></button>
+                <button id="deleteAllVersionsBtn" class="dlgBtn danger footerVersions" style="display:none;" onclick="deleteAllVersions();">
+                    <i class="trash icon"></i> <span locale="versions/removeAll">Delete All Versions</span>
+                </button>
                 <div class="spacer"></div>
-                <button class="dlgBtn footerBasic footerOther" onclick="ao_module_close();" locale="button/close">Close</button>
+                <button class="dlgBtn footerBasic footerVersions" onclick="ao_module_close();" locale="button/close">Close</button>
                 <button class="dlgBtn footerPermission" style="display:none;" onclick="ao_module_close();" locale="button/cancel">Cancel</button>
                 <button id="permApplyBtn" class="dlgBtn primary footerPermission" style="display:none;" onclick="applyPermission();" locale="button/confirm">OK</button>
             </div>
         </div>
 
+        <div class="dlgMenu" id="rowMenu"></div>
+
         <div id="dlgLoader" class="dlgLoader">
             <div class="dlgSpinner"></div>
             <div locale="loader/loadingFileSize">Calculating File Size</div>
@@ -124,10 +143,20 @@
             /*
                 file_properties.html
 
-                Three tabs over one selection: what the object is (Basic), who
-                may touch it (Permissions) and where it lives / what opens it
-                (Other). The tab strip replaced a single scrolling table that
-                mixed all three, plus two buttons that opened further windows.
+                Three tabs over one selection: everything the object is -
+                type, size, opener, paths, dates and ownership (Basic), who may
+                touch it (Permissions) and its snapshot history (Versions). The
+                tab strip replaced a single scrolling table that mixed all
+                three, plus two buttons that opened further windows.
+
+                Basic groups its rows by kind - what / opens with / where /
+                when / who - because a flat list of ten rows reads as one
+                undifferentiated block however the rows are ordered.
+
+                Versions is the whole snapshot manager, which used to be a
+                separate window reached through a button in here. Nothing about
+                it wanted its own window: it acts on the one file this dialog
+                is already about.
 
                 Permissions are a POSIX concept: on a Windows host the tab is
                 still readable but visibly out of reach, because the server
@@ -141,6 +170,11 @@
             var permissionLoaded = false;
             var currentTab = "basic";
 
+            //Version manager state
+            var versionsLoaded = false;
+            var versionList = [];
+            var versionPollTimer = null;
+
             //Octal digit to a human readable access level. Index is the digit.
             var permissionLevels = [
                 {bits: "---", key: "perm/level/none", text: "None"},
@@ -153,6 +187,12 @@
                 {bits: "rwx", key: "perm/level/readWriteExecute", text: "Read, write and execute"}
             ];
 
+            $(document).on("click", function(event){
+                if ($(event.target).closest("#rowMenu, .dlgIconBtn").length == 0){
+                    $("#rowMenu").hide();
+                }
+            });
+
             applocale.init("../locale/file_properties.json", function(){
                 applocale.translate();
                 buildPermissionSelectors();
@@ -171,15 +211,35 @@
                 $(".dlgPage").removeClass("active");
                 $("#page_" + tabName).addClass("active");
 
-                $(".footerBasic, .footerOther, .footerPermission").hide();
+                $(".footerBasic, .footerVersions, .footerPermission").hide();
                 if (tabName == "permission"){
                     $(".footerPermission").show();
                     if (!permissionLoaded){
                         loadPermission();
                     }
+                }else if (tabName == "versions"){
+                    $(".footerVersions").show();
+                    if (!versionsSupported()){
+                        //Nothing to delete where no history is kept
+                        $("#deleteAllVersionsBtn").hide();
+                    }else if (!versionsLoaded){
+                        versionsLoaded = true;
+                        loadVersionHistory();
+                    }
                 }else{
                     $(".footerBasic").show();
                 }
+
+                //Another window may snapshot the same file, but only watch for
+                //that while the list is actually on screen
+                $("#rowMenu").hide();
+                if (versionPollTimer != null){
+                    clearInterval(versionPollTimer);
+                    versionPollTimer = null;
+                }
+                if (tabName == "versions" && versionsSupported()){
+                    versionPollTimer = setInterval(checkHistoryListUpdate, 3000);
+                }
             }
 
             /* ---------------------------------------------------------------
@@ -203,7 +263,7 @@
             }
 
             /* ---------------------------------------------------------------
-                Basic and Other tabs
+                Basic and Versions tabs
             --------------------------------------------------------------- */
             function initFileProperties(){
                 if (files.length == 1){
@@ -248,6 +308,16 @@
                 return applocale.getString("properties/key/" + key, key);
             }
 
+            //Rows of the same kind travel together; the gap between groups is
+            //what keeps a ten row list scannable
+            function propGroup(rows){
+                var kept = rows.filter(function(row){ return row != ""; });
+                if (kept.length == 0){
+                    return "";
+                }
+                return '<div class="dlgPropGroup">' + kept.join("") + "</div>";
+            }
+
             function renderSingleObject(data){
                 hideLoader();
                 fileInfo = data;
@@ -274,16 +344,44 @@
                     fileTypeText = applocale.getString("type/shortcut", "System Shortcut");
                 }
 
+                //A real file is the only thing that has an opener to show
+                var hasOpener = (!data.IsDirectory && !isShortcut);
+
                 var basicHTML = "";
-                basicHTML += propRow(localizedKey("File Type"), dlgEscape(fileTypeText));
-                basicHTML += propRow(data.IsDirectory ? localizedKey("Folder Size") : localizedKey("File Size"), sizeText(data.Filesize));
-                basicHTML += propRow(localizedKey("Location"), dlgEscape(data.VirtualDirname));
-                basicHTML += propRow(localizedKey("Last Modified"), modTimeText(data));
-                basicHTML += propRow(localizedKey("Created"), creationTimeText(data));
+                //What it is
+                basicHTML += propGroup([
+                    propRow(localizedKey("File Type"), dlgEscape(fileTypeText)),
+                    propRow(data.IsDirectory ? localizedKey("Folder Size") : localizedKey("File Size"), sizeText(data.Filesize))
+                ]);
+                //What opens it
+                basicHTML += propGroup([
+                    hasOpener ? '<div id="defaultOpenerRow">' + propRow(localizedKey("Default WebApp"),
+                        '<span class="dim">' + applocale.getString("message/loading", "Loading...") + "</span>") + "</div>" : ""
+                ]);
+                //Where it lives
+                basicHTML += propGroup([
+                    propRow(localizedKey("Location"), dlgEscape(data.VirtualDirname)),
+                    propRow(localizedKey("Virtual Path"), dlgEscape(data.VirtualPath)),
+                    propRow(localizedKey("Storage Path"), dlgEscape(data.StoragePath))
+                ]);
+                //When it changed
+                basicHTML += propGroup([
+                    propRow(localizedKey("Last Modified"), modTimeText(data)),
+                    propRow(localizedKey("Created"), creationTimeText(data))
+                ]);
+                //Who it belongs to
+                basicHTML += propGroup([
+                    propRow(localizedKey("Owner"), dlgEscape(data.Owner)),
+                    propRow(localizedKey("Permission"), '<span style="font-family: Consolas, Menlo, monospace;">' + dlgEscape(data.Permission) + "</span>")
+                ]);
                 $("#basicProperties").html(basicHTML);
 
-                //Other tab
-                renderOtherTab(data, isShortcut);
+                if (hasOpener){
+                    loadDefaultOpener(data);
+                }
+
+                //Versions tab
+                renderVersionsTab(data, isShortcut);
 
                 //Permission tab defaults
                 $("#permOwnerName").text(data.Owner);
@@ -321,49 +419,280 @@
                 $("#basicMime").text(applocale.getString("selection/multi", "Multiple selections"));
 
                 var basicHTML = "";
-                basicHTML += propRow(localizedKey("File Type"), dlgEscape(
-                    fileCount + applocale.getString("counter/files", " Files") + ", " +
-                    folderCount + applocale.getString("counter/folders", " Folders")));
-                basicHTML += propRow(localizedKey("Total Size"), sizeText(totalSize));
-                basicHTML += propRow(localizedKey("Location"), dlgEscape(readable[0].VirtualDirname));
+                basicHTML += propGroup([
+                    propRow(localizedKey("File Type"), dlgEscape(
+                        fileCount + applocale.getString("counter/files", " Files") + ", " +
+                        folderCount + applocale.getString("counter/folders", " Folders"))),
+                    propRow(localizedKey("Total Size"), sizeText(totalSize))
+                ]);
+                basicHTML += propGroup([
+                    propRow(localizedKey("Location"), dlgEscape(readable[0].VirtualDirname)),
+                    propRow(localizedKey("Virtual Directory"), dlgEscape(readable[0].VirtualDirname + "/")),
+                    propRow(localizedKey("Storage Directory"), dlgEscape(readable[0].StorageDirname + "/"))
+                ]);
+                basicHTML += propGroup([
+                    propRow(localizedKey("Owner"), dlgEscape(readable[0].Owner))
+                ]);
                 $("#basicProperties").html(basicHTML);
 
-                var otherHTML = "";
-                otherHTML += propRow(localizedKey("Virtual Directory"), dlgEscape(readable[0].VirtualDirname + "/"));
-                otherHTML += propRow(localizedKey("Storage Directory"), dlgEscape(readable[0].StorageDirname + "/"));
-                otherHTML += propRow(localizedKey("Owner"), dlgEscape(readable[0].Owner));
-                $("#otherProperties").html(otherHTML);
+                //Version history is per file, so a selection has none to show
+                renderVersionsTab(null, false);
 
                 $("#permOwnerName").text(readable[0].Owner);
                 loadUserGroups(readable[0].Owner);
             }
 
-            function renderOtherTab(data, isShortcut){
-                var otherHTML = "";
+            /* ---------------------------------------------------------------
+                Versions tab
 
-                //Default opener and version history only make sense for a real file
-                if (!data.IsDirectory && !isShortcut){
-                    otherHTML += '<div id="defaultOpenerRow">' + propRow(localizedKey("Default WebApp"),
-                        '<span class="dim">' + applocale.getString("message/loading", "Loading...") + "</span>") + "</div>";
+                Snapshots are kept per file under .metadata/.localver, so a
+                folder, a shortcut or a multiple selection has nothing to show
+                and the tab greys out instead of offering actions that would
+                fail.
+            --------------------------------------------------------------- */
+            function versionsSupported(){
+                return (files.length == 1 && fileInfo.error === undefined &&
+                    fileInfo.IsDirectory === false && !isShortcutFile(fileInfo));
+            }
+
+            function isShortcutFile(data){
+                return (data != undefined && data.Basename != undefined &&
+                    data.Basename.split(".").pop() == "shortcut");
+            }
+
+            function renderVersionsTab(data, isShortcut){
+                if (data == null || data.IsDirectory || isShortcut){
+                    $("#versionsUnavailable").show();
+                    $("#versionsManager").hide();
+                    $("#deleteAllVersionsBtn").hide();
+                    $('.dlgTab[tab="versions"]').addClass("unsupported");
+                    return;
                 }
 
-                otherHTML += propRow(localizedKey("Virtual Path"), dlgEscape(data.VirtualPath));
-                otherHTML += propRow(localizedKey("Storage Path"), dlgEscape(data.StoragePath));
-                otherHTML += propRow(localizedKey("Owner"), dlgEscape(data.Owner));
-                otherHTML += propRow(localizedKey("Permission"), '<span style="font-family: Consolas, Menlo, monospace;">' + dlgEscape(data.Permission) + "</span>");
-                otherHTML += propRow(localizedKey("MIME Type"), dlgEscape(data.MimeType));
+                $("#versionsUnavailable").hide();
+                $("#versionsManager").show();
+                $("#versionList").html('<div class="dlgEmpty"><div class="dlgSpinner" style="margin: 0px auto 10px auto;"></div><div>' +
+                    applocale.getString("message/loading", "Loading...") + "</div></div>");
 
-                if (!data.IsDirectory && !isShortcut){
-                    otherHTML += '<div class="dlgDivider"></div>' +
-                        '<button class="dlgBtn" style="width: 100%;" onclick="viewVersionHistory();">' +
-                        '<i class="history icon"></i> <span>' + applocale.getString("button/versionHistory", "View File Version History") + "</span></button>";
+                //This tab may already be the one on screen when the properties
+                //finally arrive, in which case nothing else starts the load
+                if (currentTab == "versions" && !versionsLoaded){
+                    versionsLoaded = true;
+                    loadVersionHistory();
                 }
+            }
 
-                $("#otherProperties").html(otherHTML);
+            function loadVersionHistory(){
+                $.ajax({
+                    url: "../../system/file_system/versionHistory",
+                    data: {path: files[0]},
+                    method: "POST",
+                    success: function(data){
+                        if (data == undefined || data.error !== undefined){
+                            $("#versionList").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
+                                dlgEscape(data == undefined ? "No response" : data.error) + "</div></div>");
+                            return;
+                        }
+                        versionList = data.Versions;
+                        renderVersionList();
+                    },
+                    error: function(){
+                        $("#versionList").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
+                            applocale.getString("versions/failed", "Unable to read the version history") + "</div></div>");
+                    }
+                });
+            }
 
-                if (!data.IsDirectory && !isShortcut){
-                    loadDefaultOpener(data);
+            function checkHistoryListUpdate(){
+                $.ajax({
+                    url: "../../system/file_system/versionHistory",
+                    data: {path: files[0]},
+                    method: "POST",
+                    success: function(data){
+                        if (data != undefined && data.error == undefined && versionList.length != data.Versions.length){
+                            versionList = data.Versions;
+                            renderVersionList();
+                        }
+                    }
+                });
+            }
+
+            function renderVersionList(){
+                var html = "";
+
+                //The file as it stands now is the newest version. Numbers are
+                //assigned here, not by the server: a snapshot is named by its
+                //timestamp, which says nothing about the order on screen.
+                html += versionRowHTML({
+                    number: versionList.length + 1,
+                    current: true,
+                    time: fileInfo.LastModTime,
+                    size: fileInfo.Filesize,
+                    historyID: "",
+                    relpath: ""
+                });
+
+                //Server returns newest first, so the number counts down
+                for (var i = 0; i < versionList.length; i++){
+                    html += versionRowHTML({
+                        number: versionList.length - i,
+                        current: false,
+                        time: versionList[i].OverwriteTime,
+                        size: versionList[i].Filesize,
+                        historyID: versionList[i].HistoryID,
+                        relpath: versionList[i].Relpath
+                    });
+                }
+
+                $("#versionList").html(html);
+                $("#deleteAllVersionsBtn").toggleClass("disabled", versionList.length == 0);
+
+                if (versionList.length == 0){
+                    $("#versionList").append('<div class="dlgEmpty"><div>' +
+                        applocale.getString("versions/empty/desc", "This file has no previous version histories.") + "</div></div>");
+                }
+            }
+
+            function versionRowHTML(entry){
+                var title = applocale.getString("versions/label", "Version") + " " + entry.number;
+                if (entry.current){
+                    title += " " + applocale.getString("versions/current", "(current)");
+                }
+                var owner = (fileInfo.Owner == undefined || fileInfo.Owner == "") ? "" : " (" + fileInfo.Owner + ")";
+
+                return '<div class="dlgListRow static' + (entry.current ? " selected" : "") + '">' +
+                    '<div class="rowIcon"><i class="file outline icon"></i></div>' +
+                    '<div class="rowText">' +
+                        '<div class="rowTitle">' + dlgEscape(title) + "</div>" +
+                        '<div class="rowSub">' + dlgEscape(entry.time + owner) + "</div>" +
+                    "</div>" +
+                    '<div class="rowMeta">' + dlgEscape(dlgBytesToSize(entry.size)) + "</div>" +
+                    '<button class="dlgIconBtn" verid="' + dlgEscape(entry.historyID) + '" relpath="' + dlgEscape(entry.relpath) + '" ' +
+                        'iscurrent="' + (entry.current ? "true" : "false") + '" onclick="openRowMenu(this);">' +
+                        '<i class="ellipsis horizontal icon"></i></button>' +
+                    "</div>";
+            }
+
+            function openRowMenu(button){
+                var isCurrent = $(button).attr("iscurrent") == "true";
+                var versionID = $(button).attr("verid");
+                var relpath = $(button).attr("relpath");
+
+                var html = '<div class="dlgMenuItem" onclick="downloadVersion(\'' + dlgEscape(relpath) + '\');">' +
+                    '<i class="download icon"></i>' + applocale.getString("versions/action/download", "Download Version") + "</div>";
+
+                if (!isCurrent){
+                    html += '<div class="dlgMenuItem" onclick="restoreVersion(\'' + dlgEscape(versionID) + '\');">' +
+                        '<i class="history icon"></i>' + applocale.getString("versions/action/restore", "Restore This Version") + "</div>";
+                    html += '<div class="dlgMenuItem danger" onclick="deleteVersion(\'' + dlgEscape(versionID) + '\');">' +
+                        '<i class="trash icon"></i>' + applocale.getString("versions/action/delete", "Delete") + "</div>";
+                }
+
+                var menu = $("#rowMenu");
+                menu.html(html).show();
+
+                //Anchor under the button, kept inside the window
+                var offset = $(button).offset();
+                var left = offset.left + $(button).outerWidth() - menu.outerWidth();
+                var top = offset.top + $(button).outerHeight() + 4;
+                if (left < 8){
+                    left = 8;
+                }
+                if (top + menu.outerHeight() > $(window).height() - 8){
+                    top = offset.top - menu.outerHeight() - 4;
+                }
+                menu.css({left: left + "px", top: top + "px"});
+            }
+
+            function downloadVersion(relpath){
+                $("#rowMenu").hide();
+                var dirname = files[0].split("/");
+                var basename = dirname.pop();
+                var accessPath = dirname.join("/") + "/" + (relpath == "" ? basename : relpath);
+                window.open("../../media/download?file=" + accessPath);
+            }
+
+            function createSnapshot(){
+                $.ajax({
+                    url: "../../system/file_system/versionHistory",
+                    data: {path: files[0], opr: "new"},
+                    method: "POST",
+                    success: function(data){
+                        if (data != undefined && data.error != undefined){
+                            dlgToast(data.error);
+                        }else{
+                            dlgToast(applocale.getString("versions/msg/created", "New Version Created"));
+                            loadVersionHistory();
+                        }
+                    }
+                });
+            }
+
+            function restoreVersion(versionID){
+                $("#rowMenu").hide();
+                if (!confirm(applocale.getString("versions/warn/restore", "Restoring this file will remove all newer version histories of this file. Confirm?"))){
+                    return;
                 }
+                $.ajax({
+                    url: "../../system/file_system/versionHistory",
+                    data: {path: files[0], opr: "restore", histid: versionID},
+                    method: "POST",
+                    success: function(data){
+                        if (data != undefined && data.error != undefined){
+                            dlgToast(data.error);
+                            return;
+                        }
+                        dlgToast(applocale.getString("versions/msg/restored", "Restore Succeed"));
+                        //The file itself changed, so the Basic tab is stale too
+                        refreshAfterRestore();
+                    }
+                });
+            }
+
+            //A restore rewrites the file, so the size and modification time on
+            //the Basic tab are stale the moment it succeeds
+            function refreshAfterRestore(){
+                getFileProp(files[0], function(data){
+                    if (data.error === undefined){
+                        renderSingleObject(data);
+                    }
+                    loadVersionHistory();
+                });
+            }
+
+            function deleteVersion(versionID){
+                $("#rowMenu").hide();
+                $.ajax({
+                    url: "../../system/file_system/versionHistory",
+                    data: {path: files[0], opr: "delete", histid: versionID},
+                    method: "POST",
+                    success: function(data){
+                        if (data != undefined && data.error != undefined){
+                            dlgToast(data.error);
+                        }else{
+                            loadVersionHistory();
+                        }
+                    }
+                });
+            }
+
+            function deleteAllVersions(){
+                if (!confirm(applocale.getString("versions/warn/deleteall", "This operation will PERMANENTLY DELETE ALL version history of this file. Confirm?"))){
+                    return;
+                }
+                $.ajax({
+                    url: "../../system/file_system/versionHistory",
+                    data: {path: files[0], opr: "deleteAll"},
+                    method: "POST",
+                    success: function(data){
+                        if (data != undefined && data.error != undefined){
+                            dlgToast(data.error);
+                        }else{
+                            dlgToast(applocale.getString("versions/msg/deletedall", "All version history deleted"));
+                            loadVersionHistory();
+                        }
+                    }
+                });
             }
 
             function loadDefaultOpener(data){
@@ -595,17 +924,6 @@
                 }
             }
 
-            function viewVersionHistory(){
-                var hashPassthrough = encodeURIComponent(JSON.stringify(files));
-                ao_module_newfw({
-                    url: "SystemAO/file_system/file_versions.html#" + hashPassthrough,
-                    width: 560,
-                    height: 560,
-                    appicon: "img/system/file-restore.svg",
-                    title: "File Version History"
-                });
-            }
-
             //Open the opener picker for this file type
             function changeDefaultWebApp(){
                 var openFileObject = {

+ 0 - 342
src/web/SystemAO/file_system/file_versions.html

@@ -1,342 +0,0 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        <title locale="title/title">File Version History</title>
-        <meta charset="UTF-8">
-        <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
-        <link rel="stylesheet" href="../../script/semantic/semantic.min.css">
-        <script type="text/javascript" src="../../script/jquery.min.js"></script>
-        <script type="text/javascript" src="../../script/ao_module.js"></script>
-        <script type="text/javascript" src="../../script/applocale.js"></script>
-        <link rel="stylesheet" href="dialog.css">
-        <script type="text/javascript" src="js/dialog.js"></script>
-        <style>
-            .versionHead{
-                margin-bottom: 16px;
-            }
-
-            .versionHead h3{
-                margin: 0px 0px 6px 0px;
-                font-size: 17px;
-                font-weight: 600;
-                color: var(--dlg_text);
-            }
-
-            .versionHead .desc{
-                color: var(--dlg_text_dim);
-                font-size: 13px;
-                line-height: 1.5;
-            }
-
-        </style>
-    </head>
-    <body class="whiteTheme">
-        <div class="dialog">
-            <div class="dialogBody">
-                <div class="versionHead">
-                    <h3 locale="title/title">File Version History</h3>
-                    <div class="desc" locale="title/desc">Any file versions that is more than 30 days old will be automatically removed from the system.</div>
-                </div>
-
-                <button class="dlgBtn" onclick="createSnapshot();">
-                    <i class="plus icon"></i> <span locale="button/newver">Create New Version</span>
-                </button>
-
-                <div class="dlgSectionLabel" locale="section/history">Version History</div>
-                <div class="dlgList" id="versions">
-                    <div class="dlgEmpty"><div class="dlgSpinner" style="margin: 0px auto 10px auto;"></div><div locale="message/loading">Loading...</div></div>
-                </div>
-            </div>
-
-            <div class="dialogFooter">
-                <button id="deleteAllBtn" class="dlgBtn danger" onclick="deleteAllVersions();">
-                    <i class="trash icon"></i> <span locale="button/removeAll">Delete All Versions</span>
-                </button>
-                <div class="spacer"></div>
-                <button class="dlgBtn" onclick="ao_module_close();" locale="button/close">Close</button>
-            </div>
-        </div>
-
-        <div class="dlgMenu" id="rowMenu"></div>
-
-        <script>
-            /*
-                file_versions.html
-
-                The snapshots ArozOS keeps of one file, newest first, with the
-                file as it stands now sitting on top of them as the current
-                version. The per row actions moved into a menu because three
-                icon buttons per row made the list read as a toolbar rather
-                than as a history.
-
-                Version numbers are assigned here, not by the server: the
-                snapshot folder name is a timestamp, which is precise but tells
-                the reader nothing about the order they are looking at.
-            */
-            var files = ao_module_loadInputFiles();
-            var targetFile = "";
-            var currentVersionList = [];
-            var fileOwner = "";
-            var currentFileInfo = null;
-            var pollTimer = null;
-
-            applocale.init("../../SystemAO/locale/file_versions.json", function(){
-                applocale.translate();
-                dlgInitTheme();
-
-                if (files.length < 1){
-                    renderNoFile();
-                    return;
-                }
-
-                targetFile = files[0];
-                loadFileInfo();
-                loadVersionHistory(targetFile);
-
-                //Another window may snapshot the same file while this one is open
-                pollTimer = setInterval(function(){
-                    checkHistoryListUpdate(targetFile);
-                }, 3000);
-            });
-
-            $(document).on("click", function(event){
-                if ($(event.target).closest("#rowMenu, .dlgIconBtn").length == 0){
-                    $("#rowMenu").hide();
-                }
-            });
-
-            function renderNoFile(){
-                $("#versions").html('<div class="dlgEmpty"><i class="file outline icon"></i><div>' +
-                    applocale.getString("message/nofile/title", "No File") + "</div></div>");
-                $("#deleteAllBtn").addClass("disabled");
-            }
-
-            //The owner and the live file size are shown on the current version
-            //row, and neither is part of the version history response
-            function loadFileInfo(){
-                $.ajax({
-                    url: "../../system/file_system/getProperties",
-                    data: {path: targetFile},
-                    method: "POST",
-                    success: function(data){
-                        if (data == undefined || data.error !== undefined){
-                            return;
-                        }
-                        currentFileInfo = data;
-                        fileOwner = data.Owner;
-                        renderVersionList();
-                    }
-                });
-            }
-
-            function checkHistoryListUpdate(vpath){
-                $.ajax({
-                    url: "../../system/file_system/versionHistory",
-                    data: {path: vpath},
-                    method: "POST",
-                    success: function(data){
-                        if (data != undefined && data.error == undefined){
-                            if (currentVersionList.length != data.Versions.length){
-                                loadVersionHistory(vpath);
-                            }
-                        }
-                    }
-                });
-            }
-
-            function loadVersionHistory(vpath){
-                $.ajax({
-                    url: "../../system/file_system/versionHistory",
-                    data: {path: vpath},
-                    method: "POST",
-                    success: function(data){
-                        if (data == undefined || data.error !== undefined){
-                            $("#versions").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
-                                dlgEscape(data == undefined ? "No response" : data.error) + "</div></div>");
-                            return;
-                        }
-                        currentVersionList = data.Versions;
-                        renderVersionList();
-                    },
-                    error: function(){
-                        $("#versions").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
-                            applocale.getString("message/failed", "Unable to read the version history") + "</div></div>");
-                    }
-                });
-            }
-
-            function ownerSuffix(){
-                return (fileOwner == "") ? "" : " (" + fileOwner + ")";
-            }
-
-            function renderVersionList(){
-                var html = "";
-                var totalVersions = currentVersionList.length + 1;
-
-                //The file as it stands now is the newest version
-                if (currentFileInfo != null){
-                    html += versionRowHTML({
-                        number: totalVersions,
-                        current: true,
-                        time: currentFileInfo.LastModTime,
-                        size: currentFileInfo.Filesize,
-                        historyID: "",
-                        relpath: ""
-                    });
-                }
-
-                //Server returns newest first, so the number counts down
-                for (var i = 0; i < currentVersionList.length; i++){
-                    var entry = currentVersionList[i];
-                    html += versionRowHTML({
-                        number: currentVersionList.length - i,
-                        current: false,
-                        time: entry.OverwriteTime,
-                        size: entry.Filesize,
-                        historyID: entry.HistoryID,
-                        relpath: entry.Relpath
-                    });
-                }
-
-                if (currentVersionList.length == 0 && currentFileInfo == null){
-                    html = '<div class="dlgEmpty"><i class="history icon"></i><div>' +
-                        applocale.getString("message/nohist/title", "No History") + "</div><div>" +
-                        applocale.getString("message/nohist/desc", "This file has no previous version histories.") + "</div></div>";
-                }
-
-                $("#versions").html(html);
-                $("#deleteAllBtn").toggleClass("disabled", currentVersionList.length == 0);
-            }
-
-            function versionRowHTML(entry){
-                var title = applocale.getString("version/label", "Version") + " " + entry.number;
-                if (entry.current){
-                    title += " " + applocale.getString("version/current", "(current)");
-                }
-
-                return '<div class="dlgListRow static' + (entry.current ? " selected" : "") + '">' +
-                    '<div class="rowIcon"><i class="file outline icon"></i></div>' +
-                    '<div class="rowText">' +
-                        '<div class="rowTitle">' + dlgEscape(title) + "</div>" +
-                        '<div class="rowSub">' + dlgEscape(entry.time + ownerSuffix()) + "</div>" +
-                    "</div>" +
-                    '<div class="rowMeta">' + dlgEscape(dlgBytesToSize(entry.size)) + "</div>" +
-                    '<button class="dlgIconBtn" verid="' + dlgEscape(entry.historyID) + '" relpath="' + dlgEscape(entry.relpath) + '" ' +
-                        'iscurrent="' + (entry.current ? "true" : "false") + '" onclick="openRowMenu(this);">' +
-                        '<i class="ellipsis horizontal icon"></i></button>' +
-                    "</div>";
-            }
-
-            function openRowMenu(button){
-                var isCurrent = $(button).attr("iscurrent") == "true";
-                var versionID = $(button).attr("verid");
-                var relpath = $(button).attr("relpath");
-
-                var html = '<div class="dlgMenuItem" onclick="downloadVersion(\'' + dlgEscape(relpath) + '\');">' +
-                    '<i class="download icon"></i>' + applocale.getString("title/download", "Download Version") + "</div>";
-
-                if (!isCurrent){
-                    html += '<div class="dlgMenuItem" onclick="restoreVersion(\'' + dlgEscape(versionID) + '\');">' +
-                        '<i class="history icon"></i>' + applocale.getString("title/restore", "Restore This Version") + "</div>";
-                    html += '<div class="dlgMenuItem danger" onclick="deleteVersion(\'' + dlgEscape(versionID) + '\');">' +
-                        '<i class="trash icon"></i>' + applocale.getString("title/delete", "Delete") + "</div>";
-                }
-
-                var menu = $("#rowMenu");
-                menu.html(html).show();
-
-                //Anchor under the button, kept inside the window
-                var offset = $(button).offset();
-                var left = offset.left + $(button).outerWidth() - menu.outerWidth();
-                var top = offset.top + $(button).outerHeight() + 4;
-                if (left < 8){
-                    left = 8;
-                }
-                if (top + menu.outerHeight() > $(window).height() - 8){
-                    top = offset.top - menu.outerHeight() - 4;
-                }
-                menu.css({left: left + "px", top: top + "px"});
-            }
-
-            function downloadVersion(relpath){
-                $("#rowMenu").hide();
-                var dirname = targetFile.split("/");
-                dirname.pop();
-                var accessPath = dirname.join("/") + "/" + (relpath == "" ? targetFile.split("/").pop() : relpath);
-                window.open("../../media/download?file=" + accessPath);
-            }
-
-            function createSnapshot(){
-                $.ajax({
-                    url: "../../system/file_system/versionHistory",
-                    data: {path: targetFile, opr: "new"},
-                    method: "POST",
-                    success: function(data){
-                        if (data != undefined && data.error != undefined){
-                            dlgToast(data.error);
-                        }else{
-                            dlgToast(applocale.getString("msgbox/created", "New Version Created"));
-                            loadVersionHistory(targetFile);
-                        }
-                    }
-                });
-            }
-
-            function restoreVersion(versionID){
-                $("#rowMenu").hide();
-                if (!confirm(applocale.getString("warning/restore", "Restoring this file will remove all newer version histories of this file. Confirm?"))){
-                    return;
-                }
-                $.ajax({
-                    url: "../../system/file_system/versionHistory",
-                    data: {path: targetFile, opr: "restore", histid: versionID},
-                    method: "POST",
-                    success: function(data){
-                        if (data != undefined && data.error != undefined){
-                            dlgToast(data.error);
-                        }else{
-                            loadFileInfo();
-                            loadVersionHistory(targetFile);
-                            dlgToast(applocale.getString("msgbox/restored", "Restore Succeed"));
-                        }
-                    }
-                });
-            }
-
-            function deleteVersion(versionID){
-                $("#rowMenu").hide();
-                $.ajax({
-                    url: "../../system/file_system/versionHistory",
-                    data: {path: targetFile, opr: "delete", histid: versionID},
-                    method: "POST",
-                    success: function(data){
-                        if (data != undefined && data.error != undefined){
-                            dlgToast(data.error);
-                        }else{
-                            loadVersionHistory(targetFile);
-                        }
-                    }
-                });
-            }
-
-            function deleteAllVersions(){
-                if (!confirm(applocale.getString("warning/deleteall", "This operation will **PERMANENTLY DELETE ALL** version history of this file. Confirm?"))){
-                    return;
-                }
-                $.ajax({
-                    url: "../../system/file_system/versionHistory",
-                    data: {path: targetFile, opr: "deleteAll"},
-                    method: "POST",
-                    success: function(data){
-                        if (data != undefined && data.error != undefined){
-                            dlgToast(data.error);
-                        }else{
-                            dlgToast(applocale.getString("msgbox/deletedall", "All version history deleted"));
-                            loadVersionHistory(targetFile);
-                        }
-                    }
-                });
-            }
-        </script>
-    </body>
-</html>

+ 108 - 12
src/web/SystemAO/locale/file_properties.json

@@ -27,7 +27,6 @@
                 "lastmod/time/s": "",
                 "lastmod/time/ago": "前",
                 "button/changeDefault": "更改預設啟動應用",
-                "button/versionHistory": "查看檔案版本記錄",
                 "button/changeFilePermission": "更改檔案存取權限",
                 "loader/loadingFileSize": "正在計算檔案大小",
                 "selection/multi": "已選擇多個檔案",
@@ -37,7 +36,6 @@
                 "properties/error/No record": "沒有記錄",
                 "tab/basic": "基本",
                 "tab/permission": "權限",
-                "tab/other": "其他",
                 "button/copyPath": "複製路徑",
                 "button/close": "關閉",
                 "button/cancel": "取消",
@@ -76,6 +74,24 @@
                 "perm/level/readExecute": "讀取及執行",
                 "perm/level/readWrite": "讀寫",
                 "perm/level/readWriteExecute": "讀寫及執行",
+                "tab/versions": "版本",
+                "message/noVersions": "只有檔案才會保留版本記錄",
+                "versions/desc": "此檔案的舊版本將會自動於 30 天後刪除。同時間你也可以手動從下方列表中對版本備份進行刪除。",
+                "versions/newver": "建立新版本",
+                "versions/history": "版本歷史",
+                "versions/removeAll": "永久刪除所有舊版本",
+                "versions/empty/desc": "此檔案並沒有舊版本記錄",
+                "versions/action/download": "下載此版本",
+                "versions/action/restore": "恢復到此版本",
+                "versions/action/delete": "刪除此版本",
+                "versions/msg/created": "已建立新檔案版本",
+                "versions/msg/restored": "已恢復檔案到指定版本",
+                "versions/msg/deletedall": "已刪除所有舊檔案版本",
+                "versions/warn/deleteall": "此動作將會 *永久刪除* 所有此檔案的舊版本備份。確認刪除?",
+                "versions/warn/restore": "恢復到到此檔案版本將會移除所有比此版本更新的版本備份。確認恢復?",
+                "versions/label": "版本",
+                "versions/current": "(目前)",
+                "versions/failed": "無法讀取版本記錄",
                 "": ""
             },
             "titles": {
@@ -107,7 +123,6 @@
                 "lastmod/time/s": "",
                 "lastmod/time/ago": "前",
                 "button/changeDefault": "更改預設啟動應用",
-                "button/versionHistory": "查看檔案版本記錄",
                 "button/changeFilePermission": "更改檔案存取權限",
                 "loader/loadingFileSize": "正在計算檔案大小",
                 "selection/multi": "已選擇多個檔案",
@@ -117,7 +132,6 @@
                 "properties/error/No record": "沒有記錄",
                 "tab/basic": "基本",
                 "tab/permission": "權限",
-                "tab/other": "其他",
                 "button/copyPath": "複製路徑",
                 "button/close": "關閉",
                 "button/cancel": "取消",
@@ -156,6 +170,24 @@
                 "perm/level/readExecute": "讀取及執行",
                 "perm/level/readWrite": "讀寫",
                 "perm/level/readWriteExecute": "讀寫及執行",
+                "tab/versions": "版本",
+                "message/noVersions": "只有檔案才會保留版本記錄",
+                "versions/desc": "此檔案的舊版本將會自動於 30 天後刪除。同時間你也可以手動從下方列表中對版本備份進行刪除。",
+                "versions/newver": "建立新版本",
+                "versions/history": "版本歷史",
+                "versions/removeAll": "永久刪除所有舊版本",
+                "versions/empty/desc": "此檔案並沒有舊版本記錄",
+                "versions/action/download": "下載此版本",
+                "versions/action/restore": "恢復到此版本",
+                "versions/action/delete": "刪除此版本",
+                "versions/msg/created": "已建立新檔案版本",
+                "versions/msg/restored": "已恢復檔案到指定版本",
+                "versions/msg/deletedall": "已刪除所有舊檔案版本",
+                "versions/warn/deleteall": "此動作將會 *永久刪除* 所有此檔案的舊版本備份。確認刪除?",
+                "versions/warn/restore": "恢復到到此檔案版本將會移除所有比此版本更新的版本備份。確認恢復?",
+                "versions/label": "版本",
+                "versions/current": "(目前)",
+                "versions/failed": "無法讀取版本記錄",
                 "": ""
             },
             "titles": {
@@ -187,7 +219,6 @@
                 "lastmod/time/s": "",
                 "lastmod/time/ago": "前",
                 "button/changeDefault": "更改预设启动应用",
-                "button/versionHistory": "查看文件版本记录",
                 "button/changeFilePermission": "更改文件存取权限",
                 "loader/loadingFileSize": "正在计算文件大小",
                 "selection/multi": "已选择多个文件",
@@ -197,7 +228,6 @@
                 "properties/error/No record": "没有记录",
                 "tab/basic": "基本",
                 "tab/permission": "权限",
-                "tab/other": "其他",
                 "button/copyPath": "复制路径",
                 "button/close": "关闭",
                 "button/cancel": "取消",
@@ -236,6 +266,24 @@
                 "perm/level/readExecute": "读取及执行",
                 "perm/level/readWrite": "读写",
                 "perm/level/readWriteExecute": "读写及执行",
+                "tab/versions": "版本",
+                "message/noVersions": "只有文件才会保留版本记录",
+                "versions/desc": "此文件的旧版本将会自动于 30 天后删除。同时间你也可以手动从下方列表中对版本备份进行删除。",
+                "versions/newver": "创建新版本",
+                "versions/history": "版本历史",
+                "versions/removeAll": "永久删除所有旧版本",
+                "versions/empty/desc": "此文件并没有旧版本记录",
+                "versions/action/download": "下载此版本",
+                "versions/action/restore": "恢复到此版本",
+                "versions/action/delete": "删除此版本",
+                "versions/msg/created": "已创建新文件版本",
+                "versions/msg/restored": "已恢复文件到指定版本",
+                "versions/msg/deletedall": "已删除所有旧文件版本",
+                "versions/warn/deleteall": "此动作将会 *永久删除* 所有此文件的旧版本备份。确认删除?",
+                "versions/warn/restore": "恢复到到此文件版本将会移除所有比此版本更新的版本备份。确认恢复?",
+                "versions/label": "版本",
+                "versions/current": "(当前)",
+                "versions/failed": "无法读取版本记录",
                 "": ""
             },
             "titles": {
@@ -267,7 +315,6 @@
                 "lastmod/time/s": "s",
                 "lastmod/time/ago": " ago",
                 "button/changeDefault": "Change Default Opening App",
-                "button/versionHistory": "View File Version History",
                 "button/changeFilePermission": "Change File Access Permission",
                 "loader/loadingFileSize": "Calculating file size",
                 "selection/multi": "Multiple files selected",
@@ -277,7 +324,6 @@
                 "properties/error/No record": "No record",
                 "tab/basic": "Basic",
                 "tab/permission": "Permissions",
-                "tab/other": "Other",
                 "button/copyPath": "Copy Path",
                 "button/close": "Close",
                 "button/cancel": "Cancel",
@@ -316,6 +362,24 @@
                 "perm/level/readExecute": "Read and execute",
                 "perm/level/readWrite": "Read and write",
                 "perm/level/readWriteExecute": "Read, write and execute",
+                "tab/versions": "Versions",
+                "message/noVersions": "Version history is only kept for files",
+                "versions/desc": "Old versions of this file will be automatically deleted after 30 days. You can also manually delete version backups from the list below.",
+                "versions/newver": "Create New Version",
+                "versions/history": "Version History",
+                "versions/removeAll": "Permanently Delete All Old Versions",
+                "versions/empty/desc": "This file has no old version records",
+                "versions/action/download": "Download This Version",
+                "versions/action/restore": "Restore to This Version",
+                "versions/action/delete": "Delete This Version",
+                "versions/msg/created": "New file version created",
+                "versions/msg/restored": "File restored to the specified version",
+                "versions/msg/deletedall": "All old file versions deleted",
+                "versions/warn/deleteall": "This action will *permanently delete* all old version backups of this file. Confirm deletion?",
+                "versions/warn/restore": "Restoring to this file version will remove all versions newer than this one. Confirm restoration?",
+                "versions/label": "Version",
+                "versions/current": "(current)",
+                "versions/failed": "Unable to read the version history",
                 "": ""
             },
             "titles": {
@@ -347,7 +411,6 @@
                 "lastmod/time/s": "秒",
                 "lastmod/time/ago": "前",
                 "button/changeDefault": "デフォルトの開くアプリを変更",
-                "button/versionHistory": "ファイルバージョン履歴を表示",
                 "button/changeFilePermission": "ファイルアクセス権限を変更",
                 "loader/loadingFileSize": "ファイルサイズを計算中",
                 "selection/multi": "複数のファイルが選択されました",
@@ -357,7 +420,6 @@
                 "properties/error/No record": "記録なし",
                 "tab/basic": "基本",
                 "tab/permission": "権限",
-                "tab/other": "その他",
                 "button/copyPath": "パスをコピー",
                 "button/close": "閉じる",
                 "button/cancel": "キャンセル",
@@ -396,6 +458,24 @@
                 "perm/level/readExecute": "読み取りと実行",
                 "perm/level/readWrite": "読み書き",
                 "perm/level/readWriteExecute": "読み書きと実行",
+                "tab/versions": "バージョン",
+                "message/noVersions": "バージョン履歴はファイルにのみ保存されます",
+                "versions/desc": "このファイルの旧バージョンは30日後に自動的に削除されます。下のリストから手動でバージョンバックアップを削除することもできます。",
+                "versions/newver": "新バージョンを作成",
+                "versions/history": "バージョン履歴",
+                "versions/removeAll": "すべての旧バージョンを完全に削除",
+                "versions/empty/desc": "このファイルの旧バージョン記録はありません",
+                "versions/action/download": "このバージョンをダウンロード",
+                "versions/action/restore": "このバージョンに復元",
+                "versions/action/delete": "このバージョンを削除",
+                "versions/msg/created": "新しいファイルバージョンを作成しました",
+                "versions/msg/restored": "ファイルを指定バージョンに復元しました",
+                "versions/msg/deletedall": "すべての旧ファイルバージョンを削除しました",
+                "versions/warn/deleteall": "この操作はこのファイルのすべての旧バージョンバックアップを *完全に削除* します。削除を確認しますか?",
+                "versions/warn/restore": "このバージョンに復元すると、このバージョンより新しいすべてのバージョンバックアップが削除されます。復元を確認しますか?",
+                "versions/label": "バージョン",
+                "versions/current": "(現在)",
+                "versions/failed": "バージョン履歴を読み取れません",
                 "": ""
             },
             "titles": {
@@ -427,7 +507,6 @@
                 "lastmod/time/s": "초",
                 "lastmod/time/ago": " 전",
                 "button/changeDefault": "기본 열기 앱 변경",
-                "button/versionHistory": "파일 버전 기록 보기",
                 "button/changeFilePermission": "파일 접근 권한 변경",
                 "loader/loadingFileSize": "파일 크기 계산 중",
                 "selection/multi": "여러 파일 선택됨",
@@ -437,7 +516,6 @@
                 "properties/error/No record": "기록 없음",
                 "tab/basic": "기본",
                 "tab/permission": "권한",
-                "tab/other": "기타",
                 "button/copyPath": "경로 복사",
                 "button/close": "닫기",
                 "button/cancel": "취소",
@@ -476,6 +554,24 @@
                 "perm/level/readExecute": "읽기 및 실행",
                 "perm/level/readWrite": "읽기 및 쓰기",
                 "perm/level/readWriteExecute": "읽기, 쓰기 및 실행",
+                "tab/versions": "버전",
+                "message/noVersions": "버전 기록은 파일에만 보관됩니다",
+                "versions/desc": "이 파일의 이전 버전은 30일 후 자동으로 삭제됩니다. 아래 목록에서 버전 백업을 수동으로 삭제할 수도 있습니다.",
+                "versions/newver": "새 버전 만들기",
+                "versions/history": "버전 기록",
+                "versions/removeAll": "모든 이전 버전 영구 삭제",
+                "versions/empty/desc": "이 파일에는 이전 버전 기록이 없습니다",
+                "versions/action/download": "이 버전 다운로드",
+                "versions/action/restore": "이 버전으로 복원",
+                "versions/action/delete": "이 버전 삭제",
+                "versions/msg/created": "새 파일 버전을 만들었습니다",
+                "versions/msg/restored": "파일을 지정된 버전으로 복원했습니다",
+                "versions/msg/deletedall": "모든 이전 파일 버전을 삭제했습니다",
+                "versions/warn/deleteall": "이 작업은 이 파일의 모든 이전 버전 백업을 *영구적으로 삭제*합니다. 삭제를 확인하시겠습니까?",
+                "versions/warn/restore": "이 버전으로 복원하면 이보다 최신 버전의 모든 버전 백업이 제거됩니다. 복원을 확인하시겠습니까?",
+                "versions/label": "버전",
+                "versions/current": "(현재)",
+                "versions/failed": "버전 기록을 읽을 수 없습니다",
                 "": ""
             },
             "titles": {

+ 0 - 191
src/web/SystemAO/locale/file_versions.json

@@ -1,191 +0,0 @@
-{
-    "author": "tobychui",
-    "version": "1.0",
-    "keys": {
-        "zh-tw": {
-            "fwtitle": "檔案版本管理",
-            "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
-            "strings": {
-                "title/title": "檔案版本管理",
-                "title/desc": "此檔案的舊版本將會自動於 30 天後刪除。同時間你也可以手動從下方列表中對版本備份進行刪除。",
-                "button/newver": "建立新版本",
-                "button/removeAll": "永久刪除所有舊版本",
-                "message/nofile/title": "沒有檔案",
-                "message/nofile/desc": "使用方法錯誤",
-                "message/nohist/title": "沒有舊版本",
-                "message/nohist/desc": "此檔案並沒有舊版本記錄",
-                "title/download": "下載此版本",
-                "title/restore": "恢復到此版本",
-                "title/delete": "刪除此版本",
-                "msgbox/created": "已建立新檔案版本",
-                "msgbox/restored": "已恢復檔案到指定版本",
-                "msgbox/deletedall": "已刪除所有舊檔案版本",
-                "warning/deleteall": "此動作將會 *永久刪除* 所有此檔案的舊版本備份。確認刪除?",
-                "warning/restore": "恢復到到此檔案版本將會移除所有比此版本更新的版本備份。確認恢復?",
-                "section/history": "版本歷史",
-                "button/close": "關閉",
-                "version/label": "版本",
-                "version/current": "(目前)",
-                "message/loading": "載入中...",
-                "message/failed": "無法讀取版本記錄",
-                "": ""
-            },
-            "titles": {},
-            "placeholder": {}
-        },
-        "zh-hk": {
-            "fwtitle": "檔案版本管理",
-            "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
-            "strings": {
-                "title/title": "檔案版本管理",
-                "title/desc": "此檔案的舊版本將會自動於 30 天後刪除。同時間你也可以手動從下方列表中對版本備份進行刪除。",
-                "button/newver": "建立新版本",
-                "button/removeAll": "永久刪除所有舊版本",
-                "message/nofile/title": "沒有檔案",
-                "message/nofile/desc": "使用方法錯誤",
-                "message/nohist/title": "沒有舊版本",
-                "message/nohist/desc": "此檔案並沒有舊版本記錄",
-                "title/download": "下載此版本",
-                "title/restore": "恢復到此版本",
-                "title/delete": "刪除此版本",
-                "msgbox/created": "已建立新檔案版本",
-                "msgbox/restored": "已恢復檔案到指定版本",
-                "msgbox/deletedall": "已刪除所有舊檔案版本",
-                "warning/deleteall": "此動作將會 *永久刪除* 所有此檔案的舊版本備份。確認刪除?",
-                "warning/restore": "恢復到到此檔案版本將會移除所有比此版本更新的版本備份。確認恢復?",
-                "section/history": "版本歷史",
-                "button/close": "關閉",
-                "version/label": "版本",
-                "version/current": "(目前)",
-                "message/loading": "載入中...",
-                "message/failed": "無法讀取版本記錄",
-                "": ""
-            },
-            "titles": {},
-            "placeholder": {}
-        },
-        "zh-cn": {
-            "fwtitle": "文件版本管理",
-            "strings": {
-                "title/title": "文件版本管理",
-                "title/desc": "此文件的旧版本将会自动于 30 天后删除。同时间你也可以手动从下方列表中对版本备份进行删除。",
-                "button/newver": "创建新版本",
-                "button/removeAll": "永久删除所有旧版本",
-                "message/nofile/title": "没有文件",
-                "message/nofile/desc": "使用方法错误",
-                "message/nohist/title": "没有旧版本",
-                "message/nohist/desc": "此文件并没有旧版本记录",
-                "title/download": "下载此版本",
-                "title/restore": "恢复到此版本",
-                "title/delete": "删除此版本",
-                "msgbox/created": "已创建新文件版本",
-                "msgbox/restored": "已恢复文件到指定版本",
-                "msgbox/deletedall": "已删除所有旧文件版本",
-                "warning/deleteall": "此动作将会 *永久删除* 所有此文件的旧版本备份。确认删除?",
-                "warning/restore": "恢复到到此文件版本将会移除所有比此版本更新的版本备份。确认恢复?",
-                "section/history": "版本历史",
-                "button/close": "关闭",
-                "version/label": "版本",
-                "version/current": "(当前)",
-                "message/loading": "加载中...",
-                "message/failed": "无法读取版本记录",
-                "": ""
-            },
-            "titles": {},
-            "placeholder": {}
-        },
-        "en-us": {
-            "fwtitle": "File Version Management",
-            "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
-            "strings": {
-                "title/title": "File Version Management",
-                "title/desc": "Old versions of this file will be automatically deleted after 30 days. You can also manually delete version backups from the list below.",
-                "button/newver": "Create New Version",
-                "button/removeAll": "Permanently Delete All Old Versions",
-                "message/nofile/title": "No Files",
-                "message/nofile/desc": "Incorrect usage",
-                "message/nohist/title": "No Old Versions",
-                "message/nohist/desc": "This file has no old version records",
-                "title/download": "Download This Version",
-                "title/restore": "Restore to This Version",
-                "title/delete": "Delete This Version",
-                "msgbox/created": "New file version created",
-                "msgbox/restored": "File restored to the specified version",
-                "msgbox/deletedall": "All old file versions deleted",
-                "warning/deleteall": "This action will *permanently delete* all old version backups of this file. Confirm deletion?",
-                "warning/restore": "Restoring to this file version will remove all versions newer than this one. Confirm restoration?",
-                "section/history": "Version History",
-                "button/close": "Close",
-                "version/label": "Version",
-                "version/current": "(current)",
-                "message/loading": "Loading...",
-                "message/failed": "Unable to read the version history",
-                "": ""
-            },
-            "titles": {},
-            "placeholder": {}
-        },
-        "ja-jp": {
-            "fwtitle": "ファイルバージョン管理",
-            "fontFamily": "\"Meiryo UI\", \"Arial Unicode MS\", \"Hiragino Kaku Gothic Pro\"",
-            "strings": {
-                "title/title": "ファイルバージョン管理",
-                "title/desc": "このファイルの旧バージョンは30日後に自動的に削除されます。下のリストから手動でバージョンバックアップを削除することもできます。",
-                "button/newver": "新バージョンを作成",
-                "button/removeAll": "すべての旧バージョンを完全に削除",
-                "message/nofile/title": "ファイルなし",
-                "message/nofile/desc": "使用方法が正しくありません",
-                "message/nohist/title": "旧バージョンなし",
-                "message/nohist/desc": "このファイルの旧バージョン記録はありません",
-                "title/download": "このバージョンをダウンロード",
-                "title/restore": "このバージョンに復元",
-                "title/delete": "このバージョンを削除",
-                "msgbox/created": "新しいファイルバージョンを作成しました",
-                "msgbox/restored": "ファイルを指定バージョンに復元しました",
-                "msgbox/deletedall": "すべての旧ファイルバージョンを削除しました",
-                "warning/deleteall": "この操作はこのファイルのすべての旧バージョンバックアップを *完全に削除* します。削除を確認しますか?",
-                "warning/restore": "このバージョンに復元すると、このバージョンより新しいすべてのバージョンバックアップが削除されます。復元を確認しますか?",
-                "section/history": "バージョン履歴",
-                "button/close": "閉じる",
-                "version/label": "バージョン",
-                "version/current": "(現在)",
-                "message/loading": "読み込み中...",
-                "message/failed": "バージョン履歴を読み取れません",
-                "": ""
-            },
-            "titles": {},
-            "placeholder": {}
-        },
-        "ko-kr": {
-            "fwtitle": "파일 버전 관리",
-            "fontFamily": "\"Malgun Gothic\", \"Apple SD Gothic Neo\"",
-            "strings": {
-                "title/title": "파일 버전 관리",
-                "title/desc": "이 파일의 이전 버전은 30일 후 자동으로 삭제됩니다. 아래 목록에서 버전 백업을 수동으로 삭제할 수도 있습니다.",
-                "button/newver": "새 버전 만들기",
-                "button/removeAll": "모든 이전 버전 영구 삭제",
-                "message/nofile/title": "파일 없음",
-                "message/nofile/desc": "잘못된 사용 방법",
-                "message/nohist/title": "이전 버전 없음",
-                "message/nohist/desc": "이 파일에는 이전 버전 기록이 없습니다",
-                "title/download": "이 버전 다운로드",
-                "title/restore": "이 버전으로 복원",
-                "title/delete": "이 버전 삭제",
-                "msgbox/created": "새 파일 버전을 만들었습니다",
-                "msgbox/restored": "파일을 지정된 버전으로 복원했습니다",
-                "msgbox/deletedall": "모든 이전 파일 버전을 삭제했습니다",
-                "warning/deleteall": "이 작업은 이 파일의 모든 이전 버전 백업을 *영구적으로 삭제*합니다. 삭제를 확인하시겠습니까?",
-                "warning/restore": "이 버전으로 복원하면 이보다 최신 버전의 모든 버전 백업이 제거됩니다. 복원을 확인하시겠습니까?",
-                "section/history": "버전 기록",
-                "button/close": "닫기",
-                "version/label": "버전",
-                "version/current": "(현재)",
-                "message/loading": "불러오는 중...",
-                "message/failed": "버전 기록을 읽을 수 없습니다",
-                "": ""
-            },
-            "titles": {},
-            "placeholder": {}
-        }
-    }
-}