feat:Support compress image view

This commit is contained in:
liwei 2020-06-06 18:55:01 +08:00
parent b8300b7121
commit 8af40d28a7
7 changed files with 346 additions and 165 deletions

View File

@ -1,19 +1,23 @@
<template>
<div class="item"
role="button"
tabindex="0"
draggable="true"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="click"
@dblclick="open"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected">
<div
class="item"
role="button"
tabindex="0"
draggable="true"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="click"
@dblclick="open"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected"
>
<div>
<i class="material-icons">{{ icon }}</i>
<img v-if="type==='image'" :src="compressUrl" />
<i v-else class="material-icons">{{ icon }}</i>
<!-- <i class="material-icons">{{ icon }}</i> -->
</div>
<div>
@ -30,140 +34,146 @@
</template>
<script>
import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import { files as api } from '@/api'
import { mapMutations, mapGetters, mapState } from "vuex";
import { baseURL } from "@/utils/constants";
import filesize from "filesize";
import moment from "moment";
import { files as api } from "@/api";
export default {
name: 'item',
data: function () {
name: "item",
data: function() {
return {
touches: 0
}
};
},
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
props: ["name", "isDir", "url", "type", "size", "modified", "index"],
computed: {
...mapState(['selected', 'req']),
...mapGetters(['selectedCount']),
isSelected () {
return (this.selected.indexOf(this.index) !== -1)
...mapState(["selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
icon () {
if (this.isDir) return 'folder'
if (this.type === 'image') return 'insert_photo'
if (this.type === 'audio') return 'volume_up'
if (this.type === 'video') return 'movie'
return 'insert_drive_file'
icon() {
if (this.isDir) return "folder";
if (this.type === "image") return "insert_photo";
if (this.type === "audio") return "volume_up";
if (this.type === "video") return "movie";
return "insert_drive_file";
},
canDrop () {
if (!this.isDir) return false
canDrop() {
if (!this.isDir) return false;
for (let i of this.selected) {
if (this.req.items[i].url === this.url) {
return false
return false;
}
}
return true
return true;
},
compressUrl() {
const path = this.url.replace(/^\/files\//, "");
return `${baseURL}/api/compress/${path}?auth=${this.jwt}&inline=true`;
}
},
methods: {
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
humanSize: function () {
return filesize(this.size)
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
humanSize: function() {
return filesize(this.size);
},
humanTime: function () {
return moment(this.modified).fromNow()
humanTime: function() {
return moment(this.modified).fromNow();
},
dragStart: function () {
dragStart: function() {
if (this.selectedCount === 0) {
this.addSelected(this.index)
return
this.addSelected(this.index);
return;
}
if (!this.isSelected) {
this.resetSelected()
this.addSelected(this.index)
this.resetSelected();
this.addSelected(this.index);
}
},
dragOver: function (event) {
if (!this.canDrop) return
dragOver: function(event) {
if (!this.canDrop) return;
event.preventDefault()
let el = event.target
event.preventDefault();
let el = event.target;
for (let i = 0; i < 5; i++) {
if (!el.classList.contains('item')) {
el = el.parentElement
if (!el.classList.contains("item")) {
el = el.parentElement;
}
}
el.style.opacity = 1
el.style.opacity = 1;
},
drop: function (event) {
if (!this.canDrop) return
event.preventDefault()
drop: function(event) {
if (!this.canDrop) return;
event.preventDefault();
if (this.selectedCount === 0) return
if (this.selectedCount === 0) return;
let items = []
let items = [];
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
to: this.url + this.req.items[i].name
})
});
}
api.move(items)
api
.move(items)
.then(() => {
this.$store.commit('setReload', true)
this.$store.commit("setReload", true);
})
.catch(this.$showError)
.catch(this.$showError);
},
click: function (event) {
if (this.selectedCount !== 0) event.preventDefault()
click: function(event) {
if (this.selectedCount !== 0) event.preventDefault();
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index)
return
this.removeSelected(this.index);
return;
}
if (event.shiftKey) {
let fi = 0
let la = 0
let fi = 0;
let la = 0;
if (this.index > this.selected[0]) {
fi = this.selected[0] + 1
la = this.index
fi = this.selected[0] + 1;
la = this.index;
} else {
fi = this.index
la = this.selected[0] - 1
fi = this.index;
la = this.selected[0] - 1;
}
for (; fi <= la; fi++) {
this.addSelected(fi)
this.addSelected(fi);
}
return
return;
}
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected();
this.addSelected(this.index);
},
touchstart () {
touchstart() {
setTimeout(() => {
this.touches = 0
}, 300)
this.touches = 0;
}, 300);
this.touches++
this.touches++;
if (this.touches > 1) {
this.open()
this.open();
}
},
open: function () {
this.$router.push({path: this.url})
open: function() {
this.$router.push({ path: this.url });
}
}
}
};
</script>

View File

@ -1,7 +1,13 @@
<template>
<div id="previewer">
<div class="bar">
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
<button
@click="back"
class="action"
:title="$t('files.closePreview')"
:aria-label="$t('files.closePreview')"
id="close"
>
<i class="material-icons">close</i>
</button>
@ -11,10 +17,22 @@
<info-button></info-button>
</div>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<button
class="action"
@click="prev"
v-show="hasPrevious"
:aria-label="$t('buttons.previous')"
:title="$t('buttons.previous')"
>
<i class="material-icons">chevron_left</i>
</button>
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<button
class="action"
@click="next"
v-show="hasNext"
:aria-label="$t('buttons.next')"
:title="$t('buttons.next')"
>
<i class="material-icons">chevron_right</i>
</button>
@ -27,39 +45,39 @@
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="'Subtitle ' + index" :default="index === 0">
Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download">download it</a>
:label="'Subtitle ' + index"
:default="index === 0"
/>Sorry, your browser doesn't support embedded videos,
but don't worry, you can
<a :href="download">download it</a>
and watch it with your favorite video player!
</video>
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
<a v-else-if="req.type == 'blob'" :href="download">
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
<h2 class="message">
{{ $t('buttons.download') }}
<i class="material-icons">file_download</i>
</h2>
</a>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import { baseURL } from '@/utils/constants'
import { files as api } from '@/api'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download'
import ExtendedImage from './ExtendedImage'
import { mapState } from "vuex";
import url from "@/utils/url";
import { baseURL } from "@/utils/constants";
import { files as api } from "@/api";
import InfoButton from "@/components/buttons/Info";
import DeleteButton from "@/components/buttons/Delete";
import RenameButton from "@/components/buttons/Rename";
import DownloadButton from "@/components/buttons/Download";
import ExtendedImage from "./ExtendedImage";
const mediaTypes = [
"image",
"video",
"audio",
"blob"
]
const mediaTypes = ["image", "video", "audio", "blob"];
export default {
name: 'preview',
name: "preview",
components: {
InfoButton,
DeleteButton,
@ -67,94 +85,105 @@ export default {
DownloadButton,
ExtendedImage
},
data: function () {
data: function() {
return {
previousLink: '',
nextLink: '',
previousLink: "",
nextLink: "",
listing: null,
subtitles: []
}
};
},
computed: {
...mapState(['req', 'user', 'oldReq', 'jwt']),
hasPrevious () {
return (this.previousLink !== '')
...mapState(["req", "user", "oldReq", "jwt"]),
hasPrevious() {
return this.previousLink !== "";
},
hasNext () {
return (this.nextLink !== '')
hasNext() {
return this.nextLink !== "";
},
download () {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
download() {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`;
},
raw () {
return `${this.download}&inline=true`
compress() {
if (this.type === 'image') {
return `${baseURL}/api/compress${this.req.path}?auth=${this.jwt}`;
} else {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`;
}
},
raw() {
return `${this.compress}&inline=true`;
}
},
async mounted () {
window.addEventListener('keyup', this.key)
async mounted() {
window.addEventListener("keyup", this.key);
if (this.req.subtitles) {
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
this.subtitles = this.req.subtitles.map(
sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`
);
}
try {
if (this.oldReq.items) {
this.updateLinks(this.oldReq.items)
this.updateLinks(this.oldReq.items);
} else {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.updateLinks(res.items)
const path = url.removeLastDir(this.$route.path);
const res = await api.fetch(path);
this.updateLinks(res.items);
}
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
beforeDestroy () {
window.removeEventListener('keyup', this.key)
beforeDestroy() {
window.removeEventListener("keyup", this.key);
},
methods: {
back () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
back() {
let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });
},
prev () {
this.$router.push({ path: this.previousLink })
prev() {
this.$router.push({ path: this.previousLink });
},
next () {
this.$router.push({ path: this.nextLink })
next() {
this.$router.push({ path: this.nextLink });
},
key (event) {
event.preventDefault()
key(event) {
event.preventDefault();
if (event.which === 13 || event.which === 39) { // right arrow
if (this.hasNext) this.next()
} else if (event.which === 37) { // left arrow
if (this.hasPrevious) this.prev()
if (event.which === 13 || event.which === 39) {
// right arrow
if (this.hasNext) this.next();
} else if (event.which === 37) {
// left arrow
if (this.hasPrevious) this.prev();
}
},
updateLinks (items) {
updateLinks(items) {
for (let i = 0; i < items.length; i++) {
if (items[i].name !== this.req.name) {
continue
continue;
}
for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(items[j].type)) {
this.previousLink = items[j].url
break
this.previousLink = items[j].url;
break;
}
}
for (let j = i + 1; j < items.length; j++) {
if (mediaTypes.includes(items[j].type)) {
this.nextLink = items[j].url
break
this.nextLink = items[j].url;
break;
}
}
return
return;
}
}
}
}
};
</script>

View File

@ -1,6 +1,6 @@
#listing h2 {
margin: 0 0 0 0.5em;
font-size: .9em;
font-size: 0.9em;
color: rgba(0, 0, 0, 0.38);
font-weight: 500;
}
@ -10,7 +10,7 @@
overflow: hidden;
}
#listing>div {
#listing > div {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
@ -22,7 +22,7 @@
display: flex;
flex-wrap: nowrap;
color: #6f6f6f;
transition: .1s ease background, .1s ease opacity;
transition: 0.1s ease background, 0.1s ease opacity;
align-items: center;
cursor: pointer;
}
@ -52,6 +52,13 @@
vertical-align: bottom;
}
#listing .item img {
width: 4em;
height: 4em;
margin-right: 0.1em;
vertical-align: bottom;
}
.message {
text-align: center;
font-size: 2em;
@ -64,7 +71,7 @@
.message i {
font-size: 2.5em;
margin-bottom: .2em;
margin-bottom: 0.2em;
display: block;
}
@ -75,14 +82,14 @@
#listing.mosaic .item {
width: calc(33% - 1em);
margin: .5em;
margin: 0.5em;
padding: 0.5em;
border-radius: 0.2em;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.12);
}
#listing.mosaic .item:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) !important;
}
#listing.mosaic .header {
@ -116,7 +123,7 @@
display: none;
}
#listing .item[aria-selected=true] {
#listing .item[aria-selected="true"] {
background: var(--blue) !important;
color: #fff !important;
}
@ -129,6 +136,11 @@
font-size: 2em;
}
#listing.list .item div:first-of-type img {
width: 2em;
height: 2em;
}
#listing.list .item div:last-of-type {
width: calc(100% - 3em);
display: flex;
@ -151,19 +163,19 @@
#listing.list .header i {
font-size: 1.5em;
vertical-align: middle;
margin-left: .2em;
margin-left: 0.2em;
}
#listing.list .item.header {
display: flex !important;
background: #fafafa;
z-index: 999;
padding: .85em;
padding: 0.85em;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
#listing.list .item.header>div:first-child {
#listing.list .item.header > div:first-child {
width: 0;
}
@ -175,7 +187,7 @@
color: inherit;
}
#listing.list .item.header>div:first-child {
#listing.list .item.header > div:first-child {
width: 0;
}
@ -193,7 +205,7 @@
#listing.list .header i {
opacity: 0;
transition: .1s ease all;
transition: 0.1s ease all;
}
#listing.list .header p:hover i,
@ -215,7 +227,7 @@
height: 4em;
padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between;
transition: .2s ease bottom;
transition: 0.2s ease bottom;
}
#listing #multiple-selection.active {

1
go.mod
View File

@ -16,6 +16,7 @@ require (
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
github.com/mholt/archiver v3.1.1+incompatible
github.com/mitchellh/go-homedir v1.1.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/nwaples/rardecode v1.0.0 // indirect
github.com/pelletier/go-toml v1.6.0
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 // indirect

2
go.sum
View File

@ -141,6 +141,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=

View File

@ -59,6 +59,7 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
api.PathPrefix("/compress").Handler(monkey(compressHandler, "/api/compress")).Methods("GET")
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")

126
http/preview.go Normal file
View File

@ -0,0 +1,126 @@
package http
import (
"bytes"
"errors"
"fmt"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/nfnt/resize"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"mime"
"net/http"
"net/url"
)
var compressHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
if file.IsDir || file.Type != "image" {
return http.StatusNotFound, nil
}
return compressFileHandler(w, r, file)
})
func compressFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
fd, err := file.Fs.Open(file.Path)
if err != nil {
return http.StatusInternalServerError, err
}
defer fd.Close()
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
// As per RFC6266 section 4.3
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
}
buf, err := compressImageHandler(file, fd)
if err != nil {
return errToStatus(err), err
}
w.Header().Add("Content-Length", fmt.Sprintf("%d", buf.Len()))
w.Header().Add("Content-Type", mime.TypeByExtension(file.Extension))
io.Copy(w, buf)
return 0, nil
}
func compressImageHandler(file *files.FileInfo, fd io.Reader) (*bytes.Buffer, error) {
var (
buf *bytes.Buffer
m image.Image
err error
)
switch file.Extension {
case ".jpg", ".jpeg":
buf, m, err = compressImage(jpeg.Decode, fd)
if err != nil {
return nil, err
}
err = jpeg.Encode(buf, m, nil)
break
case ".png":
buf, m, err = compressImage(png.Decode, fd)
if err != nil {
return nil, err
}
err = png.Encode(buf, m)
break
case ".gif":
buf, m, err = compressImage(gif.Decode, fd)
if err != nil {
return nil, err
}
err = gif.Encode(buf, m, nil)
break
default:
return nil, errors.New("extension is not supported")
}
if err != nil {
return nil, err
}
return buf, nil
}
const maxSize = 1080
func compressImage(decode func(r io.Reader) (image.Image, error), fd io.Reader) (*bytes.Buffer, image.Image, error) {
img, err := decode(fd)
if err != nil {
return nil, nil, err
}
buf := bytes.NewBuffer([]byte{})
width := img.Bounds().Dx()
height := img.Bounds().Dy()
if width > maxSize && width > height {
width = maxSize
height = 0
} else if height > maxSize && height > width {
width = 0
height = maxSize
} else {
width = 0
height = 0
}
m := resize.Resize(uint(width), uint(height), img, resize.Lanczos3)
return buf, m, nil
}