// ==UserScript==
// @name Greasyfork Beautify
// @namespace https://github.com/kiccer
// @version 1.6.4
// @description 优化导航栏样式 / 脚本列表改为卡片布局 / 代码高亮(atom-one-dark + vscode 风格) 等……融入式美化,自然、优雅,没有突兀感,仿佛页面原本就是如此……(更多优化逐步完善中!)
// @description:en Optimize the navigation bar style / script list to card layout / code highlighting (atom-one-dark + vscode style), etc. Into the style of beautification, more natural, more elegant, no sense of abruptness, as if the page is originally so. (more optimization in progress!)
// @author kiccer<1072907338@qq.com>
// @supportURL https://github.com/kiccer/TampermonkeyScripts/issues
// @license MIT
// @match https://greasyfork.org/*
// @match https://sleazyfork.org/*
// @icon https://greasyfork.org/packs/media/images/blacklogo96-b2384000fca45aa17e45eb417cbcbb59.png
// @require https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js
// @require https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.9/index.min.js
// @require https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js
// @require https://cdn.bootcdn.net/ajax/libs/javascript-detect-element-resize/0.5.3/jquery.resize.min.js
// @require https://cdn.bootcdn.net/ajax/libs/less.js/4.1.3/less.min.js
// @require https://cdn.bootcdn.net/ajax/libs/highlight.js/11.5.1/highlight.min.js
// @require https://cdn.bootcdn.net/ajax/libs/highlight.js/11.5.1/languages/javascript.min.js
// @require https://greasyfork.org/scripts/447149-checkversion/code/checkVersion.js?version=1065242
// @resource normalize.css https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css
// @resource element-ui.css https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.9/theme-chalk/index.min.css
// @resource element-icons https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.9/theme-chalk/fonts/element-icons.ttf
// @resource atom-one-dark.css https://cdn.bootcdn.net/ajax/libs/highlight.js/11.5.1/styles/atom-one-dark.min.css
// @run-at document-start
// @grant GM_info
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_getResourceURL
// @grant GM_getResourceText
// @grant GM_registerMenuCommand
// ==/UserScript==
/* globals $ less Vue hljs checkVersion ELEMENT */
Vue.use(ELEMENT)
if (/\(Development\)$/i.test(GM_info.script.name)) {
Vue.config.devtools = true
}
// 默认设置
const defaultSettings = {
script_list_columns_num: 2,
show_install_button_in_card: true,
show_version_info_in_card: true
}
// 获取设置
const getSettings = () => {
return Object.assign(
{},
defaultSettings,
JSON.parse(GM_getValue('formData') || '{}')
)
}
const VERSION = GM_info.script.version
const settings = getSettings()
// 样式注入
GM_addStyle(GM_getResourceText('normalize.css'))
GM_addStyle(GM_getResourceText('element-ui.css'))
GM_addStyle(GM_getResourceText('atom-one-dark.css'))
const lessOptions = {}
const lessInput = `
// --------------------------------------------- 变量
@nav_height: 60px;
@user_container_height: 24px;
// --------------------------------------------- 混合宏
.ellipsis (@lines) {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.5;
-webkit-line-clamp: @lines;
}
// --------------------------------------------- 通用样式
* {
box-sizing: border-box;
outline: none;
}
body {
line-height: 1.5;
min-height: 100vh;
background-color: #f7f7f7;
> .width-constraint {
min-height: 100vh;
background-color: #fff;
padding: 20px;
padding-top: calc(@nav_height + @user_container_height + 20px);
.text-content {
border: 0;
box-shadow: none;
padding: 0;
}
}
}
a {
color: rgb(38, 38, 38);
text-decoration: none;
&:hover {
text-decoration: underline;
}
&:visited {
color: rgb(38, 38, 38);
}
}
// --------------------------------------------- element-ui
// 解决 element-icons 图标引用不到问题
@font-face {
font-family: element-icons;
src: url(${GM_getResourceURL('element-icons')}),
}
// --------------------------------------------- 代码高亮
.code-container {
background-color: #282c34;
border-radius: 8px;
max-height: 100%;
overflow: visible;
// 定义滚动条
::-webkit-scrollbar {
width: 14px;
height: 14px;
background-color: transparent;
}
// 定义滚动条轨道
::-webkit-scrollbar-track {
background-color: transparent;
}
// 定义滑块
::-webkit-scrollbar-thumb {
background-color: rgba(78, 86, 102, 0);
}
// 定义边角
::-webkit-scrollbar-corner {
background-color: transparent;
}
&:hover {
::-webkit-scrollbar-thumb {
background-color: rgba(78, 86, 102, .5);
}
}
::selection {
background-color: rgb(51, 56, 66);
}
pre {
code {
padding: 0;
font-family: Consolas;
cursor: text;
overflow: auto;
.marker {
display: inline-block;
color: #636d83;
text-align: right;
padding-right: 20px;
user-select: none;
cursor: auto;
}
}
}
}
// --------------------------------------------- 页码
.pagination {
margin-top: 20px !important;
user-select: none;
> * {
padding: 0 .5em !important;
min-width: 2em;
height: 2em;
line-height: 2;
text-align: center;
text-decoration: none !important;
}
> a {
background-color: #f7f7f7 !important;
&:hover {
background-color: #e1e1e1 !important;
}
}
}
// --------------------------------------------- 输入框
input[type=search] {
padding: 3px 6px;
padding-right: 2.4em !important;
border: 1px solid #bfbfbf;
border-radius: 4px;
}
form {
input.search-submit {
top: 50% !important;
transform: translateY(-50%);
cursor: pointer;
}
}
.home-search {
margin-bottom: 20px;
}
.sidebar-search {
margin-bottom: 20px;
input[type="search"] {
margin: 0;
}
}
// --------------------------------------------- header
#main-header {
background-color: #000;
background-image: none;
width: 100%;
padding: 0;
position: fixed;
top: 0;
z-index: 1;
user-select: none;
box-shadow: 0 0 5px 2px rgb(0 0 0 / 50%);
.width-constraint {
display: flex;
justify-content: space-between;
height: 100%;
padding: 0;
#site-name {
display: flex;
align-items: center;
a {
display: block;
}
img {
width: auto;
height: 50px;
}
#site-name-text {
margin-left: 10px;
h1 {
font-size: 36px;
}
}
}
}
#user-container {
width: 100%;
height: @user_container_height;
background-color: #343434;
.user-main {
display: flex;
justify-content: space-between;
align-items: center;
margin: auto;
max-width: 1200px;
height: @user_container_height;
padding-right: 10px;
@media screen and (max-width: 1228px) {
margin: auto 1.2vw;
}
.script-version {
font-size: 12px;
letter-spacing: 1px;
font-family: "微软雅黑";
font-weight: 200;
color: rgba(255, 255, 255, .3);
.has-new-version {
color: lime;
margin-left: 5px;
}
}
.login-info {
font-size: 14px;
}
}
}
}
#site-nav {
width: 0;
height: 0;
border: 0;
padding: 0;
overflow: hidden;
position: relative;
}
#site-nav-vue {
display: flex;
.nav-item {
line-height: @nav_height;
padding: 0 10px;
transition: all .2s ease;
text-decoration: none;
position: relative;
white-space: nowrap;
&:hover {
background-color: rgba(255, 255, 255, .2);
.sub-nav {
display: flex;
}
}
.sub-nav {
display: none;
flex-direction: column;
position: absolute;
top: 100%;
right: 0;
background-color: rgba(0, 0, 0, .8);
.nav-item {
line-height: 40px;
}
}
}
}
// --------------------------------------------- 脚本列表
#user-library-script-list,
#user-script-list,
#user-deleted-script-list,
#browse-script-list {
display: grid;
grid-template-columns: repeat(${settings.script_list_columns_num}, 1fr);
grid-gap: 20px;
border: 0;
box-shadow: none;
@media screen and (max-width: 1228px) {
grid-template-columns: repeat(1, 1fr);
}
li {
border: 1px solid #bbb;
box-shadow: 0 0 5px #ddd;
border-radius: 5px;
padding: 10px;
position: relative;
word-break: break-all;
a.script-link {
.ellipsis(2);
height: calc(3em + 8px);
font-size: 16px;
margin: 4px -10px 4px -14px;
padding: 4px 10px;
background: linear-gradient(#fff, #eee);
border-left: 7px solid #800;
box-shadow: inset 0 1px rgb(0 0 0 / 10%), inset 0 -1px rgb(0 0 0 / 10%);
}
& > article {
& > h2 {
& > .badge,
& > .name-description-separator,
& > strong {
display: none; // 兼容 “大人的Greasyfork”
}
.script-description {
.ellipsis(3);
text-indent: 2em;
margin: 10px 0 10px;
height: 4.5em;
font-size: 14px;
strong,
#install-area {
display: none; // 兼容 “大人的Greasyfork”
}
}
}
}
.inline-script-stats {
padding: 10px 0;
// margin-bottom: 10px;
border-top: 1px solid #ebebeb;
// border-bottom: 1px solid #ebebeb;
dt {
// width: 40%;
}
dd {
width: 60%;
}
}
.install-link {
float: right;
font-size: 12px;
&:hover {
transition: box-shadow .2s;
box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
}
&.lum-lightbox-loader {
border-left: 10px solid #005200;
border-right: 10px solid #005200;
position: relative;
min-height: 30px;
min-width: 70px;
&::before,
&::after {
width: 1em;
height: 1em;
margin-top: -0.5em;
border-radius: 1em;
background: hsla(0, 0%, 100%, .5);
}
}
}
}
}
// --------------------------------------------- 列表右侧选项组
.list-option-groups {
#language-selector {
+ * {
margin-top: 10px;
}
#language-selector-locale {
width: 100%;
border: 1px solid #bfbfbf;
border-radius: 4px;
}
}
}
`
less.render(lessInput, lessOptions).then(output => {
// output.css = string of css
// output.map = string of sourcemap
// output.imports = array of string filenames of the imports referenced
GM_addStyle(output.css)
}, err => {
console.error(err)
})
// 查看代码页面简化,隐藏信息
if (/https:\/\/greasyfork\.org\/[a-zA-Z-]+\/scripts\/\d+-.+\/code/.test(location.href)) {
GM_addStyle(`
#script-info header,
#install-area,
#script-feedback-suggestion {
display: none;
}
#script-content {
margin-top: 16px;
}
.code-container pre code {
max-height: calc(100vh - 267px);
}
`)
}
// 脚本卡片美化
function scriptCardBeautify () {
$(`
#user-script-list li[data-script-id],
#user-deleted-script-list li[data-script-id],
#browse-script-list li[data-script-id]
`).each((i, n) => {
const card = $(n)
const href = card.find('> article a.script-link').attr('href')
// TODO 显示脚本图标 (看情况,如果加了图标不好布局就算了)
// 判断这个卡片是否已经美化过了
const hasVersionTag = card.find('.script-show-version').length > 0
const hasDownloadBtn = card.find('.install-link-copy').length > 0
if (!(hasVersionTag && hasDownloadBtn)) {
// 信息占位
if (settings.show_version_info_in_card) {
card.find('.inline-script-stats').append(`
<dt class="script-show-version"><span>...</span></dt>
<dd class="script-show-version"><span></span></dd>
`)
}
// 下载按钮占位
if (settings.show_install_button_in_card) {
card.append(`
<a class="install-link lum-lightbox-loader"></a>
`)
}
// 增加延时,避免请求过多导致 503 错误 (每秒最多 10 个请求)
setTimeout(() => {
$.ajax({
type: 'get',
url: href,
success: res => {
const html = $(res)
if (settings.show_version_info_in_card) {
// 删除占位元素
card.find('.script-show-version').remove()
// 版本
card.find('.inline-script-stats').append(
html.find('.script-show-version')
)
}
if (settings.show_install_button_in_card) {
// 删除占位元素
card.find('.install-link.lum-lightbox-loader').remove()
// 下载按钮
card.append(
html.find('#install-area .install-link').eq(0).addClass('install-link-copy')
)
// 下载按钮文案根据已安装的版本号调整
setTimeout(() => {
const btn = card.find('.install-link-copy')[0]
if (btn) checkVersion.checkForUpdatesJS(btn, true)
})
}
}
})
}, (i % 5) * 2e3)
}
})
}
// 页面获得焦点时
window.addEventListener('focus', e => {
// 自动更新安装状态
$('.script-list li[data-script-id] a.install-link-copy').each((i, n) => {
checkVersion.checkForUpdatesJS(n, true)
})
})
// 卡片数量记录
let cardCountRecord = 0
// 兼容无限翻页插件
function compatibleWithInfiniteScroll () {
const cardCount = $('.script-list li[data-script-id]').length
if (cardCountRecord !== cardCount) {
cardCountRecord = cardCount
scriptCardBeautify()
}
}
// 页面加载完成后执行
$(() => {
// 导航
const navContainer = document.createElement('div')
navContainer.id = 'site-nav-vue'
document.querySelector('.width-constraint').appendChild(navContainer)
// eslint-disable-next-line no-unused-vars
const navApp = new Vue({
el: '#site-nav-vue',
template: `
<div id="site-nav-vue">
<a
class="nav-item"
v-for="(nav, nav_i) in navList"
:key="nav_i"
:href="nav.url"
>
<span>{{ nav.label }}</span>
<div class="sub-nav" v-if="nav.list?.length">
<a
class="nav-item"
v-for="(sub, sub_i) in nav.list"
:key="sub_i"
:href="sub.url"
>
<span>{{ sub.label }}</span>
</a>
</div>
</a>
</div>
`,
data () {
return {
navList: [...$('#site-nav > nav > li')].map(n => {
const a = $(n).find('> a')
const subNav = [...$(n).find('> nav > li')]
return {
label: a.text() || $(n).text(),
url: a.attr('href'),
list: subNav.map(m => {
const subA = $(m).find('> a')
return {
label: subA.text(),
url: subA.attr('href')
}
})
}
})
}
}
})
// 用户
const userContainer = document.createElement('div')
userContainer.id = 'user-container'
document.querySelector('#main-header').appendChild(userContainer)
// eslint-disable-next-line no-unused-vars
const userApp = new Vue({
el: '#user-container',
template: `
<div id="user-container">
<div class="user-main">
<div class="script-version">
Greasyfork Beautify v${VERSION}
<a
class="has-new-version"
href="https://greasyfork.org/scripts/446849-greasyfork-beautify/code/Greasyfork%20Beautify.user.js"
v-if="lastVersion !== '${VERSION}'"
>Update to v{{ lastVersion }}</a>
</div>
<div class="login-info">
<a
:href="dom.attr('href')"
>{{ dom.text() }}</a>
<template v-if="isLogin">
[<a :href="logoutDom.attr('href')">{{ logoutDom.text() }}</a>]
</template>
</div>
</div>
</div>
`,
data () {
return {
lastVersion: VERSION,
dom: $('#nav-user-info .user-profile-link a, #nav-user-info .sign-in-link a'),
logoutDom: $('.sign-out-link a'),
isLogin: $('.sign-out-link').length > 0 // 存在登出按钮则表示已登录
}
},
created () {
this.versionCheck()
},
methods: {
versionCheck () {
$.ajax({
url: 'https://greasyfork.org/zh-CN/scripts/446849-greasyfork-beautify',
success: res => {
const html = $(res)
this.lastVersion = html.find('dd.script-show-version span').text()
}
})
}
}
})
// 代码高亮
$('pre.lang-js').each((pre_i, pre) => {
// 调整代码,给一些压缩代码增加换行
$(pre).find('li').append('\n')
const code = $('<code class="language-javascript">').html(
pre.innerHTML
)
// 清空原始代码容器,放置新容器
$(pre)
.removeClass()
.html('')
.append(code)
// 高亮
hljs.highlightElement(pre.querySelector('code'))
// 增加行号
const html = $(pre).find('code').html()
const htmlSplit = html.split('\n')
const totalLines = htmlSplit.length
$(pre).find('code').html(
htmlSplit.map((n, i) => `<span class="marker" style="width: calc(${String(totalLines).length * 0.5}em + 20px);">${i + 1}</span>${n}`).join('\n')
)
})
// 脚本列表页面,卡片
if (settings.show_install_button_in_card || settings.show_version_info_in_card) {
compatibleWithInfiniteScroll()
$('.script-list ').resize(compatibleWithInfiniteScroll)
}
// 列表右侧选项组
$('.list-option-groups > *:eq(0)').before(
// 设置语言
$('#language-selector')
)
// 注册菜单
$('body').append($('<div id="greasyfork-beautify-settings">'))
const settingsApp = new Vue({
el: '#greasyfork-beautify-settings',
template: `
<el-dialog
width="600px"
title="Greasyfork Beautify v${VERSION}"
:visible.sync="show"
@closed="onClosed"
>
<el-form
size="mini"
label-width="120px"
:model="formData"
>
<el-form-item label="脚本列表列数">
<el-input-number
label="描述文字"
v-model="formData.script_list_columns_num"
:min="1"
:max="2"
/>
</el-form-item>
<el-form-item label="显示安装按钮">
<el-switch
v-model="formData.show_install_button_in_card"
/>
</el-form-item>
<el-form-item label="显示版本信息">
<el-switch
v-model="formData.show_version_info_in_card"
/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="onReset">重 置</el-button>
<el-button type="primary" @click="onSubmit">确 定</el-button>
</span>
</el-dialog>
`,
data () {
return {
show: false,
formData: getSettings()
}
},
methods: {
onClosed () {
Object.assign(this.formData, getSettings())
},
onReset () {
Object.assign(this.formData, defaultSettings)
},
onSubmit () {
GM_setValue('formData', JSON.stringify(this.formData))
location.reload()
}
}
})
GM_registerMenuCommand('美化设置', e => {
settingsApp.show = true
})
})