skytnt's picture
fix uncomfortable scrolling problem
2f3e3f5
raw
history blame
14.3 kB
<template>
<v-app>
<v-app-bar app dark color="primary">
<v-toolbar-title>{{i18n.title}}</v-toolbar-title>
<v-spacer></v-spacer>
<v-select style="max-width: 10rem" v-model="locale_name" @change="set_locale(locale_name)" :items="['English','Chinese']" prepend-icon="mdi-web" hide-details></v-select>
</v-app-bar>
<v-main>
<v-row class="mx-3">
<div class="col-12 offset-0 col-sm-8 offset-sm-2 col-md-8 offset-md-2 col-lg-6 offset-lg-3 col-xl-6 offset-xl-3 mt-8">
<v-textarea
v-model="prompt_title"
:label="i18n.input_title_label"
:disabled="loading"
rows="1"
prepend-icon="mdi-comment"
:hint="i18n.input_title_hint"
no-resize
></v-textarea>
<v-textarea
v-model="prompt_text"
:label="i18n.input_text_label"
:disabled="loading"
rows="2"
prepend-icon="mdi-comment"
:hint="i18n.input_text_hint"
no-resize
></v-textarea>
<v-row>
<v-checkbox class="col-6" v-model="show_romaji" :disabled="loading" @change="show_type=show_romaji?0:1" :label="i18n.show_romaji"></v-checkbox>
<v-scroll-x-transition>
<v-select class="col-6" v-if="show_romaji" v-model="romaji_system" :disabled="loading" :items="romaji_systems" :hint="i18n.romaji_system"></v-select>
</v-scroll-x-transition>
</v-row>
<v-row class="mt-n8">
<v-checkbox class="col-6" v-model="use_trans" :disabled="loading" @change="show_type=use_trans?1:0" :label="i18n.use_translate"></v-checkbox>
<v-scroll-x-transition>
<v-select class="col-6" v-if="use_trans" v-model="trans_target_lang" :disabled="loading" :items="google_supported_languages" :hint="i18n.target_lang"></v-select>
</v-scroll-x-transition>
</v-row>
<v-btn color="primary" class="mb-10" elevation="5" @click="do_gen()" :loading="loading" text block large tile>{{i18n.generate}}</v-btn>
<v-expand-transition>
<v-simple-table v-if="lyric.length !== 0" class="py-5" dense>
<template v-slot:default>
<thead>
<tr>
<th>{{i18n.lyric}}<v-btn class="ml-2" elevation="0" @click="copy_lyric('s')" fab x-small><v-icon>mdi-content-copy</v-icon></v-btn></th>
<v-scroll-x-transition>
<th v-if="use_trans||show_romaji">
<span v-if="show_romaji&&!use_trans">{{i18n.romaji}}</span>
<span v-if="!show_romaji&&use_trans">{{i18n.translation}}</span>
<v-btn v-if="use_trans&&show_romaji" class="mr-n2" elevation="0" @click="show_type=show_type===0?1:0" text small>
<span v-if="show_romaji" :class="{'grey--text':show_type===1}">{{i18n.romaji_s}}</span>/<span v-if="use_trans" :class="{'grey--text':show_type===0}">{{i18n.translation_s}}</span>
</v-btn>
<v-btn class="ml-2" elevation="0" @click="copy_lyric(show_type===0?'r':'t')" fab x-small><v-icon>mdi-content-copy</v-icon></v-btn>
</th>
</v-scroll-x-transition>
</tr>
</thead>
<tbody style="word-break: break-word; word-wrap:break-word;">
<tr v-for="(item, idx) in lyric" :key="idx" @mouseenter="hover=idx" @mouseleave="hover=-1">
<td :class="{'text-h6': idx===0, 'text-center': !item.is_lyric}">
{{item.s}}
<v-scroll-x-transition v-if="tts_support">
<v-btn v-if="!tts_loading&&item.s!==''&&(speaking===idx||(speaking===-1&&hover===idx))" :class="{'mr-n9':true,'blue--text':speaking===idx}" icon small @click="ja_tts(idx)">
<v-icon>mdi-volume-high</v-icon>
</v-btn>
</v-scroll-x-transition >
<v-progress-circular v-if="tts_support&&speaking===idx&&tts_loading" class="mr-n9" indeterminate size="20" width="2" color="primary"></v-progress-circular>
</td>
<v-scroll-x-transition>
<td v-if="use_trans||show_romaji" :class="{'text-center': !item.is_lyric}">{{show_type===0?item.r:item.t}}</td>
</v-scroll-x-transition>
</tr>
</tbody>
</template>
</v-simple-table>
</v-expand-transition>
</div>
</v-row>
<v-snackbar dark v-model="snackbar" :timeout="2000">
{{ snackbar_msg }}
<template v-slot:action="{ attrs }">
<v-btn color="blue" text v-bind="attrs" @click="snackbar = false">
Close
</v-btn>
</template>
</v-snackbar>
</v-main>
<v-footer>
<v-row class="ma-0">
<div class="text-center caption col-12 pa-0">{{i18n.footer}}</div>
<div class="text-center caption col-12 pa-0"2022 SkyTNT</div>
</v-row>
</v-footer>
</v-app>
</template>
<style lang="scss">
</style>
<script>
import axios from 'axios'
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.triml().trimr();
}
String.prototype.triml = function () {
return this.replace(/^[\s\n\t]+/g, "");
}
String.prototype.trimr = function () {
return this.replace(/[\s\n\t]+$/g, "");
}
}
function isMobile(){
return /(iPhone|iPad|iPod|iOS|Android|Windows Phone)/i.test(navigator.userAgent);
}
export default {
name: "App",
data:()=>({
locale_name: "English",
i18n:{},
use_trans: false,
google_supported_languages:['af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg', 'my', 'ca', 'ca', 'ceb', 'co', 'cs', 'da', 'nl', 'nl', 'en', 'eo', 'et', 'fi', 'fr', 'fy', 'ka', 'de', 'gd', 'gd', 'ga', 'gl', 'el', 'gu', 'ht', 'ht', 'ha', 'haw', 'he', 'hi', 'hr', 'hu', 'ig', 'is', 'id', 'it', 'jw', 'ja', 'kn', 'kk', 'km', 'ky', 'ky', 'ko', 'ku', 'lo', 'la', 'lv', 'lt', 'lb', 'lb', 'mk', 'ml', 'mi', 'mr', 'ms', 'mg', 'mt', 'mn', 'ne', 'no', 'ny', 'ny', 'ny', 'or', 'pa', 'pa', 'fa', 'pl', 'pt', 'ps', 'ps', 'ro', 'ro', 'ro', 'ru', 'si', 'si', 'sk', 'sl', 'sm', 'sn', 'sd', 'so', 'st', 'es', 'es', 'sr', 'su', 'sw', 'sv', 'ta', 'te', 'tg', 'tl', 'th', 'tr', 'ug', 'ug', 'uk', 'ur', 'uz', 'vi', 'cy', 'xh', 'yi', 'yo', 'zu', 'zh-CN', 'zh-TW'],
trans_target_lang:"zh-CN",
show_romaji:false,
show_type:0,
romaji_systems:["hepburn", "kunrei", "nippon"],
romaji_system:"hepburn",
prompt_title:"桜",
prompt_text:"",
loading:false,
lyric:[],
tts_cache:{},//tts缓存
tts_loading:false,
tts_support:false,
speaking:-1,//正在tts的行数
hover:-1,//鼠标悬停的行数
snackbar:false,
snackbar_msg:""
}),
mounted() {
this.locale_name = (navigator.language ||navigator.userLanguage)==="zh-CN"?"Chinese":"English"
if(this.locale_name==="English")
this.trans_target_lang="en";
this.set_locale(this.locale_name);
this.loads();
},
methods:{
set_locale(name){
let locale = name==="English"?"en":"zh";
let langs={
en:{
title:"Japanese Lyric Generator",
loading:"Loading",
input_title_label:"Title (can be empty)",
input_text_label:"Beginning (can be empty)",
input_title_hint:"Generate lyric for a given title",
input_text_hint:"The generator continues with this text",
show_romaji:"Show romaji",
romaji_system:"Romaji system",
use_translate:"Translate",
target_lang:"Target language",
generate:"Generate!",
lyric:"Lyric",
romaji:"Romaji",
romaji_s:"R",
translation:"Translation",
translation_s:"T",
footer:"The generated content can be used as you like, please share this site if you like.",
copy_successful:"Copy successful",
error:"Error",
error_network:"Network error",
error_failed_load_kuromoji:"Failed to load romaji converter",
error_failed_covert_romaji:"Failed to convert lyric to romaji",
error_failed_translate:"Failed to translate lyric",
error_copy:"Copy failed, browser does not support"
},
zh:{
title:"日语歌词生成器",
loading:"加载中",
input_title_label:"歌词标题(可不填)",
input_text_label:"歌词开头(可不填)",
input_title_hint:"给定标题生成歌词",
input_text_hint:"生成器以这段文本进行续写",
show_romaji:"显示罗马音",
romaji_system:"罗马音系统",
use_translate:"是否进行翻译",
target_lang:"目标语言",
generate:"生成!",
lyric:"歌词",
romaji:"罗马音",
romaji_s:"音",
translation:"翻译",
translation_s:"译",
footer:"生成的内容可以随意使用,喜欢的话就请分享本网站",
copy_successful:"复制成功",
error:"错误",
error_network:"网络错误",
error_failed_load_kuromoji:"罗马音转换器加载失败",
error_failed_covert_romaji:"转换罗马音错误",
error_failed_translate:"翻译出错",
error_copy:"复制失败,浏览器不支持"
}
};
this.i18n = langs[locale];
},
async loads(){
if(!isMobile()){
eval((await axios.get('https://l2d.fab.moe/js/autoload.js')).data);
}
},
async do_gen(){
let prompt_title = this.prompt_title.trim();
let prompt_text = this.prompt_text.trim();
this.loading = true;
this.lyric = [];
this.tts_cache = {};
try {
let result = await axios.post("gen",
{title:prompt_title, text:prompt_text})
let gen_text = `${result.data.title}\n\n${result.data.lyric}`
let lines = gen_text.split('\n')
for (let i in lines) {
let line = lines[i].trim().replace(/ /g, "\u3000")
this.lyric.push({s: line, r: "", t: "", is_lyric: line!=="---end---"})
}
this.lyric[0].is_lyric = false; //title
try {
let romajis = (await axios.post("romaji",{system:this.romaji_system, text:gen_text})).data.romaji
romajis = romajis.split('\n')
for (let i in romajis) {
this.lyric[i].r = romajis[i]
}
}catch (e) {
this.show_snackbar(this.i18n.error_failed_covert_romaji+":" + e.message)
}
if (this.use_trans) {
let trans = await this.google_translate(gen_text);
if (trans !== "") {
trans = trans.split("\n")
for (let i in trans) {
this.lyric[i].t = trans[i].trim().replace(/ /g, "\u3000")
}
}
}
this.loading = false;
} catch (e) {
console.log(e)
this.show_snackbar(this.i18n.error+":" + e.message)
this.loading = false;
}
},
async google_translate(text){
let params = {client: "gtx", dt: "t", sl: "ja", tl: this.trans_target_lang, q: text};
try {
let result = await axios.get("https://translate.googleapis.com/translate_a/single",{
params:params
});
let out = "";
let lines = result.data[0];
for(let i in lines)
out += lines[i][0];
return out;
}catch(e){
this.show_snackbar(this.i18n.error_failed_translate+":"+e.message);
}
return "";
},
async ja_tts(line){
let text = this.lyric[line].s;
if(this.speaking !== -1||text === ""){
return;
}
this.speaking = line;
try{
let context = new AudioContext();
let gain = context.createGain();
gain.gain.value=2;
if(this.tts_cache[text] === undefined){
this.tts_loading=true;
let audio_query = (await axios.post("https://voicevox-skytnt.cloud.okteto.net/audio_query",null,
{params:{speaker:4,text:text}})).data;
let wave = (await axios.post("https://voicevox-skytnt.cloud.okteto.net/synthesis",audio_query,
{params:{speaker:4},responseType:"arraybuffer"})).data;
this.tts_loading=false;
this.tts_cache[text] = await context.decodeAudioData(wave);
}
let source = context.createBufferSource();
source.buffer = this.tts_cache[text];
source.loop = false;
source.connect(gain);
gain.connect(context.destination);
await new Promise((resolve) => {
source.onended = () => {resolve()}
source.start(0);//立即播放
});
}catch (e) {
this.show_snackbar(this.i18n.error+":"+e.message)
}
this.speaking = -1;
this.tts_loading=false;//防止网络出错导致tts_loading为true
},
copy_lyric(attr_name){
let text = ""
for(let i in this.lyric){
text += this.lyric[i][attr_name] + "\n"
}
text = text.trim()
this.copy(text)
},
async copy(text){
try {
await navigator.clipboard.writeText(text)
this.show_snackbar(this.i18n.copy_successful)
}catch (e) {
let input = document.createElement('textarea')
input.style.position = 'fixed'
input.style.top = '-10000px'
input.style.zIndex = '-999'
document.body.appendChild(input)
console.log('input', input)
input.value = text
input.focus()
input.select()
try {
let result = document.execCommand('copy')
if (!result || result === 'unsuccessful') {
this.show_snackbar(this.i18n.error_copy)
} else {
this.show_snackbar(this.i18n.copy_successful)
}
} catch (e) {
this.show_snackbar(this.i18n.error_copy)
}finally {
document.body.removeChild(input)
}
}
},
show_snackbar(msg){
this.snackbar=true;
this.snackbar_msg=msg;
}
}
}
</script>