9 Commits

Author SHA1 Message Date
MoonLeeeaf
1a5afc8ad0 chore: rename symbols, add "原文" menu 2024-06-14 19:31:42 +08:00
MoonLeeeaf
4bdfad340f fix: image 2024-06-12 22:07:50 +08:00
MoonLeeeaf
6ac1b460bb feat: Markdown Code Dialog 2024-06-12 21:48:39 +08:00
MoonLeeeaf
29e224f87a test 2024-06-12 21:05:24 +08:00
MoonLeeeaf
a39973bb5c feat: markdown support 2024-06-11 22:03:41 +08:00
MoonLeeeaf
47afacbba3 chore: make limit happy 2024-06-01 14:33:40 +08:00
MoonLeeeaf
3a4d733c13 chore: 修改哈希输出(Base64改为Hex) 2024-06-01 14:33:26 +08:00
MoonLeeeaf
89263e6e2a chore: 命名 2024-06-01 14:09:45 +08:00
MoonLeeeaf
b2c8c86689 chore: 模块化前端js脚本 2024-05-31 00:08:55 +08:00
8 changed files with 1006 additions and 736 deletions

View File

@@ -82,4 +82,10 @@
width: 50px;
height: 50px;
border-radius: 50%;
}
}
.message-image {
max-width: 40%;
max-height: 40%;
border-radius: 15px;
}

View File

