yay emoji!!!
@ -15,6 +15,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve --mode development",
|
"serve": "vue-cli-service serve --mode development",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
|
"build:emoji": "ts-node --files --project tools/tsconfig.json tools/emoji.ts",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -22,6 +23,8 @@
|
|||||||
"animejs": "^3.1.0",
|
"animejs": "^3.1.0",
|
||||||
"axios": "^0.19.1",
|
"axios": "^0.19.1",
|
||||||
"date-fns": "^2.9.0",
|
"date-fns": "^2.9.0",
|
||||||
|
"emoji-datasource": "^5.0.1",
|
||||||
|
"emojilib": "^2.4.0",
|
||||||
"eventemitter3": "^4.0.0",
|
"eventemitter3": "^4.0.0",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"simple-markdown": "^0.7.2",
|
"simple-markdown": "^0.7.2",
|
||||||
@ -37,6 +40,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/animejs": "^3.1.0",
|
"@types/animejs": "^3.1.0",
|
||||||
|
"@types/node": "^13.7.0",
|
||||||
"@types/vue": "^2.0.0",
|
"@types/vue": "^2.0.0",
|
||||||
"@vue/cli-plugin-babel": "^4.1.0",
|
"@vue/cli-plugin-babel": "^4.1.0",
|
||||||
"@vue/cli-plugin-eslint": "^4.1.0",
|
"@vue/cli-plugin-eslint": "^4.1.0",
|
||||||
@ -51,6 +55,7 @@
|
|||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"sass-loader": "^8.0.0",
|
"sass-loader": "^8.0.0",
|
||||||
|
"ts-node": "^8.6.2",
|
||||||
"typescript": "~3.5.3",
|
"typescript": "~3.5.3",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
}
|
}
|
||||||
|
1
client/public/emoji.json
Normal file
BIN
client/src/assets/images/emoji/neko.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
@ -12,7 +12,7 @@
|
|||||||
@import "vendor/swal";
|
@import "vendor/swal";
|
||||||
@import "vendor/tooltip";
|
@import "vendor/tooltip";
|
||||||
@import "vendor/github";
|
@import "vendor/github";
|
||||||
@import "vendor/emoji_20";
|
@import "vendor/emoji";
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
-webkit-font-smoothing: subpixel-antialiased;
|
-webkit-font-smoothing: subpixel-antialiased;
|
||||||
|
1726
client/src/assets/styles/vendor/_emoji.scss
vendored
Normal file
5185
client/src/assets/styles/vendor/_emoji_20.scss
vendored
@ -3,7 +3,7 @@
|
|||||||
<div class="window">
|
<div class="window">
|
||||||
<div class="loading" v-if="loading">
|
<div class="loading" v-if="loading">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="@/assets/logo.svg" alt="n.eko" />
|
<img src="@/assets/images/logo.svg" alt="n.eko" />
|
||||||
<span><b>N</b>.EKO</span>
|
<span><b>N</b>.EKO</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="loader">
|
<div class="loader">
|
||||||
|
@ -42,9 +42,8 @@
|
|||||||
v-model="content"
|
v-model="content"
|
||||||
@click.stop.prevent="emoji = false"
|
@click.stop.prevent="emoji = false"
|
||||||
/>
|
/>
|
||||||
<div class="emoji" @click.stop.prevent="emoji = !emoji">
|
|
||||||
<neko-emoji v-if="emoji" @picked="onEmojiPicked" />
|
<neko-emoji v-if="emoji" @picked="onEmojiPicked" />
|
||||||
</div>
|
<i class="emoji-menu fas fa-laugh" @click.stop.prevent="emoji = !emoji"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,6 +100,8 @@
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
||||||
&.message {
|
&.message {
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
.author {
|
.author {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -134,7 +135,6 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: $text-normal;
|
color: $text-normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
@ -152,7 +152,7 @@
|
|||||||
|
|
||||||
::v-deep .content-body {
|
::v-deep .content-body {
|
||||||
color: $text-normal;
|
color: $text-normal;
|
||||||
line-height: 20px;
|
line-height: 22px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
@ -273,14 +273,15 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba($color: #fff, $alpha: 0.05);
|
background-color: rgba($color: #fff, $alpha: 0.05);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.emoji {
|
.emoji-menu {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
// background: #fff;
|
font-size: 20px;
|
||||||
margin: 3px 3px 0 0;
|
margin: 8px 5px 0 0;
|
||||||
position: relative;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="connect">
|
<div class="connect">
|
||||||
<div class="window">
|
<div class="window">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="@/assets/logo.svg" alt="n.eko" />
|
<img src="@/assets/images/logo.svg" alt="n.eko" />
|
||||||
<span><b>n</b>.eko</span>
|
<span><b>n</b>.eko</span>
|
||||||
</div>
|
</div>
|
||||||
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">
|
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">
|
||||||
|
@ -6,39 +6,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list" ref="scroll" @scroll="onScroll">
|
<div class="list" ref="scroll" @scroll="onScroll">
|
||||||
<ul :class="[search === '' ? 'group-list' : 'emoji-list']">
|
<ul :class="['group-list']" :style="{ display: search === '' ? 'flex' : 'none' }">
|
||||||
<template v-if="search === ''">
|
|
||||||
<li v-for="(group, index) in groups" :key="index" class="group" ref="groups">
|
<li v-for="(group, index) in groups" :key="index" class="group" ref="groups">
|
||||||
<span class="label">{{ group.name }}</span>
|
<span class="label">{{ group.name }}</span>
|
||||||
<ul class="emoji-list">
|
<ul class="emoji-list">
|
||||||
<li
|
<li
|
||||||
v-for="emoji in group.list"
|
v-for="emoji in index === 0 ? recent : group.list"
|
||||||
:key="`${group.id}-${emoji}`"
|
:key="`${group.id}-${emoji}`"
|
||||||
:class="['emoji', hovered === emoji ? 'active' : '']"
|
:class="['emoji-container', hovered === emoji ? 'active' : '']"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:class="['emoji-20', `e-${emoji}`]"
|
:class="['emoji']"
|
||||||
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
||||||
@click.stop.prevent="onClick($event, emoji)"
|
@click.stop.prevent="onClick($event, emoji)"
|
||||||
|
:data-emoji="emoji"
|
||||||
></span>
|
></span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</ul>
|
||||||
<template v-else>
|
<ul :class="['emoji-container']" :style="{ display: search === '' ? 'none' : 'flex' }">
|
||||||
<li v-for="emoji in filtered" :key="emoji" :class="['emoji', hovered === emoji ? 'active' : '']">
|
<li v-for="emoji in filtered" :key="emoji" :class="['emoji-item', hovered === emoji ? 'active' : '']">
|
||||||
<span
|
<span
|
||||||
:class="['emoji-20', `e-${emoji}`]"
|
:class="['emoji']"
|
||||||
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
||||||
@click.stop.prevent="onClick($event, emoji)"
|
@click.stop.prevent="onClick($event, emoji)"
|
||||||
|
:data-emoji="emoji"
|
||||||
></span>
|
></span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div class="icon-container" v-if="hovered !== ''">
|
<div class="details-container" v-if="hovered !== ''">
|
||||||
<span :class="['icon', 'emoji-20', `e-${hovered}`]"></span><span class="emoji">:{{ hovered }}:</span>
|
<span :class="['emoji']" :data-emoji="hovered" /><span class="emoji-id">:{{ hovered }}:</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="groups">
|
<div class="groups">
|
||||||
@ -46,10 +46,10 @@
|
|||||||
<li
|
<li
|
||||||
v-for="(group, index) in groups"
|
v-for="(group, index) in groups"
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="[group.id, active === group.id && search === '' ? 'active' : '']"
|
:class="[group.id, active.id === group.id && search === '' ? 'active' : '']"
|
||||||
@click.stop.prevent="scrollTo($event, index)"
|
@click.stop.prevent="scrollTo($event, index)"
|
||||||
>
|
>
|
||||||
<span :class="[`group-${group.id}`]" />
|
<span :class="[`group-${group.id} fas`]" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -65,8 +65,8 @@
|
|||||||
width: $emoji-width;
|
width: $emoji-width;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
background: $background-secondary;
|
background: $background-secondary;
|
||||||
bottom: 30px;
|
bottom: 75px;
|
||||||
right: 0;
|
right: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -169,7 +169,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
li {
|
li {
|
||||||
&.emoji {
|
&.emoji-container {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -191,7 +191,7 @@
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
background: $background-tertiary;
|
background: $background-tertiary;
|
||||||
|
|
||||||
.icon-container {
|
.details-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -200,11 +200,11 @@
|
|||||||
span {
|
span {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
&.icon {
|
&.emoji {
|
||||||
margin: 0 5px 0 10px;
|
margin: 0 5px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.emoji {
|
&.emoji-id {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -242,36 +242,42 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
&.group-recent {
|
&.group-recent::before {
|
||||||
background-color: #fff;
|
content: '\f017';
|
||||||
}
|
}
|
||||||
&.group-neko {
|
&.group-neko::before {
|
||||||
background-color: #fff;
|
content: '\f6be';
|
||||||
}
|
}
|
||||||
&.group-people {
|
&.group-emotion::before {
|
||||||
background-color: #fff;
|
content: '\f118';
|
||||||
}
|
}
|
||||||
&.group-nature {
|
&.group-people::before {
|
||||||
background-color: #fff;
|
content: '\f0c0';
|
||||||
}
|
}
|
||||||
&.group-food {
|
&.group-nature::before {
|
||||||
background-color: #fff;
|
content: '\f1b0';
|
||||||
}
|
}
|
||||||
&.group-activity {
|
&.group-food::before {
|
||||||
background-color: #fff;
|
content: '\f5d1';
|
||||||
}
|
}
|
||||||
&.group-travel {
|
&.group-activity::before {
|
||||||
background-color: #fff;
|
content: '\f44e';
|
||||||
}
|
}
|
||||||
&.group-objects {
|
&.group-travel::before {
|
||||||
background-color: #fff;
|
content: '\f1b9';
|
||||||
}
|
}
|
||||||
&.group-symbols {
|
&.group-objects::before {
|
||||||
background-color: #fff;
|
content: '\f0eb';
|
||||||
}
|
}
|
||||||
&.group-flags {
|
&.group-symbols::before {
|
||||||
background-color: #fff;
|
content: '\f86d';
|
||||||
|
}
|
||||||
|
&.group-flags::before {
|
||||||
|
content: '\f024';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,9 +289,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
|
||||||
import { list } from './emoji/list'
|
import { get, set } from '../utils/localstorage'
|
||||||
import { keywords } from './emoji/keywords'
|
|
||||||
import { groups } from './emoji/groups'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: 'neko-emoji',
|
name: 'neko-emoji',
|
||||||
@ -297,19 +301,32 @@
|
|||||||
|
|
||||||
waitingForPaint = false
|
waitingForPaint = false
|
||||||
search = ''
|
search = ''
|
||||||
active = groups[0].id
|
index = 0
|
||||||
hovered = ''
|
hovered = ''
|
||||||
|
recent: string[] = JSON.parse(get('emoji_recent', '[]'))
|
||||||
|
|
||||||
|
get active() {
|
||||||
|
return this.$accessor.emoji.groups[this.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
get keywords() {
|
||||||
|
return this.$accessor.emoji.keywords
|
||||||
|
}
|
||||||
|
|
||||||
get groups() {
|
get groups() {
|
||||||
return groups
|
return this.$accessor.emoji.groups
|
||||||
|
}
|
||||||
|
|
||||||
|
get list() {
|
||||||
|
return this.$accessor.emoji.list
|
||||||
}
|
}
|
||||||
|
|
||||||
get filtered() {
|
get filtered() {
|
||||||
const filtered = []
|
const filtered = []
|
||||||
for (const emoji of list) {
|
for (const emoji of this.list) {
|
||||||
if (
|
if (
|
||||||
emoji.includes(this.search) || typeof keywords[emoji] !== 'undefined'
|
emoji.includes(this.search) || typeof this.keywords[emoji] !== 'undefined'
|
||||||
? keywords[emoji].some(keyword => keyword.includes(this.search))
|
? this.keywords[emoji].some(keyword => keyword.includes(this.search))
|
||||||
: false
|
: false
|
||||||
) {
|
) {
|
||||||
filtered.push(emoji)
|
filtered.push(emoji)
|
||||||
@ -319,16 +336,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollTo(event: MouseEvent, index: number) {
|
scrollTo(event: MouseEvent, index: number) {
|
||||||
const ele = this._groups[index]
|
if (!this._groups[index]) {
|
||||||
if (!ele) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this._scroll.scrollTop = index == 0 ? 0 : this._groups[index].offsetTop
|
||||||
let top = ele.offsetTop
|
|
||||||
if (index == 0) {
|
|
||||||
top = 0
|
|
||||||
}
|
|
||||||
this._scroll.scrollTop = top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll() {
|
onScroll() {
|
||||||
@ -341,16 +352,17 @@
|
|||||||
onScrollPaint() {
|
onScrollPaint() {
|
||||||
this.waitingForPaint = false
|
this.waitingForPaint = false
|
||||||
let scrollTop = this._scroll.scrollTop
|
let scrollTop = this._scroll.scrollTop
|
||||||
let active = this.groups[0]
|
let active = 0
|
||||||
for (let i = 0, l = this.groups.length; i < l; i++) {
|
for (const [i, group] of this.groups.entries()) {
|
||||||
let group = this.groups[i]
|
|
||||||
let component = this._groups[i]
|
let component = this._groups[i]
|
||||||
if (component && component.offsetTop > scrollTop) {
|
if (component && component.offsetTop > scrollTop) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
active = group
|
active = i
|
||||||
|
}
|
||||||
|
if (this.index !== active) {
|
||||||
|
this.index = active
|
||||||
}
|
}
|
||||||
this.active = active.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseExit(event: MouseEvent, emoji: string) {
|
onMouseExit(event: MouseEvent, emoji: string) {
|
||||||
@ -363,6 +375,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClick(event: MouseEvent, emoji: string) {
|
onClick(event: MouseEvent, emoji: string) {
|
||||||
|
this.$accessor.emoji.setRecent(emoji)
|
||||||
this.$emit('picked', emoji)
|
this.$emit('picked', emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,27 +29,27 @@
|
|||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
|
||||||
&.celebrate {
|
&.celebrate {
|
||||||
background-image: url('../assets/celebrate.png');
|
background-image: url('../assets/images/emote/celebrate.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.clap {
|
&.clap {
|
||||||
background-image: url('../assets/clap.png');
|
background-image: url('../assets/images/emote/clap.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.exclam {
|
&.exclam {
|
||||||
background-image: url('../assets/exclam.png');
|
background-image: url('../assets/images/emote/exclam.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.heart {
|
&.heart {
|
||||||
background-image: url('../assets/heart.png');
|
background-image: url('../assets/images/emote/heart.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.laughing {
|
&.laughing {
|
||||||
background-image: url('../assets/laughing.png');
|
background-image: url('../assets/images/emote/laughing.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sleep {
|
&.sleep {
|
||||||
background-image: url('../assets/sleep.png');
|
background-image: url('../assets/images/emote/sleep.png');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,27 +30,27 @@
|
|||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
|
||||||
&.celebrate {
|
&.celebrate {
|
||||||
background-image: url('../assets/celebrate.png');
|
background-image: url('../assets/images/emote/celebrate.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.clap {
|
&.clap {
|
||||||
background-image: url('../assets/clap.png');
|
background-image: url('../assets/images/emote/clap.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.exclam {
|
&.exclam {
|
||||||
background-image: url('../assets/exclam.png');
|
background-image: url('../assets/images/emote/exclam.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.heart {
|
&.heart {
|
||||||
background-image: url('../assets/heart.png');
|
background-image: url('../assets/images/emote/heart.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.laughing {
|
&.laughing {
|
||||||
background-image: url('../assets/laughing.png');
|
background-image: url('../assets/images/emote/laughing.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sleep {
|
&.sleep {
|
||||||
background-image: url('../assets/sleep.png');
|
background-image: url('../assets/images/emote/sleep.png');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="neko">
|
<div class="neko">
|
||||||
<img src="@/assets/logo.svg" alt="n.eko" />
|
<img src="@/assets/images/logo.svg" alt="n.eko" />
|
||||||
<span><b>n</b>.eko</span>
|
<span><b>n</b>.eko</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="menu">
|
<ul class="menu">
|
||||||
|
@ -212,6 +212,18 @@ const rules: MarkdownRules = {
|
|||||||
...br,
|
...br,
|
||||||
match: md.anyScopeRegex(/^\n/),
|
match: md.anyScopeRegex(/^\n/),
|
||||||
},
|
},
|
||||||
|
emoji: {
|
||||||
|
order: md.defaultRules.strong.order,
|
||||||
|
match: source => /^:([a-zA-z_-]*):/.exec(source),
|
||||||
|
parse(capture) {
|
||||||
|
return {
|
||||||
|
id: capture[1],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html(node, output, state) {
|
||||||
|
return htmlTag('span', '', { class: `emoji`, 'data-emoji': node.id }, state)
|
||||||
|
},
|
||||||
|
},
|
||||||
emoticon: {
|
emoticon: {
|
||||||
order: md.defaultRules.text.order,
|
order: md.defaultRules.text.order,
|
||||||
match: source => /^(¯\\_\(ツ\)_\/¯)/.exec(source),
|
match: source => /^(¯\\_\(ツ\)_\/¯)/.exec(source),
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="unsupported">
|
<div class="unsupported">
|
||||||
<div class="window">
|
<div class="window">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="@/assets/logo.svg" alt="n.eko" />
|
<img src="@/assets/images/logo.svg" alt="n.eko" />
|
||||||
<span><b>n</b>.eko</span>
|
<span><b>n</b>.eko</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message">
|
<div class="message">
|
||||||
|
@ -26,5 +26,6 @@ new Vue({
|
|||||||
render: h => h(app),
|
render: h => h(app),
|
||||||
created() {
|
created() {
|
||||||
this.$client.init(this)
|
this.$client.init(this)
|
||||||
|
this.$accessor.initialise()
|
||||||
},
|
},
|
||||||
}).$mount('#neko')
|
}).$mount('#neko')
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import { PluginObject } from 'vue'
|
import { PluginObject } from 'vue'
|
||||||
import axios, { AxiosStatic } from 'axios'
|
import axios, { AxiosStatic } from 'axios'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const $http: AxiosStatic
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
$http: AxiosStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'vue/types/vue' {
|
declare module 'vue/types/vue' {
|
||||||
interface Vue {
|
interface Vue {
|
||||||
$http: AxiosStatic
|
$http: AxiosStatic
|
||||||
@ -9,7 +17,8 @@ declare module 'vue/types/vue' {
|
|||||||
|
|
||||||
const plugin: PluginObject<undefined> = {
|
const plugin: PluginObject<undefined> = {
|
||||||
install(Vue) {
|
install(Vue) {
|
||||||
Vue.prototype.$http = axios
|
window.$http = axios
|
||||||
|
Vue.prototype.$http = window.$http
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
74
client/src/store/emoji.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
|
||||||
|
import { get, set } from '~/utils/localstorage'
|
||||||
|
import { accessor } from '~/store'
|
||||||
|
|
||||||
|
export const namespaced = true
|
||||||
|
|
||||||
|
interface Group {
|
||||||
|
name: string
|
||||||
|
id: string
|
||||||
|
list: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Keywords {
|
||||||
|
[name: string]: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emojis {
|
||||||
|
groups: Group[]
|
||||||
|
keywords: Keywords
|
||||||
|
list: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: 'recent',
|
||||||
|
name: 'Recent',
|
||||||
|
list: JSON.parse(get('emoji_recent', '[]')) as string[],
|
||||||
|
},
|
||||||
|
] as Group[],
|
||||||
|
keywords: {} as Keywords,
|
||||||
|
list: [] as string[],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getters = getterTree(state, {})
|
||||||
|
|
||||||
|
export const mutations = mutationTree(state, {
|
||||||
|
setRecent(state, emoji: string) {
|
||||||
|
if (!state.groups[0].list.includes(emoji)) {
|
||||||
|
if (state.groups[0].list.length > 30) {
|
||||||
|
state.groups[0].list.shift()
|
||||||
|
}
|
||||||
|
state.groups[0].list.push(emoji)
|
||||||
|
set('emoji_recent', JSON.stringify(state.groups[0].list))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addGroup(state, group: Group) {
|
||||||
|
state.groups.push(group)
|
||||||
|
},
|
||||||
|
setKeywords(state, keywords: Keywords) {
|
||||||
|
state.keywords = keywords
|
||||||
|
},
|
||||||
|
setList(state, list: string[]) {
|
||||||
|
state.list = list
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const actions = actionTree(
|
||||||
|
{ state, getters, mutations },
|
||||||
|
{
|
||||||
|
initialise() {
|
||||||
|
$http
|
||||||
|
.get<Emojis>('/emoji.json')
|
||||||
|
.then(req => {
|
||||||
|
for (const group of req.data.groups) {
|
||||||
|
accessor.emoji.addGroup(group)
|
||||||
|
}
|
||||||
|
accessor.emoji.setList(req.data.list)
|
||||||
|
accessor.emoji.setKeywords(req.data.keywords)
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
@ -8,6 +8,7 @@ import * as remote from './remote'
|
|||||||
import * as user from './user'
|
import * as user from './user'
|
||||||
import * as settings from './settings'
|
import * as settings from './settings'
|
||||||
import * as client from './client'
|
import * as client from './client'
|
||||||
|
import * as emoji from './emoji'
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
connecting: false,
|
connecting: false,
|
||||||
@ -34,6 +35,11 @@ export const mutations = mutationTree(state, {
|
|||||||
export const actions = actionTree(
|
export const actions = actionTree(
|
||||||
{ state, mutations },
|
{ state, mutations },
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
initialise(store) {
|
||||||
|
accessor.emoji.initialise()
|
||||||
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
connect(store, { username, password }: { username: string; password: string }) {
|
connect(store, { username, password }: { username: string; password: string }) {
|
||||||
$client.connect(password, username)
|
$client.connect(password, username)
|
||||||
@ -45,7 +51,7 @@ export const storePattern = {
|
|||||||
state,
|
state,
|
||||||
mutations,
|
mutations,
|
||||||
actions,
|
actions,
|
||||||
modules: { video, chat, user, remote, settings, client },
|
modules: { video, chat, user, remote, settings, client, emoji },
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
@ -44,10 +44,6 @@ export const mutations = mutationTree(state, {
|
|||||||
export const actions = actionTree(
|
export const actions = actionTree(
|
||||||
{ state, getters, mutations },
|
{ state, getters, mutations },
|
||||||
{
|
{
|
||||||
initialise({ commit }) {
|
|
||||||
//
|
|
||||||
},
|
|
||||||
|
|
||||||
sendClipboard({ getters }, clipboard: string) {
|
sendClipboard({ getters }, clipboard: string) {
|
||||||
if (!accessor.connected || !getters.hosting) {
|
if (!accessor.connected || !getters.hosting) {
|
||||||
return
|
return
|
||||||
|
234
client/tools/emoji.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import { custom } from './emoji_custom'
|
||||||
|
|
||||||
|
const datasource = require('emoji-datasource/emoji.json') as EmojiDatasource[]
|
||||||
|
const emojis = require('emojilib/emojis.json') as { [id: string]: Emoji }
|
||||||
|
|
||||||
|
interface EmojiDatasource {
|
||||||
|
name: string
|
||||||
|
unified: string
|
||||||
|
non_qualified: string | null
|
||||||
|
docomo: string | null
|
||||||
|
au: string | null
|
||||||
|
softbank: string | null
|
||||||
|
google: string | null
|
||||||
|
image: string
|
||||||
|
sheet_x: number
|
||||||
|
sheet_y: number
|
||||||
|
short_name: string
|
||||||
|
short_names: string[]
|
||||||
|
text: string | null
|
||||||
|
texts: string | null
|
||||||
|
category: string
|
||||||
|
sort_order: number
|
||||||
|
added_in: string
|
||||||
|
has_img_apple: boolean
|
||||||
|
has_img_google: boolean
|
||||||
|
has_img_twitter: boolean
|
||||||
|
has_img_facebook: boolean
|
||||||
|
skin_variations: {
|
||||||
|
[id: string]: {
|
||||||
|
unified: string
|
||||||
|
image: string
|
||||||
|
sheet_x: number
|
||||||
|
sheet_y: number
|
||||||
|
added_in: string
|
||||||
|
has_img_apple: boolean
|
||||||
|
has_img_google: boolean
|
||||||
|
has_img_twitter: boolean
|
||||||
|
has_img_facebook: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
obsoletes: string
|
||||||
|
obsoleted_by: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emoji {
|
||||||
|
keywords: string[]
|
||||||
|
char: string
|
||||||
|
fitzpatrick_scale: boolean
|
||||||
|
category: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHEET_COLUMNS = 57
|
||||||
|
const MULTIPLY = 100 / (SHEET_COLUMNS - 1)
|
||||||
|
|
||||||
|
const css: string[] = []
|
||||||
|
const keywords: { [name: string]: string[] } = {}
|
||||||
|
const list: string[] = []
|
||||||
|
const groups: { [name: string]: string[] } = { neko: [] }
|
||||||
|
|
||||||
|
for (const emoji of custom) {
|
||||||
|
groups['neko'].push(emoji.name)
|
||||||
|
list.push(emoji.name)
|
||||||
|
keywords[emoji.name] = emoji.keywords
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
css.push(`&[data-emoji='${emoji.name}'] { background-size: contain; background-image: url('../images/emoji/${emoji.file}'); }`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const source of datasource) {
|
||||||
|
const unified = source.unified.split('-').map(v => v.toLowerCase())
|
||||||
|
|
||||||
|
let emoji: Emoji | null = null
|
||||||
|
let emoji_id: string = ''
|
||||||
|
for (const id of Object.keys(emojis)) {
|
||||||
|
if (unified.includes(emojis[id].char.codePointAt(0)!.toString(16))) {
|
||||||
|
emoji_id = id
|
||||||
|
emoji = emojis[id]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!source.has_img_twitter) {
|
||||||
|
console.log(source.short_name, 'not avalible for set twitter')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// keywords
|
||||||
|
let words: string[] = []
|
||||||
|
if (!emoji) {
|
||||||
|
console.log(source.short_name, 'no keywords')
|
||||||
|
} else {
|
||||||
|
words = [emoji_id, ...emoji.keywords]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of source.short_names) {
|
||||||
|
if (!words.includes(name)) {
|
||||||
|
words.push(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keywords[source.short_name] = words
|
||||||
|
|
||||||
|
// keywords
|
||||||
|
let group = ''
|
||||||
|
switch (source.category) {
|
||||||
|
case 'Symbols':
|
||||||
|
group = 'symbols'
|
||||||
|
break
|
||||||
|
case 'Activities':
|
||||||
|
group = 'activity'
|
||||||
|
break
|
||||||
|
case 'Flags':
|
||||||
|
group = 'flags'
|
||||||
|
break
|
||||||
|
case 'Travel & Places':
|
||||||
|
group = 'travel'
|
||||||
|
break
|
||||||
|
case 'Food & Drink':
|
||||||
|
group = 'food'
|
||||||
|
break
|
||||||
|
case 'Animals & Nature':
|
||||||
|
group = 'nature'
|
||||||
|
break
|
||||||
|
case 'People & Body':
|
||||||
|
group = 'people'
|
||||||
|
break
|
||||||
|
case 'Smileys & Emotion':
|
||||||
|
group = 'emotion'
|
||||||
|
break
|
||||||
|
case 'Objects':
|
||||||
|
group = 'objects'
|
||||||
|
break
|
||||||
|
case 'Skin Tones':
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
console.log(`unknown category ${source.category}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groups[group]) {
|
||||||
|
groups[group] = [source.short_name]
|
||||||
|
} else {
|
||||||
|
groups[group].push(source.short_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// list
|
||||||
|
list.push(source.short_name)
|
||||||
|
|
||||||
|
// css
|
||||||
|
// prettier-ignore
|
||||||
|
css.push(`&[data-emoji='${source.short_name}'] { background-position: ${MULTIPLY * source.sheet_x}% ${MULTIPLY * source.sheet_y}% }`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFile(
|
||||||
|
'src/assets/styles/vendor/_emoji.scss',
|
||||||
|
`
|
||||||
|
.emoji {
|
||||||
|
display: inline-block;
|
||||||
|
background-size: ${SHEET_COLUMNS * 100}%;
|
||||||
|
background-image: url('~emoji-datasource/img/twitter/sheets/32.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
vertical-align: bottom;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
|
||||||
|
${css.map(v => ` ${v}`).join('\n')}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
() => {
|
||||||
|
console.log('_emoji.scss done')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: 'neko',
|
||||||
|
name: 'Neko',
|
||||||
|
list: groups['neko'] ? groups['neko'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'emotion',
|
||||||
|
name: 'Emotion',
|
||||||
|
list: groups['emotion'] ? groups['emotion'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'people',
|
||||||
|
name: 'People',
|
||||||
|
list: groups['people'] ? groups['people'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nature',
|
||||||
|
name: 'Nature',
|
||||||
|
list: groups['nature'] ? groups['nature'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'food',
|
||||||
|
name: 'Food',
|
||||||
|
list: groups['food'] ? groups['food'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'activity',
|
||||||
|
name: 'Activity',
|
||||||
|
list: groups['activity'] ? groups['activity'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'travel',
|
||||||
|
name: 'Travel',
|
||||||
|
list: groups['travel'] ? groups['travel'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'objects',
|
||||||
|
name: 'Objects',
|
||||||
|
list: groups['objects'] ? groups['objects'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'symbols',
|
||||||
|
name: 'Symbols',
|
||||||
|
list: groups['symbols'] ? groups['symbols'] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flags',
|
||||||
|
name: 'Flags',
|
||||||
|
list: groups['flags'] ? groups['flags'] : [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
list,
|
||||||
|
keywords,
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFile('public/emoji.json', JSON.stringify(data), () => {
|
||||||
|
console.log('emoji.json done')
|
||||||
|
})
|
7
client/tools/emoji_custom.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const custom = [
|
||||||
|
{
|
||||||
|
name: 'neko',
|
||||||
|
file: 'neko.png',
|
||||||
|
keywords: ['neko', 'cat', 'cat butt', 'butt'],
|
||||||
|
},
|
||||||
|
]
|
13
client/tools/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
@ -13,6 +13,7 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"types": [
|
"types": [
|
||||||
|
"node",
|
||||||
"webpack-env"
|
"webpack-env"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
@ -31,6 +32,7 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
"tools/**/*.ts",
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
"src/**/*.vue",
|
"src/**/*.vue",
|
||||||
|