add front-todofeed
This commit is contained in:
21
todofeed/front-todofeed/.gitignore
vendored
Normal file
21
todofeed/front-todofeed/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
30
todofeed/front-todofeed/README.md
Normal file
30
todofeed/front-todofeed/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
frontend-todofeed
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run your tests
|
||||||
|
```
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
5
todofeed/front-todofeed/babel.config.js
Normal file
5
todofeed/front-todofeed/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/app'
|
||||||
|
]
|
||||||
|
}
|
||||||
56
todofeed/front-todofeed/package.json
Normal file
56
todofeed/front-todofeed/package.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend-todofeed",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"caver-js": "^1.3.2",
|
||||||
|
"core-js": "^2.6.5",
|
||||||
|
"identicon.js": "^2.3.3",
|
||||||
|
"js-sha256": "^0.9.0",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"vue": "^2.6.10",
|
||||||
|
"vuetify": "^2.2.18",
|
||||||
|
"vuex": "^3.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "^3.1.1",
|
||||||
|
"@vue/cli-plugin-eslint": "^3.1.1",
|
||||||
|
"@vue/cli-service": "^3.1.1",
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
|
"sass": "^1.19.0",
|
||||||
|
"sass-loader": "^8.0.0",
|
||||||
|
"vue-cli-plugin-vuetify": "^2.0.5",
|
||||||
|
"vue-template-compiler": "^2.6.10",
|
||||||
|
"vuetify-loader": "^1.3.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"rules": {},
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"plugins": {
|
||||||
|
"autoprefixer": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
todofeed/front-todofeed/public/favicon.ico
Normal file
BIN
todofeed/front-todofeed/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
19
todofeed/front-todofeed/public/index.html
Normal file
19
todofeed/front-todofeed/public/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>frontend-todofeed</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but frontend-todofeed doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
181
todofeed/front-todofeed/src/App.vue
Normal file
181
todofeed/front-todofeed/src/App.vue
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-app-bar app clipped-left dense>
|
||||||
|
<v-toolbar-title>TodoFeed</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<template v-if="isConnectWallet">
|
||||||
|
<h3 class="mr-3">{{balance}} Klay</h3>
|
||||||
|
<v-btn outlined @click="removeWallet">Remove Wallet</v-btn>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<v-btn outlined @click="showLoginBox = true">Connect to Wallet</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-app-bar>
|
||||||
|
<v-content>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-btn color="blue darken-1" class="mb-3" v-if="isConnectWallet" @click="showWriteBox = true">New ToDo</v-btn>
|
||||||
|
|
||||||
|
<Feeds v-on:verify="onVerify" />
|
||||||
|
</v-container>
|
||||||
|
</v-content>
|
||||||
|
|
||||||
|
<v-dialog v-model="showLoginBox" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
<span class="headline">Login</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<LoginBox />
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="blue darken-1" @click="showLoginBox = false">Close</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="showWriteBox" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
<span class="headline">New Todo</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<WriteBox v-on:success-write="onSuccessWrite" v-on:error-validate="onErrorValidate" />
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="blue darken-1" @click="showWriteBox = false">Close</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar" top>
|
||||||
|
{{ snackbarMsg }}
|
||||||
|
<v-btn
|
||||||
|
color="pink"
|
||||||
|
text
|
||||||
|
@click="snackbar = false"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</v-snackbar>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
import KlaytnService from './klaytn/klaytnService'
|
||||||
|
|
||||||
|
import Feeds from '@/components/Feeds.vue'
|
||||||
|
import LoginBox from '@/components/LoginBox.vue'
|
||||||
|
import WriteBox from '@/components/WriteBox.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'app',
|
||||||
|
components: {
|
||||||
|
Feeds,
|
||||||
|
LoginBox,
|
||||||
|
WriteBox
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
showLoginBox: false,
|
||||||
|
showWriteBox: false,
|
||||||
|
|
||||||
|
snackbar: false,
|
||||||
|
snackbarMsg: ''
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('wallet', [
|
||||||
|
'klaytn',
|
||||||
|
'isConnectWallet',
|
||||||
|
'myaddress',
|
||||||
|
'balance'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
await this.connect()
|
||||||
|
|
||||||
|
await this.getFeeds()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapMutations('wallet', [
|
||||||
|
'setKlaytn',
|
||||||
|
'setIsConnectWallet',
|
||||||
|
'setMyAddress',
|
||||||
|
'setBalance'
|
||||||
|
]),
|
||||||
|
...mapMutations('todos', [
|
||||||
|
'setTodos'
|
||||||
|
]),
|
||||||
|
async connect () {
|
||||||
|
const klaytn = new KlaytnService()
|
||||||
|
this.setKlaytn(klaytn)
|
||||||
|
const address = await klaytn.init()
|
||||||
|
|
||||||
|
if (address) {
|
||||||
|
this.setMyAddress(address)
|
||||||
|
|
||||||
|
this.getBalance()
|
||||||
|
this.setIsConnectWallet(true)
|
||||||
|
} else {
|
||||||
|
this.setIsConnectWallet(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getBalance () {
|
||||||
|
if (this.myaddress) {
|
||||||
|
const balance = await this.klaytn.getBalance(this.myaddress)
|
||||||
|
this.setBalance(balance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeWallet () {
|
||||||
|
this.klaytn.removeWallet()
|
||||||
|
this.setIsConnectWallet(false)
|
||||||
|
},
|
||||||
|
|
||||||
|
onErrorValidate(msg) {
|
||||||
|
this.snackbarMsg = msg
|
||||||
|
this.snackbar = true
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccessWrite(msg) {
|
||||||
|
this.snackbarMsg = msg
|
||||||
|
this.snackbar = true
|
||||||
|
|
||||||
|
this.getFeeds()
|
||||||
|
this.getBalance()
|
||||||
|
|
||||||
|
this.showWriteBox = false
|
||||||
|
},
|
||||||
|
|
||||||
|
onVerify(todoId) {
|
||||||
|
this.klaytn.verify(todoId, (receipt) => {
|
||||||
|
this.snackbarMsg = `Complete.. blocknumber: #${receipt.blockNumber} , ${receipt.transactionHash}`
|
||||||
|
this.snackbar = true
|
||||||
|
|
||||||
|
this.getFeeds()
|
||||||
|
this.getBalance()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getFeeds() {
|
||||||
|
this.klaytn.getFeeds((feed) => {
|
||||||
|
this.setTodos(feed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
todofeed/front-todofeed/src/assets/logo.png
Normal file
BIN
todofeed/front-todofeed/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
1
todofeed/front-todofeed/src/assets/logo.svg
Normal file
1
todofeed/front-todofeed/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
||||||
|
After Width: | Height: | Size: 539 B |
108
todofeed/front-todofeed/src/components/Feeds.vue
Normal file
108
todofeed/front-todofeed/src/components/Feeds.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-card class="cardContent mx-auto mb-4" max-width="400" max-height="400"
|
||||||
|
v-for="item in allTodos" :key="item.index">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-avatar color="grey"><img :src="getProfile(item.owner)" /></v-list-item-avatar>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>{{item.title}}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{getCreatedAt(item.timestamp)}}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
<div class="flex-grow-1"></div>
|
||||||
|
<a :href="getScopeUrl(item.verifier)" target="_blank">{{getVerified(item.isVerified)}}</a>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-img
|
||||||
|
:src="getImage(item.photo)"
|
||||||
|
height="194"
|
||||||
|
></v-img>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="deep-purple accent-4"
|
||||||
|
v-if="!item.isVerified"
|
||||||
|
@click="verify(item.todoId)"
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import moment from 'moment'
|
||||||
|
import {getIdenticon} from '@/util/identicon'
|
||||||
|
import { drawImageFromBytes } from '@/util/imageUtils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
items: []
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters('todos', [
|
||||||
|
'allTodos'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getProfile (user_name) {
|
||||||
|
return getIdenticon(user_name)
|
||||||
|
},
|
||||||
|
getCreatedAt (timestamp) {
|
||||||
|
return moment(timestamp * 1000).fromNow()
|
||||||
|
},
|
||||||
|
getImage (photo) {
|
||||||
|
if (photo) {
|
||||||
|
const imageUrl = drawImageFromBytes(photo)
|
||||||
|
return imageUrl
|
||||||
|
} else {
|
||||||
|
''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getVerified (isVerified) {
|
||||||
|
return (isVerified) ? "Verified" : "Not Verified"
|
||||||
|
},
|
||||||
|
getScopeUrl (address) {
|
||||||
|
if (address) {
|
||||||
|
return `https://baobab.scope.klaytn.com/account/${address}`
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verify (todoId) {
|
||||||
|
this.$emit('verify', todoId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cardContent {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: normal;
|
||||||
|
color: #17191d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item__subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: normal;
|
||||||
|
color: #7f899a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
171
todofeed/front-todofeed/src/components/LoginBox.vue
Normal file
171
todofeed/front-todofeed/src/components/LoginBox.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>Connect to Wallet</h2>
|
||||||
|
<v-card class="d-flex mx-auto mb-4 pa-2">
|
||||||
|
<template v-if="walletInstance">
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="16">
|
||||||
|
<h2>Integrated</h2>
|
||||||
|
<div class="address">{{walletInstance.address}}</div>
|
||||||
|
<v-btn outlined class="btnSubmit mt-3" @click="this.handleRemoveWallet">REMOVE WALLEt</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="16">
|
||||||
|
<input type="file" v-on:change="this.handleImport" />
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
solo
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-divider class="mt-3 mb-3"></v-divider>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="privateKey"
|
||||||
|
label="Private Key"
|
||||||
|
type="password"
|
||||||
|
solo
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-btn outlined class="btnSubmit" @click="this.handleAddWallet">ADD WALLET</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar" top>
|
||||||
|
{{ errorTxt }}
|
||||||
|
<v-btn
|
||||||
|
color="pink"
|
||||||
|
text
|
||||||
|
@click="snackbar = false"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</v-snackbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
snackbar: false,
|
||||||
|
errorTxt: '',
|
||||||
|
|
||||||
|
// accessType: 'keystore',
|
||||||
|
keystore: null,
|
||||||
|
password: '',
|
||||||
|
privateKey: null,
|
||||||
|
walletInstance: null
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('wallet', [
|
||||||
|
'klaytn'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations('wallet', [
|
||||||
|
'setIsConnectWallet',
|
||||||
|
'setMyAddress',
|
||||||
|
'setBalance'
|
||||||
|
]),
|
||||||
|
|
||||||
|
validate () {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImport (e) {
|
||||||
|
const keystore = e.target.files[0]
|
||||||
|
|
||||||
|
const fileReader = new FileReader()
|
||||||
|
fileReader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
if (!this.checkValidKeystore(e.target.result)) {
|
||||||
|
// If key store file is invalid, show message "Invalid keystore file."
|
||||||
|
this.errorTxt = 'Invalid keystore file.'
|
||||||
|
this.snackbar = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keystore = e.target.result
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.errorTxt = 'Invalid keystore file.'
|
||||||
|
this.snackbar = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileReader.readAsText(keystore)
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleAddWallet () {
|
||||||
|
try {
|
||||||
|
// Access type2: access thorugh private key
|
||||||
|
if(this.privateKey) {
|
||||||
|
await this.klaytn.integrateWallet(this.privateKey)
|
||||||
|
} else {
|
||||||
|
// Access type1: access through keystore + password
|
||||||
|
await this.klaytn.loginWithKeystore(this.keystore, this.password)
|
||||||
|
}
|
||||||
|
this.getWalletInfo()
|
||||||
|
} catch (e) {
|
||||||
|
this.errorTxt = `Password or private key doesn't match.`
|
||||||
|
this.snackbar = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getWalletInfo () {
|
||||||
|
this.walletInstance = this.klaytn.getWallet()
|
||||||
|
const address = this.walletInstance.address
|
||||||
|
if(address) {
|
||||||
|
this.setMyAddress(address)
|
||||||
|
const balance = await this.klaytn.getBalance(address)
|
||||||
|
this.setBalance(balance)
|
||||||
|
this.setIsConnectWallet(true)
|
||||||
|
} else {
|
||||||
|
this.setIsConnectWallet(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
checkValidKeystore (keystore) {
|
||||||
|
const parsedKeystore = JSON.parse(keystore)
|
||||||
|
const isValidKeystore = parsedKeystore.version &&
|
||||||
|
parsedKeystore.id &&
|
||||||
|
parsedKeystore.address &&
|
||||||
|
parsedKeystore.crypto
|
||||||
|
|
||||||
|
return isValidKeystore
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleRemoveWallet () {
|
||||||
|
this.klaytn.removeWallet()
|
||||||
|
this.setIsConnectWallet(false)
|
||||||
|
this.walletInstance = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if(this.klaytn) {
|
||||||
|
this.walletInstance = this.klaytn.getWallet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
113
todofeed/front-todofeed/src/components/WriteBox.vue
Normal file
113
todofeed/front-todofeed/src/components/WriteBox.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-card class="cardContent d-flex mx-auto mb-4 pa-2">
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="16">
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
class="mt-2"
|
||||||
|
lazy-validation
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="title"
|
||||||
|
label="Title"
|
||||||
|
:rules="titleRules"
|
||||||
|
solo
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
<input type="file" accept="image/*" v-on:change="this.handleImportFile" required />
|
||||||
|
|
||||||
|
<v-btn v-if="!isLoading" outlined @click="validate">POST</v-btn>
|
||||||
|
<v-progress-circular indeterminate v-else></v-progress-circular>
|
||||||
|
</v-form>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
import imageCompression from '@/util/imageCompression'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
title: '',
|
||||||
|
titleRules: [
|
||||||
|
v => !!v || 'Title is required',
|
||||||
|
v => (v && v.length <= 255) || 'Title must be less than 255 characters',
|
||||||
|
],
|
||||||
|
isLoading: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters('wallet', [
|
||||||
|
'klaytn'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
validate () {
|
||||||
|
if (!this.$refs.form.validate()) {
|
||||||
|
const snackbarMsg = 'form data are incorrect. please check again'
|
||||||
|
this.$emit('error-validate', snackbarMsg)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submit()
|
||||||
|
},
|
||||||
|
|
||||||
|
async compressImage (imageFile) {
|
||||||
|
try {
|
||||||
|
const MAX_IMAGE_SIZE_MB = 0.03 // 30KB
|
||||||
|
const compressedFile = await imageCompression(imageFile, MAX_IMAGE_SIZE_MB)
|
||||||
|
return compressedFile
|
||||||
|
} catch (error) {
|
||||||
|
console.error('* Fail to compress image')
|
||||||
|
return imageFile
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleImportFile (e) {
|
||||||
|
const MAX_IMAGE_SIZE = 30000 // 30KB
|
||||||
|
const file = e.target.files[0]
|
||||||
|
|
||||||
|
if (file.size > MAX_IMAGE_SIZE) {
|
||||||
|
this.imgFile = await this.compressImage(file)
|
||||||
|
} else {
|
||||||
|
this.imgFile = file
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit () {
|
||||||
|
try {
|
||||||
|
this.isLoading = true
|
||||||
|
await this.klaytn.writeTodo(this.title, this.imgFile, (receipt) => {
|
||||||
|
console.log(receipt)
|
||||||
|
|
||||||
|
const snackbarMsg = `Complete.. blocknumber: #${receipt.blockNumber} , ${receipt.transactionHash}`
|
||||||
|
|
||||||
|
this.$emit('success-write', snackbarMsg)
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
}, (error) => {
|
||||||
|
this.$emit('error-validate', error)
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
27
todofeed/front-todofeed/src/klaytn/caver.js
Normal file
27
todofeed/front-todofeed/src/klaytn/caver.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* caver-js library helps making connection with klaytn node.
|
||||||
|
* You can connect to specific klaytn node by setting 'rpcURL' value.
|
||||||
|
* default rpcURL is 'https://api.baobab.klaytn.net:8651'.
|
||||||
|
*/
|
||||||
|
import Caver from 'caver-js'
|
||||||
|
|
||||||
|
const deployedABI = require('./deployedABI.json')
|
||||||
|
|
||||||
|
const TEST_NET = 'https://api.baobab.klaytn.net:8651'
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
rpcURL: TEST_NET
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEPLOYED_ADDRESS = '0xa47898be53fba0fecdb9b4e1dadea5bf0f3c77f7' // testnet
|
||||||
|
|
||||||
|
const cav = new Caver(config.rpcURL)
|
||||||
|
|
||||||
|
const getContractInstance = () => {
|
||||||
|
const contractInstance = deployedABI
|
||||||
|
&& DEPLOYED_ADDRESS
|
||||||
|
&& new cav.klay.Contract(deployedABI, DEPLOYED_ADDRESS)
|
||||||
|
return contractInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
export {cav, getContractInstance}
|
||||||
1
todofeed/front-todofeed/src/klaytn/deployedABI.json
Normal file
1
todofeed/front-todofeed/src/klaytn/deployedABI.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"todoList","outputs":[{"name":"todoId","type":"uint256"},{"name":"owner","type":"address"},{"name":"title","type":"string"},{"name":"photo","type":"bytes"},{"name":"timestamp","type":"uint256"},{"name":"isVerified","type":"bool"},{"name":"verifier","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"todoMap","outputs":[{"name":"todoId","type":"uint256"},{"name":"owner","type":"address"},{"name":"title","type":"string"},{"name":"photo","type":"bytes"},{"name":"timestamp","type":"uint256"},{"name":"isVerified","type":"bool"},{"name":"verifier","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"todoId","type":"uint256"},{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"title","type":"string"},{"indexed":false,"name":"photo","type":"bytes"},{"indexed":false,"name":"timestamp","type":"uint256"}],"name":"TodoCompleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"todoId","type":"uint256"},{"indexed":false,"name":"verifier","type":"address"}],"name":"TodoVerified","type":"event"},{"constant":false,"inputs":[{"name":"title","type":"string"},{"name":"photo","type":"bytes"}],"name":"writeTodo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"todoId","type":"uint256"}],"name":"verifyTodo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTotalTodoCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"todoId","type":"uint256"}],"name":"getTodo","outputs":[{"name":"","type":"uint256"},{"name":"","type":"address"},{"name":"","type":"string"},{"name":"","type":"bytes"},{"name":"","type":"uint256"},{"name":"","type":"bool"},{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]
|
||||||
1
todofeed/front-todofeed/src/klaytn/deployedAddress
Normal file
1
todofeed/front-todofeed/src/klaytn/deployedAddress
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0xa47898be53fba0fecdb9b4e1dadea5bf0f3c77f7
|
||||||
150
todofeed/front-todofeed/src/klaytn/klaytnService.js
Normal file
150
todofeed/front-todofeed/src/klaytn/klaytnService.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { cav, getContractInstance } from './caver'
|
||||||
|
|
||||||
|
export default class KlaytnService {
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async init () {
|
||||||
|
const walletFromSession = sessionStorage.getItem('walletInstance')
|
||||||
|
|
||||||
|
// If 'walletInstance' value exists, add it to caver's wallet
|
||||||
|
if (walletFromSession) {
|
||||||
|
try {
|
||||||
|
const address = JSON.parse(walletFromSession).address
|
||||||
|
cav.klay.accounts.wallet.add(JSON.parse(walletFromSession))
|
||||||
|
|
||||||
|
return address
|
||||||
|
} catch (e) {
|
||||||
|
// If value in sessionStorage is invalid wallet instance,
|
||||||
|
// remove it from sessionStorage.
|
||||||
|
sessionStorage.removeItem('walletInstance')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlockNumber () {
|
||||||
|
const blockNumber = await cav.klay.getBlockNumber()
|
||||||
|
return blockNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBalance (address) {
|
||||||
|
const balance = await cav.klay.getBalance(address)
|
||||||
|
return cav.utils.fromPeb(balance, "KLAY")
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginWithKeystore (keystore, password) {
|
||||||
|
const { privateKey: privateKeyFromKeystore } = cav.klay.accounts.decrypt(keystore, password)
|
||||||
|
await this.integrateWallet(privateKeyFromKeystore)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
integrateWallet (privateKey) {
|
||||||
|
const walletInstance = cav.klay.accounts.privateKeyToAccount(privateKey)
|
||||||
|
cav.klay.accounts.wallet.add(walletInstance)
|
||||||
|
sessionStorage.setItem('walletInstance', JSON.stringify(walletInstance))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWallet () {
|
||||||
|
cav.klay.accounts.wallet.clear()
|
||||||
|
sessionStorage.removeItem('walletInstance')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getWallet () {
|
||||||
|
if (cav.klay.accounts.wallet.length) {
|
||||||
|
return cav.klay.accounts.wallet[0]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
writeTodo (title, file, dispatch, errorCb) {
|
||||||
|
const walletInstance = cav.klay.accounts.wallet && cav.klay.accounts.wallet[0]
|
||||||
|
|
||||||
|
if (!walletInstance) {
|
||||||
|
console.log('no walletInstance')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = walletInstance.address
|
||||||
|
|
||||||
|
const reader = new window.FileReader()
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const buffer = Buffer.from(reader.result)
|
||||||
|
const hexString = "0x" + buffer.toString('hex')
|
||||||
|
getContractInstance().methods.writeTodo(title, hexString).send({
|
||||||
|
from: address,
|
||||||
|
gas: '100000000',
|
||||||
|
})
|
||||||
|
.once('transactionHash', (txHash) => {
|
||||||
|
console.log(`
|
||||||
|
Sending a transaction...
|
||||||
|
txHash: ${txHash}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.once('receipt', (receipt) => {
|
||||||
|
console.log(`
|
||||||
|
Received receipt! (#${receipt.blockNumber} ,${receipt.transactionHash})
|
||||||
|
`, receipt)
|
||||||
|
|
||||||
|
dispatch(receipt)
|
||||||
|
})
|
||||||
|
.once('error', (error) => {
|
||||||
|
errorCb(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getFeeds (dispatch) {
|
||||||
|
getContractInstance().methods.getTotalTodoCount().call()
|
||||||
|
.then((totalTodoCount) => {
|
||||||
|
if (!totalTodoCount) return []
|
||||||
|
const feed = []
|
||||||
|
for (let i = totalTodoCount; i > 0; i--) {
|
||||||
|
const todo = getContractInstance().methods.getTodo(i).call()
|
||||||
|
feed.push(todo)
|
||||||
|
}
|
||||||
|
return Promise.all(feed)
|
||||||
|
})
|
||||||
|
.then((feed) => {
|
||||||
|
dispatch(feed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
verify (todoId, dispatch) {
|
||||||
|
const walletInstance = cav.klay.accounts.wallet && cav.klay.accounts.wallet[0]
|
||||||
|
|
||||||
|
if (!walletInstance) {
|
||||||
|
console.log('no walletInstance')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = walletInstance.address
|
||||||
|
getContractInstance().methods.verifyTodo(todoId).send({
|
||||||
|
from: address,
|
||||||
|
gas: '3000000'
|
||||||
|
})
|
||||||
|
.once('transactionHash', (txHash) => {
|
||||||
|
console.log(`
|
||||||
|
Sending a transaction...
|
||||||
|
txHash: ${txHash}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.once('receipt', (receipt) => {
|
||||||
|
console.log(`
|
||||||
|
Received receipt! (#${receipt.blockNumber} ,${receipt.transactionHash})
|
||||||
|
`, receipt)
|
||||||
|
|
||||||
|
dispatch(receipt)
|
||||||
|
})
|
||||||
|
.once('error', (error) => {
|
||||||
|
alert(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
todofeed/front-todofeed/src/main.js
Normal file
12
todofeed/front-todofeed/src/main.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import store from './store'
|
||||||
|
import vuetify from './plugins/vuetify';
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
vuetify,
|
||||||
|
store,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
||||||
7
todofeed/front-todofeed/src/plugins/vuetify.js
Normal file
7
todofeed/front-todofeed/src/plugins/vuetify.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Vuetify from 'vuetify/lib';
|
||||||
|
|
||||||
|
Vue.use(Vuetify);
|
||||||
|
|
||||||
|
export default new Vuetify({
|
||||||
|
});
|
||||||
14
todofeed/front-todofeed/src/store/index.js
Normal file
14
todofeed/front-todofeed/src/store/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
|
import wallet from '@/store/modules/wallet'
|
||||||
|
import todos from '@/store/modules/todos'
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
wallet,
|
||||||
|
todos
|
||||||
|
}
|
||||||
|
})
|
||||||
42
todofeed/front-todofeed/src/store/modules/todos.js
Normal file
42
todofeed/front-todofeed/src/store/modules/todos.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import klaytnService from '@/klaytn/klaytnService'
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
allTodos: (state) => {
|
||||||
|
return state.items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
setTodos (state, todos) {
|
||||||
|
|
||||||
|
todos = todos.map(feed => {
|
||||||
|
const obj = {
|
||||||
|
todoId: parseInt(feed[0]),
|
||||||
|
owner: feed[1],
|
||||||
|
title: feed[2],
|
||||||
|
photo: feed[3],
|
||||||
|
timestamp: feed[4],
|
||||||
|
isVerified: feed[5],
|
||||||
|
verifier: feed[6]
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
|
||||||
|
state.items = todos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations
|
||||||
|
}
|
||||||
41
todofeed/front-todofeed/src/store/modules/wallet.js
Normal file
41
todofeed/front-todofeed/src/store/modules/wallet.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
const state = {
|
||||||
|
klaytn: null,
|
||||||
|
isConnectWallet: false,
|
||||||
|
myaddress: '',
|
||||||
|
balance: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
klaytn: (state) => state.klaytn,
|
||||||
|
isConnectWallet: (state) => state.isConnectWallet,
|
||||||
|
myaddress: (state) => state.myaddress,
|
||||||
|
balance: (state) => state.balance
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
setKlaytn(state, klaytn) {
|
||||||
|
state.klaytn = klaytn
|
||||||
|
},
|
||||||
|
setIsConnectWallet(state, isConnected) {
|
||||||
|
state.isConnectWallet = isConnected
|
||||||
|
},
|
||||||
|
setMyAddress(state, address) {
|
||||||
|
state.myaddress = address
|
||||||
|
},
|
||||||
|
setBalance(state, balance) {
|
||||||
|
state.balance = balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations
|
||||||
|
}
|
||||||
14
todofeed/front-todofeed/src/util/identicon.js
Normal file
14
todofeed/front-todofeed/src/util/identicon.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import sha256 from 'js-sha256'
|
||||||
|
import Identicon from 'identicon.js'
|
||||||
|
|
||||||
|
const getIdenticon = (user_name) => {
|
||||||
|
if(user_name){
|
||||||
|
const data = new Identicon(sha256(user_name)).toString()
|
||||||
|
return 'data:image/png;base64,'+ data
|
||||||
|
} else {
|
||||||
|
return '/img/profile-medium.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {getIdenticon}
|
||||||
44
todofeed/front-todofeed/src/util/imageCompression.js
Normal file
44
todofeed/front-todofeed/src/util/imageCompression.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { drawImageInCanvas, getDataUrlFromFile, getFilefromDataUrl, loadImage } from './imageUtils'
|
||||||
|
|
||||||
|
async function imageCompression(file, maxSizeMB = Number.POSITIVE_INFINITY, maxWidthOrHeight) {
|
||||||
|
if (!(file instanceof Blob || file instanceof File)) {
|
||||||
|
throw new Error('The file given is not an instance of Blob or File')
|
||||||
|
} else if (!/^image/.test(file.type)) {
|
||||||
|
throw new Error('The file given is not an image')
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSizeByte = maxSizeMB * 1024 * 1024
|
||||||
|
const dataUrl = await getDataUrlFromFile(file)
|
||||||
|
const img = await loadImage(dataUrl)
|
||||||
|
const canvas = drawImageInCanvas(img, maxWidthOrHeight)
|
||||||
|
let quality = 0.9
|
||||||
|
let compressedFile = await getFilefromDataUrl(canvas.toDataURL(file.type, quality), file.name, file.lastModified)
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
while (compressedFile.size > maxSizeByte) {
|
||||||
|
canvas.width *= 0.9
|
||||||
|
canvas.height *= 0.9
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
const compressedDataUrl = canvas.toDataURL(file.type, quality)
|
||||||
|
compressedFile = await getFilefromDataUrl(compressedDataUrl, file.name, file.lastModified)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (compressedFile.size > maxSizeByte) {
|
||||||
|
quality *= 0.9
|
||||||
|
const compressedDataUrl = canvas.toDataURL(file.type, quality)
|
||||||
|
compressedFile = await getFilefromDataUrl(compressedDataUrl, file.name, file.lastModified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return compressedFile
|
||||||
|
}
|
||||||
|
|
||||||
|
imageCompression.drawImageInCanvas = drawImageInCanvas
|
||||||
|
imageCompression.getDataUrlFromFile = getDataUrlFromFile
|
||||||
|
imageCompression.getFilefromDataUrl = getFilefromDataUrl
|
||||||
|
imageCompression.loadImage = loadImage
|
||||||
|
|
||||||
|
export default imageCompression
|
||||||
76
todofeed/front-todofeed/src/util/imageUtils.js
Normal file
76
todofeed/front-todofeed/src/util/imageUtils.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
export function getDataUrlFromFile(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result)
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilefromDataUrl(dataurl, filename, lastModified = Date.now()) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const arr = dataurl.split(',')
|
||||||
|
const mime = arr[0].match(/:(.*?)/)[1]
|
||||||
|
const bstr = atob(arr[1])
|
||||||
|
let n = bstr.length
|
||||||
|
const u8arr = new Uint8Array(n)
|
||||||
|
while (n--) {
|
||||||
|
u8arr[n] = bstr.charCodeAt(n)
|
||||||
|
}
|
||||||
|
let file
|
||||||
|
try {
|
||||||
|
file = new File([u8arr], filename, { type: mime }) // Edge do not support File constructor
|
||||||
|
} catch (e) {
|
||||||
|
file = new Blob([u8arr], { type: mime })
|
||||||
|
file.name = filename
|
||||||
|
file.lastModified = lastModified
|
||||||
|
}
|
||||||
|
resolve(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadImage(src) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => {
|
||||||
|
resolve(img)
|
||||||
|
}
|
||||||
|
img.onerror = reject
|
||||||
|
img.src = src
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawImageInCanvas(img, maxWidthOrHeight) {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
if (Number.isInteger(maxWidthOrHeight) && (img.width > maxWidthOrHeight || img.height > maxWidthOrHeight)) {
|
||||||
|
if (img.width > img.height) {
|
||||||
|
canvas.width = maxWidthOrHeight
|
||||||
|
canvas.height = (img.height / img.width) * maxWidthOrHeight
|
||||||
|
} else {
|
||||||
|
canvas.width = (img.width / img.height) * maxWidthOrHeight
|
||||||
|
canvas.height = maxWidthOrHeight
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
}
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
export const drawImageFromBytes = (data) => {
|
||||||
|
/**
|
||||||
|
* data.slice(2)
|
||||||
|
* Remove prefix `0x` from hexString
|
||||||
|
*/
|
||||||
|
const hexString = data.slice(2)
|
||||||
|
const arrayBufferView = new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
|
||||||
|
const blob = new Blob([arrayBufferView], { type: 'image/jpeg' })
|
||||||
|
const urlCreator = window.URL || window.webkitURL
|
||||||
|
const imageUrl = urlCreator.createObjectURL(blob)
|
||||||
|
return imageUrl
|
||||||
|
}
|
||||||
5
todofeed/front-todofeed/vue.config.js
Normal file
5
todofeed/front-todofeed/vue.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
"transpileDependencies": [
|
||||||
|
"vuetify"
|
||||||
|
]
|
||||||
|
}
|
||||||
Submodule todofeed/frontend-todofeed deleted from 56352fc7d3
Reference in New Issue
Block a user