PicList/src/renderer/pages/Gallery.vue

770 lines
20 KiB
Vue
Raw Normal View History

2017-12-06 22:26:29 -05:00
<template>
<div id="gallery-view">
<div class="view-title">
{{ $T('GALLERY') }} - {{ filterList.length }}
<el-icon
style="margin-left: 4px"
class="cursor-pointer"
@click="toggleHandleBar"
>
<CaretBottom v-show="!handleBarActive" />
<CaretTop v-show="handleBarActive" />
</el-icon>
<span
style="position: absolute; right: 0; top: 0; margin-right: 20px; font-size: 0.8em; color: #fff;"
>同步删除云端
<el-switch
v-model="deleteCloud"
:active-text="$T('SETTINGS_OPEN')"
:inactive-text="$T('SETTINGS_CLOSE')"
@change="handleDeleteCloudFile"
/></span>
2017-12-06 22:26:29 -05:00
</div>
<transition name="el-zoom-in-top">
<el-row v-show="handleBarActive">
<el-col
:span="20"
:offset="2"
>
<el-row
class="handle-bar"
:gutter="16"
>
<el-col :span="12">
<el-select
v-model="choosedPicBed"
multiple
collapse-tags
size="small"
style="width: 100%"
:placeholder="$T('CHOOSE_SHOWED_PICBED')"
>
<el-option
2018-12-23 10:15:00 -05:00
v-for="item in picBed"
:key="item.type"
:label="item.name"
:value="item.type"
/>
</el-select>
</el-col>
<el-col :span="12">
<el-select
v-model="pasteStyle"
size="small"
style="width: 100%"
:placeholder="$T('CHOOSE_PASTE_FORMAT')"
@change="handlePasteStyleChange"
>
<el-option
v-for="(value, key) in pasteStyleMap"
:key="key"
:label="key"
:value="value"
/>
</el-select>
</el-col>
</el-row>
<el-row
class="handle-bar"
:gutter="16"
>
<el-col :span="12">
<el-input
v-model="searchText"
:placeholder="$T('SEARCH')"
size="small"
>
<template #suffix>
<el-icon
class="el-input__icon"
style="cursor: pointer;"
@click="cleanSearch"
>
<close />
</el-icon>
</template>
2018-07-23 10:24:18 -04:00
</el-input>
</el-col>
<el-col :span="4">
<div
class="item-base copy round"
:class="{ active: isMultiple(choosedList) }"
@click="multiCopy"
>
{{ $T('COPY') }}
</div>
</el-col>
<el-col :span="4">
<div
class="item-base delete round"
:class="{ active: isMultiple(choosedList) }"
@click="multiRemove"
>
{{ $T('DELETE') }}
</div>
</el-col>
<el-col :span="4">
<div
class="item-base all-pick round"
:class="{ active: filterList.length > 0 }"
@click="toggleSelectAll"
>
{{ isAllSelected? $T('CANCEL'): $T('SELECT_ALL') }}
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</transition>
<el-row
class="gallery-list"
:class="{ small: handleBarActive }"
>
<el-col
:span="20"
:offset="2"
>
2017-12-06 22:26:29 -05:00
<el-row :gutter="16">
<photo-slider
:items="filterList"
:visible="gallerySliderControl.visible"
:index="gallerySliderControl.index"
:should-transition="true"
@change-index="zoomImage"
@click-mask="handleClose"
@close-modal="handleClose"
/>
<el-col
v-for="(item, index) in filterList"
:key="item.id"
:xs="12"
:sm="12"
:md="8"
:lg="3"
:xl="2"
class="gallery-list__img"
>
2019-12-19 06:17:21 -05:00
<div
2017-12-06 22:26:29 -05:00
class="gallery-list__item"
@click="zoomImage(index)"
>
<img
v-lazy="item.imgUrl"
class="gallery-list__item-img"
>
2017-12-06 22:26:29 -05:00
</div>
<div
class="gallery-list__file-name"
:title="item.fileName"
>
{{ item.fileName }}
</div>
<el-row
class="gallery-list__tool-panel"
justify="space-between"
align="middle"
>
<el-row>
<el-icon
class="cursor-pointer document"
@click="copy(item)"
>
<Document />
</el-icon>
<el-icon
class="cursor-pointer edit"
@click="openDialog(item)"
>
<Edit />
</el-icon>
<el-icon
class="cursor-pointer delete"
@click="remove(item)"
>
<Delete />
</el-icon>
</el-row>
<el-checkbox
v-model="choosedList[item.id ? item.id : '']"
@change="(val) => handleChooseImage(val, index)"
/>
</el-row>
2017-12-06 22:26:29 -05:00
</el-col>
</el-row>
</el-col>
</el-row>
2018-04-08 11:43:34 -04:00
<el-dialog
v-model="dialogVisible"
:title="$T('CHANGE_IMAGE_URL')"
2018-04-08 11:43:34 -04:00
width="500px"
:modal-append-to-body="false"
>
<el-input v-model="imgInfo.imgUrl" />
<template #footer>
<el-button @click="dialogVisible = false">
{{ $T('CANCEL') }}
</el-button>
<el-button
type="primary"
@click="confirmModify"
>
{{ $T('CONFIRM') }}
</el-button>
</template>
2018-04-08 11:43:34 -04:00
</el-dialog>
2017-12-06 22:26:29 -05:00
</div>
</template>
<script lang="ts" setup>
import type { IResult } from '@picgo/store/dist/types'
import { PASTE_TEXT, GET_PICBEDS } from '#/events/constants'
import { CheckboxValueType, ElMessageBox, ElNotification } from 'element-plus'
import { Close, CaretBottom, Document, Edit, Delete, CaretTop } from '@element-plus/icons-vue'
2019-12-19 06:17:21 -05:00
import {
ipcRenderer,
clipboard,
IpcRendererEvent
} from 'electron'
import { computed, nextTick, onActivated, onBeforeUnmount, onBeforeMount, reactive, ref, watch } from 'vue'
import { getConfig, saveConfig, sendToMain } from '@/utils/dataSender'
import { onBeforeRouteUpdate } from 'vue-router'
import { T as $T } from '@/i18n/index'
import $$db from '@/utils/db'
import ALLApi from '@/apis/allApi'
const images = ref<ImgInfo[]>([])
const dialogVisible = ref(false)
const imgInfo = reactive({
id: '',
imgUrl: ''
2019-12-19 06:17:21 -05:00
})
const $confirm = ElMessageBox.confirm
const choosedList: IObjT<boolean> = reactive({})
const gallerySliderControl = reactive({
visible: false,
index: 0
})
const deleteCloud = ref<boolean>(false)
const choosedPicBed = ref<string[]>([])
const lastChoosed = ref<number>(-1)
const isShiftKeyPress = ref<boolean>(false)
const searchText = ref<string>('')
const handleBarActive = ref<boolean>(false)
const pasteStyle = ref<string>('')
const pasteStyleMap = {
Markdown: 'markdown',
HTML: 'HTML',
URL: 'URL',
UBB: 'UBB',
Custom: 'Custom'
}
const picBed = ref<IPicBedType[]>([])
onBeforeRouteUpdate((to, from) => {
if (from.name === 'gallery') {
clearChoosedList()
2019-12-19 06:17:21 -05:00
}
if (to.name === 'gallery') {
updateGallery()
2019-12-19 06:17:21 -05:00
}
})
// init deleteCloud
async function initDeleteCloud () {
const config = await getConfig() as any
deleteCloud.value = config.settings.deleteCloudFile || false
}
onBeforeMount(async () => {
ipcRenderer.on('updateGallery', () => {
nextTick(async () => {
updateGallery()
})
})
sendToMain(GET_PICBEDS)
ipcRenderer.on(GET_PICBEDS, getPicBeds)
updateGallery()
document.addEventListener('keydown', handleDetectShiftKey)
document.addEventListener('keyup', handleDetectShiftKey)
})
function handleDetectShiftKey (event: KeyboardEvent) {
if (event.key === 'Shift') {
if (event.type === 'keydown') {
isShiftKeyPress.value = true
} else if (event.type === 'keyup') {
isShiftKeyPress.value = false
}
}
}
const filterList = computed(() => {
return getGallery()
})
const isAllSelected = computed(() => {
const values = Object.values(choosedList)
if (values.length === 0) {
return false
} else {
return filterList.value.every(item => {
return choosedList[item.id!]
})
}
})
function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
picBed.value = picBeds
}
function getGallery (): IGalleryItem[] {
if (searchText.value || choosedPicBed.value.length > 0) {
return images.value
.filter(item => {
let isInChoosedPicBed = true
let isIncludesSearchText = true
if (choosedPicBed.value.length > 0) {
isInChoosedPicBed = choosedPicBed.value.some(type => type === item.type)
}
if (searchText.value) {
isIncludesSearchText = item.fileName?.includes(searchText.value) || false
}
return isIncludesSearchText && isInChoosedPicBed
}).map(item => {
return {
...item,
src: item.imgUrl || '',
key: (item.id || `${Date.now()}`),
intro: item.fileName || ''
}
})
} else {
return images.value.map(item => {
return {
...item,
src: item.imgUrl || '',
key: (item.id || `${Date.now()}`),
intro: item.fileName || ''
}
})
2019-12-19 06:17:21 -05:00
}
}
async function updateGallery () {
images.value = (await $$db.get({ orderBy: 'desc' })).data
}
2021-07-26 12:15:11 -04:00
watch(() => filterList, () => {
clearChoosedList()
})
function handleChooseImage (val: CheckboxValueType, index: number) {
if (val === true) {
handleBarActive.value = true
if (lastChoosed.value !== -1 && isShiftKeyPress.value) {
const min = Math.min(lastChoosed.value, index)
const max = Math.max(lastChoosed.value, index)
for (let i = min + 1; i < max; i++) {
const id = filterList.value[i].id!
choosedList[id] = true
}
}
lastChoosed.value = index
}
}
function clearChoosedList () {
isShiftKeyPress.value = false
Object.keys(choosedList).forEach(key => {
choosedList[key] = false
})
lastChoosed.value = -1
}
function zoomImage (index: number) {
gallerySliderControl.index = index
gallerySliderControl.visible = true
changeZIndexForGallery(true)
}
function changeZIndexForGallery (isOpen: boolean) {
if (isOpen) {
// @ts-ignore
document.querySelector('.main-content.el-row').style.zIndex = 101
} else {
// @ts-ignore
document.querySelector('.main-content.el-row').style.zIndex = 10
2019-12-19 06:17:21 -05:00
}
}
function handleClose () {
gallerySliderControl.index = 0
gallerySliderControl.visible = false
changeZIndexForGallery(false)
}
async function copy (item: ImgInfo) {
item.config = JSON.parse(JSON.stringify(item.config))
const copyLink = await ipcRenderer.invoke(PASTE_TEXT, item)
const obj = {
title: $T('COPY_LINK_SUCCEED'),
body: copyLink
// sometimes will cause lagging
// icon: item.url || item.imgUrl
}
const myNotification = new Notification(obj.title, obj)
myNotification.onclick = () => {
return true
2019-12-19 06:17:21 -05:00
}
}
function remove (item: ImgInfo) {
if (item.id) {
$confirm($T('TIPS_REMOVE_LINK'), $T('TIPS_NOTICE'), {
confirmButtonText: $T('CONFIRM'),
cancelButtonText: $T('CANCEL'),
type: 'warning'
}).then(async () => {
const file = await $$db.getById(item.id!)
await $$db.removeById(item.id!)
const picBedsCanbeDeleted = ['smms', 'github', 'imgur', 'tcyun', 'aliyun', 'qiniu', 'upyun', 'aws-s3', 'webdavplist']
if (await getConfig('settings.deleteCloudFile')) {
if (item.type !== undefined && picBedsCanbeDeleted.includes(item.type)) {
setTimeout(() => {
ALLApi.delete(item).then((value: boolean) => {
if (value) {
ElNotification({
title: '通知',
message: `${item.fileName} 云端删除成功`,
type: 'success'
})
} else {
ElNotification({
title: '通知',
message: `${item.fileName} 云端删除失败`,
type: 'error'
})
}
})
}, 0)
}
}
sendToMain('removeFiles', [file])
const obj = {
title: $T('OPERATION_SUCCEED'),
body: ''
}
const myNotification = new Notification(obj.title, obj)
myNotification.onclick = () => {
2018-07-24 11:15:19 -04:00
return true
}
updateGallery()
}).catch((e) => {
console.log(e)
2019-12-19 06:17:21 -05:00
return true
})
2019-12-19 06:17:21 -05:00
}
}
function handleDeleteCloudFile (val: ICheckBoxValueType) {
saveConfig({
'settings.deleteCloudFile': val
})
}
function openDialog (item: ImgInfo) {
imgInfo.id = item.id!
imgInfo.imgUrl = item.imgUrl as string
dialogVisible.value = true
}
async function confirmModify () {
await $$db.updateById(imgInfo.id, {
imgUrl: imgInfo.imgUrl
})
const obj = {
title: $T('CHANGE_IMAGE_URL_SUCCEED'),
body: imgInfo.imgUrl
}
const myNotification = new Notification(obj.title, obj)
myNotification.onclick = () => {
return true
}
dialogVisible.value = false
updateGallery()
}
function cleanSearch () {
searchText.value = ''
}
function isMultiple (obj: IObj) {
return Object.values(obj).some(item => item)
}
function toggleSelectAll () {
const result = !isAllSelected.value
filterList.value.forEach(item => {
choosedList[item.id!] = result
})
}
function multiRemove () {
// choosedList -> { [id]: true or false }; true means choosed. false means not choosed.
const multiRemoveNumber = Object.values(choosedList).filter(item => item).length
if (multiRemoveNumber) {
$confirm($T('TIPS_WILL_REMOVE_CHOOSED_IMAGES', {
m: multiRemoveNumber
}), $T('TIPS_NOTICE'), {
confirmButtonText: $T('CONFIRM'),
cancelButtonText: $T('CANCEL'),
type: 'warning'
}).then(async () => {
const files: IResult<ImgInfo>[] = []
const imageIDList = Object.keys(choosedList)
const isDeleteCloudFile = await getConfig('settings.deleteCloudFile')
const picBedsCanbeDeleted = ['smms', 'github', 'imgur', 'tcyun', 'aliyun', 'qiniu', 'upyun']
if (isDeleteCloudFile) {
for (let i = 0; i < imageIDList.length; i++) {
const key = imageIDList[i]
if (choosedList[key]) {
const file = await $$db.getById<ImgInfo>(key)
if (file) {
if (file.type !== undefined && picBedsCanbeDeleted.includes(file.type)) {
setTimeout(() => {
ALLApi.delete(file).then((value: boolean) => {
if (value) {
ElNotification({
title: '通知',
message: `${file.fileName} 云端删除成功`,
type: 'success',
duration: multiRemoveNumber > 5 ? 1000 : 2000
})
} else {
ElNotification({
title: '通知',
message: `${file.fileName} 云端删除失败`,
type: 'error',
duration: multiRemoveNumber > 5 ? 1000 : 2000
})
}
})
}, 0)
}
files.push(file)
await $$db.removeById(key)
}
}
}
} else {
for (let i = 0; i < imageIDList.length; i++) {
const key = imageIDList[i]
if (choosedList[key]) {
const file = await $$db.getById<ImgInfo>(key)
if (file) {
files.push(file)
await $$db.removeById(key)
}
}
2019-12-19 06:17:21 -05:00
}
}
clearChoosedList()
// TODO: check this
// choosedList = {} // 只有删除才能将这个置空
2018-04-08 11:43:34 -04:00
const obj = {
title: $T('OPERATION_SUCCEED'),
body: ''
2018-04-08 11:43:34 -04:00
}
sendToMain('removeFiles', files)
2019-12-19 06:17:21 -05:00
const myNotification = new Notification(obj.title, obj)
2018-04-08 11:43:34 -04:00
myNotification.onclick = () => {
return true
}
updateGallery()
}).catch(() => {
return true
})
2019-12-19 06:17:21 -05:00
}
}
async function multiCopy () {
if (Object.values(choosedList).some(item => item)) {
const copyString: string[] = []
// choosedList -> { [id]: true or false }; true means choosed. false means not choosed.
const imageIDList = Object.keys(choosedList)
for (let i = 0; i < imageIDList.length; i++) {
const key = imageIDList[i]
if (choosedList[key]) {
const item = await $$db.getById<ImgInfo>(key)
if (item) {
const txt = await ipcRenderer.invoke(PASTE_TEXT, item)
copyString.push(txt)
choosedList[key] = false
}
}
}
const obj = {
title: $T('BATCH_COPY_LINK_SUCCEED'),
body: copyString.join('\n')
}
const myNotification = new Notification(obj.title, obj)
clipboard.writeText(copyString.join('\n'))
myNotification.onclick = () => {
return true
}
2019-12-19 06:17:21 -05:00
}
}
function toggleHandleBar () {
handleBarActive.value = !handleBarActive.value
}
async function handlePasteStyleChange (val: string) {
saveConfig('settings.pasteStyle', val)
pasteStyle.value = val
}
onBeforeUnmount(() => {
ipcRenderer.removeAllListeners('updateGallery')
ipcRenderer.removeListener(GET_PICBEDS, getPicBeds)
})
onActivated(async () => {
pasteStyle.value = (await getConfig('settings.pasteStyle')) || 'markdown'
initDeleteCloud()
})
</script>
<script lang="ts">
export default {
name: 'GalleryPage'
2017-12-06 22:26:29 -05:00
}
</script>
<style lang='stylus'>
.PhotoSlider
&__BannerIcon
&:nth-child(1)
display none
&__Counter
margin-top 20px
2017-12-06 22:26:29 -05:00
.view-title
color #eee
font-size 20px
text-align center
2018-04-28 04:22:35 -04:00
margin 10px auto
.sub-title
font-size 14px
.el-icon-caret-bottom
cursor: pointer
transition all .2s ease-in-out
&.active
transform: rotate(180deg)
2019-01-23 22:29:48 -05:00
#gallery-view
position absolute
left 140px
right 0
2019-01-23 22:29:48 -05:00
height 100%
.cursor-pointer
cursor pointer
2018-06-19 01:45:07 -04:00
.item-base
background #2E2E2E
text-align center
2018-06-20 08:45:12 -04:00
padding 5px 0
2018-06-19 01:45:07 -04:00
cursor pointer
font-size 13px
transition all .2s ease-in-out
height: 28px
box-sizing: border-box
2018-07-23 10:24:18 -04:00
&.copy
cursor not-allowed
background #49B1F5
&.active
cursor pointer
background #1B9EF3
color #fff
2018-06-19 01:45:07 -04:00
&.delete
cursor not-allowed
2018-06-20 08:45:12 -04:00
background #F47466
2018-07-23 10:24:18 -04:00
&.active
cursor pointer
background #F15140
color #fff
&.all-pick
cursor not-allowed
background #69C282
&.active
cursor pointer
background #44B363
color #fff
2017-12-06 22:26:29 -05:00
#gallery-view
.round
border-radius 14px
2018-06-19 01:45:07 -04:00
.pull-right
float right
2017-12-06 22:26:29 -05:00
.gallery-list
height 100%
2017-12-06 22:26:29 -05:00
box-sizing border-box
padding 8px 0
overflow-y auto
overflow-x hidden
position absolute
top: 38px
transition all .2s ease-in-out .1s
2018-07-06 11:54:36 -04:00
width 100%
&.small
height: 100%
top: 113px
2018-06-19 01:45:07 -04:00
&__img
// height 150px
2017-12-06 22:26:29 -05:00
position relative
margin-bottom 8px
2017-12-06 22:26:29 -05:00
&__item
width 100%
height 120px
transition all .2s ease-in-out
cursor pointer
margin-bottom 4px
overflow hidden
display flex
margin-bottom 6px
2017-12-06 22:26:29 -05:00
&-fake
position absolute
top 0
2019-12-19 06:17:21 -05:00
left 0
2017-12-06 22:26:29 -05:00
opacity 0
width 100%
z-index -1
&:hover
transform scale(1.1)
&-img
width 100%
object-fit contain
2017-12-06 22:26:29 -05:00
&__tool-panel
color #ddd
margin-bottom 4px
display flex
.el-checkbox
height 16px
2017-12-06 22:26:29 -05:00
i
cursor pointer
transition all .2s ease-in-out
margin-right 4px
&.document
2017-12-06 22:26:29 -05:00
&:hover
color #49B1F5
&.edit
2018-04-08 11:43:34 -04:00
&:hover
color #69C282
&.delete
2017-12-06 22:26:29 -05:00
&:hover
color #F15140
&__file-name
overflow hidden
text-overflow ellipsis
white-space nowrap
color #ddd
font-size 14px
margin-bottom 4px
2018-06-19 01:45:07 -04:00
.handle-bar
color #ddd
margin-bottom 10px
2019-12-19 06:17:21 -05:00
</style>