feat: 格式化&修正类型

This commit is contained in:
ningmengchongshui 2024-06-17 22:52:15 +08:00
parent 94b0df3b23
commit 5e0c0dc756
30 changed files with 780 additions and 538 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

21
.prettierignore Normal file
View File

@ -0,0 +1,21 @@
# Node dependencies
node_modules
# 旧版文件夹
config
docker
lib
plugins
renderers
# 旧版文件
CHANGELOG.md
docker-compose.yaml
miao.js
# 缓存目录
data
trss.js

15
.prettierrc.json Normal file
View File

@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "none",
"useTabs": false,
"proseWrap": "preserve",
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "auto",
"quoteProps": "consistent",
"vueIndentScriptAndStyle": true
}

View File

@ -12,16 +12,14 @@ git clone --depth=1 -b system https://github.com/yoimiya-kokomi/Miao-Yunzai.git
## 功能列表
| 功能 | 指令 | 说明 |
|-------| ----- |------ |
| 功能 | 指令 | 说明 |
| ---- | ------------------- | ---- |
| 日志 | #更新日志 #运行日志 | 嘎嘎 |
| 更新| #更新 #全部更新 | 嘎嘎 |
| 状态 | #状态 | 嘎嘎 |
| 运行 | #重启 #关机 | 嘎嘎 |
| 娱乐 | #复读 | 嘎嘎|
| 表情 | #添加xxx | 嘎嘎|
| 更新 | #更新 #全部更新 | 嘎嘎 |
| 状态 | #状态 | 嘎嘎 |
| 运行 | #重启 #关机 | 嘎嘎 |
| 娱乐 | #复读 | 嘎嘎 |
| 表情 | #添加xxx | 嘎嘎 |
## 图片开发

View File

