feat: doutu

This commit is contained in:
Jocs 2018-03-06 21:09:35 +08:00
parent fdf4873399
commit ca9fc76d2b
17 changed files with 586 additions and 15 deletions

View File

@ -1,9 +1,11 @@
### 0.6.10
### 0.6.11
**Features**
- Add **dark** theme and **light** theme in both realtime preview mode and source code mode.
- Insert `doutu` into the document, use CMD + / to open the panel.
**Optimization**
- Customize the scroll bar background color and thumb color.

16
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "marktext",
"version": "0.3.1",
"version": "0.6.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -550,11 +550,11 @@
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
},
"axios": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz",
"integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
"requires": {
"follow-redirects": "1.2.6",
"follow-redirects": "1.4.1",
"is-buffer": "1.1.6"
}
},
@ -5387,9 +5387,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.6.tgz",
"integrity": "sha512-FrMqZ/FONtHnbqO651UPpfRUVukIEwJhXMfdr/JWAmrDbeYBu773b1J6gdWDyRIj4hvvzQEHoEOTrdR8o6KLYA==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz",
"integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==",
"requires": {
"debug": "3.1.0"
}

View File

@ -1,6 +1,6 @@
{
"name": "marktext",
"version": "0.6.10",
"version": "0.6.11",
"author": "Jocs <luoran1988@126.com>",
"description": "A markdown editor",
"license": "MIT",
@ -68,7 +68,7 @@
}
},
"dependencies": {
"axios": "^0.16.1",
"axios": "^0.18.0",
"cheerio": "^1.0.0-rc.2",
"codemirror": "^5.31.0",
"css-tree": "^1.0.0-alpha.27",

View File

@ -175,6 +175,35 @@ const formatCtrl = ContentState => {
block.text = generator(tokens)
}
ContentState.prototype.insertAidou = function (url) {
const { start, end } = this.cursor
const { key, offset: startOffset } = start
const { offset: endOffset } = end
const block = this.getBlock(key)
const { text } = block
if (key !== end.key) {
block.text = text.substring(0, startOffset) + `![](${url})` + text.substring(startOffset)
const offset = startOffset + 2
this.cursor = {
start: { key, offset },
end: { key, offset }
}
} else {
block.text = text.substring(0, start.offset) + `![${text.substring(startOffset, endOffset)}](${url})` + text.substring(end.offset)
this.cursor = {
start: {
key,
offset: startOffset + 2
},
end: {
key,
offset: startOffset + 2 + text.substring(startOffset, endOffset).length
}
}
}
this.render()
}
ContentState.prototype.format = function (type) {
const { start, end } = selection.getCursorRange()
const startBlock = this.getBlock(start.key)

View File

@ -454,6 +454,10 @@ class Aganippe {
this.contentState.format(type)
}
insertAidou (url) {
this.contentState.insertAidou(url)
}
search (value, opt) {
const { selectHighlight } = opt
this.contentState.search(value, opt)

View File

@ -16,6 +16,9 @@ const createWindow = (pathname, options = {}) => {
const { x, y, width, height } = mainWindowState
const winOpt = Object.assign({ x, y, width, height }, {
webPreferences: {
webSecurity: false
},
useContentSize: true,
show: false,
frame: false,

View File

@ -58,5 +58,13 @@ export default {
click: (menuItem, browserWindow) => {
actions.edit(browserWindow, 'replace')
}
}, {
type: 'separator'
}, {
label: 'Aidou',
accelerator: 'CmdOrCtrl+/',
click: (menuItem, browserWindow) => {
actions.edit(browserWindow, 'aidou')
}
}]
}

View File

@ -5,14 +5,14 @@ export default {
submenu: [{
label: 'Dark',
type: 'radio',
checked: true,
checked: false,
click (menuItem, browserWindow) {
actions.selectTheme(browserWindow, 'dark')
}
}, {
label: 'Light',
type: 'radio',
checked: false,
checked: true,
click (menuItem, browserWindow) {
actions.selectTheme(browserWindow, 'light')
}

File diff suppressed because one or more lines are too long

View File

@ -54,11 +54,15 @@
</el-button>
</div>
</el-dialog>
<aidou
@select="handleSelect"
></aidou>
</div>
</template>
<script>
import Aganippe from '../../editor'
import aidou from './aidou/aidou.vue'
import bus from '../bus'
import { animatedScrollTo } from '../../editor/utils'
@ -69,6 +73,9 @@
]
export default {
components: {
aidou
},
props: {
typewriter: {
type: Boolean,
@ -166,6 +173,9 @@
})
},
methods: {
handleSelect (url) {
this.editor && this.editor.insertAidou(url)
},
handleSearch (value, opt) {
const searchMatches = this.editor.search(value, opt)
this.$store.dispatch('SEARCH', searchMatches)

View File

@ -0,0 +1,184 @@
<template>
<div class="aidou">
<el-dialog
:visible.sync="showAiDou"
:show-close="false"
:modal="true"
custom-class="ag-dialog-table"
width="610px"
>
<div slot="title" class="search-wrapper">
<svg class="icon" aria-hidden="true" @click="shuffle">
<use xlink:href="#icon-shuffle"></use>
</svg>
<input type="text" v-model="query" class="search" @keyup.13="search" ref="search">
<svg class="icon" aria-hidden="true" @click="search">
<use xlink:href="#icon-search"></use>
</svg>
</div>
<div class="image-container" ref="emojis">
<div class="img-wrapper" v-for="(emoji, index) of aiList" :key="index" @click="handleEmojiClick(emoji)">
<img :src="emoji.link" alt="" >
</div>
<loading v-if="aiLoading"></loading>
</div>
<!-- <div slot="footer" class="dialog-footer">
<el-button @click="showAiDou = false" size="mini">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-close"></use>
</svg>
</el-button>
</div> -->
</el-dialog>
</div>
</template>
<script>
import bus from '../../bus'
import loading from './loading.vue'
import { mapState } from 'vuex'
import hotWords from './hotWords'
import resource from '../../store/resource'
export default {
components: {
loading
},
data () {
return {
showAiDou: false,
query: '',
page: 1,
size: 24,
bindScroll: false
}
},
created () {
this.$nextTick(() => {
bus.$on('aidou', this.handleShowAiDou)
})
},
beforeDestroy () {
const container = this.$refs.emojis
container.removeEventListener('scroll', this.handlerScroll)
},
computed: {
...mapState([
'aiLoading', 'aiList'
])
},
methods: {
async handleEmojiClick ({ link }) {
try {
const base64 = await resource.fetchImgToBase64(link)
const { url } = await resource.sm(base64)
this.$emit('select', url)
this.showAiDou = false
} catch (err) {
// todo handle error
console.log(err)
}
},
handleShowAiDou () {
this.showAiDou = true
if (!this.bindScroll) {
this.$nextTick(() => {
const container = this.$refs.emojis
container.addEventListener('scroll', this.handlerScroll)
this.bindScroll = true
})
}
this.$nextTick(() => {
this.$refs.search.focus()
})
},
handlerScroll (event) {
const container = this.$refs.emojis
const { offsetHeight, scrollHeight, scrollTop } = container
if (scrollHeight - scrollTop - offsetHeight <= 100 && !this.aiLoading) {
this.loadMore()
}
},
search () {
const { query, size } = this
const page = this.page = 1
const type = 'search'
const params = { query, size, page }
this.$store.dispatch('AI_SEARCH', { params, type })
},
loadMore () {
const { query, size, page } = this
const params = { query, size, page: page + 1 }
this.$store.dispatch('AI_SEARCH', { params, type: 'loadMore' })
},
shuffle () {
const luckWord = hotWords[Math.random() * hotWords.length | 0]
this.query = luckWord
this.search()
}
}
}
</script>
<style scoped>
.search-wrapper {
max-width: 410px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
align-items: center;
height: 40px;
padding: 5px;
background: #fff;
box-shadow: 0 3px 8px rgba(0,0,0,.1);
border: 1px solid #eee;
border-radius: 3px;
}
.search {
width: 100%;
height: 30px;
outline: none;
border: none;
font-size: 14px;
padding: 0 8px;
margin: 0 10px;
color: #606266;
}
.search-wrapper svg {
cursor: pointer;
margin: 0 5px;
width: 30px;
height: 30px;
color: #606266;
}
.image-container {
height: 410px;
overflow: auto;
}
.image-container .img-wrapper {
overflow: hidden;
width: 130px;
height: 130px;
margin-right: 10px;
margin-bottom: 10px;
box-sizing: border-box;
border-radius: 3px;
cursor: pointer;
transition: all .25s ease-in-out;
display: inline-block;
}
.image-container .img-wrapper img {
width: 100%;
height: 100%;
}
.image-container .img-wrapper:hover {
transform: scale(1.05);
}
.image-container .img-wrapper:nth-of-type(4n) {
margin-right: 0;
}
.dialog-footer {
text-align: center;
}
</style>

View File

@ -0,0 +1,117 @@
export default [
'bug',
'代码有毒',
'大佬',
'hentai',
'变态',
'40米',
'哈哈',
'嘿嘿',
'滑稽',
'原谅',
'喜欢',
'秋裤',
'盒子精',
'吃鸡',
'阿卡林',
'元旦快乐',
'新年快乐',
'康娜',
'赞',
'2b',
'正面上我',
'bilibili',
'胖次',
'飞机',
'哲学',
'鸡鸡',
'2233',
'233',
'蕾姆',
'拉姆',
'niconiconi',
'图样图森破',
'非战斗人员',
'天依',
'绅士',
'今天的风儿',
'战5渣',
'楼上',
'楼下',
'女装',
'吸猫',
'二次元',
'新吧唧',
'poi',
'提督',
'咸鱼',
'滴滴',
'老司机',
'香菜',
'金馆长',
'笑',
'机智',
'撩',
'套路',
'洪荒之力',
'傲娇',
'一言不合',
'百合',
'白学家',
'电学',
'白色相册',
'诚哥',
'本子',
'香蕉君',
'金坷垃',
'舰娘',
'圣杯',
'呆毛',
'咖喱棒',
'金闪闪',
'吃土',
'小目标',
'FFF团',
'友谊的小船',
'狗带',
'没想到你是这样的',
'懵逼',
'我好方',
'辣眼睛',
'猴赛雷',
'吓死宝宝了',
'宝宝',
'咋不上天',
'重要的事',
'城会玩',
'A4腰',
'城会玩',
'厉害了我的哥',
'感觉身体被掏空',
'互相伤害',
'北京瘫',
'葛优躺',
'一颗赛艇',
'因吹斯汀',
'醒醒',
'社会我',
'萝莉',
'御姐',
'正太',
'Loli',
'单身狗',
'逗比',
'坟头草',
'你开心',
'裤子都脱了',
'算我输',
'修仙',
'老哥稳',
'奶子',
'小姐姐',
'石乐志',
'皮皮虾我们走',
'我们走',
'小拳拳',
'把我的意大利'
]

View File

@ -0,0 +1,94 @@
<template>
<div class="cpt-loading">
<div class="loader">
<span v-for="i in 3" :key="i" :style="dotSize"></span>
</div>
</div>
</template>
<script>
export default {
props: {
size: {
type: Number,
default: 14
}
},
computed: {
dotSize () {
const size = `${this.size}px`
return {
width: size,
height: size
}
}
}
}
</script>
<style scoped>
.cpt-loading {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
width: 100%;
height: 100%;
}
.loader {
width: 100%;
text-align: center;
}
.loader span {
position: absolute;
display: inline-block;
border-radius: 50%;
animation: 3s infinite linear;
}
.loader span:nth-child(1) {
background: #F2F6FC;
animation: kiri 1.2s infinite linear;
}
.loader span:nth-child(2) {
z-index: 100;
background: #EBEEF5;
}
.loader span:nth-child(3) {
background: #C0C4CC;
animation: kanan 1.2s infinite linear;
}
@keyframes kanan {
0% {
transform: translateX(20px);
}
50% {
transform: translateX(-20px);
}
100% {
z-index: 200;
transform: translateX(20px);
}
}
@keyframes kiri {
0% {
z-index: 200;
transform: translateX(-20px);
}
50% {
transform: translateX(20px);
}
100% {
transform: translateX(-20px);
}
}
</style>

View File

@ -0,0 +1,38 @@
import resource from './resource'
const state = {
aiLoading: false,
aiList: []
}
const mutations = {
SET_AI_LIST (state, { data, type }) {
console.log(type)
if (type === 'search') {
state.aiList = data
} else {
state.aiList = [...state.aiList, ...data]
}
},
SET_AI_STATUS (state, bool) {
state.aiLoading = bool
}
}
const actions = {
AI_SEARCH ({ commit }, { params, type }) {
commit('SET_AI_STATUS', true)
return resource.sogou(params)
.then(({ data, total }) => {
commit('SET_AI_LIST', { data, type })
commit('SET_AI_STATUS', false)
return { data, total }
})
.catch(err => {
console.log(err)
commit('SET_AI_STATUS', false)
})
}
}
export default { state, mutations, actions }

View File

@ -2,11 +2,13 @@ import Vue from 'vue'
import Vuex from 'vuex'
import editorStore from './editor'
import aidouStore from './aidou'
Vue.use(Vuex)
const storeArray = [
editorStore
editorStore,
aidouStore
]
const { actions, mutations, state } = storeArray.reduce((acc, s) => {

View File

@ -0,0 +1,63 @@
import axios from 'axios'
import { serialize, merge, dataURItoBlob } from '../util'
const CONFIG = {
SOGOU: {
API: 'https://pic.sogou.com/pics/json.jsp',
PARAMS: {
query: '',
st: 5,
start: 0,
xml_len: 100,
reqFrom: 'wap_result'
},
HOT_SEARCH: 'https://pic.sogou.com/pic/emo/'
}
}
const resource = {
sogou ({ query, page, size }) {
const api = CONFIG.SOGOU.API
const defParams = CONFIG.SOGOU.PARAMS
const params = merge(defParams, {
query: `${query} 表情`,
start: (page - 1) * size,
xml_len: size
})
const queryURL = `${api}?${serialize(params)}`
return axios.get(queryURL, { withCredentials: true }).then(({ data = {} }) => {
console.log(data)
return {
data: (data.items || []).map(it => ({
link: it.locImageLink,
suffix: it.type
})),
total: data.totalNum || 0
}
})
},
fetchImgToBase64 (url) {
return axios.get(url, { responseType: 'blob' })
.then(({ data }) => new Promise((resolve, reject) => {
const reader = new window.FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(data)
}))
},
sm (base64) {
const api = 'https://sm.ms/api/upload'
const data = new window.FormData()
data.append('smfile', dataURItoBlob(base64, 'temp.png'))
return axios.post(api, data).then(({ data }) => {
const { data: res } = data
return {
url: res.url,
err: '',
server: 'sm'
}
})
}
}
export default resource

View File

@ -0,0 +1,17 @@
export function serialize (params) {
return Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
}
export function merge (...args) {
return Object.assign({}, ...args)
}
export function dataURItoBlob (dataURI) {
const data = dataURI.split(';base64,')
const byte = window.atob(data[1])
const mime = data[0].split(':')[1]
const ab = new ArrayBuffer(byte.length)
const ia = new Uint8Array(ab)
for (let i = 0; i < byte.length; i++) {
ia[i] = byte.charCodeAt(i)
}
return new window.Blob([ab], { type: mime })
}