master
yutent 2024-01-11 17:57:52 +08:00
parent ab165c0002
commit 15e4a8062a
11 changed files with 249 additions and 578 deletions

View File

@ -14,11 +14,6 @@ __dir__ = os.path.dirname(os.path.realpath(__file__))
web_root = os.path.join(__dir__, './webapp') web_root = os.path.join(__dir__, './webapp')
home_dir = os.getenv('HOME') home_dir = os.getenv('HOME')
config_dir = os.path.join(home_dir, '.config/dooke')
if not os.path.isdir(config_dir):
os.mkdir(config_dir)
class Application(Gtk.Application): class Application(Gtk.Application):

View File

@ -6,10 +6,21 @@
import 'es.shim' import 'es.shim'
import { html, css, Component } from 'wkit' import { html, css, Component } from 'wkit'
import { createApp, createRouter } from 'wkitd' import { createApp } from 'wkitd'
import 'ui/icon/index.js'
import 'ui/scroll/index.js'
import 'ui/layer/index.js'
import 'ui/space/index.js'
import 'ui/form/input.js'
import 'ui/form/switch.js'
import 'ui/form/button.js'
import 'ui/form/link.js'
import router from './router.js'
import store from './store.js' import store from './store.js'
import './components/home.js'
import '../components/sidebar.js'
import { noop } from './utils/index.js' import { noop } from './utils/index.js'
@ -26,6 +37,11 @@ createApp({
width: 100%; width: 100%;
height: 100vh; height: 100vh;
} }
router-view {
flex: 1;
height: 100;
}
` `
], ],
methods: { methods: {
@ -34,8 +50,18 @@ createApp({
} }
}, },
render() { render() {
return html` <wc-home @contextmenu.prevent=${noop}></wc-home> ` return html`
<wc-sidebar></wc-sidebar>
<router-view></router-view>
<wc-layer ref="context" left="100px" top="0" radius="0">
<ul class="context-menu" @click=${this.confirmAction}>
<li class="item" data-act="del">删除域名</li>
<li class="item" data-act="edit">修改域名</li>
</ul>
</wc-layer>
`
} }
}) })
.use(router)
.use(store) .use(store)
.mount() .mount()

View File

