feat: svg download

This commit is contained in:
Anthony Fu 2020-07-18 01:00:33 +08:00
parent 5363cdffca
commit 0d191ee16f
19 changed files with 303 additions and 166 deletions

View File

@ -3,6 +3,7 @@
"@antfu/eslint-config-vue"
],
"rules": {
"vue/valid-v-model": "off"
"vue/valid-v-model": "off",
"vue/singleline-html-element-content-newline": "off"
}
}

View File

@ -6,6 +6,7 @@
<title>Iconify Explorer</title>
<script src="/lib/iconify.min.js"></script>
<script src="/lib/svg-packer.js" defer></script>
<script src="/lib/jszip.min.js" defer></script>
</head>
<body class="dragging">
<div id="app"></div>

View File

@ -17,14 +17,16 @@
"vue-router": "next"
},
"devDependencies": {
"svg-packer": "^0.0.3",
"@iconify/iconify": "^1.0.7",
"@antfu/eslint-config-vue": "^0.2.13",
"@iconify/iconify": "^1.0.7",
"@iconify/json": "^1.1.186",
"@types/fs-extra": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@vue/compiler-sfc": "^3.0.0-beta.20",
"eslint": "^7.4.0",
"fs-extra": "^9.0.1",
"jszip": "^3.5.0",
"svg-packer": "^0.0.3",
"tailwindcss-dark-mode": "^1.1.4",
"ts-node": "^8.10.2",
"typescript": "3.9.3",

51
pnpm-lock.yaml generated
View File

@ -11,9 +11,11 @@ importers:
'@iconify/iconify': 1.0.7
'@iconify/json': 1.1.189
'@types/fs-extra': 9.0.1
'@typescript-eslint/eslint-plugin': 3.6.1_eslint@7.4.0+typescript@3.9.3
'@vue/compiler-sfc': 3.0.0-beta.24_vue@3.0.0-beta.24
eslint: 7.4.0
fs-extra: 9.0.1
jszip: 3.5.0
svg-packer: 0.0.3
tailwindcss-dark-mode: 1.1.5
ts-node: 8.10.2_typescript@3.9.3
@ -24,11 +26,13 @@ importers:
'@iconify/iconify': ^1.0.7
'@iconify/json': ^1.1.186
'@types/fs-extra': ^9.0.1
'@typescript-eslint/eslint-plugin': ^3.6.1
'@vue/compiler-sfc': ^3.0.0-beta.20
'@vueuse/core': ^4.0.0-beta.2
eslint: ^7.4.0
fs-extra: ^9.0.1
fuse.js: ^6.3.0
jszip: ^3.5.0
svg-packer: ^0.0.3
tailwindcss: ^1.4.6
tailwindcss-dark-mode: ^1.1.4
@ -1372,6 +1376,28 @@ packages:
optional: true
resolution:
integrity: sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg==
/@typescript-eslint/eslint-plugin/3.6.1_eslint@7.4.0+typescript@3.9.3:
dependencies:
'@typescript-eslint/experimental-utils': 3.6.1_eslint@7.4.0+typescript@3.9.3
debug: 4.1.1
eslint: 7.4.0
functional-red-black-tree: 1.0.1
regexpp: 3.1.0
semver: 7.3.2
tsutils: 3.17.1_typescript@3.9.3
typescript: 3.9.3
dev: true
engines:
node: ^10.12.0 || >=12.0.0
peerDependencies:
'@typescript-eslint/parser': ^3.0.0
eslint: ^5.0.0 || ^6.0.0 || ^7.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
resolution:
integrity: sha512-06lfjo76naNeOMDl+mWG9Fh/a0UHKLGhin+mGaIw72FUMbMGBkdi/FEJmgEDzh4eE73KIYzHWvOCYJ0ak7nrJQ==
/@typescript-eslint/experimental-utils/3.1.0_eslint@7.4.0+typescript@3.9.3:
dependencies:
'@types/json-schema': 7.0.5
@ -5184,6 +5210,10 @@ packages:
node: '>= 4'
resolution:
integrity: sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
/immediate/3.0.6:
dev: true
resolution:
integrity: sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
/import-cwd/2.1.0:
dependencies:
import-from: 2.1.0
@ -5799,6 +5829,15 @@ packages:
dev: true
resolution:
integrity: sha1-dET9hVHd8+XacZj+oMkbyDCMwnQ=
/jszip/3.5.0:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.7
set-immediate-shim: 1.0.1
dev: true
resolution:
integrity: sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==
/keyboardevent-from-electron-accelerator/2.0.0:
dev: false
resolution:
@ -5973,6 +6012,12 @@ packages:
node: '>= 0.8.0'
resolution:
integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
/lie/3.3.0:
dependencies:
immediate: 3.0.6
dev: true
resolution:
integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
/lines-and-columns/1.1.6:
dev: true
resolution:
@ -8173,6 +8218,12 @@ packages:
dev: true
resolution:
integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
/set-immediate-shim/1.0.1:
dev: true
engines:
node: '>=0.10.0'
resolution:
integrity: sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
/set-value/2.0.1:
dependencies:
extend-shallow: 2.0.1

View File

@ -56,11 +56,16 @@ async function copyLibs() {
path.join(modules, 'svg-packer/dist/index.browser.js'),
path.join(out, 'lib/svg-packer.js')
)
await fs.copy(
path.join(modules, 'jszip/dist/jszip.min.js'),
path.join(out, 'lib/jszip.min.js')
)
}
async function prepare() {
await prepareJSON()
await copyLibs()
await prepareJSON()
}
prepare()

