web client: allow to preview images and pdf
pdf depends on browser support. It does not work on mobile devices.
This commit is contained in:
parent
fc048728d9
commit
3f3591bae0
20 changed files with 208 additions and 54 deletions
|
@ -155,7 +155,8 @@ func getUserFile(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if status, err := downloadFile(w, r, connection, name, info); err != nil {
|
||||
inline := r.URL.Query().Get("inline") != ""
|
||||
if status, err := downloadFile(w, r, connection, name, info, inline); err != nil {
|
||||
resp := apiResponse{
|
||||
Error: err.Error(),
|
||||
Message: http.StatusText(status),
|
||||
|
|
|
@ -257,7 +257,9 @@ func getZipEntryName(entryPath, baseDir string) string {
|
|||
return strings.TrimPrefix(entryPath, "/")
|
||||
}
|
||||
|
||||
func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection, name string, info os.FileInfo) (int, error) {
|
||||
func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection, name string,
|
||||
info os.FileInfo, inline bool,
|
||||
) (int, error) {
|
||||
var err error
|
||||
rangeHeader := r.Header.Get("Range")
|
||||
if rangeHeader != "" && checkIfRange(r, info.ModTime()) == condFalse {
|
||||
|
@ -295,7 +297,9 @@ func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection
|
|||
}
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
w.Header().Set("Content-Type", ctype)
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%#v", path.Base(name)))
|
||||
if !inline {
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%#v", path.Base(name)))
|
||||
}
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.WriteHeader(responseStatus)
|
||||
if r.Method != http.MethodHead {
|
||||
|
|
|
@ -136,13 +136,14 @@ const (
|
|||
webClientPubSharesPathDefault = "/web/client/pubshares"
|
||||
webClientForgotPwdPathDefault = "/web/client/forgot-password"
|
||||
webClientResetPwdPathDefault = "/web/client/reset-password"
|
||||
webClientViewPDFPathDefault = "/web/client/viewpdf"
|
||||
webStaticFilesPathDefault = "/static"
|
||||
webOpenAPIPathDefault = "/openapi"
|
||||
// MaxRestoreSize defines the max size for the loaddata input file
|
||||
MaxRestoreSize = 10485760 // 10 MB
|
||||
maxRequestSize = 1048576 // 1MB
|
||||
maxLoginBodySize = 262144 // 256 KB
|
||||
httpdMaxEditFileSize = 524288 // 512 KB
|
||||
httpdMaxEditFileSize = 1048576 // 1 MB
|
||||
maxMultipartMem = 8388608 // 8MB
|
||||
osWindows = "windows"
|
||||
otpHeaderCode = "X-SFTPGO-OTP"
|
||||
|
@ -210,6 +211,7 @@ var (
|
|||
webClientLogoutPath string
|
||||
webClientForgotPwdPath string
|
||||
webClientResetPwdPath string
|
||||
webClientViewPDFPath string
|
||||
webStaticFilesPath string
|
||||
webOpenAPIPath string
|
||||
// max upload size for http clients, 1GB by default
|
||||
|
@ -570,6 +572,7 @@ func updateWebClientURLs(baseURL string) {
|
|||
webClientRecoveryCodesPath = path.Join(baseURL, webClientRecoveryCodesPathDefault)
|
||||
webClientForgotPwdPath = path.Join(baseURL, webClientForgotPwdPathDefault)
|
||||
webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)
|
||||
webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)
|
||||
}
|
||||
|
||||
func updateWebAdminURLs(baseURL string) {
|
||||
|
|
|
@ -153,6 +153,7 @@ const (
|
|||
webClientPubSharesPath = "/web/client/pubshares"
|
||||
webClientForgotPwdPath = "/web/client/forgot-password"
|
||||
webClientResetPwdPath = "/web/client/reset-password"
|
||||
webClientViewPDFPath = "/web/client/viewpdf"
|
||||
httpBaseURL = "http://127.0.0.1:8081"
|
||||
sftpServerAddr = "127.0.0.1:8022"
|
||||
smtpServerAddr = "127.0.0.1:3525"
|
||||
|
@ -9320,13 +9321,38 @@ func TestUserAPIKey(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebClientViewPDF(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, webClientViewPDFPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webClientViewPDFPath+"?path=test.pdf", nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebEditFile(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
testFile1 := "testfile1.txt"
|
||||
testFile2 := "testfile2"
|
||||
file1Size := int64(65536)
|
||||
file2Size := int64(655360)
|
||||
file2Size := int64(1048576 * 2)
|
||||
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile1), file1Size)
|
||||
assert.NoError(t, err)
|
||||
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile2), file2Size)
|
||||
|
|
|
@ -1223,10 +1223,10 @@ func (s *httpdServer) initializeRouter() {
|
|||
|
||||
router.Get(webClientLogoutPath, handleWebClientLogout)
|
||||
router.With(s.refreshCookie).Get(webClientFilesPath, handleClientGetFiles)
|
||||
router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientFilesPath, uploadUserFiles)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.refreshCookie).
|
||||
Get(webClientEditFilePath, handleClientEditFile)
|
||||
router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Patch(webClientFilesPath, renameUserFile)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
|
|
|
@ -44,6 +44,7 @@ const (
|
|||
templateClientEditFile = "editfile.html"
|
||||
templateClientShare = "share.html"
|
||||
templateClientShares = "shares.html"
|
||||
templateClientViewPDF = "viewpdf.html"
|
||||
pageClientFilesTitle = "My Files"
|
||||
pageClientSharesTitle = "Shares"
|
||||
pageClientProfileTitle = "My Profile"
|
||||
|
@ -99,11 +100,18 @@ type dirMapping struct {
|
|||
Href string
|
||||
}
|
||||
|
||||
type viewPDFPage struct {
|
||||
Title string
|
||||
URL string
|
||||
StaticURL string
|
||||
}
|
||||
|
||||
type editFilePage struct {
|
||||
baseClientPage
|
||||
CurrentDir string
|
||||
Path string
|
||||
Name string
|
||||
ReadOnly bool
|
||||
Data string
|
||||
}
|
||||
|
||||
|
@ -112,6 +120,7 @@ type filesPage struct {
|
|||
CurrentDir string
|
||||
DirsURL string
|
||||
DownloadURL string
|
||||
ViewPDFURL string
|
||||
CanAddFiles bool
|
||||
CanCreateDirs bool
|
||||
CanRename bool
|
||||
|
@ -229,6 +238,9 @@ func loadClientTemplates(templatesPath string) {
|
|||
resetPwdPaths := []string{
|
||||
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
|
||||
}
|
||||
viewPDFPaths := []string{
|
||||
filepath.Join(templatesPath, templateClientDir, templateClientViewPDF),
|
||||
}
|
||||
|
||||
filesTmpl := util.LoadTemplate(nil, filesPaths...)
|
||||
profileTmpl := util.LoadTemplate(nil, profilePaths...)
|
||||
|
@ -243,6 +255,7 @@ func loadClientTemplates(templatesPath string) {
|
|||
shareTmpl := util.LoadTemplate(nil, sharePaths...)
|
||||
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
|
||||
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
|
||||
viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)
|
||||
|
||||
clientTemplates[templateClientFiles] = filesTmpl
|
||||
clientTemplates[templateClientProfile] = profileTmpl
|
||||
|
@ -257,6 +270,7 @@ func loadClientTemplates(templatesPath string) {
|
|||
clientTemplates[templateClientShare] = shareTmpl
|
||||
clientTemplates[templateForgotPassword] = forgotPwdTmpl
|
||||
clientTemplates[templateResetPassword] = resetPwdTmpl
|
||||
clientTemplates[templateClientViewPDF] = viewPDFTmpl
|
||||
}
|
||||
|
||||
func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
|
||||
|
@ -391,12 +405,13 @@ func renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
|
|||
renderClientTemplate(w, templateClientMFA, data)
|
||||
}
|
||||
|
||||
func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string) {
|
||||
func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) {
|
||||
data := editFilePage{
|
||||
baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
|
||||
Path: fileName,
|
||||
Name: path.Base(fileName),
|
||||
CurrentDir: path.Dir(fileName),
|
||||
ReadOnly: readOnly,
|
||||
Data: fileData,
|
||||
}
|
||||
|
||||
|
@ -427,6 +442,7 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
|
|||
Error: error,
|
||||
CurrentDir: url.QueryEscape(dirName),
|
||||
DownloadURL: webClientDownloadZipPath,
|
||||
ViewPDFURL: webClientViewPDFPath,
|
||||
DirsURL: webClientDirsPath,
|
||||
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
||||
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
||||
|
@ -650,7 +666,8 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
|
|||
renderFilesPage(w, r, name, "", user)
|
||||
return
|
||||
}
|
||||
if status, err := downloadFile(w, r, connection, name, info); err != nil && status != 0 {
|
||||
inline := r.URL.Query().Get("inline") != ""
|
||||
if status, err := downloadFile(w, r, connection, name, info, inline); err != nil && status != 0 {
|
||||
if status > 0 {
|
||||
if status == http.StatusRequestedRangeNotSatisfiable {
|
||||
renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
|
||||
|
@ -723,7 +740,7 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
renderEditFilePage(w, r, name, b.String())
|
||||
renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient))
|
||||
}
|
||||
|
||||
func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -1035,3 +1052,19 @@ func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
renderClientResetPwdPage(w, "")
|
||||
}
|
||||
|
||||
func handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
name := r.URL.Query().Get("path")
|
||||
if name == "" {
|
||||
renderClientBadRequestPage(w, r, errors.New("no file specified"))
|
||||
return
|
||||
}
|
||||
name = util.CleanPath(name)
|
||||
data := viewPDFPage{
|
||||
Title: path.Base(name),
|
||||
URL: fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)),
|
||||
StaticURL: webStaticFilesPath,
|
||||
}
|
||||
renderClientTemplate(w, templateClientViewPDF, data)
|
||||
}
|
||||
|
|
|
@ -3546,6 +3546,12 @@ paths:
|
|||
description: Path to the file to download. It must be URL encoded, for example the path "my dir/àdir/file.txt" must be sent as "my%20dir%2F%C3%A0dir%2Ffile.txt"
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: inline
|
||||
required: false
|
||||
description: 'If set, the response will not have the Content-Disposition header set to `attachment`'
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
|
|
|
@ -202,6 +202,7 @@
|
|||
|
||||
function SearchCursor(doc, query, pos, options) {
|
||||
this.atOccurrence = false
|
||||
this.afterEmptyMatch = false
|
||||
this.doc = doc
|
||||
pos = pos ? doc.clipPos(pos) : Pos(0, 0)
|
||||
this.pos = {from: pos, to: pos}
|
||||
|
@ -237,21 +238,29 @@
|
|||
findPrevious: function() {return this.find(true)},
|
||||
|
||||
find: function(reverse) {
|
||||
var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))
|
||||
|
||||
// Implements weird auto-growing behavior on null-matches for
|
||||
// backwards-compatibility with the vim code (unfortunately)
|
||||
while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {
|
||||
var head = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
|
||||
if (this.afterEmptyMatch && this.atOccurrence) {
|
||||
// do not return the same 0 width match twice
|
||||
head = Pos(head.line, head.ch)
|
||||
if (reverse) {
|
||||
if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)
|
||||
else if (result.from.line == this.doc.firstLine()) result = null
|
||||
else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))
|
||||
head.ch--;
|
||||
if (head.ch < 0) {
|
||||
head.line--;
|
||||
head.ch = (this.doc.getLine(head.line) || "").length;
|
||||
}
|
||||
} else {
|
||||
if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)
|
||||
else if (result.to.line == this.doc.lastLine()) result = null
|
||||
else result = this.matches(reverse, Pos(result.to.line + 1, 0))
|
||||
head.ch++;
|
||||
if (head.ch > (this.doc.getLine(head.line) || "").length) {
|
||||
head.ch = 0;
|
||||
head.line++;
|
||||
}
|
||||
}
|
||||
if (CodeMirror.cmpPos(head, this.doc.clipPos(head)) != 0) {
|
||||
return this.atOccurrence = false
|
||||
}
|
||||
}
|
||||
var result = this.matches(reverse, head)
|
||||
this.afterEmptyMatch = result && CodeMirror.cmpPos(result.from, result.to) == 0
|
||||
|
||||
if (result) {
|
||||
this.pos = result
|
||||
|
|
20
static/vendor/codemirror/codemirror.css
vendored
20
static/vendor/codemirror/codemirror.css
vendored
|
@ -60,19 +60,13 @@
|
|||
.cm-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
.cm-fat-cursor-mark {
|
||||
background-color: rgba(20, 255, 20, 0.5);
|
||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||
-moz-animation: blink 1.06s steps(1) infinite;
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
}
|
||||
.cm-animate-fat-cursor {
|
||||
width: auto;
|
||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||
-moz-animation: blink 1.06s steps(1) infinite;
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
background-color: #7e7;
|
||||
}
|
||||
.cm-fat-cursor .CodeMirror-line::selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span::selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
|
||||
.cm-fat-cursor .CodeMirror-line::-moz-selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
|
||||
.cm-fat-cursor { caret-color: transparent; }
|
||||
@-moz-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
|
|
36
static/vendor/codemirror/codemirror.js
vendored
36
static/vendor/codemirror/codemirror.js
vendored
|
@ -2351,12 +2351,14 @@
|
|||
function mapFromLineView(lineView, line, lineN) {
|
||||
if (lineView.line == line)
|
||||
{ return {map: lineView.measure.map, cache: lineView.measure.cache} }
|
||||
for (var i = 0; i < lineView.rest.length; i++)
|
||||
{ if (lineView.rest[i] == line)
|
||||
{ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } }
|
||||
for (var i$1 = 0; i$1 < lineView.rest.length; i$1++)
|
||||
{ if (lineNo(lineView.rest[i$1]) > lineN)
|
||||
{ return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } }
|
||||
if (lineView.rest) {
|
||||
for (var i = 0; i < lineView.rest.length; i++)
|
||||
{ if (lineView.rest[i] == line)
|
||||
{ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } }
|
||||
for (var i$1 = 0; i$1 < lineView.rest.length; i$1++)
|
||||
{ if (lineNo(lineView.rest[i$1]) > lineN)
|
||||
{ return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } }
|
||||
}
|
||||
}
|
||||
|
||||
// Render a line into the hidden node display.externalMeasured. Used
|
||||
|
@ -3150,13 +3152,19 @@
|
|||
var curFragment = result.cursors = document.createDocumentFragment();
|
||||
var selFragment = result.selection = document.createDocumentFragment();
|
||||
|
||||
var customCursor = cm.options.$customCursor;
|
||||
if (customCursor) { primary = true; }
|
||||
for (var i = 0; i < doc.sel.ranges.length; i++) {
|
||||
if (!primary && i == doc.sel.primIndex) { continue }
|
||||
var range = doc.sel.ranges[i];
|
||||
if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue }
|
||||
var collapsed = range.empty();
|
||||
if (collapsed || cm.options.showCursorWhenSelecting)
|
||||
{ drawSelectionCursor(cm, range.head, curFragment); }
|
||||
if (customCursor) {
|
||||
var head = customCursor(cm, range);
|
||||
if (head) { drawSelectionCursor(cm, head, curFragment); }
|
||||
} else if (collapsed || cm.options.showCursorWhenSelecting) {
|
||||
drawSelectionCursor(cm, range.head, curFragment);
|
||||
}
|
||||
if (!collapsed)
|
||||
{ drawSelectionRange(cm, range, selFragment); }
|
||||
}
|
||||
|
@ -3174,9 +3182,8 @@
|
|||
|
||||
if (/\bcm-fat-cursor\b/.test(cm.getWrapperElement().className)) {
|
||||
var charPos = charCoords(cm, head, "div", null, null);
|
||||
if (charPos.right - charPos.left > 0) {
|
||||
cursor.style.width = (charPos.right - charPos.left) + "px";
|
||||
}
|
||||
var width = charPos.right - charPos.left;
|
||||
cursor.style.width = (width > 0 ? width : cm.defaultCharWidth()) + "px";
|
||||
}
|
||||
|
||||
if (pos.other) {
|
||||
|
@ -3649,6 +3656,7 @@
|
|||
this.vert.firstChild.style.height =
|
||||
Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
|
||||
} else {
|
||||
this.vert.scrollTop = 0;
|
||||
this.vert.style.display = "";
|
||||
this.vert.firstChild.style.height = "0";
|
||||
}
|
||||
|
@ -4501,7 +4509,7 @@
|
|||
function onScrollWheel(cm, e) {
|
||||
var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
|
||||
var pixelsPerUnit = wheelPixelsPerUnit;
|
||||
if (event.deltaMode === 0) {
|
||||
if (e.deltaMode === 0) {
|
||||
dx = e.deltaX;
|
||||
dy = e.deltaY;
|
||||
pixelsPerUnit = 1;
|
||||
|
@ -8235,7 +8243,7 @@
|
|||
}
|
||||
|
||||
function hiddenTextarea() {
|
||||
var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none");
|
||||
var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; min-height: 1em; outline: none");
|
||||
var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
|
||||
// The textarea is kept positioned near the cursor to prevent the
|
||||
// fact that it'll be scrolled into view on input from scrolling
|
||||
|
@ -9832,7 +9840,7 @@
|
|||
|
||||
addLegacyProps(CodeMirror);
|
||||
|
||||
CodeMirror.version = "5.63.1";
|
||||
CodeMirror.version = "5.64.0";
|
||||
|
||||
return CodeMirror;
|
||||
|
||||
|
|
1
static/vendor/lightbox2/css/lightbox.min.css
vendored
Normal file
1
static/vendor/lightbox2/css/lightbox.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.lb-loader,.lightbox{text-align:center;line-height:0;position:absolute;left:0}body.lb-disable-scrolling{overflow:hidden}.lightboxOverlay{position:absolute;top:0;left:0;z-index:9999;background-color:#000;filter:alpha(Opacity=80);opacity:.8;display:none}.lightbox{width:100%;z-index:10000;font-weight:400;outline:0}.lightbox .lb-image{display:block;height:auto;max-width:inherit;max-height:none;border-radius:3px;border:4px solid #fff}.lightbox a img{border:none}.lb-outerContainer{position:relative;width:250px;height:250px;margin:0 auto;border-radius:4px;background-color:#fff}.lb-outerContainer:after{content:"";display:table;clear:both}.lb-loader{top:43%;height:25%;width:100%}.lb-cancel{display:block;width:32px;height:32px;margin:0 auto;background:url(../images/loading.gif) no-repeat}.lb-nav{position:absolute;top:0;left:0;height:100%;width:100%;z-index:10}.lb-container>.nav{left:0}.lb-nav a{outline:0;background-image:url(data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)}.lb-next,.lb-prev{height:100%;cursor:pointer;display:block}.lb-nav a.lb-prev{width:34%;left:0;float:left;background:url(../images/prev.png) left 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-prev:hover{filter:alpha(Opacity=100);opacity:1}.lb-nav a.lb-next{width:64%;right:0;float:right;background:url(../images/next.png) right 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-next:hover{filter:alpha(Opacity=100);opacity:1}.lb-dataContainer{margin:0 auto;padding-top:5px;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.lb-dataContainer:after{content:"";display:table;clear:both}.lb-data{padding:0 4px;color:#ccc}.lb-data .lb-details{width:85%;float:left;text-align:left;line-height:1.1em}.lb-data .lb-caption{font-size:13px;font-weight:700;line-height:1em}.lb-data .lb-caption a{color:#4ae}.lb-data .lb-number{display:block;clear:left;padding-bottom:1em;font-size:12px;color:#999}.lb-data .lb-close{display:block;float:right;width:30px;height:30px;background:url(../images/close.png) top right no-repeat;text-align:right;outline:0;filter:alpha(Opacity=70);opacity:.7;-webkit-transition:opacity .2s;-moz-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.lb-data .lb-close:hover{cursor:pointer;filter:alpha(Opacity=100);opacity:1}
|
BIN
static/vendor/lightbox2/images/close.png
vendored
Normal file
BIN
static/vendor/lightbox2/images/close.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 280 B |
BIN
static/vendor/lightbox2/images/loading.gif
vendored
Normal file
BIN
static/vendor/lightbox2/images/loading.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
static/vendor/lightbox2/images/next.png
vendored
Normal file
BIN
static/vendor/lightbox2/images/next.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
static/vendor/lightbox2/images/prev.png
vendored
Normal file
BIN
static/vendor/lightbox2/images/prev.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
15
static/vendor/lightbox2/js/lightbox.min.js
vendored
Normal file
15
static/vendor/lightbox2/js/lightbox.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
static/vendor/pdfobject/pdfobject.min.js
vendored
Normal file
9
static/vendor/pdfobject/pdfobject.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -36,7 +36,9 @@
|
|||
<span class="font-weight-bold text-primary">Edit file "{{.Path}}"</span>
|
||||
<span class="btn-toolbar">
|
||||
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
|
||||
{{if not .ReadOnly}}
|
||||
<a id="idSave" class="btn btn-primary mx-1 my-1" href="#" onclick="saveFile()" role="button">Save</a>
|
||||
{{end}}
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
|
@ -107,6 +109,9 @@
|
|||
lineNumbers: true,
|
||||
styleActiveLine: true,
|
||||
extraKeys: {"Alt-F": "findPersistent"},
|
||||
{{if .ReadOnly}}
|
||||
readOnly: true,
|
||||
{{end}}
|
||||
autofocus: true
|
||||
});
|
||||
var filename = "{{.Path}}";
|
||||
|
@ -126,6 +131,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
{{if not .ReadOnly}}
|
||||
function saveFile() {
|
||||
$('#idSave').addClass("disabled");
|
||||
cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||
|
@ -167,5 +173,6 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
{{end}}
|
||||
</script>
|
||||
{{end}}
|
|
@ -8,6 +8,7 @@
|
|||
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/lightbox2/css/lightbox.min.css" rel="stylesheet">
|
||||
<style>
|
||||
div.dataTables_wrapper span.selected-info,
|
||||
div.dataTables_wrapper span.selected-item {
|
||||
|
@ -177,10 +178,10 @@
|
|||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.min.js"></script>
|
||||
{{if .CanAddFiles}}
|
||||
<script src="{{.StaticURL}}/vendor/lightbox2/js/lightbox.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/codemirror/codemirror.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/codemirror/meta.js"></script>
|
||||
{{end}}
|
||||
<script type="text/javascript">
|
||||
|
||||
function getIconForFile(filename) {
|
||||
|
@ -613,15 +614,37 @@
|
|||
{ "data": "edit_url",
|
||||
"render": function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
{{if .CanAddFiles}}
|
||||
var filename = row["name"];
|
||||
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
if (data != ""){
|
||||
var filename = row["name"];
|
||||
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
if (extension == "csv" || extension == "bat" || CodeMirror.findModeByExtension(extension) != null){
|
||||
{{if .CanAddFiles}}
|
||||
return `<a href="${data}"><i class="fas fa-edit"></i></a>`;
|
||||
{{else}}
|
||||
return `<a href="${data}"><i class="fas fa-eye"></i></a>`;
|
||||
{{end}}
|
||||
}
|
||||
}
|
||||
if (row["type"] == "2") {
|
||||
switch (extension) {
|
||||
case "jpeg":
|
||||
case "jpg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "webp":
|
||||
case "bmp":
|
||||
case "svg":
|
||||
case "ico":
|
||||
var view_url = row['url']+"&inline=1";
|
||||
return `<a href="${view_url}" data-lightbox="${filename}" data-title="${filename}"><i class="fas fa-eye"></i></a>`;
|
||||
case "pdf":
|
||||
if (PDFObject.supportsPDFs){
|
||||
var view_url = row['url'];
|
||||
view_url = view_url.replace('{{.FilesURL}}','{{.ViewPDFURL}}');
|
||||
return `<a href="${view_url}" target="_blank"><i class="fas fa-eye"></i></a>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
|
15
templates/webclient/viewpdf.html
Normal file
15
templates/webclient/viewpdf.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{.Title}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
PDFObject.embed("{{.URL}}", document.body);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue