webui finally
All checks were successful
mozhi pipeline / Push Docker image to Codeberg docker registry (push) Successful in 3m28s
mozhi pipeline / Build and publish artifacts (push) Successful in 12m36s

This commit is contained in:
Arya 2023-09-10 17:27:22 +05:30
parent 566eb69743
commit 8403dfe227
Signed by: arya
GPG Key ID: 842D12BDA50DF120
12 changed files with 371 additions and 448 deletions

View File

@ -76,3 +76,9 @@ These envvars turn off/on engines. By default all of them are enabled.
## Etymology
Mozhi is the word in Tamil for language. Simple as that :P
## Credits
- [Arya](https://aryak.me): creator
- [Midou36o](https://midou.dev): made the logo
- [Missuo](https://github.com/missuo): making gDeepLX that does the hard part of making DeepL work
- [SimplyTranslate](https://codeberg.org/simpleweb/simplytranslate): Inspiration and base code for the webui

View File

@ -1,5 +1,8 @@
# TODO
- Create a web interface
- Support disabling engines with Web interface
- Make LibreTranslate work with web interface
- Write an about page for web
- Make detectlanguage swap the text as well
- Interactive/Step-by-step CLI
- Ability for user to choose engines they want to use
- Proper Error handling for requests.go

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.21.0
replace github.com/OwO-Network/gdeeplx => github.com/gi-yt/gdeeplx v0.0.0-20230817133036-0eb71706cd51
require (
codeberg.org/aryak/libmozhi v0.0.0-20230909133311-5b21e2215c4c
codeberg.org/aryak/libmozhi v0.0.0-20230909142818-70619214b1ea
github.com/gofiber/fiber/v2 v2.49.1
github.com/gofiber/swagger v0.1.13
github.com/gofiber/template v1.8.0

2
go.sum
View File

@ -47,6 +47,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
codeberg.org/aryak/libmozhi v0.0.0-20230909133311-5b21e2215c4c h1:lnG0+NunYw4x6TUl00MQvucNTypqK6skms9MsKx0P2E=
codeberg.org/aryak/libmozhi v0.0.0-20230909133311-5b21e2215c4c/go.mod h1:DOxFxtSCKbs3sfKLxDwho9RFqkZy1opa67/c9v+/G+I=
codeberg.org/aryak/libmozhi v0.0.0-20230909142818-70619214b1ea h1:WqElyz20JDxXiQveo2TkNP2q0zyEK3i9VP5P6p7Dh7o=
codeberg.org/aryak/libmozhi v0.0.0-20230909142818-70619214b1ea/go.mod h1:DOxFxtSCKbs3sfKLxDwho9RFqkZy1opa67/c9v+/G+I=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=

View File

@ -1,26 +1,66 @@
package pages
import (
"fmt"
"runtime"
"github.com/gofiber/fiber/v2"
"codeberg.org/aryak/libmozhi"
)
func HandleIndex(c *fiber.Ctx) error {
// Check if all required queries are present
m := c.Queries()
sl, _ := m["sl"]
tl, _ := m["tl"]
engine, _ := m["engine"]
text, _ := m["text"]
if sl != "" && tl != "" && engine != "" && text != "" {
fmt.Println("Work")
engines := []string{"google", "deepl", "duckduckgo", "libretranslate", "mymemory", "reverso", "watson", "yandex"}
var engine string
var originalText string
if c.Query("engine") == "" {
engine = "google"
}
if c.Query("engine") != "" {
for _, name := range engines {
if c.Query("engine") == name {
engine = c.Query("engine")
}
}
if engine == "" {
engine = "google"
}
}
sourceLanguages, err1 := libmozhi.LangList(engine, "sl")
targetLanguages, err2 := libmozhi.LangList(engine, "tl")
if err1 != nil || err2 != nil {
return fiber.NewError(fiber.StatusInternalServerError, err1.Error()+err2.Error())
}
originalText = c.Query("text")
to := c.Query("to")
from := c.Query("from")
var translation libmozhi.LangOut
var tlerr error
var ttsFrom string
var ttsTo string
if engine != "" && originalText != "" && from != "" && to != "" {
translation, tlerr = libmozhi.Translate(engine, to, from, originalText)
if tlerr != nil {
return fiber.NewError(fiber.StatusInternalServerError, tlerr.Error())
}
if engine == "google" || engine == "reverso" {
if from == "auto" {
//ttsFrom = "/api/tts?lang="+translation.AutoDetect+"&engine="+engine+"&text="+originalText
ttsFrom = ""
} else {
ttsFrom = "/api/tts?lang="+from+"&engine="+engine+"&text="+originalText
}
ttsTo = "/api/tts?lang="+to+"&engine="+engine+"&text="+translation.OutputText
}
}
return c.Render("index", fiber.Map{
"host": c.Hostname(),
"fiberversion": fiber.Version,
"goversion": runtime.Version(),
"Engine": engine,
"enginesNames": engines,
"SourceLanguages": sourceLanguages,
"TargetLanguages": targetLanguages,
"OriginalText": originalText,
"Translation": translation,
"From": from,
"To": to,
"TtsFrom": ttsFrom,
"TtsTo": ttsTo,
})
}

View File

@ -1,380 +0,0 @@
/* god bless you */
:root {
--text: #fff;
--background: #252525;
--secondary-background: #353535;
--background-darker: #151515;
--accent: #00b7c3;
--yellow: #8b8000;
color-scheme: dark;
}
body {
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui,
helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
sans-serif;
color: var(--text);
background-color: var(--background-darker);
transition: ease-in-out 0.25s;
margin: 0;
}
::-webkit-scrollbar {
width: 8px;
height: 10px;
}
::-webkit-scrollbar-thumb {
border-radius: 8px;
background: var(--background);
}
main {
margin: 0 24vw;
}
.margin-ow {
margin: 0 12vw;
}
pre {
overflow: scroll;
background: var(--secondary-background);
border-radius: 4px;
padding: 4px;
}
#wrap {
display: flex;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
padding: 8px;
margin-bottom: 8px;
background-color: var(--background);
}
.index-gh {
background: var(--background);
color: var(--text);
padding: 4px;
border-radius: 8px;
}
.brand {
display: flex;
align-items: center;
gap: 8px;
transition: ease-in-out 0.25s;
color: var(--text);
}
.navbar-slogan:hover {
text-decoration: underline;
text-underline-offset: 5px;
}
.brand:hover {
color: var(--text);
text-decoration: underline;
}
.error {
background-color: var(--background);
padding: 8px;
border-radius: 4px;
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui,
helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
sans-serif;
color: var(--text);
}
.center {
text-align: center;
}
a {
color: var(--text);
text-decoration: underline;
text-underline-offset: 5px;
}
a:hover {
color: var(--accent);
}
.setup-notice {
background-color: var(--yellow);
padding: 8px;
border-radius: 4px;
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui,
helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
sans-serif;
color: var(--text);
}
.setup-notice > a {
text-decoration: underline;
text-underline-offset: 5px;
}
.setup-notice > a:hover {
color: var(--text);
opacity: 1;
}
/* URI: /explore */
.explore-card,
.user-repo-card {
background-color: var(--background);
padding: 8px;
border-radius: 4px;
margin: 8px 0 8px 0;
text-overflow: ellipsis;
white-space: wrap;
text-decoration: none;
display: flex;
transition: ease-in-out 0.25s;
}
.explore-card:hover,
.user-repo-card:hover {
background-color: var(--secondary-background);
color: var(--text);
}
/* URI: /:user */
.user-profile {
background-color: var(--background);
padding: 8px;
border-radius: 4px;
margin: 8px;
display: flex;
flex-direction: column;
align-items: center;
}
.user-profile h1 {
color: var(--accent);
margin: 4px;
word-wrap: nowrap;
text-overflow: ellipsis;
overflow-wrap: anywhere;
}
.user-profile h2 {
margin: 4px;
}
.user-profile p {
margin: 4px;
}
.user-profile img {
border: 4px solid var(--accent);
border-radius: 50%;
}
.user-bio,
.user-readme {
background-color: var(--background);
padding: 8px;
border-radius: 4px;
margin: 8px;
}
.user-bio-text,
.user-readme-text {
background-color: var(--background-darker);
padding: 8px;
border-radius: 4px;
margin-bottom: 0;
}
.user-readme-text {
margin-top: 0;
}
.social-links {
display: flex;
flex-direction: column;
}
.user-repo-card {
background-color: var(--background-darker);
padding: 4px;
border-radius: 4px;
margin: 8px 0 8px 0;
text-overflow: ellipsis;
white-space: wrap;
text-decoration: none;
display: flex;
flex-direction: column;
transition: ease-in-out 0.25s;
}
.user-repo-card > p {
margin: 8px;
}
.user-repo-card:hover {
background-color: var(--secondary-background);
color: var(--text);
}
.lang-bar:last-child {
margin-bottom: 0;
}
.lang-bar {
width: 100%;
display: inherit;
height: 10px;
border-radius:4px;
overflow: hidden; /* To force the corners to be round. */
user-select: none; /* Prevent the selection */
-webkit-user-select: none; /* Safari specific fix. */
}
/* URI: /:user/:repo */
.button {
background-color: var(--background);
padding: 8px;
border-radius: 4px;
margin: 8px;
color: var(--text);
text-decoration: none;
text-align: center;
display: inline-block;
}
.button:hover {
background-color: var(--secondary-background);
color: var(--text);
}
.button-parent {
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.file-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.file-u-list {
padding: 0;
margin: 0;
list-style-type: none;
}
/* URI: /file/:user/:repo/:file */
.user-readme pre {
padding: 8px;
border-radius: 4px;
/*white-space: pre-wrap;*/
overflow-x: auto;
}
.user-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.secondary {
background-color: var(--secondary-background);
}
/* Overwrite */
.no-margin {
margin: 0;
}
.no-padding {
padding: 0;
}
.float-right {
float: right;
display: grid;
flex: none;
align-self: flex-start;
}
@media screen and (prefers-color-scheme: light) {
:root {
--text: #000;
--background: #ececec;
--secondary-background: #f5f5f5;
--background-darker: #dddddd;
--accent: #005ec3;
--yellow: #f5f5a5;
color-scheme: light;
}
a {
color: black;
}
a:hover {
color: #005ec3;
}
.explore-card {
background-color: var(--background);
color: #000;
}
}
@media screen and (max-width: 900px) {
main {
margin: 8px;
}
#wrap {
display: flex;
flex-direction: column;
}
.float-right {
float: none;
display: inline-grid;
}
.navbar-slogan {
display: none;
}
.navbarImg {
height: 40px;
}
.explore-card {
margin-left: 0;
margin-bottom: 8px;
margin-right: 0;
}
.social-links {
flex-direction: column;
}
}
.cl {
padding-left: 8px;
}

171
public/css/style.css Normal file
View File

@ -0,0 +1,171 @@
/*General theming*/
body {
font-family: sans-serif;
margin: 20px auto;
max-width: 1100px;
line-height: 1.5em;
font-size: 1.1em;
background-color: #222;
color: #f8f9fa;
padding: 0 10px;
hyphens: auto;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
flex-wrap: wrap;
border-bottom: 1px solid #b2b2b2;
width: 95%;
}
footer {
width: 95%;
border-top: 1px solid #b2b2b2;
}
footer p {
margin: 0.2em 0 0.2em 0;
}
a {
color: #3c67ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Navigation */
nav {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
gap: 1rem;
}
nav li {
display: inline-block no;
margin-right: 20px;
}
.navlogo {
width: 50px;
height: 50px;
}
/* Theming for buttons and text areas*/
textarea {
width: 100%;
font-size: 1rem;
padding: 4px;
border: 2px solid #888888;
background-color: #222;
border-color: #9d9d9d;
color: #f8f9fa;
}
textarea:focus {
border-color: #e5ebff;
outline: 1px solid #e5ebff;
}
select {
display: flex;
padding: 4px 8px;
justify-content: space-between;
align-items: center;
flex: 1 0 0;
border-right: none;
border-bottom: none;
border-top: none;
border-radius: 4px;
border-left: 2px solid #3c67ff;
/* Accent shadow */
box-shadow: 2px 2px 0px 0px rgba(60, 103, 255, 0.25);
color: #b2b2b2;
background-color: #2f2f2f;
}
button {
display: flex;
padding: 4px 8px;
justify-content: flex-end;
align-items: center;
gap: 2px;
border: none;
border-radius: 4px;
background: #3c67ff;
}
button:hover {
box-shadow: 5px 5px 0px 0px rgba(60, 103, 255, 0.25);
cursor: pointer;
}
/* Spacing stuff */
.wrap {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.wrap.languages {
flex-wrap: nowrap;
margin-bottom: 20px;
}
.language {
margin: 0px 10px;
}
.item {
width: 100%;
height: 150px;
}
.item-wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 450px;
margin: 5px 10px;
gap: 10px;
}
/* CSS for the custom engine selector */
.custom-select {
position: relative;
display: inline-block;
}
.selected-option {
display: block;
padding: 7px;
border: 1px solid #ccc;
border-radius: 4px;
text-decoration: none;
color: #f8f9fa;
}
.options {
display: none;
position: absolute;
background-color: #222;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
list-style: none;
padding: 0;
margin: 0;
width: 100%;
z-index: 1;
}
.options li {
padding: 10px;
}
.options li:hover {
background-color: #f0f0f0;
}
.custom-select:hover .options {
display: block;
}

View File

@ -85,10 +85,18 @@ func Serve(port string) {
})
app.Get("/", pages.HandleIndex)
app.Get("/switchlanguages", func(c *fiber.Ctx) error {
engine := c.Query("engine")
from := c.Query("from")
to := c.Query("to")
text := c.Query("text")
return c.Redirect("/?engine="+engine+"&from="+to+"&to="+from+"&text="+text+"&redirected=true", 301)
})
app.Static("/css", "./public/css", staticConfig)
app.Static("/robots.txt", "./public/robots.txt", staticConfig)
app.Static("/favicon.ico", "./public/assets/favicon.ico", staticConfig)
app.Static("/mozhi.svg", "./public/assets/mozhi.svg", staticConfig)
app.Static("/mozhi.png", "./public/assets/mozhi.png", staticConfig)
// app.Get("/about", pages.HandleAbout)
api := app.Group("/api")

View File

@ -1,18 +0,0 @@
{{ template "header" .}}
<main>
<div class="center">
<h1 style="color: red">Error</h1>
<h2>
Someone pushed to production. Just kidding, that's probably not what
happened. Here's the error:
</h2>
<pre class="error">{{.error}}</pre>
<h3>
Think this is a bug?
<a href="https://codeberg.org/aryak/mozhi/issues" target="_blank"
>Create an issue on Codeberg.</a
>
</h3>
</div>
</main>
{{ template "footer" .}}

View File

@ -1,5 +1,14 @@
<footer class="center">
<a href="https://codeberg.org/aryak/mozhi">Codeberg</a> | <a href="/instance">About Instance</a>
</footer>
</body>
<br />
<footer style="text-align: center">
<br />
<p>
Copyright 2023 <a href="https://aryak.me">Arya Kiran</a> and other
contributors
</p>
<p>
View Source Code on
<a href="https://codeberg.org/aryak/mozhi">Codeberg</a>
</p>
</footer>
</body>
</html>

View File

@ -2,12 +2,27 @@
<html lang="en">
<head>
{{ if .title }}
<title>{{ .title }} - Mozhi</title>
{{ else }}
<title>{{.title}} - Mozhi</title>
{{else}}
<title>Mozhi</title>
{{ end }}
<link rel="stylesheet" href="/css/global.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{{end}}
<link rel="shortcut icon" href="/favicon.ico" />
<meta
name="description"
content="Mozhi is an alternative-frontend for many translation engines."
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset="UTF-8" />
<meta name="referrer" content="no-referrer" />
<link rel="stylesheet" href="/css/style.css" />
</head>
<body>
<header>
<a href="/"><img src="/mozhi.png" alt="Mozhi Logo" class="navlogo" /></a>
<nav>
<a href="/about">About</a>
<a href="/api/swagger">API</a>
</nav>
</header>
<br />

View File

@ -1,28 +1,95 @@
{{template "header" .}}
<main>
<h2>Mozhi</h2>
<h3>Translate</h3>
<form action="/" method="get">
<label for="textin">Text To Translate:</label>
<textarea id="textin" name="textin" rows="4" cols="50"></textarea>
<input type="checkbox" name="vehicle1" value="Bike">
<label for="vehicle1"> I have a bike</label><br>
<input type="checkbox" name="vehicle2" value="Car">
<label for="vehicle2"> I have a car</label><br>
<input type="checkbox" name="vehicle3" value="Boat" checked>
<label for="vehicle3"> I have a boat</label><br><br>
<input type="submit" value="Submit">
</form>
<h3>Info</h3>
{{ if eq .version "unknown, please build with Go 1.13+ or use Git"}} Mozhi
version: <code>unknown</code> {{ else }} Mozhi version:
<a href="https://codeberg.org/aryak/mozhi/commit/{{ .version}}"
><code>{{ .version}}</code></a
>
{{ end }}
<br />
Fiber version: {{ .fiberversion}}, running on {{ .goversion}}
<!-- Need to do this custom selector thingy since <select> cant submit on click -->
<div class="custom-select">
Translate with: <a href="#" class="selected-option">{{.Engine}}</a>
<ul class="options">
{{range .enginesNames}}
<a href="/?engine={{.}}"><li>{{.}}</li></a>
{{end}}
</ul>
</div>
<br /><br />
<form action="/" method="GET" id="translation-form">
<!-- This hidden input is so that the engine gets sent in the GET request even though its not declared here -->
<input name="engine" value="{{.Engine}}" type="hidden" />
<div class="wrap languages">
<div class="language">
<select name="from" aria-label="Source language">
{{range $key, $value := .SourceLanguages}}
<option value="{{ .Id }}" {{if eq $.From .Id}}selected{{end}}>
{{ .Name }}
</option>
{{end}}
</select>
</div>
<div class="switch_languages">
<button
id="switchbutton"
aria-label="Switch languages"
formaction="/switchlanguages?engine={{ .Engine }}"
type="submit"
>
&lt;-&gt;
</button>
</div>
<div class="language">
<select name="to" aria-label="Target language">
{{range $key, $value := .TargetLanguages}}
<option value="{{ .Id }}" {{if eq $.To .Id}}selected{{end}}>
{{ .Name }}
</option>
{{end}}
</select>
</div>
</div>
<div class="wrap">
<div class="item-wrapper">
<textarea
autofocus
class="item"
id="input"
name="text"
dir="auto"
placeholder="Enter Text Here"
>
{{ .OriginalText }}</textarea
>
{{if .TtsFrom}}
<audio controls>
<source type="audio/mpeg" src="{{ .TtsFrom }}" />
</audio>
{{end}}
</div>
<div class="item-wrapper">
<textarea
id="output"
class="translation item"
dir="auto"
placeholder="Translation"
readonly
>
{{.Translation.OutputText}}</textarea
>
{{if .Translation.AutoDetect}}
<p>
Detected Language: {{.Translation.AutoDetect}}{{end}} {{if .TtsTo}}
<audio controls>
<source type="audio/mpeg" src="{{ .TtsTo }}" />
</audio>
{{end}}
</p>
</div>
</div>
<div style="text-align: center">
<button type="submit">Translate with {{ .Engine }}!</button>
</div>
</form>
</main>
{{ template "footer" .}}