@ -1,262 +0,0 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/08 18:19:17
*/
import { html, css, Component, classMap, nextTick, outsideClick } from 'wkit'
import 'ui/icon/index.js'
import 'ui/scroll/index.js'
import 'ui/layer/index.js'
import 'ui/space/index.js'
import 'ui/form/input.js'
import 'ui/form/switch.js'
import 'ui/form/button.js'
import 'ui/form/link.js'
import './sidebar.js'
import './permission.js'
import './records.js'
import { checkPermission, getHistory, saveHosts, noop } from '../utils/index.js'
const HOST_DATA = await getHistory()
class Home extends Component {
static props = {
editDomain: '', // 当前临时要编辑的域名, 即右键菜单选择到的
permissionShow: false
}
static styles = [
css`
:host {
flex: 1;
display: flex;
width: 100%;
height: 100%;
padding: 32px;
color: var(--color-dark-1);
background: #f0f0f0;
}
.visible {
display: block;
}
ul li {
list-style: none;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
`,
css`
.main {
flex: 1;
display: flex;
flex-direction: column;
}
.main .toolbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 15px;
border-bottom: 1px solid var(--color-plain-3);
background: var(--color-plain-1);
}
.main .list {
overflow: hidden;
flex: 1;
}
`,
css`
.context-menu {
display: flex;
flex-direction: column;
width: 100px;
padding: 5px 0;
background: #fff;
}
.context-menu .item {
height: 30px;
line-height: 30px;
padding: 0 15px;
cursor: pointer;
}
.context-menu .item :hover {
background: #f2f5fc;
}
`
]
async mounted() {
let writable = await checkPermission()
if (writable) {
this.$store.HOST_DATA = HOST_DATA
this.$store.domains = Object.keys(HOST_DATA)
this.$refs.domain.mounted()
outsideClick(this.$refs.context, _ => this.$refs.context.close())
} else {
this.permissionShow = true
}
}
addRecord() {
if (this.$store.activeDomain) {
this.$store.records.push({
record: '',
value: '',
enabled: true,
remark: ''
})
nextTick(_ => (this.$refs.list.scrollTop = 1e6))
} else {
layer.toast('请先选择域名', 'warn')
}
}
showMenu(ev) {
this.$refs.context.close()
var { pageX, pageY } = ev
if (pageY + 70 > 600) {
pageY -= 70
}
var elem = ev.target
if (elem.tagName !== 'LI') {
elem = elem.parentNode
}
this.editDomain = elem.dataset.name
setTimeout(_ => {
this.$refs.context.moveTo({ left: pageX + 'px', top: pageY + 'px' })
this.$refs.context.show()
})
}
confirmAction(ev) {
this.$refs.context.close()
if (ev.target.tagName === 'LI') {
let act = ev.target.dataset.act
let { HOST_DATA, records, domains } = this.$store
let idx = domains.indexOf(this.editDomain)
if (act === 'del') {
layer
.confirm(`是否要删除域名「${this.editDomain}」?`)
.then(res => {
if (this.editDomain === this.$store.activeDomain) {
if (records.length) {
return layer.toast(
'该域名下有主机记录, 请先删除主机记录后再删除域名',
'error'
)
}
} else {
if (HOST_DATA[this.editDomain].length > 0) {
return layer.toast(
'该域名下有主机记录, 请先删除主机记录后再删除域名',
'error'
)
}
}
delete HOST_DATA[this.editDomain]
domains.splice(idx, 1)
this.editDomain = ''
this.$store.records = []
this.$store.activeDomain = domains[0]
this.$refs.domain.mounted()
this.save()
})
.catch(noop)
} else if (act === 'edit') {
layer
.prompt(
`请输入新的名字「${this.editDomain}`,
this.editDomain,
(val, done) => {
if (val === this.editDomain || HOST_DATA[val]) {
return layer.toast(`${val} 域名没有变化, 或已经存在`)
}
if (
val === 'localhost' ||
val === 'local' ||
/^[\w.]+\.[a-z]+$/.test(val)
) {
done()
} else {
layer.toast('域名格式错误', 'error')
}
}
)
.then(val => {
domains[idx] = val
HOST_DATA[val] = HOST_DATA[this.editDomain]
delete HOST_DATA[this.editDomain]
this.$store.activeDomain = val
this.editDomain = ''
this.$refs.domain.mounted()
this.save()
})
.catch(noop)
}
}
}
save() {
let { HOST_DATA, activeDomain, records } = this.$store
if (activeDomain) {
HOST_DATA[activeDomain] = records.filter(it => it.record && it.value)
}
saveHosts(HOST_DATA)
layer.toast('保存成功', 'success')
}
render() {
return html`
<wc-sidebar
ref="domain"
@toggle-domain=${_ => (this.$refs.list.scrollTop = 0)}
@show-menu=${ev => this.showMenu(ev.event)}
@save=${this.save}
></wc-sidebar>
<main class="main noselect">
<header class="toolbar">
<wc-button size="m" icon="plus" @click=${this.addRecord}
>新增记录</wc-button
>
<wc-button size="m" icon="fly" @click=${this.save}>保存</wc-button>
</header>
<wc-records class="list" ref="list"></wc-records>
</main>
<wc-permission
class=${classMap({ visible: this.permissionShow })}
></wc-permission>
<wc-layer ref="context" left="100px" top="0" radius="0">
<ul class="context-menu" @click=${this.confirmAction}>
<li class="item" data-act="del">删除域名</li>
<li class="item" data-act="edit">修改域名</li>
</ul>
</wc-layer>
`
}
}
Home.reg('home')

View File