@ -1,10 +1,17 @@
import fs from 'node:fs'
import {
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
unlink,
writeFileSync
} from 'node:fs'
import lodash from 'lodash'
import { pipeline } from 'stream'
import { promisify } from 'util'
import fetch from 'node-fetch'
import moment from 'moment'
import { pipeline } from 'stream'
import { promisify } from 'util'
import { ConfigController as cfg } from 'yunzai/config'
import { Plugin } from 'yunzai/core'
import { makeForwardMsg } from 'yunzai/core'
@ -18,6 +25,7 @@ export class add extends Plugin {
path = './data/textJson/'
facePath = './data/face/'
isGlobal = false
keyWord = null
/**
*
*/
@ -26,7 +34,7 @@ export class add extends Plugin {
name: '添加表情',
dsc: '添加表情,文字等',
*/
super();
super()
this.priority = 50000
this.rule = [
{
@ -49,9 +57,8 @@ export class add extends Plugin {
{
reg: /#(全局)?(表情|词条)(.*)/,
fnc: this.list.name
},
}
]
}
/**
@ -59,21 +66,25 @@ export class add extends Plugin {
*/
async accept() {
/** 处理消息 */
if (this.e.atBot && this.e.msg && this.e?.msg.includes('添加') && !this.e?.msg.includes('#')) {
if (
this.e.atBot &&
this.e.msg &&
this.e?.msg.includes('添加') &&
!this.e?.msg.includes('#')
) {
this.e.msg = '#' + this.e.msg
}
}
/**
*
*/
async init() {
if (!fs.existsSync(this.path)) {
fs.mkdirSync(this.path)
if (!existsSync(this.path)) {
mkdirSync(this.path)
}
if (!fs.existsSync(this.facePath)) {
fs.mkdirSync(this.facePath)
if (!existsSync(this.facePath)) {
mkdirSync(this.facePath)
}
}
@ -88,7 +99,7 @@ export class add extends Plugin {
*
*/
async add() {
this.isGlobal = this.e?.msg.includes("全局");
this.isGlobal = this.e?.msg.includes('全局')
await this.getGroupId()
if (!this.group_id) {
@ -124,8 +135,8 @@ export class add extends Plugin {
async getGroupId() {
/** 添加全局表情存入到机器人qq文件中 */
if (this.isGlobal) {
this.group_id = this.e.bot.uin;
return this.e.bot.uin;
this.group_id = this.e.bot.uin
return this.e.bot.uin
}
if (this.e.isGroup) {
@ -137,7 +148,7 @@ export class add extends Plugin {
// redis获取
let groupId = await redis.get(this.grpKey)
if (groupId) {
this.group_id = groupId
this.group_id = Number(groupId)
return this.group_id
}
@ -186,7 +197,9 @@ export class add extends Plugin {
}
if (this.e.at) {
let at = lodash.filter(this.e.message, (o) => { return o.type == 'at' && o.qq != this.e.bot.uin })
let at = lodash.filter(this.e.message, o => {
return o.type == 'at' && o.qq != this.e.bot.uin
})
if (at.length > 1) {
this.e.reply('添加错误:只能@一个人当关键词')
return false
@ -211,7 +224,7 @@ export class add extends Plugin {
if (!this.e.msg || !msg.image) return false
// #全局添加文字+表情包,无法正确添加到全局路径
this.e.isGlobal = this.isGlobal;
this.e.isGlobal = this.isGlobal
let keyWord = this.e.msg.replace(/#||图片|表情|添加|全局/g, '').trim()
if (!keyWord) return false
@ -231,9 +244,10 @@ export class add extends Plugin {
*
*/
getKeyWord() {
this.e.isGlobal = this.e.msg.includes("全局");
this.e.isGlobal = this.e.msg.includes('全局')
this.keyWord = this.e.toString()
this.keyWord = this.e
.toString()
.trim()
/** 过滤#添加 */
.replace(/#||图片|表情|添加|删除|全局/g, '')
@ -274,7 +288,7 @@ export class add extends Plugin {
* @returns
*/
async addContext() {
this.isGlobal = this.e.isGlobal || this.getContext()?.addContext?.isGlobal;
this.isGlobal = this.e.isGlobal || this.getContext()?.addContext?.isGlobal
await this.getGroupId()
/** 关键词 */
let keyWord = this.keyWord || this.getContext()?.addContext?.keyWord
@ -287,23 +301,23 @@ export class add extends Plugin {
this.finish('addContext')
for (let i in message) {
if (message[i].type == "at") {
if (message[i].type == 'at') {
if (message[i].qq == this.e.bot.uin) {
this.e.reply("添加内容不能@机器人!");
return;
this.e.reply('添加内容不能@机器人!')
return
}
}
if (message[i].type == "file") {
this.e.reply("添加错误:禁止添加文件");
return;
if (message[i].type == 'file') {
this.e.reply('添加错误:禁止添加文件')
return
}
// 保存用户信息用于追溯添加者
message[i].from_user = {
card: this.e.sender.card,
nickname: this.e.sender.nickname,
user_id: this.e.sender.user_id,
};
user_id: this.e.sender.user_id
}
}
if (message.length == 1 && message[0].type == 'image') {
@ -341,7 +355,7 @@ export class add extends Plugin {
* @returns
*/
getRetMsg() {
let retMsg = this.getContext()
const retMsg = this.getContext()
let msg = ''
if (retMsg?.addContext?.message) {
msg = retMsg.addContext.message
@ -349,7 +363,9 @@ export class add extends Plugin {
for (let i in msg) {
if (msg[i].type == 'text' && msg[i].text.includes('添加')) {
msg[i].text = this.trimAlias(msg[i].text)
msg[i].text = msg[i].text.trim().replace(/#||图片|表情|添加|全局/g, '')
msg[i].text = msg[i].text
.trim()
.replace(/#||图片|表情|添加|全局/g, '')
if (!msg[i].text) delete msg[i]
continue
}
@ -378,22 +394,25 @@ export class add extends Plugin {
obj[k] = v
}
fs.writeFileSync(`${this.path}${this.group_id}.json`, JSON.stringify(obj, '', '\t'))
writeFileSync(
`${this.path}${this.group_id}.json`,
JSON.stringify(obj, '', '\t')
)
}
/**
*
*/
saveGlobalJson() {
let obj = {};
let obj = {}
for (let [k, v] of textArr[this.e.bot.uin]) {
obj[k] = v;
obj[k] = v
}
fs.writeFileSync(
writeFileSync(
`${this.path}${this.e.bot.uin}.json`,
JSON.stringify(obj, "", "\t")
);
JSON.stringify(obj, '', '\t')
)
}
/**
@ -406,8 +425,8 @@ export class add extends Plugin {
let groupCfg = cfg.getGroup(this.group_id)
let savePath = `${this.facePath}${this.group_id}/`
if (!fs.existsSync(savePath)) {
fs.mkdirSync(savePath)
if (!existsSync(savePath)) {
mkdirSync(savePath)
}
const response = await fetch(url)
@ -428,14 +447,14 @@ export class add extends Plugin {
let type = response.headers.get('content-type').split('/')[1]
if (type == 'jpeg') type = 'jpg'
if (fs.existsSync(`${savePath}${keyWord}.${type}`)) {
if (existsSync(`${savePath}${keyWord}.${type}`)) {
keyWord = `${keyWord}_${moment().format('X')}`
}
savePath = `${savePath}${keyWord}.${type}`
const streamPipeline = promisify(pipeline)
await streamPipeline(response.body, fs.createWriteStream(savePath))
await streamPipeline(response.body, createWriteStream(savePath))
return savePath
}
@ -457,7 +476,8 @@ export class add extends Plugin {
this.initGlobalTextArr()
let keyWord = this.e.toString()
let keyWord = this.e
.toString()
.replace(/#|/g, '')
.replace(`{at:${this.e.bot.uin}}`, '')
.trim()
@ -468,7 +488,11 @@ export class add extends Plugin {
if (isNaN(keyWord)) {
num = keyWord.trim().match(/[0-9]+$/)?.[0]
if (!isNaN(num) && !textArr[this.group_id].has(keyWord) && !textArr[this.e.bot.uin].has(keyWord)) {
if (
!isNaN(num) &&
!textArr[this.group_id].has(keyWord) &&
!textArr[this.e.bot.uin].has(keyWord)
) {
keyWord = lodash.trimEnd(keyWord, num).trim()
num--
}
@ -489,7 +513,7 @@ export class add extends Plugin {
}
if (msg[0] && msg[0].local) {
if (fs.existsSync(msg[0].local)) {
if (existsSync(msg[0].local)) {
let tmp = segment.image(msg[0].local)
tmp.asface = msg[0].asface
msg = tmp
@ -502,7 +526,9 @@ export class add extends Plugin {
if (Array.isArray(msg)) {
msg.forEach(m => {
/** 去除回复@@ */
if (m?.type == 'at') { delete m.text }
if (m?.type == 'at') {
delete m.text
}
})
}
@ -545,12 +571,12 @@ export class add extends Plugin {
textArr[this.group_id] = new Map()
let path = `${this.path}${this.group_id}.json`
if (!fs.existsSync(path)) {
if (!existsSync(path)) {
return
}
try {
let text = JSON.parse(fs.readFileSync(path, 'utf8'))
let text = JSON.parse(readFileSync(path, 'utf8'))
for (let i in text) {
if (text[i][0] && !Array.isArray(text[i][0])) {
text[i] = [text[i]]
@ -567,8 +593,10 @@ export class add extends Plugin {
/** 加载表情 */
let facePath = `${this.facePath}${this.group_id}`
if (fs.existsSync(facePath)) {
const files = fs.readdirSync(`${this.facePath}${this.group_id}`).filter(file => /\.(jpeg|jpg|png|gif)$/g.test(file))
if (existsSync(facePath)) {
const files = readdirSync(`${this.facePath}${this.group_id}`).filter(
file => /\.(jpeg|jpg|png|gif)$/g.test(file)
)
for (let val of files) {
let tmp = val.split('.')
tmp[0] = tmp[0].replace(/_[0-9]{10}$/, '')
@ -576,15 +604,19 @@ export class add extends Plugin {
if (textArr[this.group_id].has(tmp[0])) continue
textArr[this.group_id].set(tmp[0], [[{
local: `${facePath}/${val}`,
asface: true
}]])
textArr[this.group_id].set(tmp[0], [
[
{
local: `${facePath}/${val}`,
asface: true
}
]
])
}
this.saveJson()
} else {
fs.mkdirSync(facePath)
mkdirSync(facePath)
}
}
@ -593,58 +625,58 @@ export class add extends Plugin {
* @returns
*/
initGlobalTextArr() {
if (textArr[this.e.bot.uin]) return;
if (textArr[this.e.bot.uin]) return
textArr[this.e.bot.uin] = new Map();
textArr[this.e.bot.uin] = new Map()
let globalPath = `${this.path}${this.e.bot.uin}.json`;
if (!fs.existsSync(globalPath)) {
return;
let globalPath = `${this.path}${this.e.bot.uin}.json`
if (!existsSync(globalPath)) {
return
}
try {
let text = JSON.parse(fs.readFileSync(globalPath, "utf8"));
let text = JSON.parse(readFileSync(globalPath, 'utf8'))
for (let i in text) {
if (text[i][0] && !Array.isArray(text[i][0])) {
text[i] = [text[i]];
text[i] = [text[i]]
}
textArr[this.e.bot.uin].set(String(i), text[i]);
textArr[this.e.bot.uin].set(String(i), text[i])
}
} catch (error) {
logger.error(`json格式错误${globalPath}`);
delete textArr[this.e.bot.uin];
return false;
logger.error(`json格式错误${globalPath}`)
delete textArr[this.e.bot.uin]
return false
}
/** 加载表情 */
let globalFacePath = `${this.facePath}${this.e.bot.uin}`;
let globalFacePath = `${this.facePath}${this.e.bot.uin}`
if (fs.existsSync(globalFacePath)) {
if (existsSync(globalFacePath)) {
const files = fs
.readdirSync(`${this.facePath}${this.e.bot.uin}`)
.filter((file) => /\.(jpeg|jpg|png|gif)$/g.test(file));
.filter(file => /\.(jpeg|jpg|png|gif)$/g.test(file))
for (let val of files) {
let tmp = val.split(".");
tmp[0] = tmp[0].replace(/_[0-9]{10}$/, "");
if (/at|image/g.test(val)) continue;
let tmp = val.split('.')
tmp[0] = tmp[0].replace(/_[0-9]{10}$/, '')
if (/at|image/g.test(val)) continue
if (textArr[this.e.bot.uin].has(tmp[0])) continue;
if (textArr[this.e.bot.uin].has(tmp[0])) continue
textArr[this.e.bot.uin].set(tmp[0], [
[
{
local: `${globalFacePath}/${val}`,
asface: true,
},
],
]);
asface: true
}
]
])
}
this.saveGlobalJson();
this.saveGlobalJson()
} else {
fs.mkdirSync(globalFacePath);
mkdirSync(globalFacePath)
}
}
@ -653,14 +685,16 @@ export class add extends Plugin {
* @returns
*/
async del() {
this.isGlobal = this.e?.msg.includes("全局");
this.isGlobal = this.e?.msg.includes('全局')
await this.getGroupId()
if (!this.group_id) return false
if (!this.checkAuth()) return
this.initTextArr()
let keyWord = this.e.toString().replace(/#||图片|表情|删除|全部|全局/g, '')
let keyWord = this.e
.toString()
.replace(/#||图片|表情|删除|全部|全局/g, '')
keyWord = this.trimAlias(keyWord)
@ -736,7 +770,7 @@ export class add extends Plugin {
img = item[0]
}
if (img.local) {
fs.unlink(img.local, () => { })
unlink(img.local, () => {})
}
})
@ -747,7 +781,7 @@ export class add extends Plugin {
*
*/
async list() {
this.isGlobal = this.e?.msg.includes("全局");
this.isGlobal = this.e?.msg.includes('全局')
let page = 1
let pageSize = 100
@ -794,7 +828,9 @@ export class add extends Plugin {
return
}
let msg = [], result = [], num = 0
let msg = [],
result = [],
num = 0
for (let i in arr) {
if (num >= page * pageSize) break
@ -816,11 +852,11 @@ export class add extends Plugin {
result.push([msg[i]])
}
/** 计算页数 */
let book = count / pageSize;
let book = count / pageSize
if (book % 1 === 0) {
book = result;
book = result
} else {
book = Math.floor(book) + 1;
book = Math.floor(book) + 1
}
if (type == 'list' && msg.length >= pageSize) {
result.push(`更多内容请翻页查看\n如#表情列表${Number(page) + 1}`)
@ -841,7 +877,9 @@ export class add extends Plugin {
*/
pagination(pageNo, pageSize, array) {
let offset = (pageNo - 1) * pageSize
return offset + pageSize >= array.length ? array.slice(offset, array.length) : array.slice(offset, offset + pageSize)
return offset + pageSize >= array.length
? array.slice(offset, array.length)
: array.slice(offset, offset + pageSize)
}
/**
@ -864,7 +902,9 @@ export class add extends Plugin {
for (let qq of tmp) {
qq = qq.match(/[1-9][0-9]{4,14}/g)[0]
let member = await await this.e.bot.getGroupMemberInfo(this.group_id, Number(qq)).catch(() => { })
let member = await await this.e.bot
.getGroupMemberInfo(this.group_id, Number(qq))
.catch(() => {})
let name = member?.card ?? member?.nickname
if (!name) continue
msg = msg.replace(`{at:${qq}}`, `@${name}`)
@ -913,7 +953,6 @@ export class add extends Plugin {
return
}
// process faces into replyArr in type:
let replyArr = []
for (let i = 0; i < faces.length; i++) {
@ -944,7 +983,11 @@ export class add extends Plugin {
return
}
let forwardMsg = await makeForwardMsg(this.e, replyArr, `表情${keyWord}详情`)
let forwardMsg = await makeForwardMsg(
this.e,
replyArr,
`表情${keyWord}详情`
)
this.e.reply(forwardMsg)
}

View File

@ -1,24 +1,23 @@
import { ConfigController as cfg } from 'yunzai/config'
import { Plugin } from 'yunzai/core'
export class disFriPoke extends Plugin {
constructor() {
/**
constructor() {
/**
name: '禁止私聊',
dsc: '对私聊禁用做处理当开启私聊禁用时只接收cookie以及抽卡链接',
*/
super()
this.event = 'notice.friend.poke'
this.priority = 0
}
/**
* default
* @returns
*/
async accept() {
if (!cfg.other?.disablePrivate) return
if (this.e.isMaster) return
this.e.reply(cfg.other.disableMsg)
return 'return'
}
super()
this.event = 'notice.friend.poke'
this.priority = 0
}
/**
* default
* @returns
*/
async accept() {
if (!cfg.other?.disablePrivate) return
if (this.e.isMaster) return
this.e.reply(cfg.other.disableMsg)
return 'return'
}
}

View File

@ -1,4 +1,3 @@
import { ConfigController as cfg } from 'yunzai/config'
import { Plugin } from 'yunzai/core'
/**
@ -29,7 +28,7 @@ export class disPri extends Plugin {
/** 发送日志文件xlsxjson */
if (this.e.file) {
if (!/(.*)\.txt|xlsx|json/ig.test(this.e.file?.name)) {
if (!/(.*)\.txt|xlsx|json/gi.test(this.e.file?.name)) {
this.sendTips()
return 'return'
} else {
@ -38,16 +37,21 @@ export class disPri extends Plugin {
}
/** 绑定ck抽卡链接 */
let wordReg = /(.*)(ltoken|_MHYUUID|authkey=)(.*)|导出记录(json)*|(记录|安卓|苹果|ck|cookie|体力)帮助|^帮助$|^#*(删除|我的)ck$|^#(我的)?(uid|UID)[0-9]{0,2}$/g
let wordReg =
/(.*)(ltoken|_MHYUUID|authkey=)(.*)|导出记录(json)*|(记录|安卓|苹果|ck|cookie|体力)帮助|^帮助$|^#*(删除|我的)ck$|^#(我的)?(uid|UID)[0-9]{0,2}$/g
/** 自定义通行字符 */
let disableAdopt = cfg.other?.disableAdopt
if (!Array.isArray(disableAdopt)) {
disableAdopt = []
}
disableAdopt = disableAdopt.filter(str => str != null && str !== '');
disableAdopt = disableAdopt.filter(str => str != null && str !== '')
let disableReg = `(.*)(${disableAdopt.join('|')})(.*)`
if (this.e.raw_message) {
if (!new RegExp(wordReg).test(this.e.raw_message) && (disableAdopt.length === 0 || !new RegExp(disableReg).test(this.e.raw_message))) {
if (
!new RegExp(wordReg).test(this.e.raw_message) &&
(disableAdopt.length === 0 ||
!new RegExp(disableReg).test(this.e.raw_message))
) {
this.sendTips()
return 'return'
}

View File

@ -1,4 +1,3 @@
import { ConfigController as cfg } from 'yunzai/config'
import { sleep } from 'yunzai/utils'
import { Plugin } from 'yunzai/core'
@ -16,7 +15,7 @@ export class friend extends Plugin {
dsc: '自动同意好友',
*/
super()
this.event = 'request.friend'
this.event = 'request.friend'
}
/**
*

View File

@ -1,4 +1,3 @@
import { ConfigController as cfg } from 'yunzai/config'
import { Plugin } from 'yunzai/core'
/**
@ -15,17 +14,17 @@ export class invite extends Plugin {
dsc: '主人邀请自动进群',
*/
super()
this.event = 'request.group.invite'
this.event = 'request.group.invite'
}
/**
*
* @returns
*/
async accept() {
if(/group/.test(this.event)){
if (/group/.test(this.event)) {
this.e.isGroup = true
}
if(!this.e.isGroup) return
if (!this.e.isGroup) return
//
if (!cfg.masterQQ || !cfg.masterQQ.includes(String(this.e.user_id))) {
logger.mark(`[邀请加群]${this.e.group_name}${this.e.group_id}`)
@ -33,8 +32,10 @@ export class invite extends Plugin {
}
logger.mark(`[主人邀请加群]${this.e.group_name}${this.e.group_id}`)
this.e.approve(true)
this.e.bot.sendPrivateMsg(this.e.user_id, `已同意加群:${this.e.group_name}`).catch((err) => {
logger.error(err)
})
this.e.bot
.sendPrivateMsg(this.e.user_id, `已同意加群:${this.e.group_name}`)
.catch(err => {
logger.error(err)
})
}
}

View File

@ -12,7 +12,7 @@ export class newcomer extends Plugin {
dsc: '新人入群欢迎',
*/
super()
this.event = 'notice.group.increase'
this.event = 'notice.group.increase'
this.priority = 5000
}
@ -38,4 +38,3 @@ export class newcomer extends Plugin {
])
}
}

View File

@ -1,30 +1,31 @@
import { Plugin } from 'yunzai/core'
export class outNotice extends Plugin {
tips = '退群了'
constructor() {
/**
tips = '退群了'
constructor() {
/**
name: '退群通知',
dsc: 'xx退群了',
*/
super()
this.event = 'notice.group.decrease'
super()
this.event = 'notice.group.decrease'
}
/**
*
* @returns
*/
async accept() {
if (this.e.user_id == this.e.bot.uin) return
let name = null,
msg = null
if (this.e.member) {
name = this.e.member.card || this.e.member.nickname
}
/**
*
* @returns
*/
async accept() {
if (this.e.user_id == this.e.bot.uin) return
let name = null, msg = null
if (this.e.member) {
name = this.e.member.card || this.e.member.nickname
}
if (name) {
msg = `${name}(${this.e.user_id}) ${this.tips}`
} else {
msg = `${this.e.user_id} ${this.tips}`
}
logger.mark(`[退出通知]${this.e.logText} ${msg}`)
await this.reply(msg)
if (name) {
msg = `${name}(${this.e.user_id}) ${this.tips}`
} else {
msg = `${this.e.user_id} ${this.tips}`
}
logger.mark(`[退出通知]${this.e.logText} ${msg}`)
await this.reply(msg)
}
}

View File

@ -3,7 +3,7 @@ import { Plugin } from 'yunzai/core'
*
*/
export class example2 extends Plugin {
constructor () {
constructor() {
/**
name: '复读',
dsc: '复读用户发送的内容,然后撤回',
@ -20,7 +20,7 @@ export class example2 extends Plugin {
/**
*
*/
async repeat () {
async repeat() {
/** 设置上下文后续接收到内容会执行doRep方法 */
this.setContext('doRep')
/** 回复 */
@ -29,7 +29,7 @@ export class example2 extends Plugin {
/**
*
*/
doRep () {
doRep() {
/** 复读内容 */
this.reply(this.e.message, false, { recallMsg: 5 })
/** 结束上下文 */

View File

@ -16,3 +16,27 @@ export * from './update.js'
export * from './example2.js'
export * from './event/newcomer.js'
export * from './event/outNotice.js'
import { Messages, Segment } from 'yunzai/core'
import { imgae } from '../image.tsx'
import { movies } from '../data.ts'
const message = new Messages()
message.response(/^你好/, async e => {
const UID = e.user_id
// render 是异步的,因此此处也是异步的
const img = await imgae.createHello(UID, {
data: { name: 'word' },
movies
})
// 判断是否成功
if (typeof img !== 'boolean') {
// 图片
e.reply(Segment.image(img))
} else {
e.reply('你好')
}
})
const word = message.ok
export { word }

View File

@ -1,7 +1,7 @@
import { Plugin } from 'yunzai/core'
import fetch from 'node-fetch'
import net from 'net'
import fs from 'fs'
import { readFileSync } from 'fs'
import YAML from 'yaml'
import { exec } from 'child_process'
@ -14,14 +14,17 @@ import { exec } from 'child_process'
* @param port
* @returns
*/
const isPortTaken = async (port) => {
return new Promise((resolve) => {
const tester = net.createServer()
const isPortTaken = async port => {
return new Promise(resolve => {
const tester = net
.createServer()
.once('error', () => resolve(true))
.once('listening', () => tester.once('close', () => resolve(false)).close())
.listen(port);
});
};
.once('listening', () =>
tester.once('close', () => resolve(false)).close()
)
.listen(port)
})
}
/**
*
@ -57,7 +60,7 @@ export class Restart extends Plugin {
async init() {
const data = await redis.get(this.key)
if (data) {
const restart = JSON.parse(data)
const restart = JSON.parse(data)
const uin = restart?.uin || Bot.uin
let time = restart.time || new Date().getTime()
time = (new Date().getTime() - time) / 1000
@ -80,9 +83,11 @@ export class Restart extends Plugin {
async restart() {
let restart_port
try {
restart_port = YAML.parse(fs.readFileSync(`./config/config/bot.yaml`, `utf-8`))
restart_port = YAML.parse(
readFileSync(`./config/config/bot.yaml`, `utf-8`)
)
restart_port = restart_port.restart_port || 27881
} catch { }
} catch {}
await this.e.reply('开始执行重启,请稍等...')
logger.mark(`${this.e.logFnc} 开始执行重启,请稍等...`)
@ -97,7 +102,9 @@ export class Restart extends Plugin {
await redis.set(this.key, data, { EX: 120 })
if (await isPortTaken(restart_port || 27881)) {
try {
const result = await fetch(`http://localhost:${restart_port || 27881}/restart`).then(res=>res.text())
const result = await fetch(
`http://localhost:${restart_port || 27881}/restart`
).then(res => res.text())
if (result !== `OK`) {
redis.del(this.key)
this.e.reply(`操作失败!`)
@ -144,7 +151,7 @@ export class Restart extends Plugin {
}
async execSync(cmd) {
return new Promise((resolve, reject) => {
return new Promise(resolve => {
exec(cmd, { windowsHide: true }, (error, stdout, stderr) => {
resolve({ error, stdout, stderr })
})
@ -154,9 +161,11 @@ export class Restart extends Plugin {
async stop() {
let restart_port
try {
restart_port = YAML.parse(fs.readFileSync(`./config/config/bot.yaml`, `utf-8`))
restart_port = YAML.parse(
readFileSync(`./config/config/bot.yaml`, `utf-8`)
)
restart_port = restart_port.restart_port || 27881
} catch { }
} catch {}
if (await isPortTaken(restart_port || 27881)) {
try {
logger.mark('关机成功,已停止运行')
@ -179,7 +188,7 @@ export class Restart extends Plugin {
await this.e.reply('关机成功,已停止运行')
let npm = await this.checkPnpm()
exec(`${npm} stop`, { windowsHide: true }, (error, stdout, stderr) => {
exec(`${npm} stop`, { windowsHide: true }, error => {
if (error) {
this.e.reply(`操作失败!\n${error.stack}`)
logger.error(`关机失败\n${error.stack}`)

View File

@ -1,8 +1,8 @@
import { Plugin } from 'yunzai/core'
import {makeForwardMsg} from 'yunzai/core'
import fs from "node:fs"
import lodash from "lodash"
import moment from "moment"
import { makeForwardMsg } from 'yunzai/core'
import { readFileSync } from 'node:fs'
import lodash from 'lodash'
import moment from 'moment'
/**
* tudo
@ -14,8 +14,9 @@ import moment from "moment"
export class sendLog extends Plugin {
lineNum = 100
maxNum = 1000
errFile = "logs/error.log"
logFile = `logs/command.${moment().format("YYYY-MM-DD")}.log`
errFile = 'logs/error.log'
logFile = `logs/command.${moment().format('YYYY-MM-DD')}.log`
keyWord = null
constructor() {
/**
name: "发送日志",
@ -26,7 +27,7 @@ export class sendLog extends Plugin {
{
reg: /^#(运行|错误)*日志[0-9]*(.*)/,
fnc: this.sendLog.name,
permission: "master"
permission: 'master'
}
]
}
@ -38,26 +39,32 @@ export class sendLog extends Plugin {
async sendLog() {
let lineNum = this.e.msg.match(/\d+/g)
if (lineNum) {
this.lineNum = lineNum[0]
this.lineNum = Number(lineNum[0])
} else {
this.keyWord = this.e.msg.replace(/#|运行|错误|日志|\d/g, "")
this.keyWord = this.e.msg.replace(/#|运行|错误|日志|\d/g, '')
}
let logFile = this.logFile
let type = "运行"
if (this.e.msg.includes("错误")) {
let type = '运行'
if (this.e.msg.includes('错误')) {
logFile = this.errFile
type = "错误"
type = '错误'
}
if (this.keyWord) type = this.keyWord
const log = this.getLog(logFile)
if (lodash.isEmpty(log))
if (lodash.isEmpty(log)) {
return this.reply(`暂无相关日志:${type}`)
}
return this.reply(await makeForwardMsg(this.e, [log.join("\n")], `最近${log.length}${type}日志`))
const data = await makeForwardMsg(
this.e,
[log.join('\n')],
`最近${log.length}${type}日志`
)
return this.reply(data)
}
/**
@ -66,25 +73,22 @@ export class sendLog extends Plugin {
* @returns
*/
getLog(logFile) {
let log = fs.readFileSync(logFile, { encoding: "utf-8" })
log = log.split("\n")
const data = readFileSync(logFile, { encoding: 'utf-8' })
let log = data.split('\n')
if (this.keyWord) {
for (const i in log)
if (!log[i].includes(this.keyWord))
delete log[i]
for (const i in log) if (!log[i].includes(this.keyWord)) delete log[i]
} else {
log = lodash.slice(log, (Number(this.lineNum) + 1) * -1)
}
log = log.reverse()
const tmp = []
for (let i of log) {
if (!i) continue
if (this.keyWord && tmp.length >= this.maxNum) return
/* eslint-disable no-control-regex */
i = i.replace(/\x1b[[0-9;]*m/g, "")
i = i.replace(/\r|\n/, "")
i = i.replace(/\x1b[[0-9;]*m/g, '')
i = i.replace(/\r|\n/, '')
tmp.push(i)
}
return tmp

View File

@ -1,4 +1,3 @@
import { ConfigController as cfg } from 'yunzai/config'
import moment from 'moment'
import { Plugin } from 'yunzai/core'
@ -41,22 +40,27 @@ export class status extends Plugin {
}
async statusMaster() {
let runTime = moment().diff(moment.unix(this.e.bot.stat.start_time), 'seconds')
let runTime = moment().diff(
moment.unix(this.e.bot.stat.start_time),
'seconds'
)
let Day = Math.floor(runTime / 3600 / 24)
let Hour = Math.floor((runTime / 3600) % 24)
let Min = Math.floor((runTime / 60) % 60)
let data = ''
if (Day > 0) {
runTime = `${Day}${Hour}小时${Min}分钟`
data = `${Day}${Hour}小时${Min}分钟`
} else {
runTime = `${Hour}小时${Min}分钟`
data = `${Hour}小时${Min}分钟`
}
let format = (bytes) => {
let format = bytes => {
return (bytes / 1024 / 1024).toFixed(2) + 'MB'
}
let msg = '-------状态-------'
msg += `\n运行时间${runTime}`
msg += `\n运行时间${data}`
msg += `\n内存使用${format(process.memoryUsage().rss)}`
msg += `\n当前版本v${cfg.package.version}`
msg += '\n-------累计-------'
@ -72,7 +76,13 @@ export class status extends Plugin {
await this.e.reply(msg)
}
async getCount(groupId:number | string = '') {
date = null
month = null
key = null
msgKey = null
screenshotKey = null
async getCount(groupId: number | string = '') {
this.date = moment().format('MMDD')
this.month = Number(moment().month()) + 1
@ -100,22 +110,25 @@ export class status extends Plugin {
let date = moment().startOf('week').add(i, 'days').format('MMDD')
week.msg += Number(await redis.get(`${this.msgKey.day}${date}`)) ?? 0
week.screenshot += Number(await redis.get(`${this.screenshotKey.day}${date}`)) ?? 0
week.screenshot +=
Number(await redis.get(`${this.screenshotKey.day}${date}`)) ?? 0
}
let count = {
total: {
msg: await redis.get(`${this.key}sendMsg:total`) || 0,
screenshot: await redis.get(`${this.key}screenshot:total`) || 0
msg: (await redis.get(`${this.key}sendMsg:total`)) || 0,
screenshot: (await redis.get(`${this.key}screenshot:total`)) || 0
},
today: {
msg: await redis.get(`${this.msgKey.day}${this.date}`) || 0,
screenshot: await redis.get(`${this.screenshotKey.day}${this.date}`) || 0
msg: (await redis.get(`${this.msgKey.day}${this.date}`)) || 0,
screenshot:
(await redis.get(`${this.screenshotKey.day}${this.date}`)) || 0
},
week,
month: {
msg: await redis.get(`${this.msgKey.month}${this.month}`) || 0,
screenshot: await redis.get(`${this.screenshotKey.month}${this.month}`) || 0
msg: (await redis.get(`${this.msgKey.month}${this.month}`)) || 0,
screenshot:
(await redis.get(`${this.screenshotKey.month}${this.month}`)) || 0
}
}
@ -128,12 +141,12 @@ export class status extends Plugin {
msg += `\n生成图片${count.total.screenshot}`
}
if (count.month.msg > 200) {
if (Number(count.month.msg) > 200) {
msg += '\n-------本周-------'
msg += `\n发送消息${count.week.msg}`
msg += `\n生成图片${count.week.screenshot}`
}
if (moment().format('D') >= 8 && count.month.msg > 400) {
if (Number(moment().format('D')) >= 8 && Number(count.month.msg) > 400) {
msg += '\n-------本月-------'
msg += `\n发送消息${count.month.msg}`
msg += `\n生成图片${count.month.screenshot}`

View File

@ -1,8 +1,8 @@
import { Plugin ,makeForwardMsg} from 'yunzai/core'
import { Plugin, makeForwardMsg } from 'yunzai/core'
import lodash from 'lodash'
import fs from 'node:fs'
import { existsSync, readdirSync } from 'node:fs'
import { BOT_NAME } from 'yunzai/config'
import { exec, execSync } from 'child_process'
import { exec, execSync } from 'child_process'
import { Restart } from './restart.js'
import { sleep } from 'yunzai/utils'
let uping = false
@ -67,20 +67,24 @@ export class update extends Plugin {
if (!plugin) return ''
}
if (!fs.existsSync(`plugins/${plugin}/.git`)) return false
if (!existsSync(`plugins/${plugin}/.git`)) return false
this.typeName = plugin
return plugin
}
async execSync(cmd) {
return new Promise((resolve, reject) => {
return new Promise(resolve => {
exec(cmd, { windowsHide: true }, (error, stdout, stderr) => {
resolve({ error, stdout, stderr })
})
})
}
isUp = null
isNowUp = null
oldCommitId = null
async runUpdate(plugin = '') {
this.isNowUp = false
@ -162,22 +166,26 @@ export class update extends Plugin {
}
if (errMsg.includes('be overwritten by merge')) {
return this.e.reply(`${msg}\n存在冲突\n${errMsg}\n请解决冲突后再更新或者执行#强制更新,放弃本地修改`)
return this.e.reply(
`${msg}\n存在冲突\n${errMsg}\n请解决冲突后再更新或者执行#强制更新,放弃本地修改`
)
}
if (stdout.includes('CONFLICT')) {
return this.e.reply(`${msg}\n存在冲突\n${errMsg}${stdout}\n请解决冲突后再更新或者执行#强制更新,放弃本地修改`)
return this.e.reply(
`${msg}\n存在冲突\n${errMsg}${stdout}\n请解决冲突后再更新或者执行#强制更新,放弃本地修改`
)
}
return this.e.reply([errMsg, stdout])
}
async updateAll() {
const dirs = fs.readdirSync('./plugins/')
const dirs = readdirSync('./plugins/')
const MSG = (message)=>{
// 收集
this.messages.push(message)
const MSG = message => {
// 收集
this.messages.push(message)
}
const testReg = /^#静默全部(强制)?更新$/.test(this.e.msg)
@ -203,7 +211,6 @@ export class update extends Plugin {
// await this.e.reply('即将执行重启,以应用更新')
setTimeout(() => this.restart(), 2000)
}
}
restart() {
@ -243,13 +250,21 @@ export class update extends Plugin {
cm = 'git config -l'
if (plugin) cm = `cd "plugins/${plugin}" && ${cm}`
end = await execSync(cm, { encoding: 'utf-8' })
end = end.match(/remote\..*\.url=.+/g).join('\n\n').replace(/remote\..*\.url=/g, '').replace(/\/\/([^@]+)@/, '//')
end = end
.match(/remote\..*\.url=.+/g)
.join('\n\n')
.replace(/remote\..*\.url=/g, '')
.replace(/\/\/([^@]+)@/, '//')
} catch (error) {
logger.error(error.toString())
await this.e.reply(error.toString())
}
return makeForwardMsg(this.e, [log, end], `${plugin || 'Miao-Yunzai'} 更新日志,共${line}`)
return makeForwardMsg(
this.e,
[log, end],
`${plugin || 'Miao-Yunzai'} 更新日志,共${line}`
)
}
async updateLog() {

View File

@ -1,9 +1,5 @@
import React from "react";
import React from 'react'
export default function List({ children }) {
return (
<ul className="divide-y divide-slate-100">
{children}
</ul>
)
}
return <ul className="divide-y divide-slate-100">{children}</ul>
}

View File

@ -1,66 +1,87 @@
import React from "react";
import React from 'react'
export interface MovieType {
id: number, //
image:string, //
title:string, // Prognosis Negative
starRating: string, // 2.66
rating:string, // PG-13
year:string, // 2021
genre:string, // Comedy
runtime:string, // 1h 46m
cast:string // Simon Pegg, Zach Galifianakis
}
export default function ListItem({ movie }: {movie:MovieType}) {
return (
<article className="flex items-start space-x-6 p-6">
<img src={movie.image} alt="" width="60" height="88" className="flex-none rounded-md bg-slate-100" />
<div className="min-w-0 relative flex-auto">
<h2 className="font-semibold text-slate-900 truncate pr-20">{movie.title}</h2>
<dl className="mt-2 flex flex-wrap text-sm leading-6 font-medium">
<div className="absolute top-0 right-0 flex items-center space-x-1">
<dt className="text-sky-500">
<span className="sr-only">Star rating</span>
<svg width="16" height="20" fill="currentColor">
<path d="M7.05 3.691c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.372 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.363-1.118L.98 9.483c-.784-.57-.381-1.81.587-1.81H5.03a1 1 0 00.95-.69L7.05 3.69z" />
</svg>
</dt>
<dd>{movie.starRating}</dd>
</div>
<div>
<dt className="sr-only">Rating</dt>
<dd className="px-1.5 ring-1 ring-slate-200 rounded">{movie.rating}</dd>
</div>
<div className="ml-2">
<dt className="sr-only">Year</dt>
<dd>{movie.year}</dd>
</div>
<div>
<dt className="sr-only">Genre</dt>
<dd className="flex items-center">
<svg width="2" height="2" fill="currentColor" className="mx-2 text-slate-300" aria-hidden="true">
<circle cx="1" cy="1" r="1" />
</svg>
{movie.genre}
</dd>
</div>
<div>
<dt className="sr-only">Runtime</dt>
<dd className="flex items-center">
<svg width="2" height="2" fill="currentColor" className="mx-2 text-slate-300" aria-hidden="true">
<circle cx="1" cy="1" r="1" />
</svg>
{movie.runtime}
</dd>
</div>
<div className="flex-none w-full mt-2 font-normal">
<dt className="sr-only">Cast</dt>
<dd className="text-slate-400">{movie.cast}</dd>
</div>
</dl>
</div>
</article>
)
}
id: number //
image: string //
title: string // Prognosis Negative
starRating: string // 2.66
rating: string // PG-13
year: string // 2021
genre: string // Comedy
runtime: string // 1h 46m
cast: string // Simon Pegg, Zach Galifianakis
}
export default function ListItem({ movie }: { movie: MovieType }) {
return (
<article className="flex items-start space-x-6 p-6">
<img
src={movie.image}
alt=""
width="60"
height="88"
className="flex-none rounded-md bg-slate-100"
/>
<div className="min-w-0 relative flex-auto">
<h2 className="font-semibold text-slate-900 truncate pr-20">
{movie.title}
</h2>
<dl className="mt-2 flex flex-wrap text-sm leading-6 font-medium">
<div className="absolute top-0 right-0 flex items-center space-x-1">
<dt className="text-sky-500">
<span className="sr-only">Star rating</span>
<svg width="16" height="20" fill="currentColor">
<path d="M7.05 3.691c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.372 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.363-1.118L.98 9.483c-.784-.57-.381-1.81.587-1.81H5.03a1 1 0 00.95-.69L7.05 3.69z" />
</svg>
</dt>
<dd>{movie.starRating}</dd>
</div>
<div>
<dt className="sr-only">Rating</dt>
<dd className="px-1.5 ring-1 ring-slate-200 rounded">
{movie.rating}
</dd>
</div>
<div className="ml-2">
<dt className="sr-only">Year</dt>
<dd>{movie.year}</dd>
</div>
<div>
<dt className="sr-only">Genre</dt>
<dd className="flex items-center">
<svg
width="2"
height="2"
fill="currentColor"
className="mx-2 text-slate-300"
aria-hidden="true"
>
<circle cx="1" cy="1" r="1" />
</svg>
{movie.genre}
</dd>
</div>
<div>
<dt className="sr-only">Runtime</dt>
<dd className="flex items-center">
<svg
width="2"
height="2"
fill="currentColor"
className="mx-2 text-slate-300"
aria-hidden="true"
>
<circle cx="1" cy="1" r="1" />
</svg>
{movie.runtime}
</dd>
</div>
<div className="flex-none w-full mt-2 font-normal">
<dt className="sr-only">Cast</dt>
<dd className="text-slate-400">{movie.cast}</dd>
</div>
</dl>
</div>
</article>
)
}

View File

@ -1,10 +1,8 @@
import React from "react";
import React from 'react'
export default function Nav({ children }) {
return (
<nav className="py-4 px-6 text-sm font-medium">
<ul className="flex space-x-3">
{children}
</ul>
</nav>
)
}
return (
<nav className="py-4 px-6 text-sm font-medium">
<ul className="flex space-x-3">{children}</ul>
</nav>
)
}

View File

@ -1,13 +1,13 @@
import React from "react";
import React from 'react'
export default function NavItem({ href, children }) {
return (
<li>
<a
href={href}
className={`block px-3 py-2 rounded-md bg-sky-500 text-white`}
>
{children}
</a>
</li>
)
}
return (
<li>
<a
href={href}
className={`block px-3 py-2 rounded-md bg-sky-500 text-white`}
>
{children}
</a>
</li>
)
}

View File

@ -1,33 +1,33 @@
import React from 'react'
import { Picture } from 'yunzai/utils'
import { createDynamic } from 'yunzai/utils'
import { PropsType } from './views/hello.tsx'
import { createDynamic } from 'yunzai/utils'
const require = createDynamic(import.meta.url)
export class Image extends Picture {
constructor() {
super()
// start
this.Pup.start()
}
/**
* html html文件
* @param uid
* @param Props
* @returns
*/
async createHello(uid: number, Props: PropsType) {
// 此作用域可被重复执行,此处将变成动态组件 - 这是危险的!
const Hello = (await require('./views/hello.tsx')).default;
// 生成 html 地址 或 html字符串
const Address = this.Com.create(<Hello {...Props} />, {
// html/hello/uid.html
join_dir: 'hello',
html_name: `${uid}.html`,
})
return this.Pup.render(Address, {
tab: ''
})
}
constructor() {
super()
// start
this.Pup.start()
}
/**
* html html文件
* @param uid
* @param Props
* @returns
*/
async createHello(uid: number, Props: PropsType) {
// 非生产环境将触发 动态组件效果
const Hello = (
await require('./views/hello.tsx', process.env.NODE_ENV != 'production')
).default
// 生成 html 地址 或 html字符串
const Address = this.Com.create(<Hello {...Props} />, {
// html/hello/uid.html
join_dir: 'hello',
html_name: `${uid}.html`
})
return this.Pup.render(Address)
}
}
// 初始化 图片生成对象
export const imgae = new Image()

View File

@ -1,7 +1,10 @@
{
"name": "system-plugin",
"type": "module",
"scripts": {},
"dependencies": {},
"devDependencies": {}
"scripts": {
"format": "prettier --write ."
},
"devDependencies": {
"prettier": "^3.0.3"
}
}

View File

@ -1,16 +1,16 @@
import React from "react"
import { movies } from "./data"
import React from 'react'
import { movies } from './data'
import { createDynamic } from 'yunzai/utils'
const require = createDynamic(import.meta.url)
const Hello = (await require('./views/hello.tsx')).default;
const Music = (await require('./views/music.tsx')).default;
const Hello = (await require('./views/hello.tsx')).default
const Music = (await require('./views/music.tsx')).default
const Config = [
{
url: "/hello",
element: <Hello data={{ name: "word" }} movies={movies} />
url: '/hello',
element: <Hello data={{ name: 'word' }} movies={movies} />
},
{
url: "/music",
url: '/music',
element: <Music />
}
]

8
tes.ts
View File

@ -1,8 +0,0 @@
// const require = async (basePath: string) => {
// const now = () => `?update=${Date.now()}`
// return (await import(`${basePath}${now()}`))
// }
// const Hello = await require('./views/hello.tsx')
// const Music = await require(`./views/music.tsx`)

View File

@ -24,16 +24,18 @@ const url: string = require('../resources/example.png')
*/
export default function App({ data, movies }: PropsType) {
return (
<div className="divide-y divide-slate-100 m-8 shadow-2xl">
<img className='h-40 w-40' src={url}></img>
<Nav>
<NavItem href="./music" >New {data.name}</NavItem>
</Nav>
<List>
{movies.map((movie) => (
<ListItem key={movie.id} movie={movie} />
))}
</List>
</div>
<section className="flex flex-col">
<div className="divide-y divide-slate-100 m-8 shadow-2xl">
<img className="h-40 w-40" src={url}></img>
<Nav>
<NavItem href="./music">New {data.name}</NavItem>
</Nav>
<List>
{movies.map(movie => (
<ListItem key={movie.id} movie={movie} />
))}
</List>
</div>
</section>
)
}

View File

@ -1,6 +1,6 @@
import React from "react";
import React from 'react'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const require = createRequire(import.meta.url)
// 图片
const url: string = require('../resources/example.png')
/**
@ -8,80 +8,164 @@ const url: string = require('../resources/example.png')
* @returns
*/
export default function App() {
return <>
<div className="bg-white border-slate-100 dark:bg-slate-800 dark:border-slate-500 border-b rounded-t-xl p-4 pb-6 sm:p-10 sm:pb-8 lg:p-6 xl:p-10 xl:pb-8 space-y-6 sm:space-y-8 lg:space-y-6 xl:space-y-8">
<div className="flex items-center space-x-4">
<img src={url} alt="" width="88" height="88" className="flex-none rounded-lg bg-slate-100" loading="lazy" />
<div className="min-w-0 flex-auto space-y-1 font-semibold">
<p className="text-cyan-500 dark:text-cyan-400 text-sm leading-6">
<abbr title="Episode">Ep.</abbr> 128
</p>
<h2 className="text-slate-500 dark:text-slate-400 text-sm leading-6 truncate">
Scaling CSS at Heroku with Utility Classes
</h2>
<p className="text-slate-900 dark:text-slate-50 text-lg">
Full Stack Radio
</p>
</div>
</div>
<div className="space-y-2">
<div className="relative">
<div className="bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
<div className="bg-cyan-500 dark:bg-cyan-400 w-1/2 h-2" role="progressbar" ></div>
</div>
<div className="ring-cyan-500 dark:ring-cyan-400 ring-2 absolute left-1/2 top-1/2 w-4 h-4 -mt-2 -ml-2 flex items-center justify-center bg-white rounded-full shadow">
<div className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full ring-1 ring-inset ring-slate-900/5"></div>
</div>
</div>
<div className="flex justify-between text-sm leading-6 font-medium tabular-nums">
<div className="text-cyan-500 dark:text-slate-100">24:16</div>
<div className="text-slate-500 dark:text-slate-400">75:50</div>
</div>
</div>
return (
<>
<div className="bg-white border-slate-100 dark:bg-slate-800 dark:border-slate-500 border-b rounded-t-xl p-4 pb-6 sm:p-10 sm:pb-8 lg:p-6 xl:p-10 xl:pb-8 space-y-6 sm:space-y-8 lg:space-y-6 xl:space-y-8">
<div className="flex items-center space-x-4">
<img
src={url}
alt=""
width="88"
height="88"
className="flex-none rounded-lg bg-slate-100"
loading="lazy"
/>
<div className="min-w-0 flex-auto space-y-1 font-semibold">
<p className="text-cyan-500 dark:text-cyan-400 text-sm leading-6">
<abbr title="Episode">Ep.</abbr> 128
</p>
<h2 className="text-slate-500 dark:text-slate-400 text-sm leading-6 truncate">
Scaling CSS at Heroku with Utility Classes
</h2>
<p className="text-slate-900 dark:text-slate-50 text-lg">
Full Stack Radio
</p>
</div>
</div>
<div className="bg-slate-50 text-slate-500 dark:bg-slate-600 dark:text-slate-200 rounded-b-xl flex items-center">
<div className="flex-auto flex items-center justify-evenly">
<button type="button" aria-label="Add to favorites">
<svg width="24" height="24">
<path d="M7 6.931C7 5.865 7.853 5 8.905 5h6.19C16.147 5 17 5.865 17 6.931V19l-5-4-5 4V6.931Z" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button type="button" className="hidden sm:block lg:hidden xl:block" aria-label="Previous">
<svg width="24" height="24" fill="none">
<path d="m10 12 8-6v12l-8-6Z" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6 6v12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button type="button" aria-label="Rewind 10 seconds">
<svg width="24" height="24" fill="none">
<path d="M6.492 16.95c2.861 2.733 7.5 2.733 10.362 0 2.861-2.734 2.861-7.166 0-9.9-2.862-2.733-7.501-2.733-10.362 0A7.096 7.096 0 0 0 5.5 8.226" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 5v3.111c0 .491.398.889.889.889H9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<div className="space-y-2">
<div className="relative">
<div className="bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
<div
className="bg-cyan-500 dark:bg-cyan-400 w-1/2 h-2"
role="progressbar"
></div>
</div>
<button type="button" className="bg-white text-slate-900 dark:bg-slate-100 dark:text-slate-700 flex-none -my-2 mx-auto w-20 h-20 rounded-full ring-1 ring-slate-900/5 shadow-md flex items-center justify-center" aria-label="Pause">
<svg width="30" height="32" fill="currentColor">
<rect x="6" y="4" width="4" height="24" rx="2" />
<rect x="20" y="4" width="4" height="24" rx="2" />
</svg>
</button>
<div className="flex-auto flex items-center justify-evenly">
<button type="button" aria-label="Skip 10 seconds">
<svg width="24" height="24" fill="none">
<path d="M17.509 16.95c-2.862 2.733-7.501 2.733-10.363 0-2.861-2.734-2.861-7.166 0-9.9 2.862-2.733 7.501-2.733 10.363 0 .38.365.711.759.991 1.176" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M19 5v3.111c0 .491-.398.889-.889.889H15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button type="button" className="hidden sm:block lg:hidden xl:block" aria-label="Next">
<svg width="24" height="24" fill="none">
<path d="M14 12 6 6v12l8-6Z" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18 6v12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button type="button" className="rounded-lg text-xs leading-6 font-semibold px-2 ring-2 ring-inset ring-slate-500 text-slate-500 dark:text-slate-100 dark:ring-0 dark:bg-slate-500">
1x
</button>
<div className="ring-cyan-500 dark:ring-cyan-400 ring-2 absolute left-1/2 top-1/2 w-4 h-4 -mt-2 -ml-2 flex items-center justify-center bg-white rounded-full shadow">
<div className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full ring-1 ring-inset ring-slate-900/5"></div>
</div>
</div>
<div className="flex justify-between text-sm leading-6 font-medium tabular-nums">
<div className="text-cyan-500 dark:text-slate-100">24:16</div>
<div className="text-slate-500 dark:text-slate-400">75:50</div>
</div>
</div>
</div>
<div className="bg-slate-50 text-slate-500 dark:bg-slate-600 dark:text-slate-200 rounded-b-xl flex items-center">
<div className="flex-auto flex items-center justify-evenly">
<button type="button" aria-label="Add to favorites">
<svg width="24" height="24">
<path
d="M7 6.931C7 5.865 7.853 5 8.905 5h6.19C16.147 5 17 5.865 17 6.931V19l-5-4-5 4V6.931Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button
type="button"
className="hidden sm:block lg:hidden xl:block"
aria-label="Previous"
>
<svg width="24" height="24" fill="none">
<path
d="m10 12 8-6v12l-8-6Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6 6v12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button type="button" aria-label="Rewind 10 seconds">
<svg width="24" height="24" fill="none">
<path
d="M6.492 16.95c2.861 2.733 7.5 2.733 10.362 0 2.861-2.734 2.861-7.166 0-9.9-2.862-2.733-7.501-2.733-10.362 0A7.096 7.096 0 0 0 5.5 8.226"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5 5v3.111c0 .491.398.889.889.889H9"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
<button
type="button"
className="bg-white text-slate-900 dark:bg-slate-100 dark:text-slate-700 flex-none -my-2 mx-auto w-20 h-20 rounded-full ring-1 ring-slate-900/5 shadow-md flex items-center justify-center"
aria-label="Pause"
>
<svg width="30" height="32" fill="currentColor">
<rect x="6" y="4" width="4" height="24" rx="2" />
<rect x="20" y="4" width="4" height="24" rx="2" />
</svg>
</button>
<div className="flex-auto flex items-center justify-evenly">
<button type="button" aria-label="Skip 10 seconds">
<svg width="24" height="24" fill="none">
<path
d="M17.509 16.95c-2.862 2.733-7.501 2.733-10.363 0-2.861-2.734-2.861-7.166 0-9.9 2.862-2.733 7.501-2.733 10.363 0 .38.365.711.759.991 1.176"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M19 5v3.111c0 .491-.398.889-.889.889H15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button
type="button"
className="hidden sm:block lg:hidden xl:block"
aria-label="Next"
>
<svg width="24" height="24" fill="none">
<path
d="M14 12 6 6v12l8-6Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18 6v12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<button
type="button"
className="rounded-lg text-xs leading-6 font-semibold px-2 ring-2 ring-inset ring-slate-500 text-slate-500 dark:text-slate-100 dark:ring-0 dark:bg-slate-500"
>
1x
</button>
</div>
</div>
</>
)
}