View File

@ -0,0 +1,111 @@
<template>
<div class="px-1 text-xl text-gray-800 flex">
<div class="relative w-4">
<IconButton class="ml-3 text-xl" active icon="carbon:overflow-menu-vertical" title="Menu" />
<select v-model="menu" class="absolute text-base top-0 bottom-0 left-0 right-0 opacity-0">
<optgroup label="Size">
<option value="large">Large Icons</option>
<option value="small">Small Icons</option>
<option value="list">List</option>
</optgroup>
<optgroup label="Actions">
<option value="select">Select mutiple</option>
</optgroup>
<optgroup label="Downloads">
<option value="download_iconfont">Iconfont</option>
<option value="download_svgs">SVGs Zip</option>
</optgroup>
</select>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent, PropType, ref, watch, nextTick } from 'vue'
import { iconSize, listType, selectingMode } from '../store'
import { CollectionMeta, install } from '../data'
import { PackIconFont, PackSvgZip } from '../utils/pack'
export default defineComponent({
props: {
collection: {
type: Object as PropType<CollectionMeta>,
},
},
setup(props) {
const menu = ref(
listType.value === 'list'
? 'list'
: iconSize.value === '2xl'
? 'small'
: 'large',
)
const packIconFont = async() => {
if (!props.collection)
return
// TODO: prompt user about size and time
// TODO: loading status
await install(props.collection.id)
await PackIconFont(
props.collection.icons.map(i => `${props.collection!.id}:${i}`),
{ fontName: props.collection.name, fileName: props.collection.id },
)
}
const packSvgs = async() => {
if (!props.collection)
return
// TODO: prompt user about size and time
// TODO: loading status
await install(props.collection.id)
await PackSvgZip(
props.collection.icons,
props.collection.id,
)
}
watch(
menu,
async(current, prev) => {
switch (current) {
case 'small':
iconSize.value = '2xl'
listType.value = 'grid'
return
case 'large':
iconSize.value = '4xl'
listType.value = 'grid'
return
case 'list':
iconSize.value = '3xl'
listType.value = 'list'
return
case 'select':
selectingMode.value = !selectingMode.value
break
case 'download_iconfont':
packIconFont()
break
case 'download_svgs':
packSvgs()
break
}
await nextTick()
menu.value = prev
},
{ flush: 'pre' },
)
return {
menu,
listType,
iconSize,
selectingMode,
}
},
})
</script>

View File