@ -1,109 +0,0 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/08 18:19:17
*/
import { html, css, Component } from 'wkit'
import { checkPermission } from '../utils/index.js'
const tips_header = `/************************************************************/
* hosts文件没有写权限 *
/************************************************************/
`
class Permission extends Component {
static styles = css`
:host {
display: none;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
.permission-error {
position: fixed;
left: 0;
top: 0;
z-index: 1024;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
padding: 24px 52px;
line-height: 1.5;
background: rgba(255, 233, 233, 0.95);
-webkit-backdrop-filter: blur(5px);
}
pre {
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
color: var(--color-red-1);
}
fieldset {
width: 600px;
padding: 0 30px 30px;
border: 1px solid var(--color-orange-1);
border-radius: 8px;
}
legend {
padding: 0 10px;
font-size: 16px;
}
dt {
margin-top: 20px;
font-weight: bold;
}
code {
display: block;
padding: 8px 10px;
margin-top: 8px;
border-left: 3px solid var(--color-plain-3);
background: rgba(255, 255, 255, 0.8);
font-family: 'Courier New', Courier, monospace;
}
`
async check() {
let writable = await checkPermission()
if (writable) {
location.reload()
} else {
layer.toast('hosts文件没有写权限, 请按提示修改', 'error')
}
}
render() {
return html`
<div class="permission-error">
<pre class="noselect">${tips_header}</pre>
<fieldset>
<legend>操作指引</legend>
<dl>
<dt>MacOS用户</dt>
<dd>打开终端, 执行以下命令</dd>
<dd><code>sudo chown $USER:admin /etc/hosts</code></dd>
<dt>Linux用户</dt>
<dd>打开终端, 执行以下命令</dd>
<dd><code>sudo chown $USER: /etc/hosts</code></dd>
<dt>完成之后</dt>
<dd>点击下面的按钮重新检测.</dd>
<dd>
<wc-button type="danger" @click=${this.check}>权限检测</wc-button>
</dd>
</dl>
</fieldset>
</div>
`
}
}
Permission.reg('permission')

View File