@@ -0,0 +1,97 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 最终执行的杂项
*/
// 感觉 window.attr 比那一堆 import 好用多了
// ================================
// 正文开始
// ================================
// 没有刷新令牌需要重新登录 或者初始化
if (!localStorage.refreshToken || localStorage.refreshToken === "")
localStorage.isSignIn = false
if (!localStorage.server || localStorage.server === "")
setUpClient()
else
setUpClient(localStorage.server)
// 登录到账号
let dialogSignIn
// 谨防 localStorage 字符串数据大坑
if (localStorage.isSignIn == "false")
dialogSignIn = new mdui.Dialog(viewBinding.dialogSignIn.get(0), {
modal: true,
closeOnEsc: false,
history: false,
}).open()
else {
(async () => viewBinding.userNick.text(await NickCache.getNick(localStorage.userName)))()
let hello
let nowHour = new Date().getHours()
if (nowHour >= 6 && nowHour <= 11) hello = "早安"
else if (nowHour == 12) hello = "中午好"
else if (nowHour >= 13 && nowHour <= 18) hello = "下午好"
else if (nowHour >= 19 && nowHour < 22) hello = "晚上好"
else hello = "晚安"
viewBinding.helloText.text(hello)
viewBinding.userHead.attr("src", CurrentUser.getUserHeadUrl(localStorage.userName))
ContactsList.reloadList()
CurrentUser.registerCallback()
}
// 感谢AI的力量
Stickyfill.add($("*").filter((a, b) => $(b).css('position') === 'sticky'))
ChatMsgAdapter.initMsgElementEvents()
ChatMsgAdapter.initInputResizer()
const showLinkDialog = (link) => mdui.alert(decodeURI(link) + "<br/>如果你确认此链接是安全的, 那么请<a class=\"mdui-text-color-theme-accent\" href=\"" + link + "\">点我</a>", '链接', () => { }, { confirmText: "关闭" })
const showImageDialog = (link, id, alt) => mdui.alert(`此图片链接来源未知: ${decodeURI(link)}<br/>如果你希望加载, 请<a class="mdui-text-color-theme-accent" mdui-dialog-close onclick="$('#${id}').html('<img src=\\'${link}\\' alt=\\'${decodeURI(alt)}\\' class=\\'message-image\\'></img>')">点我</a>`, '外部图片', () => { }, { confirmText: "关闭" })
const showCodeDialog = (code) => mdui.alert(`<pre><code>${decodeURI(code)}</code></pre>`, '代码块', () => { }, { confirmText: "关闭" })
const renderer = {
heading(text, level) {
return text
},
paragraph(text) {
return text
},
blockquote(text) {
return text
},
link(href, title, text) {
return `<a class="mdui-text-color-theme-accent" onclick="showLinkDialog('${encodeURI(href)}')">[链接] ${text}</a>`
},
image(href, title, text) {
let h = Hash.sha256(href)
let out = true
try {
out = new URL(href).hostname === new URL(location.href)
} catch(e) {}
if (out)
return `<img src="${encodeURI(href)}" alt="${text}" class="message-image"></img>`
else
return `<div id="${h}"><a class="mdui-text-color-theme-accent" onclick="showImageDialog('${encodeURI(href)}', '${h}', '${encodeURI(text)}')">[外部图片] ${text}</a></div>`
},
code(src) {
return `<a class="mdui-text-color-theme-accent" onclick="showCodeDialog(\`${encodeURI(src)}\`)">[代码块]</a>`
},
}
marked.use({
gfm: true,
renderer: renderer,
async: true,
})

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@
<script src="https://cdn.jsdelivr.net/gh/wilddeer/stickyfill@2.1.0/dist/stickyfill.min.js"></script>
<script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="icon" href="res/icon.ico" />
<title>铃之椅</title>
</head>
@@ -184,9 +185,9 @@
</div>
<div class="mdui-dialog-actions">
<button class="mdui-btn mdui-ripple"
onclick="User.signUp(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val(), () => mdui.snackbar('注册成功, 请直接点击登录即可~'))">注册</button>
onclick="CurrentUser.signUp(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val(), () => mdui.snackbar('注册成功, 请直接点击登录即可~'))">注册</button>
<button class="mdui-btn mdui-ripple" n-id="dialogSignInEnter"
onclick="User.signInWithDialog(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val())">登录</button>
onclick="CurrentUser.signInWithDialog(viewBinding.dialogSignInName.val(), viewBinding.dialogSignInPasswd.val())">登录</button>
</div>
</div>
</div>
@@ -221,7 +222,7 @@
<button class="mdui-btn mdui-ripple" n-id="dialogEditNickClose" mdui-dialog-close
onclick="new mdui.Dialog(viewBinding.dialogSettings.get(0)).open()">关闭</button>
<button class="mdui-btn mdui-ripple"
onclick="User.setNick(viewBinding.dialogEditNickNick.val(), () => {mdui.snackbar('已保存, 刷新页面生效');viewBinding.dialogEditNickClose.click()})">保存</button>
onclick="CurrentUser.setNick(viewBinding.dialogEditNickNick.val(), () => {mdui.snackbar('已保存, 刷新页面生效');viewBinding.dialogEditNickClose.click()})">保存</button>
</div>
</div>
@@ -276,7 +277,7 @@
<i class="mdui-list-item-icon mdui-icon material-icons">edit</i>
<div class="mdui-list-item-content">修改昵称</div>
</li>
<li class="mdui-list-item mdui-ripple" onclick="User.uploadHeadImage()">
<li class="mdui-list-item mdui-ripple" onclick="CurrentUser.uploadHeadImage()">
<i class="mdui-list-item-icon mdui-icon material-icons">account_circle</i>
<div class="mdui-list-item-content">上传头像</div>
</li>
@@ -289,7 +290,7 @@
</div>
<div class="mdui-hidden">
<input type="file" n-id="uploadHeadImage" name="选择头像" onchange="User.uploadHeadImageCallback(this)"
<input type="file" n-id="uploadHeadImage" name="选择头像" onchange="CurrentUser.uploadHeadImageCallback(this)"
accept="image/png, image/jpeg" />
</div>
@@ -297,7 +298,12 @@
<script src="https://unpkg.com/crypto-js@4.2.0/crypto-js.js"></script>
<script src="https://unpkg.com/socket.io-client@4.7.4/dist/socket.io.min.js"></script>
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
<script src="index.js"></script>
<!-- 核心脚本部分 -->
<script src="utils.js"></script>
<script src="manager.js"></script>
<script src="ui.js"></script>
<script src="handler.js"></script>
<script src="finally.js"></script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 资源类
*/
const viewBinding = NData.mount($("#app").get(0))
let client
/**
* 初始化客户端
* @param {String} 服务器地址
*/
function setUpClient(server) {
if (server && server !== "")
client = new io(server, {
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
else
client = new io({
auth: {
name: localStorage.isSignIn === "false" ? null : localStorage.userName
}
})
client.on("connect", () => {
User.auth()
})
}
window.viewBinding = viewBinding
window.setUpClient = setUpClient
window.client = client

71
ling_chair_http/ui.js Normal file
View File

@@ -0,0 +1,71 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 界面逻辑
*/
$.ajax({
url: "res/config.json",
dataType: "json",
success: (c) => {
viewBinding.appTitle.text(c.appTitle)
if (!c.canChangeServer) {
viewBinding.dialogSignInServerLabel.hide()
viewBinding.drawerChangeServer.hide()
}
},
})
// 关于页面
viewBinding.menuAbout.click(() => mdui.alert('这是一个开源项目<br/>作者: MoonLeeeaf<br/>欢迎访问我们的<a class="mdui-text-color-theme-accent" href="https://github.com/LingChair/LingChair">项目主页</a>', '关于 铃之椅', () => { }, { confirmText: "关闭" }))
viewBinding.drawerChangeServer.click(() => {
mdui.prompt('输入服务器地址...(为空则使用当前页面地址)', (value) => {
localStorage.server = value
mdui.snackbar("更新成功, 刷新页面生效")
}, () => { }, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.drawerSignOut.click(() => {
mdui.confirm('确定要登出账号吗', () => {
User.signOutAndReload()
}, () => { }, {
confirmText: "确定",
cancelText: "取消"
})
})
viewBinding.sendMsg.click((a) => {
let text = viewBinding.inputMsg.val()
if (text.trim() !== "")
ChatMsgAdapter.send(text)
})
viewBinding.inputMsg.keydown((e) => {
if (e.ctrlKey && e.keyCode === 13)
viewBinding.sendMsg.click()
})
viewBinding.dialogSignInPasswd.keydown((e) => {
if (e.keyCode === 13)
viewBinding.dialogSignInEnter.click()
})
viewBinding.switchNotifications.click((a) => {
if ((localStorage.useNotifications == "true" || localStorage.useNotifications != null) && localStorage.useNotifications != "false") {
localStorage.useNotifications = "false"
viewBinding.switchNotificationsIcon.text("notifications_off")
} else {
localStorage.useNotifications = "true"
viewBinding.switchNotificationsIcon.text("notifications")
}
})
if (localStorage.useNotifications == "true")
viewBinding.switchNotificationsIcon.text("notifications")
viewBinding.inputMsg.blur(() => {
window.initInputResizerResize()
})

208
ling_chair_http/utils.js Normal file
View File

@@ -0,0 +1,208 @@
/*
* ©2024 满月叶
* Github: MoonLeeeaf
* 辅助添加
*/
// 2024.5.28 睡着了
const sleep = (t) => new Promise((res) => setTimeout(res, t))
const UrlArgs = new URL(location.href).searchParams
// https://www.ruanyifeng.com/blog/2021/09/detecting-mobile-browser.html
function isMobile() {
return ('ontouchstart' in document.documentElement);
}
if (UrlArgs.get("debug")) {
let script = document.createElement('script')
script.src = "//cdn.jsdelivr.net/npm/eruda"
document.body.appendChild(script)
script.onload = () => eruda.init()
}
// 经常会因为这个指定ID为位置导致一些莫名BUG
if (location.href.includes("#")) location.replace(location.href.substring(0, location.href.indexOf("#")))
const mdui_snackbar = mdui.snackbar
mdui.snackbar = (m) => {
let t = m
if (m instanceof Object)
t = JSON.stringify(m)
mdui_snackbar(t)
}
const checkEmpty = (i) => {
if (i instanceof Array) {
for (let k of i) {
if (checkEmpty(k)) return true
}
}
return (i == null) || ("" === i) || (0 === i)
}
function escapeHTML(str) {
return str.replace(/[<>&"']/g, function (match) {
switch (match) {
case '<':
return '&lt;'
case '>':
return '&gt;'
case '&':
return '&amp;'
case '"':
return '&quot;'
case "'":
return '&#39;'
default:
return match
}
})
}
class NData {
/**
* 获取 MD5sum
* @param {String} 数据
*/
static mount(node) {
// 便捷获得指定组件
let es = node.querySelectorAll("[n-id]")
let ls = {}
es.forEach((i) => ls[$(i).attr("n-id")] = $(i))
// input 组件与 localStorage 绑定
es = node.querySelectorAll("[n-input-ls]")
es.forEach((e) => {
let j = $(e)
j.val(localStorage.getItem(j.attr("n-input-ls")))
j.blur(() => localStorage.setItem(j.attr("n-input-ls"), j.val()))
})
return ls
}
}
// https://www.runoob.com/w3cnote/javascript-copy-clipboard.html
/**
* 复制文字
* @param {String} 欲复制的文本
*/
function copyText(t) {
let btn = $("[n-id=textCopierBtn]")
btn.attr("data-clipboard-text", t)
new ClipboardJS(btn.get(0)).on('success', (e) => {
e.clearSelection()
})
btn.click()
}
// https://zhuanlan.zhihu.com/p/162910462
/**
* 格式化日期
* @param {int} 时间戳
* @param {String} 欲格式化的文本
* @returns {String} 格式后的文本
*/
Date.prototype.format = function (tms, format) {
let tmd = new Date(tms)
/*
* 例子: format="YYYY-MM-dd hh:mm:ss";
*/
var o = {
"M+": tmd.getMonth() + 1, // month
"d+": tmd.getDate(), // day
"h+": tmd.getHours(), // hour
"m+": tmd.getMinutes(), // minute
"s+": tmd.getSeconds(), // second
"q+": Math.floor((tmd.getMonth() + 3) / 3), // quarter
"S": tmd.getMilliseconds()
// millisecond
}
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (tmd.getFullYear() + "")
.substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k]
: ("00" + o[k]).substr(("" + o[k]).length));
}
}
return format;
}
// 既然已经有 Notification 了, 那用回中文也不过分吧 :)
class 通知 {
constructor() {
this.args = {}
this.title = ""
}
static checkAvailable() {
return ("Notification" in window)
}
static async request() {
if (!this.checkAvailable()) return false
return (await Notification.requestPermission())
}
setId(id) {
this.args.tag = id
return this
}
setTitle(t) {
this.title = t
return this
}
setMessage(m) {
this.args.body = m
return this
}
setIcon(i) {
this.args.icon = i
return this
}
setImage(i) {
this.args.image = i
return this
}
setData(data) {
this.args.data = data
}
show(onclick/*, onclose*/) {
if (!通知.checkAvailable()) return
if (localStorage.useNotifications !== "true") return
let n = new Notification(this.title, this.args)
n.onclick = onclick == null ? () => n.close() : (n) => onclick(n)
return n
}
}
class Hash {
/**
* 获取 MD5sum
* @param {String} 数据
* @returns {String} Hex化的哈希值
*/
static md5(data) {
return CryptoJS.MD5(data).toString(CryptoJS.enc.Hex)
}
/**
* 获取 SHA256sum
* @param {String} 数据
* @returns {String} Hex化的哈希值
*/
static sha256(data) {
return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex)
}
}
window.copyText = copyText
window.NData = NData
window.escapeHTML = escapeHTML
window.isMobile = isMobile
window.checkEmpty = checkEmpty
window.sleep = sleep
window.Hash = Hash
window.通知 = 通知

View File

@@ -48,7 +48,8 @@ if (!io.exists(vals.LINGCHAIR_SERVER_CONFIG_FILE)) io.open(vals.LINGCHAIR_SERVER
cert: "",
},
})).close()
if (!io.exists(vals.LINGCHAIR_USERS_COUNT_FILE)) io.open(vals.LINGCHAIR_USERS_COUNT_FILE, "w").write("10000").close()
if (!io.exists(vals.LINGCHAIR_USERS_COUNT_FILE))
io.open(vals.LINGCHAIR_USERS_COUNT_FILE, "w").write("10000").close()
// 加载服务端配置文件
vals.LINGCHAIR_SERVER_CONFIG = JSON.parse(io.open(vals.LINGCHAIR_SERVER_CONFIG_FILE, "r").read("*a"))