@ -11,16 +11,16 @@
</div>
</div>
<div class="flex-auto" />
<IconButton v-if='bags.length' class="text-xl mr-4 flex-none" icon="carbon:delete" @click="clear" />
<IconButton v-if="bags.length" class="text-xl mr-4 flex-none" icon="carbon:delete" @click="clear" />
<IconButton class="text-2xl flex-none" icon="carbon:close" @click="$emit('close')" />
</div>
<template v-if="bags.length">
<div class="flex-auto overflow-y-auto py-3 px-1">
<Icons :icons="bags" />
<Icons :icons="bags" style="color: #666" />
</div>
<div class="flex-none border-t border-gray-200 py-3 px-6 text-2xl text-gray-700">
<IconButton class="p-1 cursor-pointer" icon="carbon:function" text="Generate Icon Fonts" :active="true" @click="pack()" />
<IconButton class="p-1 cursor-pointer" icon="carbon:download" text="Download All SVGs" :active="true" @click="wip" />
<IconButton class="p-1 cursor-pointer" icon="carbon:function" text="Generate Icon Fonts" :active="true" @click="packIconFont" />
<IconButton class="p-1 cursor-pointer" icon="carbon:download" text="Download SVGs Zip" :active="true" @click="packSvgs" />
</div>
</template>
<template v-else>
@ -34,7 +34,7 @@
<script lang='ts'>
import { defineComponent } from 'vue'
import { bags, clearBag } from '../store'
import { PackIconsInBag } from '../utils/pack'
import { PackIconFont, PackSvgZip } from '../utils/pack'
export default defineComponent({
setup() {
@ -44,12 +44,26 @@ export default defineComponent({
clearBag()
}
const packIconFont = async() => {
// TODO: customzie
await PackIconFont(
bags.value,
)
}
const packSvgs = async() => {
// TODO: customzie
await PackSvgZip(
bags.value.map(i => i.replace(':', '-')),
'iconify-bags',
)
}
return {
clear,
bags,
pack: PackIconsInBag,
// eslint-disable-next-line no-alert
wip: () => alert('WIP'),
packIconFont,
packSvgs,
}
},
})

View File

@ -1,6 +1,6 @@
<template>
<div class="border-r border-gray-200">
<NavPlaceholder/>
<NavPlaceholder class="mb-4"/>
<!-- Collections -->
<router-link
v-for="collection in collections"

View File

@ -8,7 +8,7 @@
/>
<h1 class="text-base font-light py-2 m-auto flex-auto text-center">
<span v-if="collection"></span>
<template v-show="$route.path === '/'">
<template v-if="$route.path === '/'">
<b class="font-bold">Iconify</b>Explorer
</template>
</h1>

View File

@ -1,70 +0,0 @@
<template>
<div class="px-1 text-xl text-gray-800 flex">
<IconButton
class="ml-3"
icon="carbon:checkbox-checked"
:active="selectingMode"
@click="selectingMode = !selectingMode"
/>
<div class="mr-1 ml-4 h-full m-auto bg-gray-200 hidden sm:block" style="width:1px;" />
<IconButton
class="ml-3 hidden sm:block"
icon="carbon:hinton-plot"
title="Small"
:active="listType === 'grid' && iconSize === '2xl'"
@click="()=>setGrid('small')"
/>
<IconButton
class="ml-3 hidden sm:block"
icon="carbon:app-switcher"
title="Large"
:active="listType === 'grid' && iconSize === '4xl'"
@click="()=>setGrid('large')"
/>
<IconButton
class="ml-3 hidden sm:block"
icon="carbon:list"
title="List View"
:active="listType === 'list'"
@click="()=>setGrid('list')"
/>
</div>
</template>
<script lang='ts'>
import { defineComponent, PropType } from 'vue'
import { iconSize, listType, selectingMode } from '../store'
import { CollectionMeta } from '../data'
export default defineComponent({
props: {
collection: {
type: Object as PropType<CollectionMeta>
}
},
setup() {
const setGrid = (type: string) => {
switch (type) {
case 'small':
iconSize.value = '2xl'
listType.value = 'grid'
break
case 'large':
iconSize.value = '4xl'
listType.value = 'grid'
break
default:
iconSize.value = '3xl'
listType.value = 'list'
}
}
return {
setGrid,
listType,
iconSize,
selectingMode
}
}
})
</script>

15
src/global.d.ts vendored
View File

@ -1,8 +1,11 @@
interface Window {
Iconify: {
getSVG: (icon: string) => string | false
getSVGObject: (icon: string) => any
addCollection: (data: any) => void
/* eslint-disable no-undef */
import type JSZip from 'jszip'
import type Iconify from '@iconify/iconify'
declare global {
interface Window {
Iconify: typeof Iconify
JSZip: JSZip
SvgPacker: (options: any) => Promise<any>
}
SvgPacker: (options: any) => Promise<any>
}

View File

@ -16,7 +16,7 @@ import Footer from './components/Footer.vue'
import FAB from './components/FAB.vue'
import Drawer from './components/Drawer.vue'
import Bag from './components/Bag.vue'
import ViewControls from './components/ViewControls.vue'
import ActionsMenu from './components/ActionsMenu.vue'
import './main.css'
const app = createApp(App)
@ -54,7 +54,7 @@ app.component('Footer', Footer)
app.component('Drawer', Drawer)
app.component('FAB', FAB)
app.component('Bag', Bag)
app.component('ViewControls', ViewControls)
app.component('ActionsMenu', ActionsMenu)
app.component('NavPlaceholder', NavPlaceholder)
app.mount('#app')

View File

@ -30,27 +30,27 @@ export async function setCurrentCollection(id: string) {
loaded.value = false
installed.value = false
collection.value = null
return collection.value
return collection.value
}
loaded.value = isMetaLoaded(id)
installed.value = isInstalled(id)
if (!installed.value) {
if (!installed.value)
installed.value = await install(id)
}
if (id === 'all') {
const meta = await getFullMeta()
collection.value = {
id: 'all',
name: 'All',
icons: meta.flatMap((c) => c.icons.map((i) => `${c.id}:${i}`)),
icons: meta.flatMap(c => c.icons.map(i => `${c.id}:${i}`)),
}
} else {
}
else {
collection.value = await getMeta(id)
loaded.value = true
}
return collection.value
return collection.value
}

View File

@ -1,2 +1,2 @@
export * from './localstorage'
export * from './collection'
export * from './collection'

View File

@ -3,7 +3,7 @@ import Base64 from './base64'
const API_ENTRY = 'https://api.iconify.design'
export async function getSvg(icon: string) {
return window.Iconify.getSVG(icon) || await fetch(`${API_ENTRY}/${icon}.svg?inline=false&height=auto`).then(r => r.text()) || ''
return window.Iconify.getSVG(icon, undefined) || await fetch(`${API_ENTRY}/${icon}.svg?inline=false&height=auto`).then(r => r.text()) || ''
}
export async function getIconSnippet(icon: string, type: string): Promise<string | undefined> {

View File

@ -1,28 +1,50 @@
import { bags } from '../store'
import { getSvg } from './icons'
export async function PackIconsInBag(options: any = {}) {
if (!bags.value.length)
return
export async function LoadIconSvgs(icons: string[]) {
return await Promise.all(
icons
.filter(Boolean)
.sort()
.map(async(name) => {
return {
name,
svg: await getSvg(name),
}
}),
)
}
const icons = await Promise.all(bags.value.filter(Boolean).sort().map(async(name) => {
return {
name,
svg: await getSvg(name),
}
}))
export async function Download(url: string, name: string) {
const a = document.createElement('a')
a.href = url
a.download = name
a.click()
a.remove()
}
export async function PackIconFont(icons: string[], options: any = {}) {
if (!icons.length) return
const data = await LoadIconSvgs(icons)
const result = await window.SvgPacker({
fontName: 'Iconfiy Explorer Font',
fileName: 'iconfont',
cssPrefix: 'i',
...options,
icons,
icons: data,
})
const a = document.createElement('a')
a.href = result.zip.url
a.download = result.zip.name
a.click()
a.remove()
Download(result.zip.url, result.zip.name)
}
export async function PackSvgZip(icons: string[], name: string) {
if (!icons.length) return
const data = await LoadIconSvgs(icons)
const zip = new window.JSZip()
for (const { name, svg } of data)
zip.file(`${name}.svg`, svg)
const blob = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(blob)
Download(url, `${name}.zip`)
}

View File

@ -1,32 +1,26 @@
<template>
<WithNavbar v-if="!collection">
<div class="py-8 px-4 text-gray-700 text-center">Loading...</div>
<div class="py-8 px-4 text-gray-700 text-center">
Loading...
</div>
</WithNavbar>
<IconSet v-else :collection="collection"/>
<IconSet v-else :collection="collection" />
</template>
<script lang='ts'>
import { defineComponent, ref, watch, onUnmounted } from 'vue'
import {
isMetaLoaded,
isInstalled,
getFullMeta,
install,
getMeta,
CollectionMeta
} from '../data'
import IconSet from './IconSet.vue'
import { defineComponent, watch, onUnmounted } from 'vue'
import { useCurrentCollection, setCurrentCollection } from '../store'
import IconSet from './IconSet.vue'
export default defineComponent({
components: {
IconSet
IconSet,
},
props: {
id: {
type: String,
required: true
}
required: true,
},
},
setup(props) {
watch(
@ -34,7 +28,7 @@ export default defineComponent({
() => {
setCurrentCollection(props.id)
},
{ immediate: true }
{ immediate: true },
)
onUnmounted(() => {
@ -42,8 +36,8 @@ export default defineComponent({
})
return {
collection: useCurrentCollection()
collection: useCurrentCollection(),
}
}
},
})
</script>

View File

@ -2,14 +2,16 @@
<WithNavbar>
<div class="flex flex-auto h-full overflow-hidden">
<Drawer class="h-full overflow-auto flex-none hidden md:block" style="width:280px" />
<div class="py-5 px-5 md:px-8 h-full overflow-y-auto" v-if='collection'>
<div class="flex">
<div v-if="collection" class="py-5 px-5 md:px-8 h-full overflow-y-auto flex-auto overflow-x-hidden">
<div class="flex px-2">
<!-- Left -->
<div class="flex-auto px-2">
<NavPlaceholder class="md:hidden"/>
<NavPlaceholder class="md:hidden" />
<div class="text-gray-900 text-xl flex">
{{ collection.name }}
<div class="text-gray-900 text-xl flex select-none">
<div class="whitespace-no-wrap overflow-hidden">
{{ collection.name }}
</div>
<a
v-if="collection.url"
class="text-gray-500 hover:text-gray-900 mt-1 text-base"
@ -20,7 +22,9 @@
</a>
<div class="flex-auto" />
</div>
<div class="text-gray-500 text-xs block">{{ collection.author }}</div>
<div class="text-gray-500 text-xs block">
{{ collection.author }}
</div>
<div>
<a
class="text-gray-500 text-xs hover:text-gray-900"
@ -32,13 +36,13 @@
<!-- Right -->
<div class="flex flex-col">
<ViewControls :collection="collection" />
<ActionsMenu :collection="collection" />
<div class="flex-auto" />
</div>
</div>
<!-- Categories -->
<div class="py-2 pr-3 overflow-x-auto flex flex-no-wrap select-none">
<div class="py-2 px-1 pr-3 overflow-x-auto flex flex-no-wrap select-none">
<template v-if="collection.categories">
<div
v-for="c of Object.keys(collection.categories)"
@ -46,7 +50,9 @@
class="whitespace-no-wrap text-sm inline-block px-2 border border-gray-200 text-gray-500 rounded-full m-1 hover:bg-gray-100 cursor-pointer"
:class="c === category ? 'text-primary border-primary' : ''"
@click="toggleCategory(c)"
>{{ c }}</div>
>
{{ c }}
</div>
</template>
</div>
@ -62,8 +68,12 @@
style="color: #666"
@select="onSelect"
/>
<button v-if="icons.length > max" class="btn m-2" @click="loadMore">Load More</button>
<p class="text-gray-500 text-sm pt-4">{{ icons.length }} icons</p>
<button v-if="icons.length > max" class="btn m-2" @click="loadMore">
Load More
</button>
<p class="text-gray-500 text-sm pt-4">
{{ icons.length }} icons
</p>
</div>
<Footer />
@ -101,26 +111,17 @@
</template>
<script lang='ts'>
import { defineComponent, ref, toRefs, computed, PropType } from 'vue'
import {
iconSize,
listType,
selectingMode,
bags,
toggleBag
} from '../store'
import {CollectionMeta} from '../data'
import { useSearch } from '../hooks'
import { useRoute } from 'vue-router'
import { defineComponent, ref, computed, PropType } from 'vue'
import { iconSize, listType, selectingMode, bags, toggleBag, getSearchResults } from '../store'
import { CollectionMeta } from '../data'
import { isElectron } from '../env'
import { getSearchResults } from '../store'
export default defineComponent({
props: {
collection: {
type: Object as PropType<CollectionMeta>,
required: true
}
required: true,
},
},
setup(props) {
const { search, icons, category, collection } = getSearchResults()
@ -135,7 +136,9 @@ export default defineComponent({
}
const namespace = computed(() => {
return !collection.value || collection.value.id === 'all' ? '' : `${collection.value.id}:`
return !collection.value || collection.value.id === 'all'
? ''
: `${collection.value.id}:`
})
const onSelect = (icon: string) => {
@ -172,8 +175,8 @@ export default defineComponent({
// bags
showBag,
bags,
selectingMode
selectingMode,
}
}
},
})
</script>

View File

@ -4,10 +4,10 @@
<div
v-for="collection in collections"
:key="collection.id"
class="px-2 py-4 border-r border-b border-gray-200"
class="px-2 py-4 border-r border-b border-gray-200 relative"
>
<router-link
class="flex flex-col relative transition-all duration-300 text-gray-900 text-center justify-center hover:text-primary"
class="flex flex-col transition-all duration-300 text-gray-900 text-center justify-center hover:text-primary"
:to="`/collection/${collection.id}`"
>
<div class="flex-auto text-lg">{{ collection.name }}</div>
@ -25,12 +25,12 @@
spacing="m-1"
class="mt-2 mb-1 justify-center opacity-75 overflow-hidden flex-none pointer-events-none"
/>
<IconButton
</router-link>
<IconButton
v-if="isFavorited(collection.id)"
class="absolute top-0 right-0 p-2 text-lg"
icon="carbon:bookmark"
/>
</router-link>
</div>
</div>
<Footer />