@ -15,7 +15,7 @@ class Sidebar extends Component {
flex-direction: column; flex-direction: column;
width: 180px; width: 180px;
height: 100vh; height: 100vh;
background: var(--color-plain-2); background: var(--color-plain-1);
} }
.noselect { .noselect {
-webkit-touch-callout: none; -webkit-touch-callout: none;
@ -23,26 +23,28 @@ class Sidebar extends Component {
user-select: none; user-select: none;
} }
.domain-list { .navibar {
overflow: hidden; overflow: hidden;
flex: 1; flex: 1;
display: flex;
flex-direction: column;
width: 100%; width: 100%;
gap: 8px;
padding: 16px;
} }
.domain-list .item { .navibar .item {
display: flex; display: flex;
justify-content: flex-end; justify-content: center;
align-items: center; align-items: center;
height: 40px; height: 40px;
padding: 0 12px; padding: 0 12px;
gap: 12px;
border-radius: 20px;
cursor: pointer; cursor: pointer;
transition: background 0.15s ease-in-out; transition: background 0.15s ease-in-out;
}
.item wc-icon { --wc-icon-size: 16px;
--wc-icon-size: 12px;
margin-left: 8px;
color: var(--color-grey-2);
} }
.item:hover, .item:hover,
@ -50,114 +52,43 @@ class Sidebar extends Component {
background: #fff; background: #fff;
} }
.item.active { .item.active {
border-right: 2px solid var(--color-orange-1); color: var(--color-orange-3);
color: var(--color-orange-1); box-shadow: 0 0 6px rgba(0, 0, 0, 0.05);
}
.item.active wc-icon {
color: var(--color-orange-1);
}
.item.blank {
justify-content: center;
cursor: default;
}
.item.blank:hover {
background: none;
}
.action {
flex-shrink: 0;
display: flex;
align-items: center;
height: 50px;
padding: 0 10px;
} }
` `
addDomain() { switchNavi(id) {
layer let path = '/' + id
.prompt('请输入根域名', function (val, done) { this.$store.navi = id
if ( if (id === 'containers') {
val === 'localhost' || path = '/'
val === 'local' ||
/^[\w.\-]+\.[a-z]+$/.test(val)
) {
done()
} else {
layer.toast('域名格式错误', 'error')
} }
}) this.$router.push(path)
.then(val => { localStorage.setItem('last_navi', id)
let { HOST_DATA, records } = this.$store
this.$store.domains.push(val)
HOST_DATA[val] = []
if (!this.$store.activeDomain) {
this.toggleDomain(null, val)
}
this.$emit('save')
})
.catch(noop)
}
toggleDomain(ev, name) {
let { HOST_DATA, records } = this.$store
name = name ?? ev.currentTarget.dataset.name
this.$store.activeDomain = name
this.$store.records = records = (HOST_DATA[name] || []).sort((a, b) =>
a.record.localeCompare(b.record)
)
let tmp_records = Object.create(null)
for (let it of records) {
if (tmp_records[it.record]) {
tmp_records[it.record].push(it)
} else {
tmp_records[it.record] = [it]
}
}
this.$store.tmp_records = tmp_records
document.title = `伪域名解析 ${name} `
localStorage.setItem('last_domain', name)
nextTick(() => {
this.$emit('toggle-domain')
})
} }
mounted() { mounted() {
this.toggleDomain(null, this.$store.activeDomain) this.switchNavi(this.$store.navi)
} }
render() { render() {
return html` return html`
<wc-scroll class="domain-list noselect"> <menu class="navibar">
<ul ${this.$store.menus.map(
@contextmenu.prevent=${ev => this.$emit('show-menu', { event: ev })}
>
${this.$store.domains.map(
it => html` it => html`
<li <li
class=${classMap({ class=${classMap({
item: true, item: true,
active: it === this.$store.activeDomain active: it.id === this.$store.navi
})} })}
data-name=${it} @click=${ev => this.switchNavi(it.id)}
@click=${this.toggleDomain}
> >
<span>${it}</span> <wc-icon name=${it.icon}></wc-icon>
<wc-icon name="right"></wc-icon> <span>${it.name}</span>
</li> </li>
` `
)} )}
${this.$store.domains.length < 1 </menu>
? html`<li class="item blank">没有域名</li>`
: ''}
</ul>
</wc-scroll>
<section class="action">
<wc-button circle icon="plus" @click=${this.addDomain}> </wc-button>
</section>
` `
} }
} }

View File

@ -0,0 +1,29 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2024/01/11 17:42:40
*/
import { createRouter, createWebHistory } from 'wkitd'
import './views/home.js'
import './views/images.js'
import './views/volumes.js'
export default createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'wc-containers'
},
{
path: '/images',
name: 'wc-images'
},
{
path: '/volumes',
name: 'wc-volumes'
}
]
})

View File

@ -7,9 +7,22 @@
import { createStore } from 'wkitd' import { createStore } from 'wkitd'
export default createStore({ export default createStore({
HOST_DATA: {}, navi: localStorage.getItem('last_navi') || 'containers',
activeDomain: localStorage.getItem('last_domain') || '', //当前选中的域名 menus: [
domains: [], {
records: [], id: 'containers',
tmp_records: [] name: '容器管理',
icon: 'layout'
},
{
id: 'images',
name: '镜像管理',
icon: 'menu'
},
{
id: 'volumes',
name: '磁盘管理',
icon: 'pie'
}
]
}) })

View File

