이재빈

Add:0421

[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
.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?
#Electron-builder output
/dist_electron
\ No newline at end of file
# image-labeller
Crop된 명패 이미지 분류 작업을 도와주는 툴.
### Build
```
yarn electron:build
```
dist_electron/win-unpacked 폴더에서 image-labeller.exe를 실행.
### 사용 설명
1. 좌측 사이드바에서 Settings 클릭.
2. 결과물 CSV 파일의 이름을 지정. (저장 경로는 데이터셋이 위치한 Workspace 폴더)
3. 데이터셋의 시작 파일 이름을 지정. (비어있으면 첫 파일부터 읽음)
4. 데이터셋이 위치한 폴더를 선택.
5. 좌측 사이드바에서 Main 클릭.
6. 이미지 분류 작업 시작.
* 단축키
* 방향키 (좌/우) : 이전/다음 이미지 로딩
* 숫자키 (0~9) : 호수 입력.
* backspace / delete : 입력한 마지막 자리부터 제거.
* s : CSV 파일로 저장.
* 참고 사항
* 실제로 라벨링은 숫자 6자리로 변환됨. (hasNum, digitLen, digit1, digit2, digit3, digit4)
* 아무것도 입력하지 않고 다음 이미지 로딩 시 [0, 0, 10, 10, 10, 10]으로 자동 라벨링됨.
![img1](./src/img1.png)
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
{
"name": "image-labeller",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"core-js": "^3.6.4",
"csv-writer": "^1.6.0",
"vue": "^2.6.11",
"vue-router": "^3.1.6",
"vuetify": "^2.2.11",
"vuex": "^3.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-plugin-router": "~4.3.0",
"@vue/cli-plugin-vuex": "~4.3.0",
"@vue/cli-service": "~4.3.0",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"electron": "^6.0.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^4.12.0",
"sass": "^1.19.0",
"sass-loader": "^8.0.2",
"vue-cli-plugin-electron-builder": "~1.4.6",
"vue-cli-plugin-vuetify": "~2.0.5",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.3.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
No preview for this file type
<!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><%= htmlWebpackPlugin.options.title %></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 <%= htmlWebpackPlugin.options.title %> 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>
<template>
<v-app>
<Sidebar/>
<v-content>
<router-view></router-view>
</v-content>
</v-app>
</template>
<script>
import Sidebar from './components/Sidebar'
export default {
name: 'App',
components: {
Sidebar
},
data: () => ({
//
})
}
</script>
<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>
/* Contents */
.top-tap-bar {
padding: 5px 10px;
margin-bottom: 10px;
}
.content-wrapper {
margin: 10px;
}
'use strict'
import { app, protocol, BrowserWindow } from 'electron'
import {
createProtocol
/* installVueDevtools */
} from 'vue-cli-plugin-electron-builder/lib'
const isDevelopment = process.env.NODE_ENV !== 'production'
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }])
function createWindow () {
// Create the browser window.
win = new BrowserWindow({
width: 1000,
height: 600,
titleBarStyle: 'customButtonsOnHover',
webPreferences: {
nodeIntegration: true,
webSecurity: false
}
})
win.removeMenu() // Remove top toolbar
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
}
win.on('closed', () => {
win = null
})
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
// Devtools extensions are broken in Electron 6.0.0 and greater
// See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info
// Electron will not launch with Devtools extensions installed on Windows 10 with dark mode
// If you are not using Windows 10 dark mode, you may uncomment these lines
// In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines
// try {
// await installVueDevtools()
// } catch (e) {
// console.error('Vue Devtools failed to install:', e.toString())
// }
}
createWindow()
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
/************************************/
/* Custom */
/************************************/
var fs = require('fs')
const ipc = require('electron').ipcMain
// global variables
global.workspacePath = ''
global.resultFilename = ''
global.startFilename = ''
global.workspaceLoaded = false
global.setResultFilename = function (val) {
global.resultFilename = val
}
global.setStartFilename = function (val) {
global.startFilename = val
}
// Variables
var wsControl = {
filenames: [],
fileLength: 0,
current: {
filename: '',
filepath: '',
idx: -1,
digits: []
}
}
var resultDigits = []
// functions
function loadWorkspace () {
var files = fs.readdirSync(global.workspacePath)
files.sort((a, b) => { return Number(a.substr(0, a.length - 3)) - Number(b.substr(0, b.length - 3)) })
console.log(files)
var nowStart = (global.startFilename === '')
for (var i = 0; i < files.length; i++) {
var _file = files[i]
if (!nowStart) {
if (_file === global.startFilename) nowStart = true
else continue
}
var _suffix = _file.substr(_file.length - 4, _file.length)
if (_suffix === '.png' || _suffix === '.PNG') {
wsControl.filenames.push(_file)
wsControl.fileLength = wsControl.filenames.length
}
}
if (wsControl.fileLength === 0) {
return false
}
return true
}
function setCurrentIdx (idx) {
if (idx >= wsControl.fileLength || idx < 0) {
return false
}
wsControl.current.idx = idx
wsControl.current.filename = wsControl.filenames[idx]
wsControl.current.filepath = global.workspacePath + '\\' + wsControl.filenames[idx]
return true
}
function setCurrDigits (digits) {
wsControl.current.digits = digits
if (wsControl.current.idx < resultDigits.length) {
resultDigits[wsControl.current.idx] = digits
} else if (wsControl.current.idx >= resultDigits.length) {
resultDigits.push(digits)
}
}
function loadCurrDigits (idx) {
if (idx < 0 || idx >= resultDigits.length) wsControl.current.digits = [{ id: 0, value: '' }, { id: 1, value: '' }, { id: 2, value: '' }, { id: 3, value: '' }]
else wsControl.current.digits = resultDigits[idx]
}
function _digitsToLabels (digits) {
var result = []
var len = 0
if (digits[0].value === '') return [0, 0, 10, 10, 10, 10]
else {
result.push(1) // hasNum
result.push(0) // digitLen
for (var i = 0; i < 4; i++) {
if (digits[i].value !== '') {
result.push(digits[i].value)
len++
} else {
result.push(10)
}
}
result[1] = len
}
return result
}
// Linked with openWorkspace() in "@/views/Settings"
// listen to an open-file-dialog command and sending back selected information
const dialog = require('electron').dialog
ipc.on('open-file-dialog', function (event) {
dialog.showOpenDialog({
properties: ['openDirectory']
}, function (files) {
if (files) {
global.workspacePath = files[0]
global.workspaceLoaded = loadWorkspace()
event.sender.send('selected-file', files[0])
event.sender.send('workspace-load-event', global.workspaceLoaded)
}
})
})
// Image control
ipc.on('set-current', function (event, data) {
var ok = false
if (data.key === 'prev') {
setCurrDigits(data.digits)
ok = setCurrentIdx(wsControl.current.idx - 1)
loadCurrDigits(wsControl.current.idx)
} else if (data.key === 'next') {
setCurrDigits(data.digits)
ok = setCurrentIdx(wsControl.current.idx + 1)
loadCurrDigits(wsControl.current.idx)
}
if (ok) {
event.sender.send('current-image-changed', wsControl.current)
}
})
// Export as CSV
const createCsvWriter = require('csv-writer').createObjectCsvWriter
ipc.on('save-to-csv', function (event) {
const csvWriter = createCsvWriter({
path: global.workspacePath + '\\' + global.resultFilename,
header: [
{ id: 'filename', title: 'FILENAME' },
{ id: 'label1', title: 'hasNum' },
{ id: 'label2', title: 'digitLen' },
{ id: 'label3', title: 'DIGIT1' },
{ id: 'label4', title: 'DIGIT2' },
{ id: 'label5', title: 'DIGIT3' },
{ id: 'label6', title: 'DIGIT4' }
]
})
var records = []
for (var i = 0; i < resultDigits.length; i++) {
var record = { filename: '', label1: 0, label2: 0, label3: 0, label4: 0, label5: 0, label6: 0 }
var labels = _digitsToLabels(resultDigits[i])
console.log(labels)
record.filename = wsControl.filenames[i]
record.label1 = labels[0]
record.label2 = labels[1]
record.label3 = labels[2]
record.label4 = labels[3]
record.label5 = labels[4]
record.label6 = labels[5]
records.push(record)
}
csvWriter.writeRecords(records)
.then(() => {
event.returnValue = 'Saved successfully.'
})
.catch(function (err) {
event.returnValue = String(err)
})
})
<template>
<div class="sidebar">
<!-- Side bar -->
<v-navigation-drawer
app
dark
width="220"
permanent
>
<!-- Header -->
<v-list-item>
<v-list-item-content>
<v-list-item-title class="title nonselectable-text">
{{ title }}
</v-list-item-title>
<v-list-item-subtitle class="nonselectable-text">
{{ subtitle }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-divider></v-divider>
<!-- Sidebar Items -->
<v-list dense nav>
<v-list-item
v-for="(item, idx) in items"
:to="item.route"
:key="idx"
class="sidebar-link"
link
>
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
<!-- Version -->
<div class="version-wrapper">
<div class="body-2">{{ version.main }}</div>
<div class="caption">{{ version.build }}</div>
</div>
</v-navigation-drawer>
</div>
</template>
<script>
export default {
name: 'Sidebar',
data: () => ({
title: 'Image Labeller',
subtitle: 'for KHU Capstone Design 1',
items: [
{ title: 'Main', icon: 'mdi-view-dashboard', route: '/main' },
{ title: 'Settings', icon: 'mdi-cog-outline', route: '/settings' }
],
version: {
main: 'v0.7.3-alpha',
build: 'build-2004080227'
}
})
}
</script>
<style lang="scss" scoped>
.sidebar {
.nonselectable-text {
user-select: none;
}
.sidebar-link {
-webkit-app-region: no-drag;
}
.version-wrapper {
position: absolute;
bottom: 10px;
right: 10px;
color: #aaa;
text-align: right;
}
}
</style>
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
Vue.config.productionTip = false
new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
Vue.use(Vuetify)
export default new Vuetify({
})
import Vue from 'vue'
import VueRouter from 'vue-router'
import Main from '../views/Main.vue'
import Settings from '../views/Settings.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
redirect: '/settings'
},
{
path: '/main',
name: 'Main',
component: Main
},
{
path: '/settings',
name: 'Settings',
component: Settings
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
linkActiveClass: 'nav-item active'
})
export default router
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
<template>
<div class="content">
<div class="content-wrapper" :key="$route.fullPath">
<div v-if="!workspaceLoaded" style="color: red">Workspace not loaded!</div>
<div v-else :key="image.now">
<!-- Image Panel -->
<div class="image-panel">
<div class="image-wrapper" :key="image.filename">
<span class="body-2">{{ image.filename }}</span>
<v-img id="target-image" :src="image.filepath"></v-img>
</div>
</div>
<v-divider></v-divider>
<!-- Input Panel -->
<div class="input-panel">
<v-container>
<v-row justify="space-around">
<v-col
v-for="digit in digits"
:key="digit.id"
:id="`input-digit-`+digit.id"
cols="12"
md="3"
>
<v-sheet
class="pa-12"
color="grey lighten-3"
>
<v-sheet
:elevation="6"
class="mx-auto digit-wrapper"
height="80"
width="60"
>
<span class="display-3">{{ digit.value }}</span>
</v-sheet>
</v-sheet>
</v-col>
</v-row>
</v-container>
</div>
<!-- Control Panel -->
<v-bottom-navigation class="control-panel">
<v-btn value="prev" @click="prevImage">
<span>Prev</span>
<v-icon>mdi-skip-previous-outline</v-icon>
</v-btn>
<v-btn class="non-clickable">
<span>{{ image.filename }}</span>
</v-btn>
<v-btn value="next" @click="nextImage">
<span>Next</span>
<v-icon>mdi-skip-next-outline</v-icon>
</v-btn>
</v-bottom-navigation>
<v-snackbar
v-model="snackbar.isOpened"
:timeout="3000"
>
{{ snackbar.text }}
<v-btn
color="blue"
text
@click="snackbar.isOpened = false"
>
Close
</v-btn>
</v-snackbar>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Main',
data: () => ({
workspaceLoaded: false,
currDigitID: 0,
digits: [
{ id: 0, value: '' },
{ id: 1, value: '' },
{ id: 2, value: '' },
{ id: 3, value: '' }
],
image: {
filepath: '',
filename: '',
now: null
},
snackbar: {
isOpened: false,
text: ''
}
}),
mounted () {
// Check if workspace loaded
this.checkWorkspaceLoaded()
// Detect current image data
var vm = this
const ipc = require('electron').ipcRenderer
ipc.on('current-image-changed', function (event, curr) {
console.log(curr)
vm.image.now = curr.idx
vm.image.filepath = curr.filepath
vm.image.filename = curr.filename
// vm.currDigitID = 0
// vm.digits = curr.digits
vm.loadDigits(curr.digits)
})
// Capture keydown events
this._keyCapture()
},
methods: {
checkWorkspaceLoaded () {
const remote = require('electron').remote
this.workspaceLoaded = remote.getGlobal('workspaceLoaded')
},
saveToCSV () {
const ipc = require('electron').ipcRenderer
// ipc.send('save-to-csv')
var result = ipc.sendSync('save-to-csv')
console.log(result)
this.snackbar.text = result
this.snackbar.isOpened = true
},
prevImage () {
this._setCurrentImage('prev')
},
nextImage () {
this._setCurrentImage('next')
},
inputDigit (keyCode) {
if (this.currDigitID > 3) return false
else {
this.digits[this.currDigitID].value = keyCode - 48
this.currDigitID++
return true
}
},
removeDigit () {
if (this.currDigitID <= 0) return false
else {
this.digits[this.currDigitID - 1].value = ''
this.currDigitID--
return true
}
},
loadDigits (newDigits) {
var len = 0
for (var i = 0; i < 4; i++) {
if (newDigits[i].value !== '') len++
else break
}
this.currDigitID = len
this.digits = newDigits
console.log(len)
},
_setCurrentImage (key) {
const ipc = require('electron').ipcRenderer
ipc.send('set-current', { key: key, digits: this.digits })
},
_clearDigits () {
for (var i = 0; i < 4; i++) this.digits[i].value = ''
this.currDigitID = 0
},
_keyCapture () {
var vm = this
window.addEventListener('keyup', function (e) {
if (e.keyCode === 37) { // Left arrow
vm.prevImage()
} else if (e.keyCode === 39) { // Right arrow
vm.nextImage()
} else if (e.keyCode === 83) { // 's' Save
vm.saveToCSV()
} else if (e.keyCode >= 48 && e.keyCode <= 57) { // Numbers
vm.inputDigit(e.keyCode)
} else if (e.keyCode === 8 || e.keyCode === 46) { // Remove
vm.removeDigit()
}
})
}
}
}
</script>
<style lang="scss" scoped>
@import "@/assets/scss/common.scss";
.image-panel {
.image-wrapper {
width: 240px;
margin: 10px auto;
#target-image {
width: 240px;
height: 240px;
background-color: #000000;
}
}
}
.input-panel {
.digit-wrapper {
text-align: center;
vertical-align: middle;
display: table-cell;
}
}
.control-panel {
position: absolute;
bottom: 0;
padding-top: 5px;
.non-clickable {
cursor: default;
}
}
</style>
<template>
<div class="content">
<div class="content-wrapper" :key="$route.fullPath">
<!-- Output Control -->
<v-expansion-panels
class="control-panel"
v-model="panels[0]"
>
<v-expansion-panel>
<v-expansion-panel-header>Output File Control</v-expansion-panel-header>
<v-expansion-panel-content>
<v-text-field
v-model="output"
label="Output Filename"
prepend-icon="mdi-file-delimited-outline"
>
{{ output }}
</v-text-field>
<div class="control-button-wrapper">
<v-btn class="control-button"
small
color="primary"
@click="saveOutputFilename"
>
Save Filename
</v-btn>
</div>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-divider></v-divider>
<!-- Start Filename Control -->
<v-expansion-panels
class="start-filename-panel"
v-model="panels[1]"
>
<v-expansion-panel>
<v-expansion-panel-header>Start Filename Control</v-expansion-panel-header>
<v-expansion-panel-content>
<v-text-field
v-model="startFilename"
label="Start Filename"
prepend-icon="mdi-file-delimited-outline"
>
{{ startFilename }}
</v-text-field>
<div class="control-button-wrapper">
<v-btn class="control-button"
small
color="primary"
@click="saveStartFilename"
>
Save Filename
</v-btn>
</div>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-divider></v-divider>
<!-- Workspace Control -->
<v-expansion-panels
class="control-panel"
v-model="panels[2]"
>
<v-expansion-panel>
<v-expansion-panel-header>Workspace Control</v-expansion-panel-header>
<v-expansion-panel-content>
<span v-if='workspace !== null' class="font-italic font-weight-light">{{ workspace }}</span>
<span v-else class="font-italic font-weight-light">Please open your workspace</span>
<div class="control-button-wrapper">
<v-btn
class="control-button"
id="btn-workspace"
small
color="primary"
@click="openWorkspace"
>
Open Workspace
</v-btn>
</div>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-snackbar
v-model="snackbar.isOpened"
:timeout="4000"
>
{{ snackbar.text }}
<v-btn
color="blue"
text
@click="snackbar.isOpened = false"
>
Close
</v-btn>
</v-snackbar>
</div>
</div>
</template>
<script>
export default {
name: 'settings',
data: () => ({
panels: [0, 0, 0],
workspace: null,
output: 'result.csv',
startFilename: '0.png',
snackbar: {
isOpened: false,
text: ''
}
}),
mounted () {
this.getSetting()
},
methods: {
getSetting () {
const remote = require('electron').remote
var _workspace = remote.getGlobal('workspacePath')
if (_workspace !== '') this.workspace = _workspace
else this.workspace = null
var _result = remote.getGlobal('resultFilename')
if (_result !== '') this.output = _result
else {
this.output = 'result.csv'
remote.getGlobal('setResultFilename')(this.output)
}
},
openWorkspace () {
var vm = this
const ipc = require('electron').ipcRenderer
ipc.send('open-file-dialog')
ipc.on('selected-file', function (event, path) {
vm.workspace = `${path}`
})
ipc.on('workspace-load-event', function (event, ok) {
if (ok === true) {
vm.snackbar.text = 'Workspace loaded successfully!'
vm.snackbar.isOpened = true
} else {
vm.snackbar.text = 'Workspace loaded failed!'
vm.snackbar.isOpened = true
}
})
},
saveOutputFilename () {
const remote = require('electron').remote
remote.getGlobal('setResultFilename')(this.output)
},
saveStartFilename () {
const remote = require('electron').remote
remote.getGlobal('setStartFilename')(this.startFilename)
}
}
}
</script>
<style lang="scss" scoped>
@import "@/assets/scss/common.scss";
.control-panel {
margin: 10px 0;
}
.control-button-wrapper {
float: right;
.control-button {
margin-right: 10px;
}
}
</style>
module.exports = {
transpileDependencies: [
'vuetify'
]
}
This diff could not be displayed because it is too large.