@ -6,95 +6,10 @@
import { nextTick } from 'wkit' import { nextTick } from 'wkit'
const APP_CONFIG_DIR = `${native.env.CONFIG_DIR}/hosts-switch`
const HOST_FILE = `${APP_CONFIG_DIR}/host.cache`
const LOCK_FILE = `${APP_CONFIG_DIR}/lock`
export function noop() {} export function noop() {}
export function checkPermission() { export function checkPermission() {}
return native.fs.access('/etc/hosts', 'a+')
}
export async function getHistory() { export async function getHistory() {}
if (await native.fs.isfile(LOCK_FILE)) {
let cache = await native.fs.read(HOST_FILE)
return JSON.parse(cache)
}
let cache = await native.fs.read('/etc/hosts') export function saveHosts(dict) {}
let records = cache.split(/[\n\r]+/)
let list = []
let dict = {}
records.forEach(str => {
str = str.trim()
let matches = str.match(/^(#*?)\s*(\d+\.\d+\.\d+\.\d+)\s+(.*)/)
if (matches) {
let names = matches[3].split(/\s+/).map(it => it.trim())
let name
while ((name = names.pop())) {
list.push({ ip: matches[2], enabled: !matches[1], name })
}
}
})
records = null
list.forEach(it => {
it.name = it.name.split('.')
let domain = it.name.splice(-2, 2).join('.')
if (domain === 'com.cn' || domain === 'org.cn' || domain === 'net.cn') {
domain = it.name.pop() + '.' + domain
}
if (dict[domain]) {
dict[domain].push({
value: it.ip,
enabled: it.enabled,
record: it.name.join('.') || '@',
remark: ''
})
} else {
dict[domain] = [
{
value: it.ip,
enabled: it.enabled,
record: it.name.join('.') || '@',
remark: ''
}
]
}
})
list = null
try {
await native.fs.write(HOST_FILE, JSON.stringify(dict))
await native.fs.write(LOCK_FILE, '')
} catch (err) {}
return dict
}
export function saveHosts(dict) {
nextTick(async () => {
var txt = ''
for (let k in dict) {
for (let it of dict[k]) {
if (it.enabled) {
var name = it.record === '@' ? '' : it.record
if (name) {
name += '.'
}
txt += `${it.value.padEnd(15, ' ')} ${name + k}\n`
}
}
txt += '\n'
}
try {
await native.fs.write(HOST_FILE, JSON.stringify(dict))
await native.fs.write('/etc/hosts', txt)
} catch (err) {
layer.alert(err)
}
})
}

View File

@ -0,0 +1,133 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/08/08 18:19:17
*/
import { html, css, Component, classMap, nextTick, outsideClick } from 'wkit'
// import '../components/records.js'
import { noop } from '../utils/index.js'
class Home extends Component {
static props = {}
static styles = [
css`
:host {
display: block;
width: 100%;
height: 100%;
color: var(--color-dark-1);
background: #fff;
}
.visible {
display: block;
}
ul li {
list-style: none;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
`,
css`
.main {
display: flex;
flex-direction: column;
padding: 16px;
}
.main .toolbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
border-bottom: 1px solid var(--color-plain-3);
}
.main .list {
overflow: hidden;
flex: 1;
}
`,
css`
.context-menu {
display: flex;
flex-direction: column;
width: 100px;
padding: 5px 0;
background: #fff;
}
.context-menu .item {
height: 30px;
line-height: 30px;
padding: 0 15px;
cursor: pointer;
}
.context-menu .item :hover {
background: #f2f5fc;
}
`
]
async mounted() {
// outsideClick(this.$refs.context, _ => this.$refs.context.close())
}
showMenu(ev) {
this.$refs.context.close()
var { pageX, pageY } = ev
if (pageY + 70 > 600) {
pageY -= 70
}
var elem = ev.target
if (elem.tagName !== 'LI') {
elem = elem.parentNode
}
this.editDomain = elem.dataset.name
setTimeout(_ => {
this.$refs.context.moveTo({ left: pageX + 'px', top: pageY + 'px' })
this.$refs.context.show()
})
}
confirmAction(ev) {
this.$refs.context.close()
if (ev.target.tagName === 'LI') {
let act = ev.target.dataset.act
if (act === 'del') {
} else if (act === 'edit') {
}
}
}
save() {
let { HOST_DATA, activeDomain, records } = this.$store
if (activeDomain) {
HOST_DATA[activeDomain] = records.filter(it => it.record && it.value)
}
saveHosts(HOST_DATA)
layer.toast('保存成功', 'success')
}
render() {
return html`
<main class="main noselect">
<header class="toolbar">
<wc-input round></wc-input>
</header>
</main>
`
}
}
Home.reg('containers')

View File

View File