diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-04-14 16:39:53 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-04-14 16:39:53 +0900 |
| commit | 449dc17df8fe3b6cc220aaabd576b0f04f6028da (patch) | |
| tree | 33e5c4da1b43f1bba0a02501150c760623fc9798 /src | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.76.0 (diff) | |
| download | misskey-449dc17df8fe3b6cc220aaabd576b0f04f6028da.tar.gz misskey-449dc17df8fe3b6cc220aaabd576b0f04f6028da.tar.bz2 misskey-449dc17df8fe3b6cc220aaabd576b0f04f6028da.zip | |
Merge branch 'develop'
Diffstat (limited to 'src')
255 files changed, 3016 insertions, 1834 deletions
diff --git a/src/client/components/abuse-report-window.vue b/src/client/components/abuse-report-window.vue index 7dbb9657bd..df5b594c0b 100644 --- a/src/client/components/abuse-report-window.vue +++ b/src/client/components/abuse-report-window.vue @@ -8,19 +8,15 @@ </template> </I18n> </template> - <div class="dpvffvvy"> + <div class="dpvffvvy _monolithic_"> <div class="_section"> - <div class="_content"> - <MkTextarea v-model:value="comment"> - <span>{{ $ts.details }}</span> - <template #desc>{{ $ts.fillAbuseReportDescription }}</template> - </MkTextarea> - </div> + <MkTextarea v-model:value="comment"> + <span>{{ $ts.details }}</span> + <template #desc>{{ $ts.fillAbuseReportDescription }}</template> + </MkTextarea> </div> <div class="_section"> - <div class="_content"> - <MkButton @click="send" primary full :disabled="comment.length === 0">{{ $ts.send }}</MkButton> - </div> + <MkButton @click="send" primary full :disabled="comment.length === 0">{{ $ts.send }}</MkButton> </div> </div> </XWindow> @@ -80,6 +76,6 @@ export default defineComponent({ <style lang="scss" scoped> .dpvffvvy { - --section-padding: 16px; + --root-margin: 16px; } </style> diff --git a/src/client/components/channel-preview.vue b/src/client/components/channel-preview.vue index e222ad7ae7..4dc633bcb7 100644 --- a/src/client/components/channel-preview.vue +++ b/src/client/components/channel-preview.vue @@ -123,7 +123,7 @@ export default defineComponent({ > footer { padding: 12px 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); > span { opacity: 0.7; diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue index be9f01ca1f..433655d6ed 100644 --- a/src/client/components/date-separated-list.vue +++ b/src/client/components/date-separated-list.vue @@ -37,14 +37,16 @@ export default defineComponent({ }); } + const noGap = [...document.querySelectorAll('._noGap_')].some(el => el.contains(this.$parent.$el)); + return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { - class: 'sqadhkmv _list_', + class: 'sqadhkmv' + (noGap ? ' _block' : ''), name: 'list', tag: 'div', 'data-direction': this.direction, 'data-reversed': this.reversed ? 'true' : 'false', } : { - class: 'sqadhkmv _list_', + class: 'sqadhkmv', }, this.items.map((item, i) => { const el = this.$slots.default({ item: item @@ -117,11 +119,7 @@ export default defineComponent({ transform: translateY(-64px); } } -} -</style> -<style lang="scss"> -.sqadhkmv { > .separator { text-align: center; @@ -155,4 +153,25 @@ export default defineComponent({ } } } + +._noGap_ .sqadhkmv { + > * { + margin: 0 !important; + border: none; + border-radius: 0; + box-shadow: none; + + &:not(:last-child) { + border-bottom: solid 0.5px var(--divider); + } + } +} + +._inContainer_ .sqadhkmv > * { + margin: 0 !important; + border: none; + border-bottom: solid 0.5px var(--divider); + border-radius: 0; + box-shadow: none; +} </style> diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue index 03f2da008d..fb8b50d25a 100644 --- a/src/client/components/drive.file.vue +++ b/src/client/components/drive.file.vue @@ -330,8 +330,8 @@ export default defineComponent({ } > .thumbnail { - width: 128px; - height: 128px; + width: 110px; + height: 110px; margin: auto; } diff --git a/src/client/components/drive.vue b/src/client/components/drive.vue index 150d0d8774..103ae9c11e 100644 --- a/src/client/components/drive.vue +++ b/src/client/components/drive.vue @@ -11,7 +11,7 @@ <span class="folder current" v-if="folder != null">{{ folder.name }}</span> </div> </nav> - <div class="main _section" :class="{ uploading: uploadings.length > 0, fetching }" + <div class="main" :class="{ uploading: uploadings.length > 0, fetching }" ref="main" @dragover.prevent.stop="onDragover" @dragenter="onDragenter" @@ -704,6 +704,7 @@ export default defineComponent({ > .main { flex: 1; overflow: auto; + padding: var(--margin); &, * { user-select: none; @@ -735,7 +736,7 @@ export default defineComponent({ > .folder, > .file { flex-grow: 1; - width: 144px; + width: 128px; margin: 4px; box-sizing: border-box; } @@ -743,7 +744,7 @@ export default defineComponent({ > .padding { flex-grow: 1; pointer-events: none; - width: 144px + 8px; + width: 128px + 8px; } } diff --git a/src/client/components/emoji-picker-dialog.vue b/src/client/components/emoji-picker-dialog.vue index 5bdbc330ad..c4b12e2f61 100644 --- a/src/client/components/emoji-picker-dialog.vue +++ b/src/client/components/emoji-picker-dialog.vue @@ -123,7 +123,7 @@ export default defineComponent({ > .index { min-height: var(--height); position: relative; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); > .arrow { position: absolute; @@ -181,7 +181,7 @@ export default defineComponent({ } &.result { - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); &:empty { display: none; diff --git a/src/client/components/emoji-picker-window.vue b/src/client/components/emoji-picker-window.vue index 5504eaecd6..53b6ae6b32 100644 --- a/src/client/components/emoji-picker-window.vue +++ b/src/client/components/emoji-picker-window.vue @@ -119,7 +119,7 @@ export default defineComponent({ > .index { min-height: var(--height); position: relative; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); > .arrow { position: absolute; @@ -177,7 +177,7 @@ export default defineComponent({ } &.result { - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); &:empty { display: none; diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue index 573833b9d3..a212c15049 100644 --- a/src/client/components/emoji-picker.vue +++ b/src/client/components/emoji-picker.vue @@ -402,7 +402,7 @@ export default defineComponent({ > .tab { flex: 1; height: 38px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); &.active { border-top: solid 1px var(--accent); @@ -425,7 +425,7 @@ export default defineComponent({ > div { &:not(.index) { padding: 4px 0 8px 0; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } > header { @@ -492,7 +492,7 @@ export default defineComponent({ } &.result { - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); &:empty { display: none; diff --git a/src/client/components/form/base.vue b/src/client/components/form/base.vue index 249b49c675..84438a5b32 100644 --- a/src/client/components/form/base.vue +++ b/src/client/components/form/base.vue @@ -20,12 +20,16 @@ export default defineComponent({ <style lang="scss" scoped> .rbusrurv { - line-height: 1.4em; + // 他のCSSからも参照されるので消さないように + --formXPadding: 32px; + --formYPadding: 32px; + + line-height: 1.3em; background: var(--bg); - padding: 32px; + padding: var(--formYPadding) var(--formXPadding); &:not(.wide).max-width_400px { - padding: 32px 0; + --formXPadding: 0px; > ::v-deep(*) { ._formPanel { diff --git a/src/client/components/form/form.scss b/src/client/components/form/form.scss index c7f4373544..8c01fad727 100644 --- a/src/client/components/form/form.scss +++ b/src/client/components/form/form.scss @@ -1,32 +1,48 @@ ._formPanel { background: var(--panel); border-radius: var(--radius); + transition: background 0.2s ease; &._formClickable { &:hover { //background: var(--panelHighlight); } + + &:active { + background: var(--panelHighlight); + transition: background 0s; + } } } -._formLabel { +._formLabel, +._formCaption { font-size: 80%; - padding: 0 16px 8px 16px; - opacity: 0.8; + color: var(--fgTransparentWeak); &:empty { display: none; } } +._formLabel { + position: sticky; + top: var(--stickyTop, 0px); + z-index: 2; + margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1); + padding: 8px calc(16px + var(--formXPadding)) 8px calc(16px + var(--formXPadding)); + background: var(--X17); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); +} + +._themeChanging_ ._formLabel { + transition: none !important; + background: transparent; +} + ._formCaption { - font-size: 80%; padding: 8px 16px 0 16px; - opacity: 0.8; - - &:empty { - display: none; - } } ._formItem { diff --git a/src/client/components/form/info.vue b/src/client/components/form/info.vue new file mode 100644 index 0000000000..a9224c7e65 --- /dev/null +++ b/src/client/components/form/info.vue @@ -0,0 +1,49 @@ +<template> +<div class="fzenkabp _formItem"> + <div class="_formPanel" :class="{ warn }"> + <i v-if="warn"><Fa :icon="faExclamationTriangle"/></i> + <i v-else><Fa :icon="faInfoCircle"/></i> + <slot></slot> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import { faInfoCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + +export default defineComponent({ + props: { + warn: { + type: Boolean, + required: false, + default: false + }, + }, + data() { + return { + faInfoCircle, faExclamationTriangle + }; + } +}); +</script> + +<style lang="scss" scoped> +.fzenkabp { + > div { + padding: 14px 16px; + font-size: 90%; + background: var(--infoBg); + color: var(--infoFg); + + &.warn { + background: var(--infoWarnBg); + color: var(--infoWarnFg); + } + + > i { + margin-right: 4px; + } + } +} +</style> diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue index f0aa6b0534..c0fa3e716e 100644 --- a/src/client/components/form/input.vue +++ b/src/client/components/form/input.vue @@ -215,7 +215,7 @@ export default defineComponent({ } > .input { - $height: 52px; + $height: 48px; position: relative; > input { diff --git a/src/client/components/form/link.vue b/src/client/components/form/link.vue index 2efc6b58c9..af36bcf22c 100644 --- a/src/client/components/form/link.vue +++ b/src/client/components/form/link.vue @@ -66,6 +66,7 @@ export default defineComponent({ &.active { color: var(--accent); + background: var(--panelHighlight); } > .icon { diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue index 4561df32e1..3daa7e5bbd 100644 --- a/src/client/components/form/radios.vue +++ b/src/client/components/form/radios.vue @@ -69,8 +69,8 @@ export default defineComponent({ display: inline-block; vertical-align: bottom; position: relative; - width: 20px; - height: 20px; + width: 16px; + height: 16px; margin-right: 8px; background: none; border: 2px solid var(--inputBorder); diff --git a/src/client/components/form/range.vue b/src/client/components/form/range.vue index 3452184c55..65d665c70a 100644 --- a/src/client/components/form/range.vue +++ b/src/client/components/form/range.vue @@ -69,7 +69,7 @@ export default defineComponent({ position: relative; > .main { - padding: 24px 16px; + padding: 22px 16px; > input { display: block; diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue index b865372f56..01f28587dc 100644 --- a/src/client/components/form/select.vue +++ b/src/client/components/form/select.vue @@ -97,7 +97,7 @@ export default defineComponent({ font: inherit; font-weight: normal; font-size: 1em; - height: 52px; + height: 48px; background: none; border: none; border-radius: 0; diff --git a/src/client/components/form/switch.vue b/src/client/components/form/switch.vue index a2941c5996..e7ef714c49 100644 --- a/src/client/components/form/switch.vue +++ b/src/client/components/form/switch.vue @@ -57,7 +57,7 @@ export default defineComponent({ > .main { position: relative; display: flex; - padding: 16px; + padding: 14px 16px; cursor: pointer; > * { diff --git a/src/client/components/global/a.vue b/src/client/components/global/a.vue index a8a597b2bb..7ad62a7310 100644 --- a/src/client/components/global/a.vue +++ b/src/client/components/global/a.vue @@ -93,6 +93,10 @@ export default defineComponent({ os.pageWindow(this.to); }, + modalWindow() { + os.modalPageWindow(this.to); + }, + popout() { popout(this.to); }, @@ -111,6 +115,8 @@ export default defineComponent({ if (this.behavior) { if (this.behavior === 'window') { return this.window(); + } else if (this.behavior === 'modalWindow') { + return this.modalWindow(); } } diff --git a/src/client/components/global/acct.vue b/src/client/components/global/acct.vue index a969636a7e..70f2954cb0 100644 --- a/src/client/components/global/acct.vue +++ b/src/client/components/global/acct.vue @@ -1,5 +1,5 @@ <template> -<span class="mk-acct" v-once> +<span class="mk-acct"> <span class="name">@{{ user.username }}</span> <span class="host" v-if="user.host || detail || $store.state.showFullAcct">@{{ user.host || host }}</span> </span> @@ -7,11 +7,20 @@ <script lang="ts"> import { defineComponent } from 'vue'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import { host } from '@client/config'; export default defineComponent({ - props: ['user', 'detail'], + props: { + user: { + type: Object, + required: true + }, + detail: { + type: Boolean, + default: false + }, + }, data() { return { host: toUnicode(host), diff --git a/src/client/components/global/url.vue b/src/client/components/global/url.vue index c89536ebdb..f68a3c00be 100644 --- a/src/client/components/global/url.vue +++ b/src/client/components/global/url.vue @@ -22,7 +22,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; -import { toUnicode as decodePunycode } from 'punycode'; +import { toUnicode as decodePunycode } from 'punycode/'; import { url as local } from '@client/config'; import { isDeviceTouch } from '@client/scripts/is-device-touch'; import * as os from '@client/os'; diff --git a/src/client/components/launch-pad.vue b/src/client/components/launch-pad.vue index a81320954c..7610b44eb5 100644 --- a/src/client/components/launch-pad.vue +++ b/src/client/components/launch-pad.vue @@ -146,7 +146,7 @@ export default defineComponent({ > .sub { margin-top: 8px; padding-top: 8px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } } </style> diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue index 41760d98d7..4de5daa84f 100644 --- a/src/client/components/media-image.vue +++ b/src/client/components/media-image.vue @@ -123,7 +123,7 @@ export default defineComponent({ .gqnyydlz { position: relative; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); > i { display: block; diff --git a/src/client/components/mention.vue b/src/client/components/mention.vue index 322e56d957..b9bd6b320b 100644 --- a/src/client/components/mention.vue +++ b/src/client/components/mention.vue @@ -16,7 +16,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import { host as localHost } from '@client/config'; import { wellKnownServices } from '../../well-known-services'; import * as os from '@client/os'; diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts index 28ac9b8942..b8e948a188 100644 --- a/src/client/components/mfm.ts +++ b/src/client/components/mfm.ts @@ -1,6 +1,5 @@ import { VNode, defineComponent, h } from 'vue'; -import { MfmForest } from '@client/../mfm/prelude'; -import { parse, parsePlain } from '@client/../mfm/parse'; +import * as mfm from 'mfm-js'; import MkUrl from '@client/components/global/url.vue'; import MkLink from '@client/components/link.vue'; import MkMention from '@client/components/mention.vue'; @@ -46,17 +45,17 @@ export default defineComponent({ render() { if (this.text == null || this.text == '') return; - const ast = (this.plain ? parsePlain : parse)(this.text); + const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text); const validTime = (t: string | null | undefined) => { if (t == null) return null; return t.match(/^[0-9.]+s$/) ? t : null; }; - const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => { - switch (token.node.type) { + const genEl = (ast: mfm.MfmNode[]) => concat(ast.map((token): VNode[] => { + switch (token.type) { case 'text': { - const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n'); + const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (!this.plain) { const x = text.split('\n') @@ -83,38 +82,38 @@ export default defineComponent({ } case 'fn': { - // TODO: CSSを文字列で組み立てていくと token.node.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる + // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる let style; - switch (token.node.props.name) { + switch (token.props.name) { case 'tada': { style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : ''); break; } case 'jelly': { - const speed = validTime(token.node.props.args.speed) || '1s'; + const speed = validTime(token.props.args.speed) || '1s'; style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); break; } case 'twitch': { - const speed = validTime(token.node.props.args.speed) || '0.5s'; + const speed = validTime(token.props.args.speed) || '0.5s'; style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : ''; break; } case 'shake': { - const speed = validTime(token.node.props.args.speed) || '0.5s'; + const speed = validTime(token.props.args.speed) || '0.5s'; style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : ''; break; } case 'spin': { const direction = - token.node.props.args.left ? 'reverse' : - token.node.props.args.alternate ? 'alternate' : + token.props.args.left ? 'reverse' : + token.props.args.alternate ? 'alternate' : 'normal'; const anime = - token.node.props.args.x ? 'mfm-spinX' : - token.node.props.args.y ? 'mfm-spinY' : + token.props.args.x ? 'mfm-spinX' : + token.props.args.y ? 'mfm-spinY' : 'mfm-spin'; - const speed = validTime(token.node.props.args.speed) || '1.5s'; + const speed = validTime(token.props.args.speed) || '1.5s'; style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; break; } @@ -128,8 +127,8 @@ export default defineComponent({ } case 'flip': { const transform = - (token.node.props.args.h && token.node.props.args.v) ? 'scale(-1, -1)' : - token.node.props.args.v ? 'scaleY(-1)' : + (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : + token.props.args.v ? 'scaleY(-1)' : 'scaleX(-1)'; style = `transform: ${transform};`; break; @@ -148,12 +147,12 @@ export default defineComponent({ } case 'font': { const family = - token.node.props.args.serif ? 'serif' : - token.node.props.args.monospace ? 'monospace' : - token.node.props.args.cursive ? 'cursive' : - token.node.props.args.fantasy ? 'fantasy' : - token.node.props.args.emoji ? 'emoji' : - token.node.props.args.math ? 'math' : + token.props.args.serif ? 'serif' : + token.props.args.monospace ? 'monospace' : + token.props.args.cursive ? 'cursive' : + token.props.args.fantasy ? 'fantasy' : + token.props.args.emoji ? 'emoji' : + token.props.args.math ? 'math' : null; if (family) style = `font-family: ${family};`; break; @@ -165,7 +164,7 @@ export default defineComponent({ } } if (style == null) { - return h('span', {}, ['[', token.node.props.name, ...genEl(token.children), ']']); + return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']); } else { return h('span', { style: 'display: inline-block;' + style, @@ -188,7 +187,7 @@ export default defineComponent({ case 'url': { return [h(MkUrl, { key: Math.random(), - url: token.node.props.url, + url: token.props.url, rel: 'nofollow noopener', })]; } @@ -196,7 +195,7 @@ export default defineComponent({ case 'link': { return [h(MkLink, { key: Math.random(), - url: token.node.props.url, + url: token.props.url, rel: 'nofollow noopener', }, genEl(token.children))]; } @@ -204,32 +203,31 @@ export default defineComponent({ case 'mention': { return [h(MkMention, { key: Math.random(), - host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, - username: token.node.props.username + host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, + username: token.props.username })]; } case 'hashtag': { return [h(MkA, { key: Math.random(), - to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, + to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`, style: 'color:var(--hashtag);' - }, `#${token.node.props.hashtag}`)]; + }, `#${token.props.hashtag}`)]; } case 'blockCode': { return [h(MkCode, { key: Math.random(), - code: token.node.props.code, - lang: token.node.props.lang, + code: token.props.code, + lang: token.props.lang, })]; } case 'inlineCode': { return [h(MkCode, { key: Math.random(), - code: token.node.props.code, - lang: token.node.props.lang, + code: token.props.code, inline: true })]; } @@ -246,10 +244,19 @@ export default defineComponent({ } } - case 'emoji': { + case 'emojiCode': { return [h(MkEmoji, { key: Math.random(), - emoji: token.node.props.name ? `:${token.node.props.name}:` : token.node.props.emoji, + emoji: `:${token.props.name}:`, + customEmojis: this.customEmojis, + normal: this.plain + })]; + } + + case 'unicodeEmoji': { + return [h(MkEmoji, { + key: Math.random(), + emoji: token.props.emoji, customEmojis: this.customEmojis, normal: this.plain })]; @@ -258,7 +265,7 @@ export default defineComponent({ case 'mathInline': { return [h(MkFormula, { key: Math.random(), - formula: token.node.props.formula, + formula: token.props.formula, block: false })]; } @@ -266,7 +273,7 @@ export default defineComponent({ case 'mathBlock': { return [h(MkFormula, { key: Math.random(), - formula: token.node.props.formula, + formula: token.props.formula, block: true })]; } @@ -274,12 +281,12 @@ export default defineComponent({ case 'search': { return [h(MkGoogle, { key: Math.random(), - q: token.node.props.query + q: token.props.query })]; } default: { - console.error('unrecognized ast type:', token.node.type); + console.error('unrecognized ast type:', token.type); return []; } diff --git a/src/client/components/modal-page-window.vue b/src/client/components/modal-page-window.vue new file mode 100644 index 0000000000..474a67f985 --- /dev/null +++ b/src/client/components/modal-page-window.vue @@ -0,0 +1,213 @@ +<template> +<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> + <div class="hrmcaedk _popup _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> + <div class="header"> + <button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button> + <button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button> + <span class="title"> + <XHeader :info="pageInfo" :with-back="false"/> + </span> + <button class="_button" @click="$refs.modal.close()"><Fa :icon="faTimes"/></button> + </div> + <div class="body _flat_"> + <keep-alive> + <component :is="component" v-bind="props" :ref="changePage"/> + </keep-alive> + </div> + </div> +</MkModal> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import { faExternalLinkAlt, faExpandAlt, faLink, faChevronLeft, faColumns, faTimes } from '@fortawesome/free-solid-svg-icons'; +import MkModal from '@client/components/ui/modal.vue'; +import XHeader from '@client/ui/_common_/header.vue'; +import { popout } from '@client/scripts/popout'; +import copyToClipboard from '@client/scripts/copy-to-clipboard'; +import { resolve } from '@client/router'; +import { url } from '@client/config'; +import * as symbols from '@client/symbols'; + +export default defineComponent({ + components: { + MkModal, + XHeader, + }, + + inject: { + sideViewHook: { + default: null + } + }, + + provide() { + return { + navHook: (path) => { + this.navigate(path); + } + }; + }, + + props: { + initialPath: { + type: String, + required: true, + }, + initialComponent: { + type: Object, + required: true, + }, + initialProps: { + type: Object, + required: false, + default: () => {}, + }, + }, + + emits: ['closed'], + + data() { + return { + width: 860, + height: 660, + pageInfo: null, + path: this.initialPath, + component: this.initialComponent, + props: this.initialProps, + history: [], + faChevronLeft, faTimes, + }; + }, + + computed: { + url(): string { + return url + this.path; + }, + + contextmenu() { + return [{ + type: 'label', + text: this.path, + }, { + icon: faExpandAlt, + text: this.$ts.showInPage, + action: this.expand + }, this.sideViewHook ? { + icon: faColumns, + text: this.$ts.openInSideView, + action: () => { + this.sideViewHook(this.path); + this.$refs.window.close(); + } + } : undefined, { + icon: faExternalLinkAlt, + text: this.$ts.popout, + action: this.popout + }, null, { + icon: faExternalLinkAlt, + text: this.$ts.openInNewTab, + action: () => { + window.open(this.url, '_blank'); + this.$refs.window.close(); + } + }, { + icon: faLink, + text: this.$ts.copyLink, + action: () => { + copyToClipboard(this.url); + } + }]; + }, + }, + + methods: { + changePage(page) { + if (page == null) return; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; + } + }, + + navigate(path, record = true) { + if (record) this.history.push(this.path); + this.path = path; + const { component, props } = resolve(path); + this.component = component; + this.props = props; + }, + + back() { + this.navigate(this.history.pop(), false); + }, + + expand() { + this.$router.push(this.path); + this.$refs.window.close(); + }, + + popout() { + popout(this.path, this.$el); + this.$refs.window.close(); + }, + }, +}); +</script> + +<style lang="scss" scoped> +.hrmcaedk { + overflow: hidden; + display: flex; + flex-direction: column; + contain: content; + + --root-margin: 24px; + + @media (max-width: 500px) { + --root-margin: 16px; + } + + > .header { + $height: 52px; + $height-narrow: 42px; + display: flex; + flex-shrink: 0; + box-shadow: 0px 1px var(--divider); + + > button { + height: $height; + width: $height; + + @media (max-width: 500px) { + height: $height-narrow; + width: $height-narrow; + } + } + + > .title { + flex: 1; + line-height: $height; + padding-left: 32px; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; + + @media (max-width: 500px) { + line-height: $height-narrow; + padding-left: 16px; + } + } + + > button + .title { + padding-left: 0; + } + } + + > .body { + overflow: auto; + background: var(--bg); + } +} +</style> diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index fb4f9502b3..b25c97543b 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -1,6 +1,6 @@ <template> <div - class="note _panel" + class="note _block" v-if="!muted" v-show="!isDeleted" :tabindex="!isDeleted ? '-1' : null" @@ -120,11 +120,11 @@ </template> <script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue'; -import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons'; +import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; +import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip, faShareAlt } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import { parse } from '../../mfm/parse'; -import { sum, unique } from '../../prelude/array'; +import * as mfm from 'mfm-js'; +import { sum } from '../../prelude/array'; import XSub from './note.sub.vue'; import XNoteHeader from './note-header.vue'; import XNotePreview from './note-preview.vue'; @@ -141,6 +141,7 @@ import { userPage } from '@client/filters/user'; import * as os from '@client/os'; import { noteActions, noteViewInterruptors } from '@client/store'; import { reactionPicker } from '@client/scripts/reaction-picker'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; function markRawAll(...xs) { for (const x of xs) { @@ -252,21 +253,7 @@ export default defineComponent({ urls(): string[] { if (this.appearNote.text) { - const ast = parse(this.appearNote.text); - // TODO: 再帰的にURL要素がないか調べる - const urls = unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - - // unique without hash - // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] - const removeHash = x => x.replace(/#[^#]*$/, ''); - - return urls.reduce((array, url) => { - const removed = removeHash(url); - if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); - return array; - }, []); + return extractUrlFromMfm(mfm.parse(this.appearNote.text)); } else { return null; } @@ -638,6 +625,11 @@ export default defineComponent({ window.open(this.appearNote.url || this.appearNote.uri, '_blank'); } } : undefined, + { + icon: faShareAlt, + text: this.$ts.share, + action: this.share + }, null, statePromise.then(state => state.isFavorited ? { icon: faStar, @@ -863,6 +855,14 @@ export default defineComponent({ }); }, + share() { + navigator.share({ + title: this.$t('noteOf', { user: this.appearNote.user.name }), + text: this.appearNote.text, + url: `${url}/notes/${this.appearNote.id}` + }); + }, + focus() { this.$el.focus(); }, @@ -1020,7 +1020,7 @@ export default defineComponent({ margin: 0 0.5em; padding: 4px 6px; font-size: 80%; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: 4px; } @@ -1123,7 +1123,7 @@ export default defineComponent({ } > .reply { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } &.max-width_500px { diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue index a6e9b6fe56..ab40c5fd4a 100644 --- a/src/client/components/note-header.vue +++ b/src/client/components/note-header.vue @@ -78,7 +78,7 @@ export default defineComponent({ margin: 0 .5em 0 0; padding: 1px 6px; font-size: 80%; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: 3px; } diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue index 853d481406..899c4b2f16 100644 --- a/src/client/components/note.sub.vue +++ b/src/client/components/note.sub.vue @@ -139,7 +139,7 @@ export default defineComponent({ } > .reply { - border-left: solid 1px var(--divider); + border-left: solid 0.5px var(--divider); margin-top: 10px; } } diff --git a/src/client/components/note.vue b/src/client/components/note.vue index b54cadfc80..0e153033ca 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -1,6 +1,6 @@ <template> <div - class="tkcbzcuz _panel" + class="tkcbzcuz" v-if="!muted" v-show="!isDeleted" :tabindex="!isDeleted ? '-1' : null" @@ -90,7 +90,7 @@ </div> </article> </div> -<div v-else class="_panel muted" @click="muted = false"> +<div v-else class="muted" @click="muted = false"> <I18n :src="$ts.userSaysSomething" tag="small"> <template #name> <MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId"> @@ -102,11 +102,11 @@ </template> <script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue'; -import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons'; +import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; +import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip, faShareAlt } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import { parse } from '../../mfm/parse'; -import { sum, unique } from '../../prelude/array'; +import * as mfm from 'mfm-js'; +import { sum } from '../../prelude/array'; import XSub from './note.sub.vue'; import XNoteHeader from './note-header.vue'; import XNotePreview from './note-preview.vue'; @@ -123,6 +123,7 @@ import { userPage } from '@client/filters/user'; import * as os from '@client/os'; import { noteActions, noteViewInterruptors } from '@client/store'; import { reactionPicker } from '@client/scripts/reaction-picker'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; function markRawAll(...xs) { for (const x of xs) { @@ -238,21 +239,7 @@ export default defineComponent({ urls(): string[] { if (this.appearNote.text) { - const ast = parse(this.appearNote.text); - // TODO: 再帰的にURL要素がないか調べる - const urls = unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - - // unique without hash - // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] - const removeHash = x => x.replace(/#[^#]*$/, ''); - - return urls.reduce((array, url) => { - const removed = removeHash(url); - if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); - return array; - }, []); + return extractUrlFromMfm(mfm.parse(this.appearNote.text)); } else { return null; } @@ -613,6 +600,11 @@ export default defineComponent({ window.open(this.appearNote.url || this.appearNote.uri, '_blank'); } } : undefined, + { + icon: faShareAlt, + text: this.$ts.share, + action: this.share + }, null, statePromise.then(state => state.isFavorited ? { icon: faStar, @@ -838,6 +830,14 @@ export default defineComponent({ }); }, + share() { + navigator.share({ + title: this.$t('noteOf', { user: this.appearNote.user.name }), + text: this.appearNote.text, + url: `${url}/notes/${this.appearNote.id}` + }); + }, + focus() { this.$el.focus(); }, @@ -863,7 +863,7 @@ export default defineComponent({ .tkcbzcuz { position: relative; transition: box-shadow 0.1s ease; - overflow: hidden; + overflow: clip; contain: content; // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 @@ -994,11 +994,12 @@ export default defineComponent({ > .avatar { flex-shrink: 0; display: block; - //position: sticky; - //top: 72px; margin: 0 14px 8px 0; width: 58px; height: 58px; + position: sticky; + top: calc(22px + var(--stickyTop, 0px)); + left: 0; } > .main { @@ -1119,7 +1120,7 @@ export default defineComponent({ } > .reply { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } &.max-width_500px { @@ -1142,6 +1143,7 @@ export default defineComponent({ margin: 0 10px 8px 0; width: 50px; height: 50px; + top: calc(14px + var(--stickyTop, 0px)); } } } diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 80a9502d5f..aedf11bc40 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -1,5 +1,5 @@ <template> -<div class="_list_"> +<div> <div class="_fullinfo" v-if="empty"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $ts.noNotes }}</div> @@ -15,7 +15,7 @@ </div> <XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> - <XNote :note="note" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> + <XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> </XList> <div v-show="more && !reversed" style="margin-top: var(--margin);"> diff --git a/src/client/components/notification-setting-window.vue b/src/client/components/notification-setting-window.vue index a3e221f7b2..5f16c042bf 100644 --- a/src/client/components/notification-setting-window.vue +++ b/src/client/components/notification-setting-window.vue @@ -9,17 +9,19 @@ @closed="$emit('closed')" > <template #header>{{ $ts.notificationSetting }}</template> - <div v-if="showGlobalToggle" class="_section"> - <MkSwitch v-model:value="useGlobalSetting"> - {{ $ts.useGlobalSetting }} - <template #desc>{{ $ts.useGlobalSettingDesc }}</template> - </MkSwitch> - </div> - <div v-if="!useGlobalSetting" class="_section"> - <MkInfo>{{ $ts.notificationSettingDesc }}</MkInfo> - <MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton> - <MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton> - <MkSwitch v-for="type in notificationTypes" :key="type" v-model:value="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch> + <div class="_monolithic_"> + <div v-if="showGlobalToggle" class="_section"> + <MkSwitch v-model:value="useGlobalSetting"> + {{ $ts.useGlobalSetting }} + <template #desc>{{ $ts.useGlobalSettingDesc }}</template> + </MkSwitch> + </div> + <div v-if="!useGlobalSetting" class="_section"> + <MkInfo>{{ $ts.notificationSettingDesc }}</MkInfo> + <MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton> + <MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton> + <MkSwitch v-for="type in notificationTypes" :key="type" v-model:value="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch> + </div> </div> </XModalWindow> </template> diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue index 01b3ae4396..baafa86f4d 100644 --- a/src/client/components/notifications.vue +++ b/src/client/components/notifications.vue @@ -1,5 +1,6 @@ <template> <div class="mfcuwfyp _noGap_"> + <div class="_magnet"></div> <XList class="notifications" :items="items" v-slot="{ item: notification }"> <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> <XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> diff --git a/src/client/components/page-preview.vue b/src/client/components/page-preview.vue index 2125ddc534..cd896445a7 100644 --- a/src/client/components/page-preview.vue +++ b/src/client/components/page-preview.vue @@ -1,5 +1,5 @@ <template> -<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _panel" tabindex="-1"> +<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block _isolated" tabindex="-1"> <div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div> <article> <header> @@ -35,7 +35,6 @@ export default defineComponent({ <style lang="scss" scoped> .vhpxefrj { display: block; - width: 100%; &:hover { text-decoration: none; diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue index ca6f4dd73e..1afde25501 100644 --- a/src/client/components/page-window.vue +++ b/src/client/components/page-window.vue @@ -14,7 +14,7 @@ <button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button> <button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button> </template> - <div class="yrolvcoq" style="min-height: 100%; background: var(--bg);"> + <div class="yrolvcoq _flat_"> <component :is="component" v-bind="props" :ref="changePage"/> </div> </XWindow> @@ -29,6 +29,7 @@ import { popout } from '@client/scripts/popout'; import copyToClipboard from '@client/scripts/copy-to-clipboard'; import { resolve } from '@client/router'; import { url } from '@client/config'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -123,8 +124,8 @@ export default defineComponent({ methods: { changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; } }, @@ -155,6 +156,7 @@ export default defineComponent({ <style lang="scss" scoped> .yrolvcoq { - --section-padding: 16px; + min-height: 100%; + background: var(--bg); } </style> diff --git a/src/client/components/page/page.text.vue b/src/client/components/page/page.text.vue index 491c62be26..580c5a93bf 100644 --- a/src/client/components/page/page.text.vue +++ b/src/client/components/page/page.text.vue @@ -9,8 +9,8 @@ import { TextBlock } from '@client/scripts/hpml/block'; import { Hpml } from '@client/scripts/hpml/evaluator'; import { defineAsyncComponent, defineComponent, PropType } from 'vue'; -import { parse } from '../../../mfm/parse'; -import { unique } from '../../../prelude/array'; +import * as mfm from 'mfm-js'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; export default defineComponent({ components: { @@ -34,11 +34,7 @@ export default defineComponent({ computed: { urls(): string[] { if (this.text) { - const ast = parse(this.text); - // TODO: 再帰的にURL要素がないか調べる - return unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); + return extractUrlFromMfm(mfm.parse(this.text)); } else { return []; } diff --git a/src/client/components/poll.vue b/src/client/components/poll.vue index af3b3804ab..6cf6a8e918 100644 --- a/src/client/components/poll.vue +++ b/src/client/components/poll.vue @@ -110,7 +110,7 @@ export default defineComponent({ position: relative; margin: 4px 0; padding: 4px 8px; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: 4px; overflow: hidden; cursor: pointer; diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index 7d2355c190..ce79f34d62 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -56,12 +56,12 @@ import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlob import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons'; import insertTextAtCursor from 'insert-text-at-cursor'; import { length } from 'stringz'; -import { toASCII } from 'punycode'; +import { toASCII } from 'punycode/'; import XNotePreview from './note-preview.vue'; -import { parse } from '../../mfm/parse'; +import * as mfm from 'mfm-js'; import { host, url } from '@client/config'; import { erase, unique } from '../../prelude/array'; -import extractMentions from '@/misc/extract-mentions'; +import { extractMentions } from '@/misc/extract-mentions'; import getAcct from '@/misc/acct/render'; import { formatTimeString } from '@/misc/format-time-string'; import { Autocomplete } from '@client/scripts/autocomplete'; @@ -229,7 +229,7 @@ export default defineComponent({ } if (this.reply && this.reply.text != null) { - const ast = parse(this.reply.text); + const ast = mfm.parse(this.reply.text); for (const x of extractMentions(ast)) { const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`; @@ -580,7 +580,7 @@ export default defineComponent({ this.deleteDraft(); this.$emit('posted'); if (this.text && this.text != '') { - const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag); + const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); } @@ -767,7 +767,7 @@ export default defineComponent({ > .cw { z-index: 1; padding-bottom: 8px; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); } > .text { diff --git a/src/client/components/remote-caution.vue b/src/client/components/remote-caution.vue index 98c7aaaa6e..c9c5ceea4c 100644 --- a/src/client/components/remote-caution.vue +++ b/src/client/components/remote-caution.vue @@ -1,5 +1,5 @@ <template> -<div class="jmgmzlwq _panel"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $ts.remoteUserCaution }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div> +<div class="jmgmzlwq _block"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $ts.remoteUserCaution }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div> </template> <script lang="ts"> diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue index 120da63f30..1cd79bac1d 100755 --- a/src/client/components/signin.vue +++ b/src/client/components/signin.vue @@ -1,5 +1,5 @@ <template> -<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> +<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> <div class="auth _section"> <div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div> <div class="normal-signin" v-if="!totpLogin"> @@ -39,16 +39,16 @@ </div> </div> <div class="social _section"> - <a class="_borderButton _vMargin" v-if="meta && meta.enableTwitterIntegration" :href="`${apiUrl}/signin/twitter`"><Fa :icon="faTwitter" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Twitter' }) }}</a> - <a class="_borderButton _vMargin" v-if="meta && meta.enableGithubIntegration" :href="`${apiUrl}/signin/github`"><Fa :icon="faGithub" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'GitHub' }) }}</a> - <a class="_borderButton _vMargin" v-if="meta && meta.enableDiscordIntegration" :href="`${apiUrl}/signin/discord`"><Fa :icon="faDiscord" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Discord' }) }}</a> + <a class="_borderButton _gap" v-if="meta && meta.enableTwitterIntegration" :href="`${apiUrl}/signin/twitter`"><Fa :icon="faTwitter" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Twitter' }) }}</a> + <a class="_borderButton _gap" v-if="meta && meta.enableGithubIntegration" :href="`${apiUrl}/signin/github`"><Fa :icon="faGithub" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'GitHub' }) }}</a> + <a class="_borderButton _gap" v-if="meta && meta.enableDiscordIntegration" :href="`${apiUrl}/signin/discord`"><Fa :icon="faDiscord" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Discord' }) }}</a> </div> </form> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import { faLock, faGavel } from '@fortawesome/free-solid-svg-icons'; import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'; import MkButton from './ui/button.vue'; diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue index 072a5ac19f..df1a525055 100644 --- a/src/client/components/signup-dialog.vue +++ b/src/client/components/signup-dialog.vue @@ -7,8 +7,10 @@ > <template #header>{{ $ts.signup }}</template> - <div class="_section"> - <XSignup :auto-set="autoSet" @signup="onSignup"/> + <div class="_monolithic_"> + <div class="_section"> + <XSignup :auto-set="autoSet" @signup="onSignup"/> + </div> </div> </XModalWindow> </template> diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue index 1ce9fc2789..66d01213bc 100644 --- a/src/client/components/signup.vue +++ b/src/client/components/signup.vue @@ -55,7 +55,7 @@ import { defineComponent, defineAsyncComponent } from 'vue'; import { faLock, faExclamationTriangle, faSpinner, faCheck, faKey } from '@fortawesome/free-solid-svg-icons'; const getPasswordStrength = require('syuilo-password-strength'); -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import { host, url } from '@client/config'; import MkButton from './ui/button.vue'; import MkInput from './ui/input.vue'; diff --git a/src/client/components/taskmanager.api-window.vue b/src/client/components/taskmanager.api-window.vue index 9b6c3f16d0..c9b2c43413 100644 --- a/src/client/components/taskmanager.api-window.vue +++ b/src/client/components/taskmanager.api-window.vue @@ -9,7 +9,7 @@ <template #header>Req Viewer</template> <div class="rlkneywz"> - <MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);"> + <MkTab v-model:value="tab" style="border-bottom: solid 0.5px var(--divider);"> <option value="req">Request</option> <option value="res">Response</option> </MkTab> diff --git a/src/client/components/taskmanager.vue b/src/client/components/taskmanager.vue index af9033178e..1339e2e352 100644 --- a/src/client/components/taskmanager.vue +++ b/src/client/components/taskmanager.vue @@ -4,7 +4,7 @@ <Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager </template> <div class="qljqmnzj _monospace"> - <MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);"> + <MkTab v-model:value="tab" style="border-bottom: solid 0.5px var(--divider);"> <option value="windows">Windows</option> <option value="stream">Stream</option> <option value="streamPool">Stream (Pool)</option> @@ -215,7 +215,7 @@ export default defineComponent({ width: 100%; padding: 8px 16px; box-sizing: border-box; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); font-size: 0.9em; > div { diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue index c2ab0b005d..faa3984638 100644 --- a/src/client/components/timeline.vue +++ b/src/client/components/timeline.vue @@ -56,6 +56,7 @@ export default defineComponent({ includeLocalRenotes: this.$store.state.showLocalRenotes }, query: {}, + date: null }; }, @@ -157,7 +158,7 @@ export default defineComponent({ endpoint: endpoint, limit: 10, params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), + untilDate: this.date?.getTime(), ...this.baseQuery, ...this.query }) }; @@ -171,6 +172,11 @@ export default defineComponent({ methods: { focus() { this.$refs.tl.focus(); + }, + + timetravel(date?: Date) { + this.date = date; + this.$refs.tl.reload(); } } }); diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue index c3353cca89..427421af7d 100644 --- a/src/client/components/ui/container.vue +++ b/src/client/components/ui/container.vue @@ -1,5 +1,5 @@ <template> -<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }"> +<div class="ukygtjoj _block" :class="{ naked, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }"> <header v-if="showHeader" ref="header"> <div class="title"><slot name="header"></slot></div> <div class="sub"> @@ -116,7 +116,7 @@ export default defineComponent({ .ukygtjoj { position: relative; - overflow: hidden; + overflow: clip; &.naked { background: transparent !important; @@ -133,10 +133,12 @@ export default defineComponent({ } > header { - position: relative; + position: sticky; + top: var(--stickyTop, 0px); + left: 0; color: var(--panelHeaderFg); background: var(--panelHeaderBg); - box-shadow: 0 1px 0 0 var(--panelHeaderDivider); + border-bottom: solid 0.5px var(--panelHeaderDivider); z-index: 2; line-height: 1.4em; @@ -172,7 +174,7 @@ export default defineComponent({ padding: 24px; & + ._content { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } } } diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue index 1cd67c2521..aee3c0ccaa 100644 --- a/src/client/components/ui/folder.vue +++ b/src/client/components/ui/folder.vue @@ -98,11 +98,12 @@ export default defineComponent({ > header { display: flex; position: relative; - z-index: 2; - // TODO - // position: sticky; - // top: var(--stickyTopOffset); - // backdrop-filter: blur(20px); + z-index: 10; + position: sticky; + top: var(--stickyTop, 0px); + background: var(--X17); + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(20px); > .title { margin: 0; @@ -137,4 +138,10 @@ export default defineComponent({ } } } + +._flat_ .ssazuxis { + > header { + padding: 0 16px; + } +} </style> diff --git a/src/client/components/ui/info.vue b/src/client/components/ui/info.vue index 12b3dbcfd1..ad1b9ebb58 100644 --- a/src/client/components/ui/info.vue +++ b/src/client/components/ui/info.vue @@ -53,4 +53,8 @@ export default defineComponent({ margin-right: 4px; } } + +._flat_ .fpezltsf { + border-radius: 0; +} </style> diff --git a/src/client/components/ui/modal-window.vue b/src/client/components/ui/modal-window.vue index 2cdf961379..90b803801d 100644 --- a/src/client/components/ui/modal-window.vue +++ b/src/client/components/ui/modal-window.vue @@ -1,6 +1,6 @@ <template> <MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> - <div class="ebkgoccj _popup _narrow_" @keydown="onKeydown" :style="{ width: `${width}px`, height: height ? `${height}px` : null }"> + <div class="ebkgoccj _popup _narrow_" @keydown="onKeydown" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }"> <div class="header"> <button class="_button" v-if="withOkButton" @click="$emit('close')"><Fa :icon="faTimes"/></button> <span class="title"> @@ -61,6 +61,11 @@ export default defineComponent({ required: false, default: true, }, + scroll: { + type: Boolean, + required: false, + default: true, + }, }, emits: ['click', 'close', 'closed', 'ok'], @@ -94,10 +99,10 @@ export default defineComponent({ flex-direction: column; contain: content; - --section-padding: 24px; + --root-margin: 24px; @media (max-width: 500px) { - --section-padding: 16px; + --root-margin: 16px; } > .header { diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue index db6564bacc..3b11213426 100644 --- a/src/client/components/ui/modal.vue +++ b/src/client/components/ui/modal.vue @@ -1,5 +1,5 @@ <template> -<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> +<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? popup ? 500 : 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> <div v-show="manualShowing != null ? manualShowing : showing" class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div class="bg _modalBg" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content"> @@ -183,9 +183,6 @@ export default defineComponent({ <style lang="scss" scoped> .modal-enter-active, .modal-leave-active { - // CSS的には無意味だけどこれが無いとVueが認識しない - transition: opacity 0.3s, transform 0.3s !important; - > .bg { transition: opacity 0.3s !important; } @@ -207,9 +204,6 @@ export default defineComponent({ } .modal-popup-enter-active, .modal-popup-leave-active { - // CSS的には無意味だけどこれが無いとVueが認識しない - transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1) !important; - > .bg { transition: opacity 0.3s !important; } diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue index 1613644c87..70676cdaf5 100644 --- a/src/client/components/ui/window.vue +++ b/src/client/components/ui/window.vue @@ -395,7 +395,7 @@ export default defineComponent({ position: fixed; top: 0; left: 0; - z-index: 5000; + z-index: 10000; // mk-modalのと同じでなければならない &.front { z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない diff --git a/src/client/components/user-info.vue b/src/client/components/user-info.vue index 34ea38c3b4..ac2f9a75a6 100644 --- a/src/client/components/user-info.vue +++ b/src/client/components/user-info.vue @@ -104,7 +104,7 @@ export default defineComponent({ > .description { padding: 16px; font-size: 0.8em; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); > .mfm { display: -webkit-box; @@ -116,7 +116,7 @@ export default defineComponent({ > .status { padding: 10px 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); > div { display: inline-block; diff --git a/src/client/components/user-list.vue b/src/client/components/user-list.vue index 418c4127d9..38d9df63bb 100644 --- a/src/client/components/user-list.vue +++ b/src/client/components/user-list.vue @@ -1,7 +1,7 @@ <template> <MkError v-if="error" @retry="init()"/> -<div v-else class="efvhhmdq"> +<div v-else class="efvhhmdq _isolated"> <div class="no-users" v-if="empty"> <p>{{ $ts.noUsers }}</p> </div> diff --git a/src/client/components/user-select-dialog.vue b/src/client/components/user-select-dialog.vue index e21deea178..05a43402a8 100644 --- a/src/client/components/user-select-dialog.vue +++ b/src/client/components/user-select-dialog.vue @@ -153,7 +153,7 @@ export default defineComponent({ > .user { display: flex; align-items: center; - padding: 8px var(--section-padding); + padding: 8px var(--root-margin); font-size: 14px; &:hover { diff --git a/src/client/components/visibility-picker.vue b/src/client/components/visibility-picker.vue index 0b98d30b9d..caa2b116a6 100644 --- a/src/client/components/visibility-picker.vue +++ b/src/client/components/visibility-picker.vue @@ -97,7 +97,7 @@ export default defineComponent({ > .divider { margin: 8px 0; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } > button { diff --git a/src/client/directives/index.ts b/src/client/directives/index.ts index 7b9d31a601..6513350e0d 100644 --- a/src/client/directives/index.ts +++ b/src/client/directives/index.ts @@ -7,6 +7,7 @@ import tooltip from './tooltip'; import hotkey from './hotkey'; import appear from './appear'; import anim from './anim'; +import stickyContainer from './sticky-container'; export default function(app: App) { app.directive('userPreview', userPreview); @@ -17,4 +18,5 @@ export default function(app: App) { app.directive('hotkey', hotkey); app.directive('appear', appear); app.directive('anim', anim); + app.directive('sticky-container', stickyContainer); } diff --git a/src/client/directives/sticky-container.ts b/src/client/directives/sticky-container.ts new file mode 100644 index 0000000000..60b442eba4 --- /dev/null +++ b/src/client/directives/sticky-container.ts @@ -0,0 +1,15 @@ +import { Directive } from 'vue'; + +export default { + mounted(src, binding, vn) { + //const query = binding.value; + + const header = src.children[0]; + const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px'; + src.style.setProperty('--stickyTop', `${parseInt(currentStickyTop) + header.offsetHeight}px`); + header.style.setProperty('--stickyTop', currentStickyTop); + header.style.position = 'sticky'; + header.style.top = 'var(--stickyTop)'; + header.style.zIndex = '1'; + }, +} as Directive; diff --git a/src/client/init.ts b/src/client/init.ts index bacc839c29..daedce4aee 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -6,7 +6,7 @@ import '@client/style.scss'; import * as Sentry from '@sentry/browser'; import { Integrations } from '@sentry/tracing'; -import { createApp, watch } from 'vue'; +import { computed, createApp, watch } from 'vue'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import widgets from '@client/widgets'; @@ -25,7 +25,6 @@ import { fetchInstance, instance } from '@client/instance'; import { makeHotkey } from '@client/scripts/hotkey'; import { search } from '@client/scripts/search'; import { isMobile } from '@client/scripts/is-mobile'; -import { getThemes } from '@client/theme-store'; import { initializeSw } from '@client/scripts/initialize-sw'; import { reloadChannel } from '@client/scripts/unison-reload'; import { reactionPicker } from '@client/scripts/reaction-picker'; @@ -36,6 +35,13 @@ console.info(`Misskey v${version}`); window.onerror = null; window.onunhandledrejection = null; +// 後方互換性のため。 +// TODO: そのうち消す +if ((typeof ColdDeviceStorage.get('lightTheme') === 'string') || (typeof ColdDeviceStorage.get('darkTheme') === 'string')) { + ColdDeviceStorage.set('lightTheme', require('@client/themes/l-light.json5')); + ColdDeviceStorage.set('darkTheme', require('@client/themes/d-dark.json5')); +} + if (_DEV_) { console.warn('Development mode!!!'); @@ -161,6 +167,7 @@ const app = createApp(await ( ui === 'deck' ? import('@client/ui/deck.vue') : ui === 'desktop' ? import('@client/ui/desktop.vue') : ui === 'chat' ? import('@client/ui/chat/index.vue') : + ui === 'pope' ? import('@client/ui/universal.vue') : import('@client/ui/default.vue') ).then(x => x.default)); @@ -204,12 +211,24 @@ if (splash) { } watch(defaultStore.reactiveState.darkMode, (darkMode) => { - import('@client/scripts/theme').then(({ builtinThemes }) => { - const themes = builtinThemes.concat(getThemes()); - applyTheme(themes.find(x => x.id === (darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')))); - }); + applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); }, { immediate: localStorage.theme == null }); +const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); +const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); + +watch(darkTheme, (theme) => { + if (defaultStore.state.darkMode) { + applyTheme(theme); + } +}); + +watch(lightTheme, (theme) => { + if (!defaultStore.state.darkMode) { + applyTheme(theme); + } +}); + //#region Sync dark mode if (ColdDeviceStorage.get('syncDeviceDarkMode')) { defaultStore.set('darkMode', isDeviceDarkmode()); diff --git a/src/client/os.ts b/src/client/os.ts index e1707531de..b159cf509d 100644 --- a/src/client/os.ts +++ b/src/client/os.ts @@ -203,6 +203,15 @@ export function pageWindow(path: string) { }, {}, 'closed'); } +export function modalPageWindow(path: string) { + const { component, props } = resolve(path); + popup(import('@client/components/modal-page-window.vue'), { + initialPath: path, + initialComponent: markRaw(component), + initialProps: props, + }, {}, 'closed'); +} + export function dialog(props: Record<string, any>) { return new Promise((resolve, reject) => { popup(import('@client/components/dialog.vue'), props, { diff --git a/src/client/pages/_error_.vue b/src/client/pages/_error_.vue index b317915589..67c1a1991c 100644 --- a/src/client/pages/_error_.vue +++ b/src/client/pages/_error_.vue @@ -14,6 +14,7 @@ import { defineComponent } from 'vue'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import MkButton from '@client/components/ui/button.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -21,7 +22,7 @@ export default defineComponent({ }, data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.error, icon: faExclamationTriangle }, diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue index 2d1da88fa1..aae8a7052a 100644 --- a/src/client/pages/about-misskey.vue +++ b/src/client/pages/about-misskey.vue @@ -1,5 +1,5 @@ <template> -<div style="overflow: hidden;"> +<div style="overflow: clip;"> <FormBase class="znqjceqz"> <div id="debug"></div> <section class="_formItem about"> @@ -14,7 +14,7 @@ {{ $ts._aboutMisskey.about }} </section> <FormGroup> - <FormLink to="https://github.com/syuilo/misskey" external> + <FormLink to="https://github.com/misskey-dev/misskey" external> <template #icon><Fa :icon="faCode"/></template> {{ $ts._aboutMisskey.source }} <template #suffix>GitHub</template> @@ -40,7 +40,8 @@ <FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink> <FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink> <FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> - <template #caption><MkLink url="https://github.com/syuilo/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template> + <FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> + <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template> </FormGroup> <FormGroup> <template #label><Mfm text="[jelly ❤]"/> {{ $ts._aboutMisskey.patrons }}</template> @@ -63,6 +64,7 @@ import FormKeyValueView from '@client/components/form/key-value-view.vue'; import MkLink from '@client/components/link.vue'; import { physics } from '@client/scripts/physics.ts'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; const patrons = [ 'Satsuki Yanagi', @@ -114,7 +116,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.aboutMisskey, icon: null }, diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue index 911fc0bc62..4084256cf4 100644 --- a/src/client/pages/about.vue +++ b/src/client/pages/about.vue @@ -48,6 +48,7 @@ import FormGroup from '@client/components/form/group.vue'; import FormKeyValueView from '@client/components/form/key-value-view.vue'; import * as os from '@client/os'; import number from '@client/filters/number'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -59,7 +60,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.instanceInfo, icon: faInfoCircle }, diff --git a/src/client/pages/advanced-theme-editor.vue b/src/client/pages/advanced-theme-editor.vue index fb00c8c8ac..fff525a32d 100644 --- a/src/client/pages/advanced-theme-editor.vue +++ b/src/client/pages/advanced-theme-editor.vue @@ -12,7 +12,7 @@ </div> </section> <section class="_section"> - <div class="_content _card _vMargin"> + <div class="_content _card _gap"> <div class="_content"> <MkInput v-model:value="name" required><span>{{ $ts.name }}</span></MkInput> <MkInput v-model:value="author" required><span>{{ $ts.author }}</span></MkInput> @@ -24,7 +24,7 @@ </div> </div> </div> - <div class="_content _card _vMargin"> + <div class="_content _card _gap"> <div class="list-view _content"> <div class="item" v-for="([ k, v ], i) in theme" :key="k"> <div class="_inputs"> @@ -94,7 +94,7 @@ import { defineComponent } from 'vue'; import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons'; import * as JSON5 from 'json5'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import MkRadio from '@client/components/ui/radio.vue'; import MkButton from '@client/components/ui/button.vue'; @@ -109,6 +109,7 @@ import { host } from '@client/config'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; import { addTheme } from '@client/theme-store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -122,7 +123,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.themeEditor, icon: faPalette, }, diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue index 7c8ab28d8e..4e5f0e7f9c 100644 --- a/src/client/pages/announcements.vue +++ b/src/client/pages/announcements.vue @@ -1,7 +1,7 @@ <template> <div class="_section"> <MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content"> - <section class="_card announcement _vMargin" v-for="(announcement, i) in items" :key="announcement.id"> + <section class="_card announcement _gap" v-for="(announcement, i) in items" :key="announcement.id"> <div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> <div class="_content"> <Mfm :text="announcement.text"/> @@ -21,6 +21,7 @@ import { faCheck, faBroadcastTower } from '@fortawesome/free-solid-svg-icons'; import MkPagination from '@client/components/ui/pagination.vue'; import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -30,7 +31,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.announcements, icon: faBroadcastTower }, diff --git a/src/client/pages/api-console.vue b/src/client/pages/api-console.vue index f5b4dceb26..669e814778 100644 --- a/src/client/pages/api-console.vue +++ b/src/client/pages/api-console.vue @@ -1,25 +1,25 @@ <template> -<div> -<section class="_section"> - <MkInput v-model:value="endpoint" :datalist="endpoints" @update:value="onEndpointChange()"> - <span>Endpoint</span> - </MkInput> - <MkTextarea v-model:value="body" code> - <span>Params (JSON or JSON5)</span> - </MkTextarea> - <MkSwitch v-model:value="withCredential"> - With credential - </MkSwitch> - <MkButton primary full @click="send" :disabled="sending"> - <template v-if="sending"><MkEllipsis/></template> - <template v-else><Fa :icon="faPaperPlane"/> Send</template> - </MkButton> -</section> -<section class="_section" v-if="res"> - <MkTextarea v-model:value="res" code readonly tall> - <span>Response</span> - </MkTextarea> -</section> +<div class="_root"> + <div class="_block" style="padding: 24px;"> + <MkInput v-model:value="endpoint" :datalist="endpoints" @update:value="onEndpointChange()"> + <span>Endpoint</span> + </MkInput> + <MkTextarea v-model:value="body" code> + <span>Params (JSON or JSON5)</span> + </MkTextarea> + <MkSwitch v-model:value="withCredential"> + With credential + </MkSwitch> + <MkButton primary full @click="send" :disabled="sending"> + <template v-if="sending"><MkEllipsis/></template> + <template v-else><Fa :icon="faPaperPlane"/> Send</template> + </MkButton> + </div> + <div v-if="res" class="_block" style="padding: 24px;"> + <MkTextarea v-model:value="res" code readonly tall> + <span>Response</span> + </MkTextarea> + </div> </div> </template> @@ -32,6 +32,7 @@ import MkInput from '@client/components/ui/input.vue'; import MkTextarea from '@client/components/ui/textarea.vue'; import MkSwitch from '@client/components/ui/switch.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -40,7 +41,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: 'API console', icon: faTerminal }, diff --git a/src/client/pages/channel-editor.vue b/src/client/pages/channel-editor.vue index f5b8a66a6f..7216aaec4a 100644 --- a/src/client/pages/channel-editor.vue +++ b/src/client/pages/channel-editor.vue @@ -30,6 +30,7 @@ import MkButton from '@client/components/ui/button.vue'; import MkInput from '@client/components/ui/input.vue'; import { selectFile } from '@client/scripts/select-file'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -45,7 +46,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.channelId ? { + [symbols.PAGE_INFO]: computed(() => this.channelId ? { title: this.$ts._channel.edit, icon: faSatelliteDish, } : { diff --git a/src/client/pages/channel.vue b/src/client/pages/channel.vue index d2ddebb4a8..0d0184a517 100644 --- a/src/client/pages/channel.vue +++ b/src/client/pages/channel.vue @@ -1,6 +1,6 @@ <template> <div v-if="channel" class="_section"> - <div class="wpgynlbz _content _panel _vMargin" :class="{ hide: !showBanner }"> + <div class="wpgynlbz _content _panel _gap" :class="{ hide: !showBanner }"> <XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> <button class="_button toggle" @click="() => showBanner = !showBanner"> <template v-if="showBanner"><Fa :icon="faAngleUp"/></template> @@ -20,9 +20,9 @@ </div> </div> - <XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed v-if="$i"/> + <XPostForm :channel="channel" class="post-form _content _panel _gap" fixed v-if="$i"/> - <XTimeline class="_content _vMargin _noGap_" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/> + <XTimeline class="_content _gap _noGap_" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/> </div> </template> @@ -35,6 +35,7 @@ import XPostForm from '@client/components/post-form.vue'; import XTimeline from '@client/components/timeline.vue'; import XChannelFollowButton from '@client/components/channel-follow-button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -53,7 +54,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.channel ? { + [symbols.PAGE_INFO]: computed(() => this.channel ? { title: this.channel.name, icon: faSatelliteDish, } : null), diff --git a/src/client/pages/channels.vue b/src/client/pages/channels.vue index d75c4e5ba6..ebf1e7b871 100644 --- a/src/client/pages/channels.vue +++ b/src/client/pages/channels.vue @@ -11,20 +11,20 @@ <div class="_section"> <div class="_content grwlizim featured" v-if="tab === 'featured'"> <MkPagination :pagination="featuredPagination" #default="{items}"> - <MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/> + <MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/> </MkPagination> </div> <div class="_content grwlizim following" v-if="tab === 'following'"> <MkPagination :pagination="followingPagination" #default="{items}"> - <MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/> + <MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/> </MkPagination> </div> <div class="_content grwlizim owned" v-if="tab === 'owned'"> <MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton> <MkPagination :pagination="ownedPagination" #default="{items}"> - <MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/> + <MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/> </MkPagination> </div> </div> @@ -39,6 +39,7 @@ import MkChannelPreview from '@client/components/channel-preview.vue'; import MkPagination from '@client/components/ui/pagination.vue'; import MkButton from '@client/components/ui/button.vue'; import MkTab from '@client/components/tab.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -46,7 +47,7 @@ export default defineComponent({ }, data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.channel, icon: faSatelliteDish, action: { diff --git a/src/client/pages/clip.vue b/src/client/pages/clip.vue index c6e46a9834..493a34e7f0 100644 --- a/src/client/pages/clip.vue +++ b/src/client/pages/clip.vue @@ -1,6 +1,6 @@ <template> <div v-if="clip" class="_section"> - <div class="okzinsic _content _panel _vMargin"> + <div class="okzinsic _content _panel _gap"> <div class="description" v-if="clip.description"> <Mfm :text="clip.description" :is-note="false" :i="$i"/> </div> @@ -9,7 +9,7 @@ </div> </div> - <XNotes class="_content _vMargin" :pagination="pagination" :detail="true"/> + <XNotes class="_content _gap" :pagination="pagination" :detail="true"/> </div> </template> @@ -20,6 +20,7 @@ import MkContainer from '@client/components/ui/container.vue'; import XPostForm from '@client/components/post-form.vue'; import XNotes from '@client/components/notes.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -37,7 +38,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.clip ? { + [symbols.PAGE_INFO]: computed(() => this.clip ? { title: this.clip.name, icon: faPaperclip, action: { @@ -142,7 +143,7 @@ export default defineComponent({ > .user { $height: 32px; padding: 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); line-height: $height; > .avatar { diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue index 7155cc1563..cf3628dafb 100644 --- a/src/client/pages/doc.vue +++ b/src/client/pages/doc.vue @@ -3,7 +3,7 @@ <div class="title">{{ title }}</div> <div class="body" v-html="body"></div> <div class="footer"> - <MkLink :url="`https://github.com/syuilo/misskey/blob/master/src/docs/${lang}/${doc}.md`" class="at">{{ $ts.docSource }}</MkLink> + <MkLink :url="`https://github.com/misskey-dev/misskey/blob/master/src/docs/${lang}/${doc}.md`" class="at">{{ $ts.docSource }}</MkLink> </div> </div> </template> @@ -15,6 +15,7 @@ import MarkdownIt from 'markdown-it'; import MarkdownItAnchor from 'markdown-it-anchor'; import { url, lang } from '@client/config'; import MkLink from '@client/components/link.vue'; +import * as symbols from '@client/symbols'; const markdown = MarkdownIt({ html: true @@ -38,7 +39,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.title ? { + [symbols.PAGE_INFO]: computed(() => this.title ? { title: this.title, icon: faQuestionCircle, } : null), @@ -150,7 +151,7 @@ export default defineComponent({ font-size: 1.25em; padding: 0 0 0.5em 0; margin: 1.5em 0 1em 0; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); } ::v-deep(table) { @@ -170,7 +171,7 @@ export default defineComponent({ ::v-deep(kbd.key) { display: inline-block; padding: 6px 8px; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: 4px; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } diff --git a/src/client/pages/docs.vue b/src/client/pages/docs.vue index f57be3d323..92eab86716 100644 --- a/src/client/pages/docs.vue +++ b/src/client/pages/docs.vue @@ -16,11 +16,12 @@ import { defineComponent } from 'vue'; import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons' import { url, lang } from '@client/config'; +import * as symbols from '@client/symbols'; export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.help, icon: faQuestionCircle }, diff --git a/src/client/pages/drive.vue b/src/client/pages/drive.vue index 754581a8b6..33bbfbc50f 100644 --- a/src/client/pages/drive.vue +++ b/src/client/pages/drive.vue @@ -6,9 +6,10 @@ <script lang="ts"> import { computed, defineComponent } from 'vue'; -import { faCloud, faEllipsisH } from '@fortawesome/free-solid-svg-icons'; +import { faCloud } from '@fortawesome/free-solid-svg-icons'; import XDrive from '@client/components/drive.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -17,22 +18,13 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: computed(() => this.folder ? this.folder.name : this.$ts.drive), icon: faCloud, - action: { - icon: faEllipsisH, - handler: this.menu - } + menu: () => this.$refs.drive.getMenu() }, folder: null, }; }, - - methods: { - menu(ev) { - os.modalMenu(this.$refs.drive.getMenu(), ev.currentTarget || ev.target); - } - } }); </script> diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue index da2eaffb84..7e0acaddf7 100644 --- a/src/client/pages/explore.vue +++ b/src/client/pages/explore.vue @@ -1,40 +1,42 @@ <template> -<div class="lznhrdub"> - <div class="_section"> - <MkInput v-model:value="query" :debounce="true" type="search"><template #icon><Fa :icon="faSearch"/></template><span>{{ $ts.searchUser }}</span></MkInput> +<div class="lznhrdub _root"> + <div> + <div class="_isolated"> + <MkInput v-model:value="query" :debounce="true" type="search"><template #icon><Fa :icon="faSearch"/></template><span>{{ $ts.searchUser }}</span></MkInput> + </div> - <XUserList v-if="query" class="_vMargin" :pagination="searchPagination" ref="search"/> + <XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/> - <div class="localfedi7 _panel _vMargin" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> + <div class="localfedi7 _block _isolated" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> <header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header> <div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div> </div> <template v-if="tag == null"> - <MkFolder class="_vMargin" persist-key="explore-pinned-users"> + <MkFolder class="_gap" persist-key="explore-pinned-users"> <template #header><Fa :icon="faBookmark" fixed-width style="margin-right: 0.5em;"/>{{ $ts.pinnedUsers }}</template> <XUserList :pagination="pinnedUsers"/> </MkFolder> - <MkFolder class="_vMargin" persist-key="explore-popular-users"> + <MkFolder class="_gap" persist-key="explore-popular-users"> <template #header><Fa :icon="faChartLine" fixed-width style="margin-right: 0.5em;"/>{{ $ts.popularUsers }}</template> <XUserList :pagination="popularUsers"/> </MkFolder> - <MkFolder class="_vMargin" persist-key="explore-recently-updated-users"> + <MkFolder class="_gap" persist-key="explore-recently-updated-users"> <template #header><Fa :icon="faCommentAlt" fixed-width style="margin-right: 0.5em;"/>{{ $ts.recentlyUpdatedUsers }}</template> <XUserList :pagination="recentlyUpdatedUsers"/> </MkFolder> - <MkFolder class="_vMargin" persist-key="explore-recently-registered-users"> + <MkFolder class="_gap" persist-key="explore-recently-registered-users"> <template #header><Fa :icon="faPlus" fixed-width style="margin-right: 0.5em;"/>{{ $ts.recentlyRegisteredUsers }}</template> <XUserList :pagination="recentlyRegisteredUsers"/> </MkFolder> </template> </div> - <div class="_section"> - <div class="localfedi7 _panel _vMargin" v-if="tag == null" :style="{ backgroundImage: `url(/static-assets/client/fedi.jpg)` }"> + <div> + <div class="localfedi7 _block _isolated" v-if="tag == null" :style="{ backgroundImage: `url(/static-assets/client/fedi.jpg)` }"> <header><span>{{ $ts.exploreFediverse }}</span></header> </div> - <MkFolder :body-togglable="true" :expanded="false" ref="tags" class="_vMargin"> + <MkFolder :body-togglable="true" :expanded="false" ref="tags" class="_gap"> <template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ $ts.popularTags }}</template> <div class="vxjfqztj"> @@ -43,21 +45,21 @@ </div> </MkFolder> - <MkFolder v-if="tag != null" :key="`${tag}`" class="_vMargin"> + <MkFolder v-if="tag != null" :key="`${tag}`" class="_gap"> <template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ tag }}</template> <XUserList :pagination="tagUsers"/> </MkFolder> <template v-if="tag == null"> - <MkFolder class="_vMargin"> + <MkFolder class="_gap"> <template #header><Fa :icon="faChartLine" fixed-width style="margin-right: 0.5em;"/>{{ $ts.popularUsers }}</template> <XUserList :pagination="popularUsersF"/> </MkFolder> - <MkFolder class="_vMargin"> + <MkFolder class="_gap"> <template #header><Fa :icon="faCommentAlt" fixed-width style="margin-right: 0.5em;"/>{{ $ts.recentlyUpdatedUsers }}</template> <XUserList :pagination="recentlyUpdatedUsersF"/> </MkFolder> - <MkFolder class="_vMargin"> + <MkFolder class="_gap"> <template #header><Fa :icon="faRocket" fixed-width style="margin-right: 0.5em;"/>{{ $ts.recentlyDiscoveredUsers }}</template> <XUserList :pagination="recentlyRegisteredUsersF"/> </MkFolder> @@ -75,6 +77,7 @@ import MkFolder from '@client/components/ui/folder.vue'; import MkInput from '@client/components/ui/input.vue'; import number from '@client/filters/number'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -92,7 +95,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.explore, icon: faHashtag }, diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue index c87855484d..7ecd327137 100644 --- a/src/client/pages/favorites.vue +++ b/src/client/pages/favorites.vue @@ -10,6 +10,7 @@ import { faStar } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import XNotes from '@client/components/notes.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -18,7 +19,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.favorites, icon: faStar }, diff --git a/src/client/pages/featured.vue b/src/client/pages/featured.vue index 3866c4b489..cd7343f583 100644 --- a/src/client/pages/featured.vue +++ b/src/client/pages/featured.vue @@ -9,6 +9,7 @@ import { defineComponent } from 'vue'; import { faFireAlt } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import XNotes from '@client/components/notes.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -17,7 +18,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.featured, icon: faFireAlt }, diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue index 64e6fa03e6..309c5b4fdf 100644 --- a/src/client/pages/follow-requests.vue +++ b/src/client/pages/follow-requests.vue @@ -35,6 +35,7 @@ import { faUserClock, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons import MkPagination from '@client/components/ui/pagination.vue'; import { userPage, acct } from '../filters/user'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -43,7 +44,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.followRequests, icon: faUserClock, }, diff --git a/src/client/pages/instance/abuses.vue b/src/client/pages/instance/abuses.vue index d7d7697ebd..736f05cc72 100644 --- a/src/client/pages/instance/abuses.vue +++ b/src/client/pages/instance/abuses.vue @@ -34,7 +34,7 @@ --> <MkPagination :pagination="pagination" #default="{items}" ref="reports" style="margin-top: var(--margin);"> - <div class="bcekxzvu _card _vMargin" v-for="report in items" :key="report.id"> + <div class="bcekxzvu _card _gap" v-for="report in items" :key="report.id"> <div class="_content target"> <MkAvatar class="avatar" :user="report.targetUser"/> <div class="info"> @@ -72,6 +72,7 @@ import MkSelect from '@client/components/ui/select.vue'; import MkPagination from '@client/components/ui/pagination.vue'; import { acct } from '../../filters/user'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -83,7 +84,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.abuseReports, icon: faExclamationCircle }, diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue index 15e5f389bc..f9d58a29c4 100644 --- a/src/client/pages/instance/announcements.vue +++ b/src/client/pages/instance/announcements.vue @@ -3,7 +3,7 @@ <div class="_section"> <div class="_content"> <MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton> - <section class="_card _vMargin announcements" v-for="announcement in announcements"> + <section class="_card _gap announcements" v-for="announcement in announcements"> <div class="_content announcement"> <MkInput v-model:value="announcement.title"> <span>{{ $ts.title }}</span> @@ -34,6 +34,7 @@ import MkButton from '@client/components/ui/button.vue'; import MkInput from '@client/components/ui/input.vue'; import MkTextarea from '@client/components/ui/textarea.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -44,7 +45,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.announcements, icon: faBroadcastTower }, diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue index 01e207f491..722ed0063f 100644 --- a/src/client/pages/instance/emojis.vue +++ b/src/client/pages/instance/emojis.vue @@ -59,6 +59,7 @@ import MkPagination from '@client/components/ui/pagination.vue'; import MkTab from '@client/components/tab.vue'; import { selectFile } from '@client/scripts/select-file'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -70,7 +71,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.customEmojis, icon: faLaugh, action: { diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue index ba452c5458..2a820e5baf 100644 --- a/src/client/pages/instance/federation.vue +++ b/src/client/pages/instance/federation.vue @@ -67,6 +67,7 @@ import MkSelect from '@client/components/ui/select.vue'; import MkPagination from '@client/components/ui/pagination.vue'; import MkInstanceInfo from './instance.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -78,7 +79,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.federation, icon: faGlobe }, diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue index aa66713789..e7de050df8 100644 --- a/src/client/pages/instance/files.vue +++ b/src/client/pages/instance/files.vue @@ -35,7 +35,7 @@ </MkInput> </div> <MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files"> - <button class="file _panel _button _vMargin" v-for="file in items" :key="file.id" @click="show(file, $event)"> + <button class="file _panel _button _gap" v-for="file in items" :key="file.id" @click="show(file, $event)"> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> <div class="body"> <div> @@ -71,6 +71,7 @@ import MkPagination from '@client/components/ui/pagination.vue'; import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue'; import bytes from '@client/filters/bytes'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -83,7 +84,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.files, icon: faCloud }, diff --git a/src/client/pages/instance/index.metrics.vue b/src/client/pages/instance/index.metrics.vue index b467ce06f5..48844e0681 100644 --- a/src/client/pages/instance/index.metrics.vue +++ b/src/client/pages/instance/index.metrics.vue @@ -4,7 +4,7 @@ <template #header><Fa :icon="faHeartbeat"/> {{ $ts.metrics }}</template> <div class="_section" style="padding: 0 var(--margin);"> <div class="_content"> - <MkContainer :body-togglable="false" class="_vMargin"> + <MkContainer :body-togglable="false" class="_gap"> <template #header><Fa :icon="faMicrochip"/>{{ $ts.cpuAndMemory }}</template> <!-- <template #func> @@ -27,7 +27,7 @@ </div> </MkContainer> - <MkContainer :body-togglable="false" class="_vMargin"> + <MkContainer :body-togglable="false" class="_gap"> <template #header><Fa :icon="faHdd"/> {{ $ts.disk }}</template> <!-- <template #func> @@ -50,7 +50,7 @@ </div> </MkContainer> - <MkContainer :body-togglable="false" class="_vMargin"> + <MkContainer :body-togglable="false" class="_gap"> <template #header><Fa :icon="faExchangeAlt"/> {{ $ts.network }}</template> <!-- <template #func> diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 5a35309ed4..54a0584ccc 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -4,9 +4,9 @@ <template #header><Fa :icon="faTachometerAlt"/> {{ $ts.overview }}</template> <div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> - <MkInstanceStats :chart-limit="300" :detailed="true" class="_vMargin" ref="stats"/> + <MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/> - <MkContainer :body-togglable="true" class="_vMargin"> + <MkContainer :body-togglable="true" class="_gap"> <template #header><Fa :icon="faInfoCircle"/>{{ $ts.instanceInfo }}</template> <div class="_content"> @@ -19,7 +19,7 @@ </div> </MkContainer> - <MkContainer :body-togglable="true" :scrollable="true" class="_vMargin" style="height: 300px;"> + <MkContainer :body-togglable="true" :scrollable="true" class="_gap" style="height: 300px;"> <template #header><Fa :icon="faDatabase"/>{{ $ts.database }}</template> <div class="_content" v-if="dbInfo"> @@ -70,6 +70,7 @@ import number from '../../filters/number'; import MkInstanceInfo from './instance.vue'; import XMetrics from './index.metrics.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -85,7 +86,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { tabs: [{ id: 'index', title: null, diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue index 118e8eae6e..1adb3ab9d2 100644 --- a/src/client/pages/instance/instance.vue +++ b/src/client/pages/instance/instance.vue @@ -500,12 +500,12 @@ export default defineComponent({ } &:not(:first-child) { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } } > .chart { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); padding: 16px 0 12px 0; > .header { diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue index de1e81750f..f27546a401 100644 --- a/src/client/pages/instance/logs.vue +++ b/src/client/pages/instance/logs.vue @@ -37,6 +37,7 @@ import MkInput from '@client/components/ui/input.vue'; import MkSelect from '@client/components/ui/select.vue'; import MkTextarea from '@client/components/ui/textarea.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -48,7 +49,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.serverLogs, icon: faStream }, diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue index 17c8430a01..249babcf41 100644 --- a/src/client/pages/instance/queue.vue +++ b/src/client/pages/instance/queue.vue @@ -21,6 +21,7 @@ import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; import MkButton from '@client/components/ui/button.vue'; import XQueue from './queue.chart.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -30,7 +31,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.jobQueue, icon: faExchangeAlt, }, diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue index ef11366a14..d97a07cbb2 100644 --- a/src/client/pages/instance/relays.vue +++ b/src/client/pages/instance/relays.vue @@ -28,6 +28,7 @@ import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; import MkButton from '@client/components/ui/button.vue'; import MkInput from '@client/components/ui/input.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -37,7 +38,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.relays, icon: faProjectDiagram, }, diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index 891eba3cf0..038ecfd5ac 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -1,6 +1,6 @@ <template> <div v-if="meta" class="_section"> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faInfoCircle"/> {{ $ts.basicInfo }}</div> <div class="_content"> <MkInput v-model:value="name">{{ $ts.instanceName }}</MkInput> @@ -20,7 +20,7 @@ <MkInput v-model:value="pinnedClipId">{{ $ts.pinnedClipId }}</MkInput> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_content"> <MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()"><template #icon><Fa :icon="faPencilAlt"/></template>{{ $ts.maxNoteTextLength }}</MkInput> </div> @@ -34,7 +34,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faUser"/> {{ $ts.registration }}</div> <div class="_content"> <MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $ts.enableRegistration }}</MkSwitch> @@ -42,7 +42,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faShieldAlt"/> {{ $ts.hcaptcha }}</div> <div class="_content"> <MkSwitch v-model:value="enableHcaptcha">{{ $ts.enableHcaptcha }}</MkSwitch> @@ -60,7 +60,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faShieldAlt"/> {{ $ts.recaptcha }}</div> <div class="_content"> <MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $ts.enableRecaptcha }}</MkSwitch> @@ -78,7 +78,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faEnvelope" /> {{ $ts.emailConfig }}</div> <div class="_content"> <MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></MkSwitch> @@ -101,7 +101,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faBolt"/> {{ $ts.serviceworker }}</div> <div class="_content"> <MkSwitch v-model:value="enableServiceWorker">{{ $ts.enableServiceworker }}<template #desc>{{ $ts.serviceworkerInfo }}</template></MkSwitch> @@ -117,7 +117,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faThumbtack"/> {{ $ts.pinnedUsers }}</div> <div class="_content"> <MkTextarea v-model:value="pinnedUsers"> @@ -129,7 +129,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faThumbtack"/> {{ $ts.pinnedPages }}</div> <div class="_content"> <MkTextarea v-model:value="pinnedPages"> @@ -141,7 +141,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faCloud"/> {{ $ts.files }}</div> <div class="_content"> <MkSwitch v-model:value="cacheRemoteFiles">{{ $ts.cacheRemoteFiles }}<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template></MkSwitch> @@ -154,7 +154,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faCloud"/> {{ $ts.objectStorage }}</div> <div class="_content"> <MkSwitch v-model:value="useObjectStorage">{{ $ts.useObjectStorage }}</MkSwitch> @@ -183,7 +183,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faGhost"/> {{ $ts.proxyAccount }}</div> <div class="_content"> <MkInput :value="proxyAccount ? proxyAccount.username : null" disabled><template #prefix>@</template>{{ $ts.proxyAccount }}<template #desc>{{ $ts.proxyAccountDescription }}</template></MkInput> @@ -191,7 +191,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faBan"/> {{ $ts.blockedInstances }}</div> <div class="_content"> <MkTextarea v-model:value="blockedHosts"> @@ -203,7 +203,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faShareAlt"/> {{ $ts.integration }}</div> <div class="_content"> <header><Fa :icon="faTwitter"/> Twitter</header> @@ -237,7 +237,7 @@ </div> </section> - <section class="_card _vMargin"> + <section class="_card _gap"> <div class="_title"><Fa :icon="faArchway" /> Summaly Proxy</div> <div class="_content"> <MkInput v-model:value="summalyProxy">URL</MkInput> @@ -261,6 +261,7 @@ import { url } from '@client/config'; import getAcct from '@/misc/acct/render'; import * as os from '@client/os'; import { fetchInstance } from '@client/instance'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -274,7 +275,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.instance, icon: faCog, }, diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue index 013eed3971..e998971830 100644 --- a/src/client/pages/instance/users.vue +++ b/src/client/pages/instance/users.vue @@ -53,7 +53,7 @@ </div> <MkPagination :pagination="pagination" #default="{items}" class="users" ref="users"> - <button class="user _panel _button _vMargin" v-for="user in items" :key="user.id" @click="show(user)"> + <button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)"> <MkAvatar class="avatar" :user="user" :disable-link="true"/> <div class="body"> <header> @@ -89,6 +89,7 @@ import MkSelect from '@client/components/ui/select.vue'; import MkPagination from '@client/components/ui/pagination.vue'; import { acct } from '../../filters/user'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -100,7 +101,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.users, icon: faUsers, action: { diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue index 5bd051453a..042c3a498b 100644 --- a/src/client/pages/mentions.vue +++ b/src/client/pages/mentions.vue @@ -9,6 +9,7 @@ import { defineComponent } from 'vue'; import { faAt } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import XNotes from '@client/components/notes.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -17,7 +18,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.mentions, icon: faAt }, diff --git a/src/client/pages/messages.vue b/src/client/pages/messages.vue index 02aa9c6fd3..09d92e51ba 100644 --- a/src/client/pages/messages.vue +++ b/src/client/pages/messages.vue @@ -9,6 +9,7 @@ import { defineComponent } from 'vue'; import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import XNotes from '@client/components/notes.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -17,7 +18,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.directNotes, icon: faEnvelope }, diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue index c906b968a4..e5ad6b01a5 100644 --- a/src/client/pages/messaging/index.vue +++ b/src/client/pages/messaging/index.vue @@ -1,40 +1,38 @@ <template> -<div class="_section"> - <div class="mk-messaging _content" v-size="{ max: [400] }"> - <MkButton @click="start" primary class="start"><Fa :icon="faPlus"/> {{ $ts.startMessaging }}</MkButton> +<div class="yweeujhr _root" v-size="{ max: [400] }"> + <MkButton @click="start" primary class="start"><Fa :icon="faPlus"/> {{ $ts.startMessaging }}</MkButton> - <div class="history" v-if="messages.length > 0"> - <MkA v-for="(message, i) in messages" - class="message _panel" - :class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" - :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" - :data-index="i" - :key="message.id" - v-anim="i" - > - <div> - <MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/> - <header v-if="message.groupId"> - <span class="name">{{ message.group.name }}</span> - <MkTime :time="message.createdAt" class="time"/> - </header> - <header v-else> - <span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span> - <span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span> - <MkTime :time="message.createdAt" class="time"/> - </header> - <div class="body"> - <p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p> - </div> + <div class="history" v-if="messages.length > 0"> + <MkA v-for="(message, i) in messages" + class="message _block" + :class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" + :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" + :data-index="i" + :key="message.id" + v-anim="i" + > + <div> + <MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/> + <header v-if="message.groupId"> + <span class="name">{{ message.group.name }}</span> + <MkTime :time="message.createdAt" class="time"/> + </header> + <header v-else> + <span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span> + <span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span> + <MkTime :time="message.createdAt" class="time"/> + </header> + <div class="body"> + <p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p> </div> - </MkA> - </div> - <div class="_fullinfo" v-if="!fetching && messages.length == 0"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> - <div>{{ $ts.noHistory }}</div> - </div> - <MkLoading v-if="fetching"/> + </div> + </MkA> </div> + <div class="_fullinfo" v-if="!fetching && messages.length == 0"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ $ts.noHistory }}</div> + </div> + <MkLoading v-if="fetching"/> </div> </template> @@ -45,6 +43,7 @@ import getAcct from '@/misc/acct/render'; import MkButton from '@client/components/ui/button.vue'; import { acct } from '../../filters/user'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -53,7 +52,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.messaging, icon: faComments }, @@ -167,10 +166,10 @@ export default defineComponent({ </script> <style lang="scss" scoped> -.mk-messaging { +.yweeujhr { > .start { - margin: 0 auto var(--margin) auto; + margin: var(--margin) auto var(--margin) auto; } > .history { diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue index 5826b8d0d2..c547e18850 100644 --- a/src/client/pages/messaging/messaging-room.form.vue +++ b/src/client/pages/messaging/messaging-room.form.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-messaging-form _panel" +<div class="pemppnzi _block" @dragover.stop="onDragover" @drop.stop="onDrop" > @@ -230,7 +230,7 @@ export default defineComponent({ </script> <style lang="scss" scoped> -.mk-messaging-form { +.pemppnzi { position: relative; > textarea { diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue index a6d142bd34..3755bc2b5c 100644 --- a/src/client/pages/messaging/messaging-room.message.vue +++ b/src/client/pages/messaging/messaging-room.message.vue @@ -37,8 +37,8 @@ <script lang="ts"> import { defineComponent } from 'vue'; -import { parse } from '../../../mfm/parse'; -import { unique } from '../../../prelude/array'; +import * as mfm from 'mfm-js'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; import MkUrlPreview from '@client/components/url-preview.vue'; import * as os from '@client/os'; @@ -60,10 +60,7 @@ export default defineComponent({ }, urls(): string[] { if (this.message.text) { - const ast = parse(this.message.text); - return unique(ast - .filter(t => ((t.node.type === 'url' || t.node.type === 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); + return extractUrlFromMfm(mfm.parse(this.message.text)); } else { return []; } diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue index 8473bb36c2..dae4590213 100644 --- a/src/client/pages/messaging/messaging-room.vue +++ b/src/client/pages/messaging/messaging-room.vue @@ -47,6 +47,7 @@ import { isBottom, onScrollBottom, scroll } from '@client/scripts/scroll'; import * as os from '@client/os'; import { popout } from '@client/scripts/popout'; import * as sound from '@client/scripts/sound'; +import * as symbols from '@client/symbols'; const Component = defineComponent({ components: { @@ -70,7 +71,7 @@ const Component = defineComponent({ data() { return { - INFO: computed(() => !this.fetching ? this.user ? { + [symbols.PAGE_INFO]: computed(() => !this.fetching ? this.user ? { userName: this.user, avatar: this.user, action: { diff --git a/src/client/pages/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue index 4452604e4e..b30c3414e2 100644 --- a/src/client/pages/mfm-cheat-sheet.vue +++ b/src/client/pages/mfm-cheat-sheet.vue @@ -272,6 +272,7 @@ import { defineComponent } from 'vue'; import { faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; import MkTextarea from '@client/components/ui/textarea.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -280,7 +281,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts._mfm.cheatSheet, icon: faQuestionCircle, }, diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue index f4d5f4aad6..dfb752b831 100644 --- a/src/client/pages/my-antennas/index.vue +++ b/src/client/pages/my-antennas/index.vue @@ -18,6 +18,7 @@ import { faSatellite, faPlus } from '@fortawesome/free-solid-svg-icons'; import MkPagination from '@client/components/ui/pagination.vue'; import MkButton from '@client/components/ui/button.vue'; import XAntenna from './index.antenna.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -28,7 +29,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.manageAntennas, icon: faSatellite, action: { diff --git a/src/client/pages/my-clips/index.vue b/src/client/pages/my-clips/index.vue index b724dfb6b7..09cd7f828a 100644 --- a/src/client/pages/my-clips/index.vue +++ b/src/client/pages/my-clips/index.vue @@ -4,7 +4,7 @@ <div class="_content"> <MkPagination :pagination="pagination" #default="{items}" ref="list" class="list"> - <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin"> + <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> <b>{{ item.name }}</b> <div v-if="item.description" class="description">{{ item.description }}</div> </MkA> @@ -19,6 +19,7 @@ import { faPlus, faPaperclip } from '@fortawesome/free-solid-svg-icons'; import MkPagination from '@client/components/ui/pagination.vue'; import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -28,7 +29,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.clip, icon: faPaperclip, action: { @@ -96,7 +97,7 @@ export default defineComponent({ > .description { margin-top: 8px; padding-top: 8px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } } } diff --git a/src/client/pages/my-groups/group.vue b/src/client/pages/my-groups/group.vue index 8d0d8716c7..0631118ca3 100644 --- a/src/client/pages/my-groups/group.vue +++ b/src/client/pages/my-groups/group.vue @@ -12,7 +12,7 @@ </transition> <transition name="zoom" mode="out-in"> - <div v-if="group" class="_section members _vMargin"> + <div v-if="group" class="_section members _gap"> <div class="_title">{{ $ts.members }}</div> <div class="_content"> <div class="users"> @@ -39,6 +39,7 @@ import { faTimes, faUsers } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -54,7 +55,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.group ? { + [symbols.PAGE_INFO]: computed(() => this.group ? { title: this.group.name, icon: faUsers, } : null), diff --git a/src/client/pages/my-groups/index.vue b/src/client/pages/my-groups/index.vue index 2bc0d36b98..5125ce3f4f 100644 --- a/src/client/pages/my-groups/index.vue +++ b/src/client/pages/my-groups/index.vue @@ -54,6 +54,7 @@ import MkContainer from '@client/components/ui/container.vue'; import MkAvatars from '@client/components/avatars.vue'; import MkTab from '@client/components/tab.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -66,7 +67,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.groups, icon: faUsers }, diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue index bbb2192aa1..e680b90d1a 100644 --- a/src/client/pages/my-lists/index.vue +++ b/src/client/pages/my-lists/index.vue @@ -16,6 +16,7 @@ import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons'; import MkPagination from '@client/components/ui/pagination.vue'; import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -25,7 +26,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.manageLists, icon: faListUl, action: { diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue index dcaae222cc..21c8a696b9 100644 --- a/src/client/pages/my-lists/list.vue +++ b/src/client/pages/my-lists/list.vue @@ -11,7 +11,7 @@ </transition> <transition name="zoom" mode="out-in"> - <div v-if="list" class="_section members _vMargin"> + <div v-if="list" class="_section members _gap"> <div class="_title">{{ $ts.members }}</div> <div class="_content"> <div class="users"> @@ -38,6 +38,7 @@ import { faTimes, faListUl } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -46,7 +47,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.list ? { + [symbols.PAGE_INFO]: computed(() => this.list ? { title: this.list.name, icon: faListUl, } : null), diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue index 61ca3870c5..b13bdac2b8 100644 --- a/src/client/pages/not-found.vue +++ b/src/client/pages/not-found.vue @@ -11,11 +11,12 @@ import { defineComponent } from 'vue'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.notFound, icon: faExclamationTriangle }, diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue index eb46c49d8b..871bdd7200 100644 --- a/src/client/pages/note.vue +++ b/src/client/pages/note.vue @@ -1,19 +1,19 @@ <template> -<div class="fcuexfpr"> +<div class="fcuexfpr _root"> <div v-if="note" class="note" v-anim> - <div class="_section" v-if="showNext"> + <div class="_gap" v-if="showNext"> <XNotes class="_content _noGap_" :pagination="next"/> </div> - <div class="_section main"> - <MkButton v-if="!showNext && hasNext" class="load next _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton> - <div class="_content _vMargin"> - <MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_vMargin"/> - <XNoteDetailed v-model:note="note" :key="note.id" class="_vMargin"/> + <div class="main _gap"> + <MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton> + <div class="_content _gap"> + <MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_gap"/> + <XNoteDetailed v-model:note="note" :key="note.id" class="_gap"/> </div> - <div class="_content clips _vMargin" v-if="clips && clips.length > 0"> + <div class="_content clips _gap" v-if="clips && clips.length > 0"> <div class="title">{{ $ts.clip }}</div> - <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin"> + <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> <b>{{ item.name }}</b> <div v-if="item.description" class="description">{{ item.description }}</div> <div class="user"> @@ -21,10 +21,10 @@ </div> </MkA> </div> - <MkButton v-if="!showPrev && hasPrev" class="load prev _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton> + <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton> </div> - <div class="_section" v-if="showPrev"> + <div class="_gap" v-if="showPrev"> <XNotes class="_content _noGap_" :pagination="prev"/> </div> </div> @@ -44,6 +44,7 @@ import XNotes from '@client/components/notes.vue'; import MkRemoteCaution from '@client/components/remote-caution.vue'; import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -61,9 +62,14 @@ export default defineComponent({ }, data() { return { - INFO: computed(() => this.note ? { + [symbols.PAGE_INFO]: computed(() => this.note ? { title: this.$ts.note, avatar: this.note.user, + path: `/notes/${this.note.id}`, + share: { + title: this.$t('noteOf', { user: this.note.user.name }), + text: this.note.text, + }, } : null), note: null, clips: null, @@ -137,6 +143,7 @@ export default defineComponent({ > .main { > .load { min-width: 0; + margin: 0 auto; border-radius: 999px; &.next { @@ -165,7 +172,7 @@ export default defineComponent({ > .user { $height: 32px; padding-top: 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); line-height: $height; > .avatar { diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue index a6d73b4290..25605988ed 100644 --- a/src/client/pages/notifications.vue +++ b/src/client/pages/notifications.vue @@ -1,17 +1,16 @@ <template> -<div> - <div class="_section"> - <XNotifications class="_content" @before="before" @after="after" page/> - </div> +<div class="_root"> + <XNotifications class="_content" @before="before" @after="after" page/> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import { faBell } from '@fortawesome/free-solid-svg-icons'; +import { faBell, faCheck } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import XNotifications from '@client/components/notifications.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -20,9 +19,16 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.notifications, - icon: faBell + icon: faBell, + actions: [{ + text: this.$ts.markAllAsRead, + icon: faCheck, + handler: () => { + os.apiWithDialog('notifications/mark-all-as-read'); + } + }] }, }; }, diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue index 762ee774ed..f8f81541ff 100644 --- a/src/client/pages/page-editor/page-editor.vue +++ b/src/client/pages/page-editor/page-editor.vue @@ -1,87 +1,85 @@ <template> -<div class="_section"> - <div class="_content"> - <MkA class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $ts._pages.viewPage }}</MkA> +<div class="_root"> + <MkA class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $ts._pages.viewPage }}</MkA> - <div class="buttons" style="margin: 16px 0;"> - <MkButton inline @click="save" primary class="save" v-if="!readonly"><Fa :icon="faSave"/> {{ $ts.save }}</MkButton> - <MkButton inline @click="duplicate" class="duplicate" v-if="pageId"><Fa :icon="faCopy"/> {{ $ts.duplicate }}</MkButton> - <MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> - </div> + <div class="buttons" style="margin: 16px;"> + <MkButton inline @click="save" primary class="save" v-if="!readonly"><Fa :icon="faSave"/> {{ $ts.save }}</MkButton> + <MkButton inline @click="duplicate" class="duplicate" v-if="pageId"><Fa :icon="faCopy"/> {{ $ts.duplicate }}</MkButton> + <MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> + </div> - <MkContainer :body-togglable="true" :expanded="true" class="_vMargin"> - <template #header><Fa :icon="faCog"/> {{ $ts._pages.pageSetting }}</template> - <div class="_section"> - <MkInput v-model:value="title"> - <span>{{ $ts._pages.title }}</span> - </MkInput> + <MkContainer :body-togglable="true" :expanded="true" class="_gap"> + <template #header><Fa :icon="faCog"/> {{ $ts._pages.pageSetting }}</template> + <div style="padding: 16px;"> + <MkInput v-model:value="title"> + <span>{{ $ts._pages.title }}</span> + </MkInput> - <MkInput v-model:value="summary"> - <span>{{ $ts._pages.summary }}</span> - </MkInput> + <MkInput v-model:value="summary"> + <span>{{ $ts._pages.summary }}</span> + </MkInput> - <MkInput v-model:value="name"> - <template #prefix>{{ url }}/@{{ author.username }}/pages/</template> - <span>{{ $ts._pages.url }}</span> - </MkInput> + <MkInput v-model:value="name"> + <template #prefix>{{ url }}/@{{ author.username }}/pages/</template> + <span>{{ $ts._pages.url }}</span> + </MkInput> - <MkSwitch v-model:value="alignCenter">{{ $ts._pages.alignCenter }}</MkSwitch> + <MkSwitch v-model:value="alignCenter">{{ $ts._pages.alignCenter }}</MkSwitch> - <MkSelect v-model:value="font"> - <template #label>{{ $ts._pages.font }}</template> - <option value="serif">{{ $ts._pages.fontSerif }}</option> - <option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option> - </MkSelect> + <MkSelect v-model:value="font"> + <template #label>{{ $ts._pages.font }}</template> + <option value="serif">{{ $ts._pages.fontSerif }}</option> + <option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option> + </MkSelect> - <MkSwitch v-model:value="hideTitleWhenPinned">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch> + <MkSwitch v-model:value="hideTitleWhenPinned">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch> - <div class="eyeCatch"> - <MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><Fa :icon="faPlus"/> {{ $ts._pages.eyeCatchingImageSet }}</MkButton> - <div v-else-if="eyeCatchingImage"> - <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/> - <MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><Fa :icon="faTrashAlt"/> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton> - </div> + <div class="eyeCatch"> + <MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><Fa :icon="faPlus"/> {{ $ts._pages.eyeCatchingImageSet }}</MkButton> + <div v-else-if="eyeCatchingImage"> + <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/> + <MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><Fa :icon="faTrashAlt"/> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton> </div> </div> - </MkContainer> + </div> + </MkContainer> - <MkContainer :body-togglable="true" :expanded="true" class="_vMargin"> - <template #header><Fa :icon="faStickyNote"/> {{ $ts._pages.contents }}</template> - <div class="_section"> - <XBlocks class="content" v-model:value="content" :hpml="hpml"/> + <MkContainer :body-togglable="true" :expanded="true" class="_gap"> + <template #header><Fa :icon="faStickyNote"/> {{ $ts._pages.contents }}</template> + <div style="padding: 16px;"> + <XBlocks class="content" v-model:value="content" :hpml="hpml"/> - <MkButton @click="add()" v-if="!readonly"><Fa :icon="faPlus"/></MkButton> - </div> - </MkContainer> + <MkButton @click="add()" v-if="!readonly"><Fa :icon="faPlus"/></MkButton> + </div> + </MkContainer> - <MkContainer :body-togglable="true" class="_vMargin"> - <template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> - <div class="qmuvgica"> - <XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> - <template #item="{element}"> - <XVariable - :value="element" - :removable="true" - @remove="() => removeVariable(element)" - :hpml="hpml" - :name="element.name" - :title="element.name" - :draggable="true" - /> - </template> - </XDraggable> + <MkContainer :body-togglable="true" class="_gap"> + <template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> + <div class="qmuvgica"> + <XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> + <template #item="{element}"> + <XVariable + :value="element" + :removable="true" + @remove="() => removeVariable(element)" + :hpml="hpml" + :name="element.name" + :title="element.name" + :draggable="true" + /> + </template> + </XDraggable> - <MkButton @click="addVariable()" class="add" v-if="!readonly"><Fa :icon="faPlus"/></MkButton> - </div> - </MkContainer> + <MkButton @click="addVariable()" class="add" v-if="!readonly"><Fa :icon="faPlus"/></MkButton> + </div> + </MkContainer> - <MkContainer :body-togglable="true" :expanded="true" class="_vMargin"> - <template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> - <div> - <MkTextarea class="_code" v-model:value="script"/> - </div> - </MkContainer> - </div> + <MkContainer :body-togglable="true" :expanded="true" class="_gap"> + <template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> + <div> + <MkTextarea class="_code" v-model:value="script"/> + </div> + </MkContainer> </div> </template> @@ -110,6 +108,7 @@ import { url } from '@client/config'; import { collectPageVars } from '@client/scripts/collect-page-vars'; import * as os from '@client/os'; import { selectFile } from '@client/scripts/select-file'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -134,7 +133,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => { + [symbols.PAGE_INFO]: computed(() => { let title = this.$ts._pages.newPage; if (this.initPageId) { title = this.$ts._pages.editPage; diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue index 121383a7a6..57bbeba6b2 100644 --- a/src/client/pages/page.vue +++ b/src/client/pages/page.vue @@ -1,25 +1,25 @@ <template> -<div class="xcukqgmh" v-if="page" :key="page.id"> - <div class="_section main"> - <div class="_content"> - <div class="banner"> - <img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/> - </div> - <div> - <XPage :page="page"/> - <small style="display: block; opacity: 0.7; margin-top: 1em;">@{{ page.user.username }}</small> - </div> +<div class="xcukqgmh _root" v-if="page" :key="page.id" v-size="{ max: [450] }"> + <div class="_magnet"></div> + + <div class="_block main"> + <!-- + <div class="header"> + <h1>{{ page.title }}</h1> </div> - </div> - <div class="_section like"> - <div class="_content"> - <button class="_button" @click="unlike()" v-if="page.isLiked" :title="$ts._pages.unlike"><Fa :icon="faHeartS"/></button> - <button class="_button" @click="like()" v-else :title="$ts._pages.like"><Fa :icon="faHeartR"/></button> - <span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span> + --> + <div class="banner"> + <img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/> </div> - </div> - <div class="_section links"> - <div class="_content"> + <div class="content"> + <XPage :page="page"/> + <small style="display: block; opacity: 0.7; margin-top: 1em;">@{{ page.user.username }}</small> + </div> + <div class="like"> + <MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><Fa :icon="faHeartS"/><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton> + <MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><Fa :icon="faHeartR"/><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton> + </div> + <div class="links"> <MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA> <template v-if="$i && $i.id === page.userId"> <MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA> @@ -28,19 +28,26 @@ </template> </div> </div> + <div class="footer"> + <div><Fa :icon="faClock"/> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div> + <div v-if="page.createdAt != page.updatedAt"><Fa :icon="faClock"/> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div> + </div> </div> </template> <script lang="ts"> import { computed, defineComponent } from 'vue'; import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; -import { faHeart as faHeartR } from '@fortawesome/free-regular-svg-icons'; +import { faHeart as faHeartR, faClock } from '@fortawesome/free-regular-svg-icons'; import XPage from '@client/components/page/page.vue'; +import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { - XPage + XPage, + MkButton, }, props: { @@ -56,12 +63,17 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.page ? { + [symbols.PAGE_INFO]: computed(() => this.page ? { title: computed(() => this.page.title || this.page.name), avatar: this.page.user, + path: `/@${this.page.user.username}/pages/${this.page.name}`, + share: { + title: this.page.title || this.page.name, + text: this.page.summary, + }, } : null), page: null, - faHeartS, faHeartR + faHeartS, faHeartR, faClock, }; }, @@ -92,7 +104,7 @@ export default defineComponent({ }, like() { - os.api('pages/like', { + os.apiWithDialog('pages/like', { pageId: this.page.id, }).then(() => { this.page.isLiked = true; @@ -100,8 +112,14 @@ export default defineComponent({ }); }, - unlike() { - os.api('pages/unlike', { + async unlike() { + const confirm = await os.dialog({ + type: 'warning', + showCancelButton: true, + text: this.$ts.unlikeConfirm, + }); + if (confirm.canceled) return; + os.apiWithDialog('pages/unlike', { pageId: this.page.id, }).then(() => { this.page.isLiked = false; @@ -120,25 +138,64 @@ export default defineComponent({ <style lang="scss" scoped> .xcukqgmh { + --padding: 32px; + + &.max-width_450px { + --padding: 16px; + } + > .main { - > ._content { - > .banner { - > img { - display: block; - width: 100%; - height: 120px; - object-fit: cover; + > .header { + padding: 16px; + + > h1 { + margin: 0; + } + } + + > .banner { + > img { + display: block; + width: 100%; + height: 120px; + object-fit: cover; + } + } + + > .content { + padding: var(--padding); + } + + > .like { + padding: var(--padding); + border-top: solid 0.5px var(--divider); + + > .button { + --accent: rgb(216 71 106); + --X8: rgb(241 92 128); + --buttonBg: rgb(216 71 106 / 5%); + --buttonHoverBg: rgb(216 71 106 / 10%); + + ::v-deep(.count) { + margin-left: 0.5em; } } } - } - > .links { - > ._content { + > .links { + padding: var(--padding); + border-top: solid 0.5px var(--divider); + > .link { margin-right: 0.75em; } } } + + > .footer { + margin: var(--padding); + font-size: 85%; + opacity: 0.75; + } } </style> diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue index 9bedda04ef..8aea7e6b95 100644 --- a/src/client/pages/pages.vue +++ b/src/client/pages/pages.vue @@ -37,6 +37,7 @@ import MkPagePreview from '@client/components/page-preview.vue'; import MkPagination from '@client/components/ui/pagination.vue'; import MkButton from '@client/components/ui/button.vue'; import MkTab from '@client/components/tab.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -44,13 +45,14 @@ export default defineComponent({ }, data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.pages, icon: faStickyNote, - action: { + actions: [{ icon: faPlus, + text: this.$ts.create, handler: this.create - } + }] }, tab: 'featured', featuredPagesPagination: { diff --git a/src/client/pages/preview.vue b/src/client/pages/preview.vue index 0b0293f69a..bd4e08db62 100644 --- a/src/client/pages/preview.vue +++ b/src/client/pages/preview.vue @@ -8,6 +8,7 @@ import { defineComponent } from 'vue'; import { faEye } from '@fortawesome/free-solid-svg-icons'; import MkSample from '@client/components/sample.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -16,7 +17,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.preview, icon: faEye, }, diff --git a/src/client/pages/reversi/game.vue b/src/client/pages/reversi/game.vue index 3a88b02583..896dbc39cc 100644 --- a/src/client/pages/reversi/game.vue +++ b/src/client/pages/reversi/game.vue @@ -10,6 +10,7 @@ import GameSetting from './game.setting.vue'; import GameBoard from './game.board.vue'; import * as os from '@client/os'; import { faGamepad } from '@fortawesome/free-solid-svg-icons'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -26,7 +27,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts._reversi.reversi, icon: faGamepad }, diff --git a/src/client/pages/reversi/index.vue b/src/client/pages/reversi/index.vue index f40990b37c..d590bbeb9f 100644 --- a/src/client/pages/reversi/index.vue +++ b/src/client/pages/reversi/index.vue @@ -65,6 +65,7 @@ import * as os from '@client/os'; import MkButton from '@client/components/ui/button.vue'; import MkFolder from '@client/components/ui/folder.vue'; import { faGamepad } from '@fortawesome/free-solid-svg-icons'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -75,7 +76,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts._reversi.reversi, icon: faGamepad }, @@ -259,7 +260,7 @@ export default defineComponent({ > footer { display: flex; align-items: baseline; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); padding: 6px 8px; font-size: 0.9em; diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue index e6e2809725..e1de52b8f1 100644 --- a/src/client/pages/room/room.vue +++ b/src/client/pages/room/room.vue @@ -63,6 +63,7 @@ import MkSelect from '@client/components/ui/select.vue'; import { selectFile } from '@client/scripts/select-file'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; +import * as symbols from '@client/symbols'; let room: Room; @@ -82,7 +83,7 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.user ? { + [symbols.PAGE_INFO]: computed(() => this.user ? { title: this.$ts.room, avatar: this.user, } : null), diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue index febc9c5016..3664765d02 100644 --- a/src/client/pages/scratchpad.vue +++ b/src/client/pages/scratchpad.vue @@ -1,18 +1,18 @@ <template> <div class="iltifgqe"> - <div class="editor _panel _vMargin"> + <div class="editor _panel _gap"> <PrismEditor class="_code code" v-model="code" :highlight="highlighter" :line-numbers="false"/> <MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><Fa :icon="faPlay"/></MkButton> </div> - <MkContainer :body-togglable="true" class="_vMargin"> + <MkContainer :body-togglable="true" class="_gap"> <template #header><Fa fixed-width/>{{ $ts.output }}</template> <div class="bepmlvbi"> <div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> </div> </MkContainer> - <div class="_vMargin"> + <div class="_gap"> {{ $ts.scratchpadDescription }} </div> </div> @@ -33,6 +33,7 @@ import MkContainer from '@client/components/ui/container.vue'; import MkButton from '@client/components/ui/button.vue'; import { createAiScriptEnv } from '@client/scripts/aiscript/api'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -43,7 +44,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.scratchpad, icon: faTerminal, }, diff --git a/src/client/pages/search.vue b/src/client/pages/search.vue index 28d2aec992..b670714730 100644 --- a/src/client/pages/search.vue +++ b/src/client/pages/search.vue @@ -11,6 +11,7 @@ import { computed, defineComponent } from 'vue'; import { faSearch } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import XNotes from '@client/components/notes.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -19,7 +20,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: computed(() => this.$t('searchWith', { q: this.$route.query.q })), icon: faSearch }, diff --git a/src/client/pages/settings/2fa.vue b/src/client/pages/settings/2fa.vue index 48f1bc6e7b..361611bcb2 100644 --- a/src/client/pages/settings/2fa.vue +++ b/src/client/pages/settings/2fa.vue @@ -79,6 +79,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -90,7 +91,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.twoStepAuthentication, icon: faLock }, diff --git a/src/client/pages/settings/account-info.vue b/src/client/pages/settings/account-info.vue index 1b8baadf44..955a0f7845 100644 --- a/src/client/pages/settings/account-info.vue +++ b/src/client/pages/settings/account-info.vue @@ -143,6 +143,7 @@ import FormKeyValueView from '@client/components/form/key-value-view.vue'; import * as os from '@client/os'; import number from '@client/filters/number'; import bytes from '@client/filters/bytes'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -159,7 +160,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.accountInfo, icon: faInfoCircle }, @@ -168,7 +169,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); os.api('users/stats', { userId: this.$i.id diff --git a/src/client/pages/settings/api.vue b/src/client/pages/settings/api.vue index da9ed88b55..9b53399870 100644 --- a/src/client/pages/settings/api.vue +++ b/src/client/pages/settings/api.vue @@ -16,6 +16,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -28,7 +29,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: 'API', icon: faKey }, @@ -37,7 +38,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/apps.vue b/src/client/pages/settings/apps.vue index d0fcacfaed..82bf9b7f8f 100644 --- a/src/client/pages/settings/apps.vue +++ b/src/client/pages/settings/apps.vue @@ -47,6 +47,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -58,7 +59,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.installedApps, icon: faPlug, }, @@ -74,7 +75,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/deck.vue b/src/client/pages/settings/deck.vue index ce4099db2e..84992adc09 100644 --- a/src/client/pages/settings/deck.vue +++ b/src/client/pages/settings/deck.vue @@ -41,6 +41,7 @@ import FormGroup from '@client/components/form/group.vue'; import { deckStore } from '@client/ui/deck/deck-store'; import * as os from '@client/os'; import { unisonReload } from '@client/scripts/unison-reload'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -56,7 +57,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.deck, icon: faColumns }, @@ -87,7 +88,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue index 4684d3b554..675b025ab8 100644 --- a/src/client/pages/settings/drive.vue +++ b/src/client/pages/settings/drive.vue @@ -44,6 +44,7 @@ import FormKeyValueView from '@client/components/form/key-value-view.vue'; import FormBase from '@client/components/form/base.vue'; import * as os from '@client/os'; import bytes from '@client/filters/bytes'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -57,7 +58,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.drive, icon: faCloud }, @@ -100,7 +101,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/email-address.vue b/src/client/pages/settings/email-address.vue index 56e9973aeb..97c5d396ce 100644 --- a/src/client/pages/settings/email-address.vue +++ b/src/client/pages/settings/email-address.vue @@ -20,6 +20,7 @@ import FormInput from '@client/components/form/input.vue'; import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -33,7 +34,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.emailAddress, icon: faEnvelope }, @@ -48,7 +49,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/email-notification.vue b/src/client/pages/settings/email-notification.vue index 5894b8475c..cc28bac4b0 100644 --- a/src/client/pages/settings/email-notification.vue +++ b/src/client/pages/settings/email-notification.vue @@ -32,6 +32,8 @@ import FormSwitch from '@client/components/form/switch.vue'; import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -45,7 +47,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.emailNotification, icon: faEnvelope }, @@ -69,7 +71,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/email.vue b/src/client/pages/settings/email.vue index bd2e4634d1..04f433f9ae 100644 --- a/src/client/pages/settings/email.vue +++ b/src/client/pages/settings/email.vue @@ -30,6 +30,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormSwitch from '@client/components/form/switch.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -44,7 +45,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.email, icon: faEnvelope }, @@ -53,7 +54,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/experimental-features.vue b/src/client/pages/settings/experimental-features.vue index 1cbda219d7..25453b7e10 100644 --- a/src/client/pages/settings/experimental-features.vue +++ b/src/client/pages/settings/experimental-features.vue @@ -15,6 +15,7 @@ import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import FormKeyValueView from '@client/components/form/key-value-view.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -31,7 +32,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.experimentalFeatures, icon: faFlask }, @@ -40,7 +41,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue index 6612fc0ca2..7e80e2249f 100644 --- a/src/client/pages/settings/general.vue +++ b/src/client/pages/settings/general.vue @@ -98,6 +98,7 @@ import { defaultStore } from '@client/store'; import { ColdDeviceStorage } from '@client/store'; import * as os from '@client/os'; import { unisonReload } from '@client/scripts/unison-reload'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -115,7 +116,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.general, icon: faCogs }, @@ -191,7 +192,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue index 2f80ef0c6d..1591a9d548 100644 --- a/src/client/pages/settings/import-export.vue +++ b/src/client/pages/settings/import-export.vue @@ -34,6 +34,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import * as os from '@client/os'; import { selectFile } from '@client/scripts/select-file'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -46,7 +47,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.importAndExport, icon: faBoxes }, @@ -55,7 +56,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index 3000356713..eb7469c861 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -1,40 +1,42 @@ <template> <div class="vvcocwet" :class="{ wide: !narrow }" ref="el"> - <FormBase class="nav" v-if="!narrow || page == null" :force-wide="!narrow"> - <FormGroup> - <template #label>{{ $ts.basicSettings }}</template> - <FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><Fa :icon="faUser"/></template>{{ $ts.profile }}</FormLink> - <FormLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><Fa :icon="faLockOpen"/></template>{{ $ts.privacy }}</FormLink> - <FormLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><Fa :icon="faLaugh"/></template>{{ $ts.reaction }}</FormLink> - <FormLink :active="page === 'drive'" replace to="/settings/drive"><template #icon><Fa :icon="faCloud"/></template>{{ $ts.drive }}</FormLink> - <FormLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><Fa :icon="faBell"/></template>{{ $ts.notifications }}</FormLink> - <FormLink :active="page === 'email'" replace to="/settings/email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $ts.email }}</FormLink> - <FormLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><Fa :icon="faShareAlt"/></template>{{ $ts.integration }}</FormLink> - <FormLink :active="page === 'security'" replace to="/settings/security"><template #icon><Fa :icon="faLock"/></template>{{ $ts.security }}</FormLink> - </FormGroup> - <FormGroup> - <template #label>{{ $ts.clientSettings }}</template> - <FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><Fa :icon="faCogs"/></template>{{ $ts.general }}</FormLink> - <FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $ts.theme }}</FormLink> - <FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $ts.sidebar }}</FormLink> - <FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $ts.sounds }}</FormLink> - <FormLink :active="page === 'plugin'" replace to="/settings/plugin"><template #icon><Fa :icon="faPlug"/></template>{{ $ts.plugins }}</FormLink> - </FormGroup> - <FormGroup> - <template #label>{{ $ts.otherSettings }}</template> - <FormLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><Fa :icon="faBoxes"/></template>{{ $ts.importAndExport }}</FormLink> - <FormLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><Fa :icon="faBan"/></template>{{ $ts.muteAndBlock }}</FormLink> - <FormLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><Fa :icon="faCommentSlash"/></template>{{ $ts.wordMute }}</FormLink> - <FormLink :active="page === 'api'" replace to="/settings/api"><template #icon><Fa :icon="faKey"/></template>API</FormLink> - <FormLink :active="page === 'other'" replace to="/settings/other"><template #icon><Fa :icon="faEllipsisH"/></template>{{ $ts.other }}</FormLink> - </FormGroup> - <FormGroup> - <FormButton @click="clear">{{ $ts.clearCache }}</FormButton> - </FormGroup> - <FormGroup> - <FormButton @click="logout" danger>{{ $ts.logout }}</FormButton> - </FormGroup> - </FormBase> + <div class="nav" v-if="!narrow || page == null"> + <FormBase> + <FormGroup> + <template #label>{{ $ts.basicSettings }}</template> + <FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><Fa :icon="faUser"/></template>{{ $ts.profile }}</FormLink> + <FormLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><Fa :icon="faLockOpen"/></template>{{ $ts.privacy }}</FormLink> + <FormLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><Fa :icon="faLaugh"/></template>{{ $ts.reaction }}</FormLink> + <FormLink :active="page === 'drive'" replace to="/settings/drive"><template #icon><Fa :icon="faCloud"/></template>{{ $ts.drive }}</FormLink> + <FormLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><Fa :icon="faBell"/></template>{{ $ts.notifications }}</FormLink> + <FormLink :active="page === 'email'" replace to="/settings/email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $ts.email }}</FormLink> + <FormLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><Fa :icon="faShareAlt"/></template>{{ $ts.integration }}</FormLink> + <FormLink :active="page === 'security'" replace to="/settings/security"><template #icon><Fa :icon="faLock"/></template>{{ $ts.security }}</FormLink> + </FormGroup> + <FormGroup> + <template #label>{{ $ts.clientSettings }}</template> + <FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><Fa :icon="faCogs"/></template>{{ $ts.general }}</FormLink> + <FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $ts.theme }}</FormLink> + <FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $ts.sidebar }}</FormLink> + <FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $ts.sounds }}</FormLink> + <FormLink :active="page === 'plugin'" replace to="/settings/plugin"><template #icon><Fa :icon="faPlug"/></template>{{ $ts.plugins }}</FormLink> + </FormGroup> + <FormGroup> + <template #label>{{ $ts.otherSettings }}</template> + <FormLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><Fa :icon="faBoxes"/></template>{{ $ts.importAndExport }}</FormLink> + <FormLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><Fa :icon="faBan"/></template>{{ $ts.muteAndBlock }}</FormLink> + <FormLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><Fa :icon="faCommentSlash"/></template>{{ $ts.wordMute }}</FormLink> + <FormLink :active="page === 'api'" replace to="/settings/api"><template #icon><Fa :icon="faKey"/></template>API</FormLink> + <FormLink :active="page === 'other'" replace to="/settings/other"><template #icon><Fa :icon="faEllipsisH"/></template>{{ $ts.other }}</FormLink> + </FormGroup> + <FormGroup> + <FormButton @click="clear">{{ $ts.clearCache }}</FormButton> + </FormGroup> + <FormGroup> + <FormButton @click="logout" danger>{{ $ts.logout }}</FormButton> + </FormGroup> + </FormBase> + </div> <div class="main"> <component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/> </div> @@ -53,6 +55,7 @@ import FormButton from '@client/components/form/button.vue'; import { scroll } from '@client/scripts/scroll'; import { signout } from '@client/account'; import { unisonReload } from '@client/scripts/unison-reload'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -63,17 +66,19 @@ export default defineComponent({ }, props: { - page: { + initialPage: { type: String, required: false } }, setup(props, context) { - const INFO = ref({ + const indexInfo = { title: i18n.locale.settings, icon: faCog - }); + }; + const INFO = ref(indexInfo); + const page = ref(props.initialPage); const narrow = ref(false); const view = ref(null); const el = ref(null); @@ -82,8 +87,8 @@ export default defineComponent({ }; const pageProps = ref({}); const component = computed(() => { - if (props.page == null) return null; - switch (props.page) { + if (page.value == null) return null; + switch (page.value) { case 'profile': return defineAsyncComponent(() => import('./profile.vue')); case 'privacy': return defineAsyncComponent(() => import('./privacy.vue')); case 'reaction': return defineAsyncComponent(() => import('./reaction.vue')); @@ -116,10 +121,10 @@ export default defineComponent({ case 'registry': return defineAsyncComponent(() => import('./registry.vue')); case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue')); } - if (props.page.startsWith('registry/keys/system/')) { + if (page.value.startsWith('registry/keys/system/')) { return defineAsyncComponent(() => import('./registry.keys.vue')); } - if (props.page.startsWith('registry/value/system/')) { + if (page.value.startsWith('registry/value/system/')) { return defineAsyncComponent(() => import('./registry.value.vue')); } }); @@ -127,12 +132,12 @@ export default defineComponent({ watch(component, () => { pageProps.value = {}; - if (props.page) { - if (props.page.startsWith('registry/keys/system/')) { - pageProps.value.scope = props.page.replace('registry/keys/system/', '').split('/'); + if (page.value) { + if (page.value.startsWith('registry/keys/system/')) { + pageProps.value.scope = page.value.replace('registry/keys/system/', '').split('/'); } - if (props.page.startsWith('registry/value/system/')) { - const path = props.page.replace('registry/value/system/', '').split('/'); + if (page.value.startsWith('registry/value/system/')) { + const path = page.value.replace('registry/value/system/', '').split('/'); pageProps.value.xKey = path.pop(); pageProps.value.scope = path; } @@ -143,12 +148,27 @@ export default defineComponent({ }); }, { immediate: true }); + watch(() => props.initialPage, () => { + if (props.initialPage == null && !narrow.value) { + page.value = 'profile'; + } else { + page.value = props.initialPage; + if (props.initialPage == null) { + INFO.value = indexInfo; + } + } + }); + onMounted(() => { - narrow.value = el.value.offsetWidth < 1025; + narrow.value = el.value.offsetWidth < 800; + if (!narrow.value) { + page.value = 'profile'; + } }); return { - INFO, + [symbols.PAGE_INFO]: INFO, + page, narrow, view, el, @@ -175,25 +195,20 @@ export default defineComponent({ display: flex; max-width: 1100px; margin: 0 auto; + height: 100%; > .nav { width: 32%; box-sizing: border-box; border-right: solid 0.5px var(--divider); + overflow: auto; } > .main { flex: 1; min-width: 0; + overflow: auto; --baseContentWidth: 100%; - - ::v-deep(._section) { - padding: 0 0 32px 0; - - & + ._section { - padding-top: 32px; - } - } } } } diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue index e2ab11841e..49f955bc35 100644 --- a/src/client/pages/settings/integration.vue +++ b/src/client/pages/settings/integration.vue @@ -37,6 +37,7 @@ import { apiUrl } from '@client/config'; import FormBase from '@client/components/form/base.vue'; import MkButton from '@client/components/ui/button.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -48,7 +49,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.integration, icon: faShareAlt }, @@ -80,7 +81,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); document.cookie = `igi=${this.$i.token}; path=/;` + ` max-age=31536000;` + diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue index f7eebbb9cc..11450e049b 100644 --- a/src/client/pages/settings/mute-block.vue +++ b/src/client/pages/settings/mute-block.vue @@ -6,7 +6,7 @@ </MkTab> <div v-if="tab === 'mute'"> <MkPagination :pagination="mutingPagination" class="muting"> - <template #empty><MkInfo>{{ $ts.noUsers }}</MkInfo></template> + <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template> <template #default="{items}"> <FormGroup> <FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)"> @@ -18,7 +18,7 @@ </div> <div v-if="tab === 'block'"> <MkPagination :pagination="blockingPagination" class="blocking"> - <template #empty><MkInfo>{{ $ts.noUsers }}</MkInfo></template> + <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template> <template #default="{items}"> <FormGroup> <FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)"> @@ -36,18 +36,19 @@ import { defineComponent } from 'vue'; import { faBan } from '@fortawesome/free-solid-svg-icons'; import MkPagination from '@client/components/ui/pagination.vue'; import MkTab from '@client/components/tab.vue'; -import MkInfo from '@client/components/ui/info.vue'; +import FormInfo from '@client/components/form/info.vue'; import FormLink from '@client/components/form/link.vue'; import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import { userPage } from '@client/filters/user'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { MkPagination, MkTab, - MkInfo, + FormInfo, FormBase, FormGroup, FormLink, @@ -57,7 +58,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.muteAndBlock, icon: faBan }, @@ -74,7 +75,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue index b26c2805ad..ea72bcfee8 100644 --- a/src/client/pages/settings/notifications.vue +++ b/src/client/pages/settings/notifications.vue @@ -19,6 +19,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import { notificationTypes } from '../../../types'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -32,7 +33,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.notifications, icon: faBell }, @@ -41,7 +42,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue index e5cfc5ee1e..2bd9c2476c 100644 --- a/src/client/pages/settings/other.vue +++ b/src/client/pages/settings/other.vue @@ -44,6 +44,7 @@ import { debug } from '@client/config'; import { defaultStore } from '@client/store'; import { signout } from '@client/account'; import { unisonReload } from '@client/scripts/unison-reload'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -59,7 +60,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.other, icon: faEllipsisH }, @@ -73,7 +74,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/plugin.install.vue b/src/client/pages/settings/plugin.install.vue index 70c302b36e..bc80188fc6 100644 --- a/src/client/pages/settings/plugin.install.vue +++ b/src/client/pages/settings/plugin.install.vue @@ -1,6 +1,6 @@ <template> <FormBase> - <MkInfo warn>{{ $ts._plugin.installWarn }}</MkInfo> + <FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo> <FormGroup> <FormTextarea v-model:value="code" tall> @@ -25,10 +25,11 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormLink from '@client/components/form/link.vue'; import FormButton from '@client/components/form/button.vue'; -import MkInfo from '@client/components/ui/info.vue'; +import FormInfo from '@client/components/form/info.vue'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; import { unisonReload } from '@client/scripts/unison-reload'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -39,14 +40,14 @@ export default defineComponent({ FormGroup, FormLink, FormButton, - MkInfo, + FormInfo, }, emits: ['info'], data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts._plugin.install, icon: faDownload }, @@ -56,7 +57,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/plugin.manage.vue b/src/client/pages/settings/plugin.manage.vue index 0bc04493a7..d7aabe560e 100644 --- a/src/client/pages/settings/plugin.manage.vue +++ b/src/client/pages/settings/plugin.manage.vue @@ -36,19 +36,18 @@ import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fo import MkButton from '@client/components/ui/button.vue'; import MkTextarea from '@client/components/ui/textarea.vue'; import MkSelect from '@client/components/ui/select.vue'; -import MkInfo from '@client/components/ui/info.vue'; import FormSwitch from '@client/components/form/switch.vue'; import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { MkButton, MkTextarea, MkSelect, - MkInfo, FormSwitch, FormBase, FormGroup, @@ -58,7 +57,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts._plugin.manage, icon: faPlug }, @@ -68,7 +67,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/plugin.vue b/src/client/pages/settings/plugin.vue index b101420d0d..bee4e57ec3 100644 --- a/src/client/pages/settings/plugin.vue +++ b/src/client/pages/settings/plugin.vue @@ -13,6 +13,7 @@ import FormGroup from '@client/components/form/group.vue'; import FormLink from '@client/components/form/link.vue'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -24,7 +25,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.plugins, icon: faPlug }, @@ -34,7 +35,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, }); </script> diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue index f58faab0b5..0542c527f9 100644 --- a/src/client/pages/settings/privacy.vue +++ b/src/client/pages/settings/privacy.vue @@ -36,6 +36,7 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import * as os from '@client/os'; import { defaultStore } from '@client/store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -49,7 +50,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.privacy, icon: faLockOpen }, @@ -74,7 +75,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue index 61f0a8e198..5ec580a206 100644 --- a/src/client/pages/settings/profile.vue +++ b/src/client/pages/settings/profile.vue @@ -59,6 +59,7 @@ import FormGroup from '@client/components/form/group.vue'; import { host, langs } from '@client/config'; import { selectFile } from '@client/scripts/select-file'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -75,7 +76,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.profile, icon: faUser }, @@ -136,7 +137,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue index 170f8c9a0a..0293f53fa8 100644 --- a/src/client/pages/settings/reaction.vue +++ b/src/client/pages/settings/reaction.vue @@ -45,6 +45,7 @@ import FormBase from '@client/components/form/base.vue'; import FormButton from '@client/components/form/button.vue'; import * as os from '@client/os'; import { defaultStore } from '@client/store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -59,7 +60,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.reaction, icon: faLaugh, action: { @@ -87,7 +88,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/registry.keys.vue b/src/client/pages/settings/registry.keys.vue index 243672000b..5cdfdc1332 100644 --- a/src/client/pages/settings/registry.keys.vue +++ b/src/client/pages/settings/registry.keys.vue @@ -24,7 +24,6 @@ import { defineAsyncComponent, defineComponent } from 'vue'; import { faCogs } from '@fortawesome/free-solid-svg-icons'; import * as JSON5 from 'json5'; -import MkInfo from '@client/components/ui/info.vue'; import FormSwitch from '@client/components/form/switch.vue'; import FormSelect from '@client/components/form/select.vue'; import FormLink from '@client/components/form/link.vue'; @@ -33,10 +32,10 @@ import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import FormKeyValueView from '@client/components/form/key-value-view.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { - MkInfo, FormBase, FormSelect, FormSwitch, @@ -56,7 +55,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.registry, icon: faCogs }, @@ -71,7 +70,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); this.fetch(); }, diff --git a/src/client/pages/settings/registry.value.vue b/src/client/pages/settings/registry.value.vue index e760e4b1e5..7d5756af99 100644 --- a/src/client/pages/settings/registry.value.vue +++ b/src/client/pages/settings/registry.value.vue @@ -1,6 +1,6 @@ <template> <FormBase> - <MkInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</MkInfo> + <FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo> <template v-if="value"> <FormGroup> @@ -39,7 +39,7 @@ import { defineAsyncComponent, defineComponent } from 'vue'; import { faCogs, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; import * as JSON5 from 'json5'; -import MkInfo from '@client/components/ui/info.vue'; +import FormInfo from '@client/components/form/info.vue'; import FormSwitch from '@client/components/form/switch.vue'; import FormSelect from '@client/components/form/select.vue'; import FormTextarea from '@client/components/form/textarea.vue'; @@ -48,10 +48,11 @@ import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import FormKeyValueView from '@client/components/form/key-value-view.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { - MkInfo, + FormInfo, FormBase, FormSelect, FormSwitch, @@ -74,7 +75,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.registry, icon: faCogs }, @@ -91,7 +92,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); this.fetch(); }, diff --git a/src/client/pages/settings/registry.vue b/src/client/pages/settings/registry.vue index 5e061f95f8..085389fc95 100644 --- a/src/client/pages/settings/registry.vue +++ b/src/client/pages/settings/registry.vue @@ -12,7 +12,6 @@ import { defineAsyncComponent, defineComponent } from 'vue'; import { faCogs } from '@fortawesome/free-solid-svg-icons'; import * as JSON5 from 'json5'; -import MkInfo from '@client/components/ui/info.vue'; import FormSwitch from '@client/components/form/switch.vue'; import FormSelect from '@client/components/form/select.vue'; import FormLink from '@client/components/form/link.vue'; @@ -21,10 +20,10 @@ import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import FormKeyValueView from '@client/components/form/key-value-view.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { - MkInfo, FormBase, FormSelect, FormSwitch, @@ -38,7 +37,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.registry, icon: faCogs }, @@ -51,7 +50,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue index 7d6aaa652f..64733c55a2 100644 --- a/src/client/pages/settings/security.vue +++ b/src/client/pages/settings/security.vue @@ -32,6 +32,7 @@ import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import FormPagination from '@client/components/form/pagination.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -46,7 +47,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.security, icon: faLock }, @@ -59,7 +60,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/sidebar.vue b/src/client/pages/settings/sidebar.vue index bbb1b43afb..adeec2f636 100644 --- a/src/client/pages/settings/sidebar.vue +++ b/src/client/pages/settings/sidebar.vue @@ -29,6 +29,7 @@ import FormButton from '@client/components/form/button.vue'; import * as os from '@client/os'; import { sidebarDef } from '@client/sidebar'; import { defaultStore } from '@client/store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -42,7 +43,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.sidebar, icon: faListUl }, @@ -65,7 +66,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue index 8305744109..54be003115 100644 --- a/src/client/pages/settings/sounds.vue +++ b/src/client/pages/settings/sounds.vue @@ -28,6 +28,7 @@ import FormGroup from '@client/components/form/group.vue'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; import { playFile } from '@client/scripts/sound'; +import * as symbols from '@client/symbols'; const soundsTypes = [ null, @@ -68,7 +69,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.sounds, icon: faMusic }, @@ -100,7 +101,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/theme.install.vue b/src/client/pages/settings/theme.install.vue index 6184383657..744d1aba44 100644 --- a/src/client/pages/settings/theme.install.vue +++ b/src/client/pages/settings/theme.install.vue @@ -26,6 +26,7 @@ import { applyTheme, validateTheme } from '@client/scripts/theme'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; import { addTheme, getThemes } from '@client/theme-store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -42,7 +43,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts._theme.install, icon: faDownload }, @@ -52,7 +53,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue index da7bb27030..ea9d5949ff 100644 --- a/src/client/pages/settings/theme.manage.vue +++ b/src/client/pages/settings/theme.manage.vue @@ -13,6 +13,9 @@ <FormInput readonly :value="selectedTheme.author"> <span>{{ $ts.author }}</span> </FormInput> + <FormTextarea readonly :value="selectedTheme.desc" v-if="selectedTheme.desc"> + <span>{{ $ts._theme.description }}</span> + </FormTextarea> <FormTextarea readonly tall :value="selectedThemeCode"> <span>{{ $ts._theme.code }}</span> <template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template> @@ -38,6 +41,7 @@ import copyToClipboard from '@client/scripts/copy-to-clipboard'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; import { getThemes, removeTheme } from '@client/theme-store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -54,7 +58,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts._theme.manage, icon: faFolderOpen }, @@ -82,7 +86,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { @@ -93,6 +97,7 @@ export default defineComponent({ uninstall() { removeTheme(this.selectedTheme); + this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId); this.selectedThemeId = null; os.success(); }, diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue index 14cdb533ac..606e10ab7a 100644 --- a/src/client/pages/settings/theme.vue +++ b/src/client/pages/settings/theme.vue @@ -1,24 +1,5 @@ <template> <FormBase> - <FormSelect v-model:value="lightTheme" v-if="!darkMode"> - <template #label>{{ $ts.themeForLightMode }}</template> - <optgroup :label="$ts.lightThemes"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$ts.darkThemes"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </FormSelect> - <FormSelect v-model:value="darkTheme" v-else> - <template #label>{{ $ts.themeForDarkMode }}</template> - <optgroup :label="$ts.darkThemes"> - <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - <optgroup :label="$ts.lightThemes"> - <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> - </optgroup> - </FormSelect> - <FormGroup> <div class="rfqxtzch _formItem _formPanel"> <div class="darkMode"> @@ -45,6 +26,47 @@ <FormSwitch v-model:value="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch> </FormGroup> + <template v-if="darkMode"> + <FormSelect v-model:value="darkThemeId"> + <template #label>{{ $ts.themeForDarkMode }}</template> + <optgroup :label="$ts.darkThemes"> + <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + <optgroup :label="$ts.lightThemes"> + <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + </FormSelect> + <FormSelect v-model:value="lightThemeId"> + <template #label>{{ $ts.themeForLightMode }}</template> + <optgroup :label="$ts.lightThemes"> + <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + <optgroup :label="$ts.darkThemes"> + <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + </FormSelect> + </template> + <template v-else> + <FormSelect v-model:value="lightThemeId"> + <template #label>{{ $ts.themeForLightMode }}</template> + <optgroup :label="$ts.lightThemes"> + <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + <optgroup :label="$ts.darkThemes"> + <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + </FormSelect> + <FormSelect v-model:value="darkThemeId"> + <template #label>{{ $ts.themeForDarkMode }}</template> + <optgroup :label="$ts.darkThemes"> + <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + <optgroup :label="$ts.lightThemes"> + <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </optgroup> + </FormSelect> + </template> + <FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $ts.setWallpaper }}</FormButton> <FormButton primary v-else @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton> @@ -55,7 +77,7 @@ <FormGroup> <FormLink to="/theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }}</FormLink> - <FormLink to="/advanced-theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink> + <!--<FormLink to="/advanced-theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>--> </FormGroup> <FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink> @@ -63,7 +85,7 @@ </template> <script lang="ts"> -import { computed, defineComponent, onMounted, ref, watch } from 'vue'; +import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue'; import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye, faGlobe, faPaintRoller } from '@fortawesome/free-solid-svg-icons'; import FormSwitch from '@client/components/form/switch.vue'; import FormSelect from '@client/components/form/select.vue'; @@ -71,13 +93,14 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormLink from '@client/components/form/link.vue'; import FormButton from '@client/components/form/button.vue'; -import { builtinThemes, applyTheme } from '@client/scripts/theme'; +import { builtinThemes } from '@client/scripts/theme'; import { selectFile } from '@client/scripts/select-file'; import { isDeviceDarkmode } from '@client/scripts/is-device-darkmode'; import { ColdDeviceStorage } from '@client/store'; import { i18n } from '@client/i18n'; import { defaultStore } from '@client/store'; import { fetchThemes, getThemes } from '@client/theme-store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -101,24 +124,28 @@ export default defineComponent({ const themes = computed(() => builtinThemes.concat(installedThemes.value)); const darkThemes = computed(() => themes.value.filter(t => t.base == 'dark' || t.kind == 'dark')); const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light')); - const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); - const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); - const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); - const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); - const wallpaper = ref(localStorage.getItem('wallpaper')); - const themesCount = installedThemes.value.length; - - watch(darkTheme, () => { - if (defaultStore.state.darkMode) { - applyTheme(themes.value.find(x => x.id === darkTheme.value)); + const darkTheme = ColdDeviceStorage.ref('darkTheme'); + const darkThemeId = computed({ + get() { + return darkTheme.value.id; + }, + set(id) { + ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id)) } }); - - watch(lightTheme, () => { - if (!defaultStore.state.darkMode) { - applyTheme(themes.value.find(x => x.id === lightTheme.value)); + const lightTheme = ColdDeviceStorage.ref('lightTheme'); + const lightThemeId = computed({ + get() { + return lightTheme.value.id; + }, + set(id) { + ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id)) } }); + const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); + const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); + const wallpaper = ref(localStorage.getItem('wallpaper')); + const themesCount = installedThemes.value.length; watch(syncDeviceDarkMode, () => { if (syncDeviceDarkMode) { @@ -139,16 +166,22 @@ export default defineComponent({ emit('info', INFO); }); + onActivated(() => { + fetchThemes().then(() => { + installedThemes.value = getThemes(); + }); + }); + fetchThemes().then(() => { installedThemes.value = getThemes(); }); return { - INFO, + [symbols.PAGE_INFO]: INFO, darkThemes, lightThemes, - darkTheme, - lightTheme, + darkThemeId, + lightThemeId, darkMode, syncDeviceDarkMode, themesCount, diff --git a/src/client/pages/settings/update.vue b/src/client/pages/settings/update.vue index a8da8bf11c..d7b2adae56 100644 --- a/src/client/pages/settings/update.vue +++ b/src/client/pages/settings/update.vue @@ -1,8 +1,8 @@ <template> <FormBase> <template v-if="meta"> - <MkInfo v-if="version === meta.version">{{ $ts.youAreRunningUpToDateClient }}</MkInfo> - <MkInfo v-else warn>{{ $ts.newVersionOfClientAvailable }}</MkInfo> + <FormInfo v-if="version === meta.version">{{ $ts.youAreRunningUpToDateClient }}</FormInfo> + <FormInfo v-else warn>{{ $ts.newVersionOfClientAvailable }}</FormInfo> </template> <FormGroup> <template #label>{{ instanceName }}</template> @@ -38,9 +38,10 @@ import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; import FormButton from '@client/components/form/button.vue'; import FormKeyValueView from '@client/components/form/key-value-view.vue'; -import MkInfo from '@client/components/ui/info.vue'; +import FormInfo from '@client/components/form/info.vue'; import * as os from '@client/os'; import { version, instanceName } from '@client/config'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -51,14 +52,14 @@ export default defineComponent({ FormLink, FormGroup, FormKeyValueView, - MkInfo, + FormInfo, }, emits: ['info'], data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: 'Misskey Update', icon: faSyncAlt }, @@ -70,7 +71,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); os.api('meta', { detail: false @@ -79,7 +80,7 @@ export default defineComponent({ localStorage.setItem('v', meta.version); }); - fetch('https://api.github.com/repos/syuilo/misskey/releases', { + fetch('https://api.github.com/repos/misskey-dev/misskey/releases', { method: 'GET', }) .then(res => res.json()) diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue index 26cc7a3d85..79de2ebbdf 100644 --- a/src/client/pages/settings/word-mute.vue +++ b/src/client/pages/settings/word-mute.vue @@ -7,14 +7,14 @@ <FormBase> <div class="_formItem"> <div v-show="tab === 'soft'"> - <MkInfo>{{ $ts._wordMute.softDescription }}</MkInfo> + <FormInfo>{{ $ts._wordMute.softDescription }}</FormInfo> <FormTextarea v-model:value="softMutedWords"> <span>{{ $ts._wordMute.muteWords }}</span> <template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template> </FormTextarea> </div> <div v-show="tab === 'hard'"> - <MkInfo>{{ $ts._wordMute.hardDescription }}</MkInfo> + <FormInfo>{{ $ts._wordMute.hardDescription }}</FormInfo> <FormTextarea v-model:value="hardMutedWords"> <span>{{ $ts._wordMute.muteWords }}</span> <template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template> @@ -37,10 +37,11 @@ import FormTextarea from '@client/components/form/textarea.vue'; import FormBase from '@client/components/form/base.vue'; import FormKeyValueView from '@client/components/form/key-value-view.vue'; import FormButton from '@client/components/form/button.vue'; +import FormInfo from '@client/components/form/info.vue'; import MkTab from '@client/components/tab.vue'; -import MkInfo from '@client/components/ui/info.vue'; import * as os from '@client/os'; import number from '@client/filters/number'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -49,14 +50,14 @@ export default defineComponent({ FormTextarea, FormKeyValueView, MkTab, - MkInfo, + FormInfo, }, emits: ['info'], data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.wordMute, icon: faCommentSlash }, @@ -92,7 +93,7 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this.INFO); + this.$emit('info', this[symbols.PAGE_INFO]); }, methods: { diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue index 41ce4d21f7..313b73b9cb 100644 --- a/src/client/pages/share.vue +++ b/src/client/pages/share.vue @@ -17,6 +17,7 @@ import { faShareAlt } from '@fortawesome/free-solid-svg-icons'; import MkButton from '@client/components/ui/button.vue'; import XPostForm from '@client/components/post-form.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -26,7 +27,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.share, icon: faShareAlt }, diff --git a/src/client/pages/tag.vue b/src/client/pages/tag.vue index 7561eba2ed..813181dd1f 100644 --- a/src/client/pages/tag.vue +++ b/src/client/pages/tag.vue @@ -9,6 +9,7 @@ import { defineComponent } from 'vue'; import { faHashtag } from '@fortawesome/free-solid-svg-icons'; import Progress from '@client/scripts/loading'; import XNotes from '@client/components/notes.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -24,7 +25,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.tag, icon: faHashtag }, diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue index af8395b2c3..252fa1c828 100644 --- a/src/client/pages/test.vue +++ b/src/client/pages/test.vue @@ -1,7 +1,7 @@ <template> <div class="_section"> <div class="_content"> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">Dialog</div> <div class="_content"> <MkInput v-model:value="dialogTitle"> @@ -30,7 +30,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">Form</div> <div class="_content"> <MkInput v-model:value="formTitle"> @@ -46,7 +46,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">MFM</div> <div class="_content"> <MkTextarea v-model:value="mfm"> @@ -58,7 +58,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">selectDriveFile</div> <div class="_content"> <MkSwitch v-model:value="selectDriveFileMultiple"> @@ -71,7 +71,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">selectDriveFolder</div> <div class="_content"> <MkSwitch v-model:value="selectDriveFolderMultiple"> @@ -84,7 +84,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">selectUser</div> <div class="_content"> <MkButton @click="selectUser()">selectUser</MkButton> @@ -94,7 +94,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">Notification</div> <div class="_content"> <MkInput v-model:value="notificationIconUrl"> @@ -110,7 +110,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">Waiting dialog</div> <div class="_content"> <MkButton inline @click="openWaitingDialog()">icon only</MkButton> @@ -118,7 +118,7 @@ </div> </div> - <div class="_card _vMargin"> + <div class="_card _gap"> <div class="_title">Messaging window</div> <div class="_content"> <MkButton @click="messagingWindowOpen()">open</MkButton> @@ -139,6 +139,7 @@ import MkSwitch from '@client/components/ui/switch.vue'; import MkTextarea from '@client/components/ui/textarea.vue'; import MkRadio from '@client/components/ui/radio.vue'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -151,7 +152,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: 'TEST', icon: faExclamationTriangle }, diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue index 5ee0a704b0..db273746a8 100644 --- a/src/client/pages/theme-editor.vue +++ b/src/client/pages/theme-editor.vue @@ -35,6 +35,7 @@ </div> </div> </div> + <FormGroup v-if="codeEnabled"> <FormTextarea v-model:value="themeCode" tall> <span>{{ $ts._theme.code }}</span> @@ -42,6 +43,14 @@ <FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton> </FormGroup> <FormButton v-else @click="codeEnabled = true"><Fa :icon="faCode"/> {{ $ts.editCode }}</FormButton> + + <FormGroup v-if="descriptionEnabled"> + <FormTextarea v-model:value="description"> + <span>{{ $ts._theme.description }}</span> + </FormTextarea> + </FormGroup> + <FormButton v-else @click="descriptionEnabled = true">{{ $ts.addDescription }}</FormButton> + <FormGroup> <FormButton @click="showPreview"><Fa :icon="faEye"/> {{ $ts.preview }}</FormButton> <FormButton @click="saveAs" primary><Fa :icon="faSave"/> {{ $ts.saveAs }}</FormButton> @@ -52,7 +61,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; import { faPalette, faSave, faEye, faCode } from '@fortawesome/free-solid-svg-icons'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import * as tinycolor from 'tinycolor2'; import { v4 as uuid} from 'uuid'; import * as JSON5 from 'json5'; @@ -67,6 +76,7 @@ import { host } from '@client/config'; import * as os from '@client/os'; import { ColdDeviceStorage } from '@client/store'; import { addTheme } from '@client/theme-store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -78,7 +88,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: this.$ts.themeEditor, icon: faPalette, }, @@ -87,6 +97,8 @@ export default defineComponent({ props: lightTheme.props } as Theme, codeEnabled: false, + descriptionEnabled: false, + description: null, themeCode: null, bgColors: [ { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, @@ -217,12 +229,13 @@ export default defineComponent({ this.theme.id = uuid(); this.theme.name = name; this.theme.author = `@${this.$i.username}@${toUnicode(host)}`; + if (this.description) this.theme.desc = this.description; addTheme(this.theme); applyTheme(this.theme); if (this.$store.state.darkMode) { - ColdDeviceStorage.set('darkTheme', this.theme.id); + ColdDeviceStorage.set('darkTheme', this.theme); } else { - ColdDeviceStorage.set('lightTheme', this.theme.id); + ColdDeviceStorage.set('lightTheme', this.theme); } this.changed = false; os.dialog({ diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue index e0c0b2995a..751137c942 100644 --- a/src/client/pages/timeline.vue +++ b/src/client/pages/timeline.vue @@ -1,51 +1,51 @@ <template> -<div class="cmuxhskf" v-hotkey.global="keymap"> +<div class="cmuxhskf _root" v-hotkey.global="keymap"> <div class="new" v-if="queue > 0" :style="{ width: width + 'px' }"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> - <div class="_section"> - <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _content _vMargin"/> - <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _panel _content _vMargin" fixed/> - <div class="tabs _panel _vMargin"> - <div class="left"> - <button class="_button tab" @click="() => { src = 'home'; saveSrc(); }" :class="{ active: src === 'home' }" v-tooltip="$ts._timelines.home"><Fa :icon="faHome"/></button> - <button class="_button tab" @click="() => { src = 'local'; saveSrc(); }" :class="{ active: src === 'local' }" v-tooltip="$ts._timelines.local" v-if="isLocalTimelineAvailable"><Fa :icon="faComments"/></button> - <button class="_button tab" @click="() => { src = 'social'; saveSrc(); }" :class="{ active: src === 'social' }" v-tooltip="$ts._timelines.social" v-if="isLocalTimelineAvailable"><Fa :icon="faShareAlt"/></button> - <button class="_button tab" @click="() => { src = 'global'; saveSrc(); }" :class="{ active: src === 'global' }" v-tooltip="$ts._timelines.global" v-if="isGlobalTimelineAvailable"><Fa :icon="faGlobe"/></button> - <span class="divider"></span> - <button class="_button tab" @click="() => { src = 'mentions'; saveSrc(); }" :class="{ active: src === 'mentions' }" v-tooltip="$ts.mentions"><Fa :icon="faAt"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadMentions"/></button> - <button class="_button tab" @click="() => { src = 'directs'; saveSrc(); }" :class="{ active: src === 'directs' }" v-tooltip="$ts.directNotes"><Fa :icon="faEnvelope"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadSpecifiedNotes"/></button> - </div> - <div class="right"> - <button class="_button tab" @click="chooseChannel" :class="{ active: src === 'channel' }" v-tooltip="$ts.channel"><Fa :icon="faSatelliteDish"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadChannel"/></button> - <button class="_button tab" @click="chooseAntenna" :class="{ active: src === 'antenna' }" v-tooltip="$ts.antennas"><Fa :icon="faSatellite"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadAntenna"/></button> - <button class="_button tab" @click="chooseList" :class="{ active: src === 'list' }" v-tooltip="$ts.lists"><Fa :icon="faListUl"/></button> - </div> + <div class="_magnet"></div> + <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> + <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> + <div class="tabs _block"> + <div class="left"> + <button class="_button tab" @click="() => { src = 'home'; saveSrc(); }" :class="{ active: src === 'home' }" v-tooltip="$ts._timelines.home"><Fa :icon="faHome"/></button> + <button class="_button tab" @click="() => { src = 'local'; saveSrc(); }" :class="{ active: src === 'local' }" v-tooltip="$ts._timelines.local" v-if="isLocalTimelineAvailable"><Fa :icon="faComments"/></button> + <button class="_button tab" @click="() => { src = 'social'; saveSrc(); }" :class="{ active: src === 'social' }" v-tooltip="$ts._timelines.social" v-if="isLocalTimelineAvailable"><Fa :icon="faShareAlt"/></button> + <button class="_button tab" @click="() => { src = 'global'; saveSrc(); }" :class="{ active: src === 'global' }" v-tooltip="$ts._timelines.global" v-if="isGlobalTimelineAvailable"><Fa :icon="faGlobe"/></button> + <span class="divider"></span> + <button class="_button tab" @click="() => { src = 'mentions'; saveSrc(); }" :class="{ active: src === 'mentions' }" v-tooltip="$ts.mentions"><Fa :icon="faAt"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadMentions"/></button> + <button class="_button tab" @click="() => { src = 'directs'; saveSrc(); }" :class="{ active: src === 'directs' }" v-tooltip="$ts.directNotes"><Fa :icon="faEnvelope"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadSpecifiedNotes"/></button> + </div> + <div class="right"> + <button class="_button tab" @click="chooseChannel" :class="{ active: src === 'channel' }" v-tooltip="$ts.channel"><Fa :icon="faSatelliteDish"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadChannel"/></button> + <button class="_button tab" @click="chooseAntenna" :class="{ active: src === 'antenna' }" v-tooltip="$ts.antennas"><Fa :icon="faSatellite"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadAntenna"/></button> + <button class="_button tab" @click="chooseList" :class="{ active: src === 'list' }" v-tooltip="$ts.lists"><Fa :icon="faListUl"/></button> </div> - <XTimeline ref="tl" - class="_content _vMargin" - :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" - :src="src" - :list="list ? list.id : null" - :antenna="antenna ? antenna.id : null" - :channel="channel ? channel.id : null" - :sound="true" - @before="before()" - @after="after()" - @queue="queueUpdated" - /> </div> + <XTimeline ref="tl" + class="_gap" + :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" + :src="src" + :list="list ? list.id : null" + :antenna="antenna ? antenna.id : null" + :channel="channel ? channel.id : null" + :sound="true" + @before="before()" + @after="after()" + @queue="queueUpdated" + /> </div> </template> <script lang="ts"> import { defineComponent, defineAsyncComponent, computed } from 'vue'; import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faSatelliteDish, faCircle, faEllipsisH, faPencilAlt, faAt } from '@fortawesome/free-solid-svg-icons'; -import { faComments, faEnvelope } from '@fortawesome/free-regular-svg-icons'; +import { faComments, faEnvelope, faCalendarAlt } from '@fortawesome/free-regular-svg-icons'; import Progress from '@client/scripts/loading'; import XTimeline from '@client/components/timeline.vue'; import XPostForm from '@client/components/post-form.vue'; import { scroll } from '@client/scripts/scroll'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ name: 'timeline', @@ -65,13 +65,14 @@ export default defineComponent({ menuOpened: false, queue: 0, width: 0, - INFO: computed(() => ({ + [symbols.PAGE_INFO]: computed(() => ({ title: this.$ts.timeline, icon: this.src === 'local' ? faComments : this.src === 'social' ? faShareAlt : this.src === 'global' ? faGlobe : faHome, - action: { - icon: faPencilAlt, - handler: () => os.post() - } + actions: [{ + icon: faCalendarAlt, + text: this.$ts.jumpToSpecifiedDate, + handler: this.timetravel + }] })), faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faSatelliteDish, faCircle, faEllipsisH, faAt, faEnvelope, }; @@ -200,6 +201,18 @@ export default defineComponent({ }); }, + async timetravel() { + const { canceled, result: date } = await os.dialog({ + title: this.$ts.date, + input: { + type: 'date' + } + }); + if (canceled) return; + + this.$refs.tl.timetravel(new Date(date)); + }, + focus() { (this.$refs.tl as any).focus(); } @@ -221,71 +234,66 @@ export default defineComponent({ } } - > ._section { - > .tabs { - display: flex; - box-sizing: border-box; - padding: 0 8px; - max-width: var(--baseContentWidth); - margin-left: auto; - margin-right: auto; - white-space: nowrap; - overflow: auto; + > .tabs { + display: flex; + box-sizing: border-box; + padding: 0 8px; + white-space: nowrap; + overflow: auto; - // 影の都合上 - position: relative; + // 影の都合上 + position: relative; - > .right { - margin-left: auto; - } - - > .left, > .right { - > .tab { - position: relative; - height: 50px; - padding: 0 12px; + > .right { + margin-left: auto; + } - &:hover { - color: var(--fgHighlighted); - } + > .left, > .right { + > .tab { + position: relative; + height: 50px; + padding: 0 12px; - &.active { - color: var(--fgHighlighted); + &:hover { + color: var(--fgHighlighted); + } - &:after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - right: 0; - margin: 0 auto; - width: calc(100% - 16px); - height: 4px; - background: var(--accent); - border-radius: 8px 8px 0 0; - } - } + &.active { + color: var(--fgHighlighted); - > .i { + &:after { + content: ""; + display: block; position: absolute; - top: 16px; - right: 8px; - color: var(--indicator); - font-size: 8px; - animation: blink 1s infinite; + bottom: 0; + left: 0; + right: 0; + margin: 0 auto; + width: calc(100% - 16px); + height: 4px; + background: var(--accent); + border-radius: 8px 8px 0 0; } } - > .divider { - display: inline-block; - width: 1px; - height: 28px; - vertical-align: middle; - margin: 0 8px; - background: var(--divider); + > .i { + position: absolute; + top: 16px; + right: 8px; + color: var(--indicator); + font-size: 8px; + animation: blink 1s infinite; } } + + > .divider { + display: inline-block; + width: 1px; + height: 28px; + vertical-align: middle; + margin: 0 8px; + background: var(--divider); + } } } } diff --git a/src/client/pages/user/clips.vue b/src/client/pages/user/clips.vue index cf713d6daa..9c77bbad47 100644 --- a/src/client/pages/user/clips.vue +++ b/src/client/pages/user/clips.vue @@ -1,7 +1,7 @@ <template> <div> <MkPagination :pagination="pagination" #default="{items}" ref="list"> - <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin"> + <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> <b>{{ item.name }}</b> <div v-if="item.description" class="description">{{ item.description }}</div> </MkA> diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue index eef8409a06..1fce74ec17 100644 --- a/src/client/pages/user/follow-list.vue +++ b/src/client/pages/user/follow-list.vue @@ -1,7 +1,7 @@ <template> <div> - <MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers _content" ref="list"> - <div class="users"> + <MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list"> + <div class="users _isolated"> <MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/> </div> </MkPagination> diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue index 8c824a2111..4941abdade 100644 --- a/src/client/pages/user/index.timeline.vue +++ b/src/client/pages/user/index.timeline.vue @@ -1,11 +1,11 @@ <template> -<div> - <MkTab v-model:value="with_" class="_vMargin"> +<div class="yrzkoczt" v-sticky-container> + <MkTab v-model:value="with_" class="_gap tab"> <option :value="null">{{ $ts.notes }}</option> <option value="replies">{{ $ts.notesAndReplies }}</option> <option value="files">{{ $ts.withFiles }}</option> </MkTab> - <XNotes ref="timeline" class="_vMargin" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> + <XNotes ref="timeline" class="_noGap_" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> </div> </template> @@ -56,3 +56,11 @@ export default defineComponent({ }, }); </script> + +<style lang="scss" scoped> +.yrzkoczt { + > .tab { + background: var(--bg); + } +} +</style> diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue index 5ca29a3e41..52b2725964 100644 --- a/src/client/pages/user/index.vue +++ b/src/client/pages/user/index.vue @@ -1,9 +1,9 @@ <template> <div> <div class="ftskorzw wide _section" v-if="user && narrow === false"> - <MkRemoteCaution v-if="user.host != null" :href="user.url" class="_vMargin"/> + <MkRemoteCaution v-if="user.host != null" :href="user.url" class="_gap"/> - <div class="banner-container _vMargin" :style="style"> + <div class="banner-container _gap" :style="style"> <div class="banner" ref="banner" :style="style"></div> </div> <div class="contents"> @@ -56,11 +56,11 @@ </dd> </dl> </div> - <XActivity :user="user" :key="user.id" class="_vMargin"/> - <XPhotos :user="user" :key="user.id" class="_vMargin"/> + <XActivity :user="user" :key="user.id" class="_gap"/> + <XPhotos :user="user" :key="user.id" class="_gap"/> </div> <div class="main"> - <div class="nav _vMargin"> + <div class="nav _gap"> <MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link"> <Fa :icon="faCommentAlt" class="icon"/> <span>{{ $ts.notes }}</span> @@ -79,29 +79,29 @@ </div> </div> <template v-if="page === 'index'"> - <div v-if="user.pinnedNotes.length > 0" class="_vMargin"> - <XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> + <div v-if="user.pinnedNotes.length > 0" class="_gap"> + <XNote v-for="note in user.pinnedNotes" class="note _gap" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> </div> - <div class="_vMargin"> + <div class="_gap"> <XUserTimeline :user="user"/> </div> </template> - <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_vMargin"/> - <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_vMargin"/> - <XClips v-else-if="page === 'clips'" :user="user" class="_vMargin"/> - <XPages v-else-if="page === 'pages'" :user="user" class="_vMargin"/> + <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_gap"/> + <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_gap"/> + <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> + <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> </div> </div> </div> - <div class="ftskorzw narrow _section" v-else-if="user && narrow === true" v-size="{ max: [500] }"> + <div class="ftskorzw narrow _root" v-else-if="user && narrow === true" v-size="{ max: [500] }"> <!-- TODO --> <!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $ts.userSuspended }}</div> --> <!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $ts.userSilenced }}</div> --> - <div class="profile _content _vMargin"> - <MkRemoteCaution v-if="user.host != null" :href="user.url" class="_vMargin"/> + <div class="profile"> + <MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/> - <div class="_vMargin _panel main" :key="user.id"> + <div class="_block main" :key="user.id"> <div class="banner-container" :style="style"> <div class="banner" ref="banner" :style="style"></div> <div class="fade"></div> @@ -177,37 +177,39 @@ </div> </div> - <div class="nav _vMargin"> - <MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link"> - <Fa :icon="faCommentAlt" class="icon"/> - <span>{{ $ts.notes }}</span> - </MkA> - <MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link"> - <Fa :icon="faPaperclip" class="icon"/> - <span>{{ $ts.clips }}</span> - </MkA> - <MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link"> - <Fa :icon="faFileAlt" class="icon"/> - <span>{{ $ts.pages }}</span> - </MkA> - </div> + <div class="contents"> + <div class="nav _gap"> + <MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link"> + <Fa :icon="faCommentAlt" class="icon"/> + <span>{{ $ts.notes }}</span> + </MkA> + <MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link"> + <Fa :icon="faPaperclip" class="icon"/> + <span>{{ $ts.clips }}</span> + </MkA> + <MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link"> + <Fa :icon="faFileAlt" class="icon"/> + <span>{{ $ts.pages }}</span> + </MkA> + </div> - <template v-if="page === 'index'"> - <div class="_content _vMargin"> - <div v-if="user.pinnedNotes.length > 0" class="_vMargin"> - <XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> + <template v-if="page === 'index'"> + <div> + <div v-if="user.pinnedNotes.length > 0"> + <XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> + </div> + <XPhotos :user="user" :key="user.id"/> + <XActivity :user="user" :key="user.id"/> </div> - <XPhotos :user="user" :key="user.id" class="_vMargin"/> - <XActivity :user="user" :key="user.id" class="_vMargin"/> - </div> - <div class="_content _vMargin"> - <XUserTimeline :user="user" class="_content"/> - </div> - </template> - <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _vMargin"/> - <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _vMargin"/> - <XClips v-else-if="page === 'clips'" :user="user" class="_vMargin"/> - <XPages v-else-if="page === 'pages'" :user="user" class="_vMargin"/> + <div> + <XUserTimeline :user="user"/> + </div> + </template> + <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/> + <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/> + <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> + <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> + </div> </div> <div v-else-if="error"> <MkError @retry="fetch()"/> @@ -234,6 +236,7 @@ import { getUserMenu } from '@client/scripts/get-user-menu'; import number from '../../filters/number'; import { userPage, acct as getAcct } from '../../filters/user'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -265,13 +268,14 @@ export default defineComponent({ data() { return { - INFO: computed(() => this.user ? { + [symbols.PAGE_INFO]: computed(() => this.user ? { + title: this.user.name ? `${this.user.name} (@${this.user.username})` : `@${this.user.username}`, userName: this.user, avatar: this.user, - action: { - icon: faEllipsisH, - handler: this.menu - } + path: `/@${this.user.username}`, + share: { + title: this.user.name, + }, } : null), user: null, error: null, @@ -304,7 +308,7 @@ export default defineComponent({ mounted() { window.requestAnimationFrame(this.parallaxLoop); - this.narrow = this.$el.clientWidth < 1000; + this.narrow = true; //this.$el.clientWidth < 1000; }, beforeUnmount() { @@ -415,7 +419,7 @@ export default defineComponent({ font-size: 80%; padding: 8px 12px; margin-bottom: 20px; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: 999px; } } @@ -423,7 +427,7 @@ export default defineComponent({ > .status { display: flex; padding: 20px 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); font-size: 90%; > a { @@ -451,13 +455,13 @@ export default defineComponent({ > .description { padding: 20px 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); font-size: 90%; } > .fields { padding: 20px 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); font-size: 90%; > .field { @@ -540,9 +544,8 @@ export default defineComponent({ } .ftskorzw.narrow { - max-width: 100vw; box-sizing: border-box; - overflow: hidden; + overflow: clip; > .punished { font-size: 0.8em; @@ -654,7 +657,7 @@ export default defineComponent({ text-align: center; padding: 50px 8px 16px 8px; font-weight: bold; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); > .bottom { > * { @@ -689,7 +692,7 @@ export default defineComponent({ > .fields { padding: 24px; font-size: 0.9em; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); > .field { display: flex; @@ -726,7 +729,7 @@ export default defineComponent({ > .status { display: flex; padding: 24px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); > a { flex: 1; @@ -753,41 +756,42 @@ export default defineComponent({ } } - > .nav { - display: flex; - align-items: center; - margin-top: var(--margin); - //font-size: 120%; - font-weight: bold; + > .contents { + > .nav { + display: flex; + align-items: center; + //font-size: 120%; + font-weight: bold; - > .link { - flex: 1; - display: inline-block; - padding: 16px; - text-align: center; - border-bottom: solid 3px transparent; + > .link { + flex: 1; + display: inline-block; + padding: 16px; + text-align: center; + border-bottom: solid 3px transparent; - &:hover { - text-decoration: none; - } + &:hover { + text-decoration: none; + } - &.active { - color: var(--accent); - border-bottom-color: var(--accent); - } + &.active { + color: var(--accent); + border-bottom-color: var(--accent); + } - &:not(.active):hover { - color: var(--fgHighlighted); - } + &:not(.active):hover { + color: var(--fgHighlighted); + } - > .icon { - margin-right: 6px; + > .icon { + margin-right: 6px; + } } } - } - > .content { - margin-bottom: var(--margin); + > .content { + margin-bottom: var(--margin); + } } &.max-width_500px { @@ -831,8 +835,22 @@ export default defineComponent({ } } - > .nav { - font-size: 80%; + > .contents { + > .nav { + font-size: 80%; + } + } + } +} + +._flat_ .ftskorzw.narrow { + > .profile { + > .warn { + margin: 0; + } + + > .main { + margin-top: 0; } } } diff --git a/src/client/pages/user/pages.vue b/src/client/pages/user/pages.vue index 1d2e96b351..34ac9d1ba6 100644 --- a/src/client/pages/user/pages.vue +++ b/src/client/pages/user/pages.vue @@ -1,7 +1,7 @@ <template> <div> <MkPagination :pagination="pagination" #default="{items}" ref="list"> - <MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_vMargin"/> + <MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/> </MkPagination> </div> </template> diff --git a/src/client/pages/v.vue b/src/client/pages/v.vue index a9deea24b4..37a850b625 100644 --- a/src/client/pages/v.vue +++ b/src/client/pages/v.vue @@ -14,11 +14,12 @@ import { defineComponent } from 'vue'; import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { version } from '@client/config'; +import * as symbols from '@client/symbols'; export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: 'Misskey', icon: null }, diff --git a/src/client/pages/welcome.entrance.a.vue b/src/client/pages/welcome.entrance.a.vue index 9a24f868b6..7b02c44923 100644 --- a/src/client/pages/welcome.entrance.a.vue +++ b/src/client/pages/welcome.entrance.a.vue @@ -53,7 +53,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; import { faEllipsisH, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import XSigninDialog from '@client/components/signin-dialog.vue'; import XSignupDialog from '@client/components/signup-dialog.vue'; import MkButton from '@client/components/ui/button.vue'; @@ -283,7 +283,7 @@ export default defineComponent({ } > .status { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); padding: 32px; font-size: 90%; @@ -291,7 +291,7 @@ export default defineComponent({ > span:not(:last-child) { padding-right: 1em; margin-right: 1em; - border-right: solid 1px var(--divider); + border-right: solid 0.5px var(--divider); } } diff --git a/src/client/pages/welcome.entrance.b.vue b/src/client/pages/welcome.entrance.b.vue index 6e93f17404..d8622e4d8e 100644 --- a/src/client/pages/welcome.entrance.b.vue +++ b/src/client/pages/welcome.entrance.b.vue @@ -37,7 +37,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; import { faEllipsisH, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import XSigninDialog from '@client/components/signin-dialog.vue'; import XSignupDialog from '@client/components/signup-dialog.vue'; import MkButton from '@client/components/ui/button.vue'; diff --git a/src/client/pages/welcome.entrance.c.vue b/src/client/pages/welcome.entrance.c.vue index cefe239da9..47ddf9e5ed 100644 --- a/src/client/pages/welcome.entrance.c.vue +++ b/src/client/pages/welcome.entrance.c.vue @@ -57,7 +57,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; import { faEllipsisH, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode/'; import XSigninDialog from '@client/components/signin-dialog.vue'; import XSignupDialog from '@client/components/signup-dialog.vue'; import MkButton from '@client/components/ui/button.vue'; @@ -255,7 +255,7 @@ export default defineComponent({ } > .status { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); padding: 32px; font-size: 90%; @@ -263,7 +263,7 @@ export default defineComponent({ > span:not(:last-child) { padding-right: 1em; margin-right: 1em; - border-right: solid 1px var(--divider); + border-right: solid 0.5px var(--divider); } } diff --git a/src/client/pages/welcome.vue b/src/client/pages/welcome.vue index 845a304854..b6a715830d 100644 --- a/src/client/pages/welcome.vue +++ b/src/client/pages/welcome.vue @@ -11,6 +11,7 @@ import XSetup from './welcome.setup.vue'; import XEntrance from './welcome.entrance.a.vue'; import { instanceName } from '@client/config'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -20,7 +21,7 @@ export default defineComponent({ data() { return { - INFO: { + [symbols.PAGE_INFO]: { title: instanceName, icon: null }, diff --git a/src/client/router.ts b/src/client/router.ts index 53516db97b..3effb2edbe 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -22,7 +22,7 @@ export const router = createRouter({ { path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) }, { path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, { path: '/@:acct/room', props: true, component: page('room/room') }, - { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) }, + { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) }, { path: '/announcements', component: page('announcements') }, { path: '/about', component: page('about') }, { path: '/about-misskey', component: page('about-misskey') }, diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts index eab893a386..99c54c69c5 100644 --- a/src/client/scripts/autocomplete.ts +++ b/src/client/scripts/autocomplete.ts @@ -1,6 +1,6 @@ import { Ref, ref } from 'vue'; import * as getCaretCoordinates from 'textarea-caret'; -import { toASCII } from 'punycode'; +import { toASCII } from 'punycode/'; import { popup } from '@client/os'; export class Autocomplete { diff --git a/src/client/scripts/sticky-sidebar.ts b/src/client/scripts/sticky-sidebar.ts index 9d46a7831f..18670bc037 100644 --- a/src/client/scripts/sticky-sidebar.ts +++ b/src/client/scripts/sticky-sidebar.ts @@ -1,40 +1,45 @@ export class StickySidebar { private lastScrollTop = 0; + private container: HTMLElement; private el: HTMLElement; private spacer: HTMLElement; private marginTop: number; private isTop = false; private isBottom = false; + private offsetTop: number; - constructor(el: StickySidebar['el'], spacer: StickySidebar['spacer'], marginTop = 0) { - this.el = el; - this.spacer = spacer; + constructor(container: StickySidebar['container'], marginTop = 0) { + this.container = container; + this.el = this.container.children[0] as HTMLElement; + this.el.style.position = 'sticky'; + this.spacer = document.createElement('div'); + this.container.prepend(this.spacer); this.marginTop = marginTop; + this.offsetTop = this.container.getBoundingClientRect().top; } public calc(scrollTop: number) { if (scrollTop > this.lastScrollTop) { // downscroll - const overflow = this.el.clientHeight - window.innerHeight; + const overflow = Math.max(0, (this.el.clientHeight + this.marginTop) - window.innerHeight); this.el.style.bottom = null; - this.el.style.top = `${-overflow}px`; + this.el.style.top = `${-overflow + this.marginTop}px`; this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight); if (this.isTop) { this.isTop = false; - this.spacer.style.marginTop = `${this.lastScrollTop}px`; + this.spacer.style.marginTop = `${Math.max(0, this.lastScrollTop + this.marginTop - this.offsetTop)}px`; } } else { // upscroll - const overflow = this.el.clientHeight - window.innerHeight; + const overflow = (this.el.clientHeight + this.marginTop) - window.innerHeight; this.el.style.top = null; - this.el.style.bottom = `${-overflow - this.marginTop}px`; + this.el.style.bottom = `${-overflow}px`; this.isTop = scrollTop <= this.el.offsetTop; if (this.isBottom) { this.isBottom = false; - const overflow = this.el.clientHeight - window.innerHeight; - this.spacer.style.marginTop = `${this.lastScrollTop - (overflow + this.marginTop)}px`; + this.spacer.style.marginTop = `${this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`; } } diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts index c1580c6367..b0bf620a7d 100644 --- a/src/client/scripts/theme.ts +++ b/src/client/scripts/theme.ts @@ -9,18 +9,19 @@ export type Theme = { props: Record<string, string>; }; -export const lightTheme: Theme = require('../themes/_light.json5'); -export const darkTheme: Theme = require('../themes/_dark.json5'); +export const lightTheme: Theme = require('@client/themes/_light.json5'); +export const darkTheme: Theme = require('@client/themes/_dark.json5'); export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); export const builtinThemes = [ - require('../themes/l-light.json5'), - require('../themes/l-apricot.json5'), + require('@client/themes/l-light.json5'), + require('@client/themes/l-apricot.json5'), + require('@client/themes/l-rainy.json5'), - require('../themes/d-dark.json5'), - require('../themes/d-persimmon.json5'), - require('../themes/d-black.json5'), + require('@client/themes/d-dark.json5'), + require('@client/themes/d-persimmon.json5'), + require('@client/themes/d-black.json5'), ] as Theme[]; let timeout = null; @@ -28,10 +29,10 @@ let timeout = null; export function applyTheme(theme: Theme, persist = true) { if (timeout) clearTimeout(timeout); - document.documentElement.classList.add('changing-theme'); + document.documentElement.classList.add('_themeChanging_'); timeout = setTimeout(() => { - document.documentElement.classList.remove('changing-theme'); + document.documentElement.classList.remove('_themeChanging_'); }, 1000); // Deep copy diff --git a/src/client/sidebar.ts b/src/client/sidebar.ts index 97036042cf..09b69fd815 100644 --- a/src/client/sidebar.ts +++ b/src/client/sidebar.ts @@ -143,6 +143,12 @@ export const sidebarDef = { unisonReload(); } }, { + text: 'pope', + action: () => { + localStorage.setItem('ui', 'pope'); + unisonReload(); + } + }, { text: 'Chat (β)', action: () => { localStorage.setItem('ui', 'chat'); diff --git a/src/client/store.ts b/src/client/store.ts index e6fdd12f1d..cdc244537f 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -150,7 +150,7 @@ export const defaultStore = markRaw(new Storage('base', { }, showGapBetweenNotesInTimeline: { where: 'device', - default: true + default: false }, darkMode: { where: 'device', @@ -212,10 +212,8 @@ type Plugin = { */ export class ColdDeviceStorage { public static default = { - // TODO: テーマをアカウントに保存するようになったのにもかかわらず、以下のどのテーマを使うかという情報だけがブラウザ保存になっていて、アカウント切り替えたりログアウトしたときに不具合が発生するのでなんとかする - // テーマIDを保存するのではなく、テーマ自体を保存するようにすれば解決するかも - darkTheme: '8050783a-7f63-445a-b270-36d0f6ba1677', - lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37', + lightTheme: require('@client/themes/l-light.json5') as Theme, + darkTheme: require('@client/themes/d-dark.json5') as Theme, syncDeviceDarkMode: true, chatOpenBehavior: 'page' as 'page' | 'window' | 'popout', plugins: [] as Plugin[], diff --git a/src/client/style.scss b/src/client/style.scss index b6a83d967e..e9c28f1827 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -82,9 +82,9 @@ html { } } -html.changing-theme { +html._themeChanging_ { &, * { - transition: background 1s ease !important; + transition: background 1s ease, border 1s ease !important; } } @@ -241,12 +241,22 @@ hr { border-radius: var(--radius); //border: var(--panelBorder); box-shadow: var(--panelShadow); - overflow: hidden; + overflow: clip; +} + +._block { + @extend ._panel; + margin: var(--margin) 0; +} + +._gap { + margin: var(--margin) 0; } ._card { @extend ._panel; + // TODO: _cardTitle に > ._title { margin: 0; padding: 22px 32px; @@ -262,6 +272,7 @@ hr { } } + // TODO: _cardContent に > ._content { padding: 32px; @@ -274,12 +285,13 @@ hr { } & + ._content { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } } + // TODO: _cardFooter に > ._footer { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); padding: 24px 32px; @media (max-width: 500px) { @@ -288,26 +300,6 @@ hr { } } -._noGap_ ._list_ { - @extend ._panel; - - > * { - margin: 0 !important; - border: none; - border-bottom: solid 1px var(--divider); - border-radius: 0; - box-shadow: none; - } -} - -._inContainer_ ._list_ > * { - margin: 0 !important; - border: none; - border-bottom: solid 1px var(--divider); - border-radius: 0; - box-shadow: none; -} - ._borderButton { @extend ._button; display: block; @@ -315,7 +307,7 @@ hr { padding: 10px; box-sizing: border-box; text-align: center; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: var(--radius); &:active { @@ -329,47 +321,59 @@ hr { contain: content; } -._section { - padding: var(--section-padding, 32px); +._root { + box-sizing: border-box; + margin: var(--root-margin, 32px) auto; + max-width: min(var(--baseContentWidth), calc(100% - (var(--root-margin, 32px) * 2))); - &:empty { - display: none; + @media (max-width: 500px) { + --root-margin: 10px; } +} - &:not(:empty) + ._section { - border-top: solid 1px var(--divider); +._monolithic_ { + ._section { + box-sizing: border-box; + padding: var(--root-margin, 32px); + + @media (max-width: 500px) { + --root-margin: 10px; + } + + & + ._section:not(:empty) { + border-top: solid 0.5px var(--divider); + } } +} - @media (max-width: 500px) { - padding: var(--section-padding, 10px); +._flat_ { + --root-margin: 0; + --baseContentWidth: 100%; + --panelShadow: none; - > ._title { - font-size: 1.1em; - font-weight: bold; - } + ._block { + //border-top: solid 0.5px var(--divider); + //border-bottom: solid 0.5px var(--divider); + border-radius: 0; + box-shadow: none; } - > ._title, - > ._content { - box-sizing: border-box; - max-width: var(--baseContentWidth); - margin: 0 auto; + ._isolated { + margin: var(--margin); } - > ._title { - margin-bottom: 24px; - font-weight: bold; + ._block._isolated { + border-radius: var(--radius); } - &._fitBottom { - padding-bottom: 0; - } -} + ._magnet { + margin-bottom: calc(var(--margin) * -1); + } -._narrow_ ._section { - > ._title { - padding: 8px; - font-size: 1em; + @media (max-width: 500px) { + ._root { + --root-margin: 0; + } } } @@ -394,12 +398,6 @@ hr { backdrop-filter: blur(15px); } -._vMargin { - & + ._vMargin { - margin-top: var(--margin); - } -} - ._table { > ._row { display: flex; diff --git a/src/client/symbols.ts b/src/client/symbols.ts new file mode 100644 index 0000000000..6913f29c28 --- /dev/null +++ b/src/client/symbols.ts @@ -0,0 +1 @@ +export const PAGE_INFO = Symbol('Page info'); diff --git a/src/client/theme-store.ts b/src/client/theme-store.ts index 3793debb9e..f291069692 100644 --- a/src/client/theme-store.ts +++ b/src/client/theme-store.ts @@ -1,6 +1,5 @@ import { api } from '@client/os'; import { $i } from '@client/account'; -import { ColdDeviceStorage } from './store'; import { Theme } from './scripts/theme'; const lsCacheKey = $i ? `themes:${$i.id}` : ''; diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5 index 2fa4853e6f..6414a7ad42 100644 --- a/src/client/themes/_dark.json5 +++ b/src/client/themes/_dark.json5 @@ -30,7 +30,7 @@ panelShadow: '" 0 8px 24px rgba(0, 0, 0, 0.12)', acrylicPanel: ':alpha<0.5<@panel', shadow: 'rgba(0, 0, 0, 0.3)', - header: ':alpha<0.7<@bg', + header: ':alpha<0.7<@panel', navBg: '@panel', navFg: '@fg', navHoverFg: ':lighten<17<@fg', diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5 index 94e6977502..0438b54a49 100644 --- a/src/client/themes/_light.json5 +++ b/src/client/themes/_light.json5 @@ -30,7 +30,7 @@ panelShadow: '" 0 8px 24px rgb(21 43 75 / 8%)', acrylicPanel: ':alpha<0.5<@panel', shadow: 'rgba(0, 0, 0, 0.1)', - header: ':alpha<0.7<@bg', + header: ':alpha<0.7<@panel', navBg: '@panel', navFg: '@fg', navHoverFg: ':darken<17<@fg', diff --git a/src/client/themes/d-dark.json5 b/src/client/themes/d-dark.json5 index 7dd29b4a0f..337eaa6396 100644 --- a/src/client/themes/d-dark.json5 +++ b/src/client/themes/d-dark.json5 @@ -18,7 +18,7 @@ panelHeaderDivider: '@divider', infoFg: '@accent', infoBg: 'rgb(0, 0, 0)', - header: ':alpha<0.7<@bg', + header: ':alpha<0.7<@panel', navBg: '#363636', renote: '@accent', mention: '#da6d35', diff --git a/src/client/themes/d-persimmon.json5 b/src/client/themes/d-persimmon.json5 index 862ccc6cea..a1ebaf59eb 100644 --- a/src/client/themes/d-persimmon.json5 +++ b/src/client/themes/d-persimmon.json5 @@ -16,7 +16,6 @@ panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)', infoFg: '@fg', infoBg: '#333c3b', - header: ':alpha<0.7<@bg', navBg: '#141714', renote: '@accent', mention: '@accent', diff --git a/src/client/themes/l-light.json5 b/src/client/themes/l-light.json5 index 34be20fae0..fdc1700b95 100644 --- a/src/client/themes/l-light.json5 +++ b/src/client/themes/l-light.json5 @@ -9,9 +9,9 @@ props: { bg: '#f9f9f9', - fg: '#636b71', + fg: '#676767', divider: 'rgb(223, 223, 223)', - header: ':alpha<0.7<@bg', + header: ':alpha<0.7<@panel', navBg: '#fff', panel: '#fff', panelShadow: '" 0 8px 24px rgb(21 43 75 / 8%)', diff --git a/src/client/themes/l-rainy.json5 b/src/client/themes/l-rainy.json5 new file mode 100644 index 0000000000..1edde1cabf --- /dev/null +++ b/src/client/themes/l-rainy.json5 @@ -0,0 +1,21 @@ +{ + id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', + + name: 'Mi Rainy', + author: 'syuilo', + + base: 'light', + + props: { + accent: '#5db0da', + bg: 'rgb(246 248 249)', + fg: '#636b71', + panel: '#fff', + divider: 'rgb(230 233 234)', + panelHeaderDivider: '@divider', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '@accent', + }, +} diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue index f150653a84..a60e1df73f 100644 --- a/src/client/ui/_common_/header.vue +++ b/src/client/ui/_common_/header.vue @@ -1,25 +1,32 @@ <template> -<div class="fdidabkb" :class="{ center }" :style="`--height:${height};`"> +<div class="fdidabkb" :class="{ center }" :style="`--height:${height};`" :key="key"> <transition :name="$store.state.animation ? 'header' : ''" mode="out-in" appear> - <button class="_button back" v-if="withBack && canBack" @click.stop="back()"><Fa :icon="faChevronLeft"/></button> + <button class="_button back" v-if="withBack && canBack" @click.stop="back()" v-tooltip="$ts.goBack"><Fa :icon="faChevronLeft"/></button> </transition> <template v-if="info"> <div class="titleContainer"> <div class="title"> <Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/> <MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true"/> - <span v-if="info.title" class="text">{{ info.title }}</span> - <MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> + <MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> + <span v-else-if="info.title" class="text">{{ info.title }}</span> </div> </div> - <button class="_button action" v-if="info.action" @click.stop="info.action.handler"><Fa :icon="info.action.icon" :key="info.action.icon"/></button> + <div class="buttons"> + <template v-if="info.actions && showActions"> + <button v-for="action in info.actions" class="_button button" @click.stop="action.handler" v-tooltip="action.text"><Fa :icon="action.icon"/></button> + </template> + <button v-if="showMenu" class="_button button" @click.stop="menu"><Fa :icon="faEllipsisH"/></button> + </div> </template> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import { faChevronLeft, faCircle } from '@fortawesome/free-solid-svg-icons'; +import { faChevronLeft, faCircle, faShareAlt, faEllipsisH } from '@fortawesome/free-solid-svg-icons'; +import { modalMenu } from '@client/os'; +import { url } from '@client/config'; export default defineComponent({ props: { @@ -41,12 +48,27 @@ export default defineComponent({ data() { return { canBack: false, + showActions: false, height: 0, - faChevronLeft, faCircle + key: 0, + faChevronLeft, faCircle, faShareAlt, faEllipsisH, }; }, + computed: { + showMenu() { + if (this.info.actions != null && !this.showActions) return true; + if (this.info.menu != null) return true; + if (this.info.share != null) return true; + return false; + } + }, + watch: { + info() { + this.key++; + }, + $route: { handler(to, from) { this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); @@ -57,8 +79,10 @@ export default defineComponent({ mounted() { this.height = this.$el.parentElement.offsetHeight + 'px'; + this.showActions = this.$el.parentElement.offsetWidth >= 500; new ResizeObserver((entries, observer) => { this.height = this.$el.parentElement.offsetHeight + 'px'; + this.showActions = this.$el.parentElement.offsetWidth >= 500; }).observe(this.$el); }, @@ -66,6 +90,33 @@ export default defineComponent({ back() { if (this.canBack) this.$router.back(); }, + + share() { + navigator.share({ + url: url + this.info.path, + ...this.info.share, + }); + }, + + menu(ev) { + let menu = this.info.menu ? this.info.menu() : []; + if (!this.showActions && this.info.actions) { + menu = [...this.info.actions.map(x => ({ + text: x.text, + icon: x.icon, + action: x.handler + })), menu.length > 0 ? null : undefined, ...menu]; + } + if (this.info.share) { + if (menu.length > 0) menu.push(null); + menu.push({ + text: this.$ts.share, + icon: faShareAlt, + action: this.share + }); + } + modalMenu(menu, ev.currentTarget || ev.target); + } } }); </script> @@ -74,59 +125,37 @@ export default defineComponent({ .fdidabkb { &.center { text-align: center; - } - - > .back { - height: var(--height); - width: var(--height); - } - - > .action { - height: var(--height); - width: var(--height); - } - - > .titleContainer { - width: calc(100% - (var(--height) * 2)); - - > .title { - height: var(--height); - > .avatar { - $size: 32px; - margin: calc((var(--height) - #{$size}) / 2) 8px calc((var(--height) - #{$size}) / 2) 0; - pointer-events: none; - } + > .titleContainer { + margin: 0 auto; } } -} -</style> -<style lang="scss" scoped> -.fdidabkb { > .back { position: absolute; z-index: 1; top: 0; left: 0; + height: var(--height); + width: var(--height); } - > .action { + > .buttons { position: absolute; z-index: 1; top: 0; right: 0; - } - &.center { - > .titleContainer { - margin: 0 auto; + > .button { + height: var(--height); + width: var(--height); } } > .titleContainer { overflow: auto; white-space: nowrap; + width: calc(100% - (var(--height) * 2)); > .title { display: inline-block; @@ -136,16 +165,7 @@ export default defineComponent({ text-overflow: ellipsis; padding: 0 16px; position: relative; - - > .indicator { - position: absolute; - top: initial; - right: 8px; - top: 8px; - color: var(--indicator); - font-size: 12px; - animation: blink 1s infinite; - } + height: var(--height); > .icon + .text { margin-left: 8px; @@ -157,6 +177,8 @@ export default defineComponent({ width: $size; height: $size; vertical-align: bottom; + margin: calc((var(--height) - #{$size}) / 2) 8px calc((var(--height) - #{$size}) / 2) 0; + pointer-events: none; } } } diff --git a/src/client/components/sidebar.vue b/src/client/ui/_common_/sidebar.vue index 61439781b4..6243d6fcc2 100644 --- a/src/client/components/sidebar.vue +++ b/src/client/ui/_common_/sidebar.vue @@ -235,12 +235,12 @@ export default defineComponent({ }, more(ev) { - os.popup(import('./launch-pad.vue'), {}, { + os.popup(import('@client/components/launch-pad.vue'), {}, { }, 'closed'); }, addAcount() { - os.popup(import('./signin-dialog.vue'), {}, { + os.popup(import('@client/components/signin-dialog.vue'), {}, { done: res => { addAccount(res.id, res.i); os.success(); @@ -249,7 +249,7 @@ export default defineComponent({ }, createAccount() { - os.popup(import('./signup-dialog.vue'), {}, { + os.popup(import('@client/components/signup-dialog.vue'), {}, { done: res => { addAccount(res.id, res.i); this.switchAccountWithToken(res.i); @@ -380,7 +380,7 @@ export default defineComponent({ > .divider { margin: 16px 0; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } > .item { @@ -443,13 +443,13 @@ export default defineComponent({ &:first-child { top: 0; margin-bottom: 16px; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); } &:last-child { bottom: 0; margin-top: 16px; - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } } } diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue index 91d3fb5c9d..d5c455d123 100644 --- a/src/client/ui/chat/index.vue +++ b/src/client/ui/chat/index.vue @@ -136,7 +136,7 @@ import { defineComponent, defineAsyncComponent } from 'vue'; import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, faAt, faLink, faEllipsisH, faGlobe } from '@fortawesome/free-solid-svg-icons'; import { faBell, faStar as farStar, faEnvelope, faComments, faCalendarAlt } from '@fortawesome/free-regular-svg-icons'; import { instanceName, url } from '@client/config'; -import XSidebar from '@client/components/sidebar.vue'; +import XSidebar from '@client/ui/_common_/sidebar.vue'; import XWidgets from './widgets.vue'; import XCommon from '../_common_/common.vue'; import XSide from './side.vue'; @@ -354,7 +354,7 @@ export default defineComponent({ flex-direction: column; width: 250px; height: 100vh; - border-right: solid 1px var(--divider); + border-right: solid 0.5px var(--divider); > .header, > .footer { $padding: 8px; @@ -367,11 +367,11 @@ export default defineComponent({ user-select: none; &.header { - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); } &.footer { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } > .left, > .right { @@ -526,7 +526,7 @@ export default defineComponent({ padding: $padding; box-sizing: border-box; background-color: var(--panel); - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); user-select: none; > .left { @@ -599,7 +599,7 @@ export default defineComponent({ > .side { width: 350px; - border-left: solid 1px var(--divider); + border-left: solid 0.5px var(--divider); &.widgets.sideViewOpening { @media (max-width: 1400px) { diff --git a/src/client/ui/chat/note-header.vue b/src/client/ui/chat/note-header.vue index 55228c4c38..be08183d39 100644 --- a/src/client/ui/chat/note-header.vue +++ b/src/client/ui/chat/note-header.vue @@ -79,7 +79,7 @@ export default defineComponent({ margin: 0 .5em 0 0; padding: 1px 6px; font-size: 80%; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: 3px; } diff --git a/src/client/ui/chat/note.sub.vue b/src/client/ui/chat/note.sub.vue index 6c778d1468..bb528dd936 100644 --- a/src/client/ui/chat/note.sub.vue +++ b/src/client/ui/chat/note.sub.vue @@ -130,7 +130,7 @@ export default defineComponent({ } > .reply { - border-left: solid 1px var(--divider); + border-left: solid 0.5px var(--divider); margin-top: 10px; } } diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue index 4afd7989e1..77b7440ca1 100644 --- a/src/client/ui/chat/note.vue +++ b/src/client/ui/chat/note.vue @@ -101,11 +101,11 @@ </template> <script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue'; -import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons'; +import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; +import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip, faShareAlt } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import { parse } from '../../../mfm/parse'; -import { sum, unique } from '../../../prelude/array'; +import * as mfm from 'mfm-js'; +import { sum } from '../../../prelude/array'; import XSub from './note.sub.vue'; import XNoteHeader from './note-header.vue'; import XNotePreview from './note-preview.vue'; @@ -122,6 +122,7 @@ import { userPage } from '@client/filters/user'; import * as os from '@client/os'; import { noteActions, noteViewInterruptors } from '@client/store'; import { reactionPicker } from '@client/scripts/reaction-picker'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; function markRawAll(...xs) { for (const x of xs) { @@ -238,21 +239,7 @@ export default defineComponent({ urls(): string[] { if (this.appearNote.text) { - const ast = parse(this.appearNote.text); - // TODO: 再帰的にURL要素がないか調べる - const urls = unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - - // unique without hash - // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] - const removeHash = x => x.replace(/#[^#]*$/, ''); - - return urls.reduce((array, url) => { - const removed = removeHash(url); - if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); - return array; - }, []); + return extractUrlFromMfm(mfm.parse(this.appearNote.text)); } else { return null; } @@ -620,6 +607,11 @@ export default defineComponent({ window.open(this.appearNote.url || this.appearNote.uri, '_blank'); } } : undefined, + { + icon: faShareAlt, + text: this.$ts.share, + action: this.share + }, null, statePromise.then(state => state.isFavorited ? { icon: faStar, @@ -849,6 +841,14 @@ export default defineComponent({ }); }, + share() { + navigator.share({ + title: this.$t('noteOf', { user: this.appearNote.user.name }), + text: this.appearNote.text, + url: `${url}/notes/${this.appearNote.id}` + }); + }, + focus() { this.$el.focus(); }, @@ -1140,7 +1140,7 @@ export default defineComponent({ } > .reply { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } } diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue index 5bb1a04d58..a9413ea8bc 100644 --- a/src/client/ui/chat/post-form.vue +++ b/src/client/ui/chat/post-form.vue @@ -52,11 +52,11 @@ import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlob import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons'; import insertTextAtCursor from 'insert-text-at-cursor'; import { length } from 'stringz'; -import { toASCII } from 'punycode'; -import { parse } from '../../../mfm/parse'; +import { toASCII } from 'punycode/'; +import * as mfm from 'mfm-js'; import { host, url } from '@client/config'; import { erase, unique } from '../../../prelude/array'; -import extractMentions from '@/misc/extract-mentions'; +import { extractMentions } from '@/misc/extract-mentions'; import getAcct from '@/misc/acct/render'; import { formatTimeString } from '@/misc/format-time-string'; import { Autocomplete } from '@client/scripts/autocomplete'; @@ -216,7 +216,7 @@ export default defineComponent({ } if (this.reply && this.reply.text != null) { - const ast = parse(this.reply.text); + const ast = mfm.parse(this.reply.text); for (const x of extractMentions(ast)) { const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`; @@ -567,7 +567,7 @@ export default defineComponent({ this.deleteDraft(); this.$emit('posted'); if (this.text && this.text != '') { - const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag); + const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); } @@ -615,7 +615,7 @@ export default defineComponent({ <style lang="scss" scoped> .pxiwixjf { position: relative; - border: solid 1px var(--divider); + border: solid 0.5px var(--divider); border-radius: 8px; > .form { @@ -696,7 +696,7 @@ export default defineComponent({ > .cw { z-index: 1; padding-bottom: 8px; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); } > .text { diff --git a/src/client/ui/chat/side.vue b/src/client/ui/chat/side.vue index 2645874ce4..2f182175ba 100644 --- a/src/client/ui/chat/side.vue +++ b/src/client/ui/chat/side.vue @@ -17,6 +17,7 @@ import * as os from '@client/os'; import copyToClipboard from '@client/scripts/copy-to-clipboard'; import { resolve } from '@client/router'; import { url } from '@client/config'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -51,8 +52,8 @@ export default defineComponent({ methods: { changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; } }, @@ -117,7 +118,7 @@ export default defineComponent({ .mrajymqm { $header-height: 54px; // TODO: どこかに集約したい - --section-padding: 16px; + --root-margin: 16px; --margin: var(--marginHalf); height: 100%; @@ -137,7 +138,7 @@ export default defineComponent({ -webkit-backdrop-filter: blur(32px); backdrop-filter: blur(32px); background-color: var(--header); - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); box-sizing: border-box; > ._button { diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue index a63db17b01..0429dbc9b1 100644 --- a/src/client/ui/deck.vue +++ b/src/client/ui/deck.vue @@ -36,7 +36,7 @@ import { } from '@fortawesome/free-regular-svg-icons'; import { v4 as uuid } from 'uuid'; import { host } from '@client/config'; import DeckColumnCore from '@client/ui/deck/column-core.vue'; -import XSidebar from '@client/components/sidebar.vue'; +import XSidebar from '@client/ui/_common_/sidebar.vue'; import { getScrollContainer } from '@client/scripts/scroll'; import * as os from '@client/os'; import { sidebarDef } from '@client/sidebar'; diff --git a/src/client/ui/deck/column.vue b/src/client/ui/deck/column.vue index 6a242c691a..3fae7c27ee 100644 --- a/src/client/ui/deck/column.vue +++ b/src/client/ui/deck/column.vue @@ -265,7 +265,7 @@ export default defineComponent({ <style lang="scss" scoped> .dnpfarvg { - --section-padding: 10px; + --root-margin: 10px; height: 100%; overflow: hidden; diff --git a/src/client/ui/deck/main-column.vue b/src/client/ui/deck/main-column.vue index 4577b0b533..2206fa5e13 100644 --- a/src/client/ui/deck/main-column.vue +++ b/src/client/ui/deck/main-column.vue @@ -4,7 +4,7 @@ <XHeader :info="pageInfo"/> </template> - <router-view v-slot="{ Component }"> + <router-view v-slot="{ Component }" class="_flat_"> <transition> <keep-alive :include="['timeline']"> <component :is="Component" :ref="changePage" @contextmenu.stop="onContextmenu"/> @@ -22,6 +22,7 @@ import XNotes from '@client/components/notes.vue'; import XHeader from '@client/ui/_common_/header.vue'; import { deckStore } from '@client/ui/deck/deck-store'; import * as os from '@client/os'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -51,8 +52,8 @@ export default defineComponent({ methods: { changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; } }, diff --git a/src/client/ui/default.side.vue b/src/client/ui/default.side.vue index 995f987a6a..89b648244f 100644 --- a/src/client/ui/default.side.vue +++ b/src/client/ui/default.side.vue @@ -20,6 +20,7 @@ import * as os from '@client/os'; import copyToClipboard from '@client/scripts/copy-to-clipboard'; import { resolve } from '@client/router'; import { url } from '@client/config'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -54,8 +55,8 @@ export default defineComponent({ methods: { changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; } }, @@ -118,7 +119,7 @@ export default defineComponent({ .qvzfzxam { $header-height: 58px; // TODO: どこかに集約したい - --section-padding: 16px; + --root-margin: 16px; --margin: var(--marginHalf); > .container { diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue new file mode 100644 index 0000000000..3e956679cd --- /dev/null +++ b/src/client/ui/default.sidebar.vue @@ -0,0 +1,381 @@ +<template> +<div class="npcljfve" :class="{ iconOnly }"> + <button class="item _button account" @click="openAccountMenu"> + <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/> + </button> + <div class="post" @click="post"> + <MkButton class="button" primary full> + <Fa :icon="faPencilAlt" fixed-width/><span class="text" v-if="!iconOnly">{{ $ts.note }}</span> + </MkButton> + </div> + <div class="divider"></div> + <MkA class="item index" active-class="active" to="/" exact> + <Fa :icon="faHome" fixed-width/><span class="text">{{ $ts.timeline }}</span> + </MkA> + <template v-for="item in menu"> + <div v-if="item === '-'" class="divider"></div> + <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to"> + <Fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $ts[menuDef[item].title] }}</span> + <i v-if="menuDef[item].indicated"><Fa :icon="faCircle"/></i> + </component> + </template> + <div class="divider"></div> + <button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$i.isAdmin || $i.isModerator" @click="oepnInstanceMenu"> + <Fa :icon="faServer" fixed-width/><span class="text">{{ $ts.instance }}</span> + </button> + <button class="item _button" @click="more"> + <Fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $ts.more }}</span> + <i v-if="otherNavItemIndicated"><Fa :icon="faCircle"/></i> + </button> + <MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null"> + <Fa :icon="faCog" fixed-width/><span class="text">{{ $ts.settings }}</span> + </MkA> + <div class="divider"></div> + <div class="foo"> + <MkEmoji :normal="true" :no-style="true" emoji="🍮"/> + </div> + <!--<MisskeyLogo class="misskey"/>--> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram, faStream, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; +import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; +import { host } from '@client/config'; +import { search } from '@client/scripts/search'; +import * as os from '@client/os'; +import { sidebarDef } from '@client/sidebar'; +import { getAccounts, addAccount, login } from '@client/account'; +import MkButton from '@client/components/ui/button.vue'; +import { StickySidebar } from '@client/scripts/sticky-sidebar'; +import MisskeyLogo from '@/../assets/client/misskey.svg'; + +export default defineComponent({ + components: { + MkButton, + MisskeyLogo, + }, + + data() { + return { + host: host, + accounts: [], + connection: null, + menuDef: sidebarDef, + iconOnly: false, + settingsWindowed: false, + faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram + }; + }, + + computed: { + menu(): string[] { + return this.$store.state.menu; + }, + + otherNavItemIndicated(): boolean { + for (const def in this.menuDef) { + if (this.menu.includes(def)) continue; + if (this.menuDef[def].indicated) return true; + } + return false; + }, + }, + + watch: { + '$store.reactiveState.sidebarDisplay.value'() { + this.calcViewState(); + }, + + iconOnly() { + this.$nextTick(() => { + this.$emit('change-view-mode'); + }); + }, + }, + + created() { + window.addEventListener('resize', this.calcViewState); + this.calcViewState(); + }, + + mounted() { + const sticky = new StickySidebar(this.$el.parentElement, 16); + window.addEventListener('scroll', () => { + sticky.calc(window.scrollY); + }, { passive: true }); + }, + + methods: { + calcViewState() { + this.iconOnly = (window.innerWidth <= 1400) || (this.$store.state.sidebarDisplay === 'icon'); + this.settingsWindowed = (window.innerWidth > 1400); + }, + + post() { + os.post(); + }, + + search() { + search(); + }, + + async openAccountMenu(ev) { + const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id); + const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) }); + + const accountItemPromises = storedAccounts.map(a => new Promise(res => { + accountsPromise.then(accounts => { + const account = accounts.find(x => x.id === a.id); + if (account == null) return res(null); + res({ + type: 'user', + user: account, + action: () => { this.switchAccount(account); } + }); + }); + })); + + os.modalMenu([...[{ + type: 'link', + text: this.$ts.profile, + to: `/@${ this.$i.username }`, + avatar: this.$i, + }, null, ...accountItemPromises, { + icon: faPlus, + text: this.$ts.addAcount, + action: () => { + os.modalMenu([{ + text: this.$ts.existingAcount, + action: () => { this.addAcount(); }, + }, { + text: this.$ts.createAccount, + action: () => { this.createAccount(); }, + }], ev.currentTarget || ev.target); + }, + }]], ev.currentTarget || ev.target, { + align: 'left' + }); + }, + + oepnInstanceMenu(ev) { + os.modalMenu([{ + type: 'link', + text: this.$ts.dashboard, + to: '/instance', + icon: faTachometerAlt, + }, null, this.$i.isAdmin ? { + type: 'link', + text: this.$ts.settings, + to: '/instance/settings', + icon: faCog, + } : undefined, { + type: 'link', + text: this.$ts.customEmojis, + to: '/instance/emojis', + icon: faLaugh, + }, { + type: 'link', + text: this.$ts.users, + to: '/instance/users', + icon: faUsers, + }, { + type: 'link', + text: this.$ts.files, + to: '/instance/files', + icon: faCloud, + }, { + type: 'link', + text: this.$ts.jobQueue, + to: '/instance/queue', + icon: faExchangeAlt, + }, { + type: 'link', + text: this.$ts.federation, + to: '/instance/federation', + icon: faGlobe, + }, { + type: 'link', + text: this.$ts.relays, + to: '/instance/relays', + icon: faProjectDiagram, + }, { + type: 'link', + text: this.$ts.announcements, + to: '/instance/announcements', + icon: faBroadcastTower, + }, { + type: 'link', + text: this.$ts.abuseReports, + to: '/instance/abuses', + icon: faExclamationCircle, + }, { + type: 'link', + text: this.$ts.logs, + to: '/instance/logs', + icon: faStream, + }], ev.currentTarget || ev.target); + }, + + more(ev) { + os.popup(import('@client/components/launch-pad.vue'), {}, { + }, 'closed'); + }, + + addAcount() { + os.popup(import('@client/components/signin-dialog.vue'), {}, { + done: res => { + addAccount(res.id, res.i); + os.success(); + }, + }, 'closed'); + }, + + createAccount() { + os.popup(import('@client/components/signup-dialog.vue'), {}, { + done: res => { + addAccount(res.id, res.i); + this.switchAccountWithToken(res.i); + }, + }, 'closed'); + }, + + switchAccount(account: any) { + const storedAccounts = getAccounts(); + const token = storedAccounts.find(x => x.id === account.id).token; + this.switchAccountWithToken(token); + }, + + switchAccountWithToken(token: string) { + login(token); + }, + } +}); +</script> + +<style lang="scss" scoped> +.npcljfve { + $ui-font-size: 1em; // TODO: どこかに集約したい + $nav-icon-only-width: 78px; // TODO: どこかに集約したい + $avatar-size: 32px; + $avatar-margin: 8px; + + padding: 0 16px; + box-sizing: border-box; + width: 260px; + + &.iconOnly { + flex: 0 0 $nav-icon-only-width; + width: $nav-icon-only-width !important; + + > .divider { + margin: 8px auto; + width: calc(100% - 32px); + } + + > .post { + > .button { + width: 46px; + height: 46px; + padding: 0; + } + } + + > .item { + padding-left: 0; + width: 100%; + text-align: center; + font-size: $ui-font-size * 1.1; + line-height: 3.7rem; + + > [data-icon], + > .avatar { + margin-right: 0; + } + + > i { + left: 10px; + } + + > .text { + display: none; + } + + } + } + + > .divider { + margin: 10px 0; + border-top: solid 0.5px var(--divider); + } + + > .post { + position: sticky; + top: 0; + z-index: 1; + padding: 16px 0; + background: var(--bg); + + > .button { + min-width: 0; + } + } + + > .misskey { + fill: currentColor; + } + + > .foo { + text-align: center; + padding: 8px 0 16px 0; + opacity: 0.5; + } + + > .item { + position: relative; + display: block; + font-size: $ui-font-size; + line-height: 2.6rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + + > [data-icon] { + width: 32px; + } + + > [data-icon], + > .avatar { + margin-right: $avatar-margin; + } + + > .avatar { + width: $avatar-size; + height: $avatar-size; + vertical-align: middle; + } + + > i { + position: absolute; + top: 0; + left: 20px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + } + + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } + + &.active { + color: var(--navActive); + } + } +} +</style> diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue index 38f98f6365..ec59bf0039 100644 --- a/src/client/ui/default.vue +++ b/src/client/ui/default.vue @@ -1,13 +1,15 @@ <template> -<div class="mk-app" :class="{ wallpaper }"> - <XSidebar ref="nav" class="sidebar"/> +<div class="mk-app" :class="{ wallpaper, isMobile }"> + <div class="columns" :class="{ fullView }"> + <div class="sidebar" ref="sidebar" v-if="!isMobile"> + <XSidebar/> + </div> - <div class="contents" ref="contents" :class="{ withHeader: $store.state.titlebar }" @contextmenu.stop="onContextmenu"> - <header v-if="$store.state.titlebar" class="header" ref="header" @click="onHeaderClick"> - <XHeader :info="pageInfo"/> - </header> - <main ref="main"> - <div class="content"> + <main class="main _panel" @contextmenu.stop="onContextmenu"> + <header v-if="$store.state.titlebar" class="header" @click="onHeaderClick"> + <XHeader :info="pageInfo"/> + </header> + <div class="content" :class="{ _flat_: !fullView }"> <router-view v-slot="{ Component }"> <transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition"> <keep-alive :include="['timeline']"> @@ -16,26 +18,22 @@ </transition> </router-view> </div> - <div class="spacer"></div> </main> - </div> - - <XSide v-if="isDesktop" class="side" ref="side"/> - <div v-if="isDesktop" class="widgets"> - <div ref="widgetsSpacer"></div> - <XWidgets @mounted="attachSticky"/> + <div v-if="isDesktop" class="widgets" ref="widgets"> + <XWidgets @mounted="attachSticky"/> + </div> </div> - <div class="buttons" :class="{ navHidden }"> - <button class="button nav _button" @click="showNav" ref="navButton"><Fa :icon="faBars"/><i v-if="navIndicated"><Fa :icon="faCircle"/></i></button> + <div class="buttons" v-if="isMobile"> + <button class="button nav _button" @click="showDrawerNav" ref="navButton"><Fa :icon="faBars"/><i v-if="navIndicated"><Fa :icon="faCircle"/></i></button> <button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><Fa :icon="faHome"/></button> <button class="button notifications _button" @click="$router.push('/my/notifications')"><Fa :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></button> <button class="button widget _button" @click="widgetsShowing = true"><Fa :icon="faLayerGroup"/></button> <button class="button post _button" @click="post"><Fa :icon="faPencilAlt"/></button> </div> - <button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><Fa :icon="faLayerGroup"/></button> + <XDrawerSidebar ref="drawerNav" class="sidebar" v-if="isMobile"/> <transition name="tray-back"> <div class="tray-back _modalBg" @@ -55,43 +53,38 @@ <script lang="ts"> import { defineComponent, defineAsyncComponent } from 'vue'; -import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faExpand, faPencilAlt, faCompress } from '@fortawesome/free-solid-svg-icons'; import { faBell } from '@fortawesome/free-regular-svg-icons'; import { instanceName } from '@client/config'; import { StickySidebar } from '@client/scripts/sticky-sidebar'; -import XSidebar from '@client/components/sidebar.vue'; +import XSidebar from './default.sidebar.vue'; +import XDrawerSidebar from '@client/ui/_common_/sidebar.vue'; import XCommon from './_common_/common.vue'; import XHeader from './_common_/header.vue'; -import XSide from './default.side.vue'; import * as os from '@client/os'; import { sidebarDef } from '@client/sidebar'; +import * as symbols from '@client/symbols'; const DESKTOP_THRESHOLD = 1100; +const MOBILE_THRESHOLD = 600; export default defineComponent({ components: { XCommon, XSidebar, + XDrawerSidebar, XHeader, XWidgets: defineAsyncComponent(() => import('./default.widgets.vue')), - XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる - }, - - provide() { - return { - sideViewHook: this.isDesktop ? (url) => { - this.$refs.side.navigate(url); - } : null - }; }, data() { return { pageInfo: null, - isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, menuDef: sidebarDef, - navHidden: false, + isMobile: window.innerWidth <= MOBILE_THRESHOLD, + isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, widgetsShowing: false, + fullView: false, wallpaper: localStorage.getItem('wallpaper') != null, faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, }; @@ -125,46 +118,23 @@ export default defineComponent({ }, mounted() { - this.adjustUI(); - - const ro = new ResizeObserver((entries, observer) => { - this.adjustUI(); - }); - - ro.observe(this.$refs.contents); - - window.addEventListener('resize', this.adjustUI, { passive: true }); - - if (!this.isDesktop) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true; - }, { passive: true }); - } + window.addEventListener('resize', () => { + this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD); + this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD); + }, { passive: true }); }, methods: { changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; document.title = `${this.pageInfo.title} | ${instanceName}`; } }, - adjustUI() { - const navWidth = this.$refs.nav.$el.offsetWidth; - this.navHidden = navWidth === 0; - if (this.$refs.contents == null) return; - const width = this.$refs.contents.offsetWidth; - if (this.$refs.header) this.$refs.header.style.width = `${width}px`; - }, - - showNav() { - this.$refs.nav.show(); - }, - - attachSticky(el) { - const sticky = new StickySidebar(el, this.$refs.widgetsSpacer); + attachSticky() { + const sticky = new StickySidebar(this.$refs.widgets, 16); window.addEventListener('scroll', () => { sticky.calc(window.scrollY); }, { passive: true }); @@ -178,6 +148,10 @@ export default defineComponent({ window.scroll({ top: 0, behavior: 'smooth' }); }, + showDrawerNav() { + this.$refs.drawerNav.show(); + }, + onTransition() { if (window._scroll) window._scroll(); }, @@ -201,10 +175,10 @@ export default defineComponent({ type: 'label', text: path, }, { - icon: faColumns, - text: this.$ts.openInSideView, + icon: this.fullView ? faCompress : faExpand, + text: this.fullView ? this.$ts.quitFullView : this.$ts.fullView, action: () => { - this.$refs.side.navigate(path); + this.fullView = !this.fullView; } }, { icon: faWindowMaximize, @@ -242,99 +216,119 @@ export default defineComponent({ } .mk-app { - $header-height: 58px; // TODO: どこかに集約したい - $ui-font-size: 1em; // TODO: どこかに集約したい - $widgets-hide-threshold: 1090px; + $header-height: 50px; + $ui-font-size: 1em; + $widgets-hide-threshold: 1200px; + $nav-icon-only-width: 78px; // TODO: どこかに集約したい // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ min-height: calc(var(--vh, 1vh) * 100); box-sizing: border-box; - display: flex; &.wallpaper { background: var(--wallpaperOverlay); //backdrop-filter: blur(4px); } - > .contents { - width: 100%; - min-width: 0; + &.isMobile { + > .columns { + display: block; + margin: 0; + + > .main { + margin: 0; + padding-bottom: 92px; + border: none; + width: 100%; + border-radius: 0; - &.withHeader { - padding-top: $header-height; + > .header { + width: 100%; + } + } } + } - > .header { - position: fixed; - z-index: 1000; - top: 0; - height: $header-height; - width: 100%; - line-height: $header-height; - text-align: center; - font-weight: bold; - //background-color: var(--panel); - -webkit-backdrop-filter: blur(32px); - backdrop-filter: blur(32px); - background-color: var(--header); - //border-bottom: solid 1px var(--divider); - user-select: none; + > .columns { + display: flex; + justify-content: center; + max-width: 100%; + margin: 32px 0; + + &.fullView { + margin: 0; + + > .sidebar { + display: none; + } + + > .widgets { + display: none; + } + + > .main { + margin: 0; + border-radius: 0; + box-shadow: none; + width: 100%; + } } - > main { + > .main { min-width: 0; + width: 750px; + margin: 0 16px 0 0; + background: var(--bg); + --margin: 12px; + + > .header { + position: sticky; + z-index: 1000; + top: 0; + height: $header-height; + line-height: $header-height; + -webkit-backdrop-filter: blur(32px); + backdrop-filter: blur(32px); + background-color: var(--header); + border-bottom: solid 0.5px var(--divider); + } > .content { - > * { - // ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ - min-height: calc((var(--vh, 1vh) * 100) - #{$header-height}); - } + background: var(--bg); + --stickyTop: #{$header-height}; } - > .spacer { - height: 82px; + @media (max-width: 850px) { + padding-top: $header-height; - @media (min-width: ($widgets-hide-threshold + 1px)) { - display: none; + > .header { + position: fixed; + width: calc(100% - #{$nav-icon-only-width}); } } } - } - > .side { - min-width: 370px; - max-width: 370px; - border-left: solid 1px var(--divider); - } + > .widgets { + //--panelShadow: none; - > .widgets { - padding: 0 var(--margin); - border-left: solid 1px var(--divider); - - @media (max-width: $widgets-hide-threshold) { - display: none; + @media (max-width: $widgets-hide-threshold) { + display: none; + } } - } - > .widgetButton { - display: block; - position: fixed; - z-index: 1000; - bottom: 32px; - right: 32px; - width: 64px; - height: 64px; - border-radius: 100%; - box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); - font-size: 22px; - background: var(--panel); + @media (max-width: 850px) { + margin: 0; - &.navHidden { - display: none; - } + > .sidebar { + border-right: solid 0.5px var(--divider); + } - @media (min-width: ($widgets-hide-threshold + 1px)) { - display: none; + > .main { + margin: 0; + border-radius: 0; + box-shadow: none; + width: 100%; + } } } @@ -349,10 +343,7 @@ export default defineComponent({ -webkit-backdrop-filter: blur(32px); backdrop-filter: blur(32px); background-color: var(--header); - - &:not(.navHidden) { - display: none; - } + border-top: solid 0.5px var(--divider); > .button { position: relative; @@ -429,6 +420,3 @@ export default defineComponent({ } } </style> - -<style lang="scss"> -</style> diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue index 35d3442bb2..b12de841a7 100644 --- a/src/client/ui/default.widgets.vue +++ b/src/client/ui/default.widgets.vue @@ -61,8 +61,6 @@ export default defineComponent({ .efzpzdvf { position: sticky; height: min-content; - min-height: 100vh; - padding: var(--margin) 0; box-sizing: border-box; > * { diff --git a/src/client/ui/desktop.vue b/src/client/ui/desktop.vue index 1480fd1840..a60aed6841 100644 --- a/src/client/ui/desktop.vue +++ b/src/client/ui/desktop.vue @@ -12,7 +12,7 @@ import { host } from '@client/config'; import { search } from '@client/scripts/search'; import XCommon from './_common_/common.vue'; import * as os from '@client/os'; -import XSidebar from '@client/components/sidebar.vue'; +import XSidebar from '@client/ui/_common_/sidebar.vue'; import { sidebarDef } from '@client/sidebar'; import { ColdDeviceStorage } from '@client/store'; diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue new file mode 100644 index 0000000000..3e2ee28817 --- /dev/null +++ b/src/client/ui/universal.vue @@ -0,0 +1,435 @@ +<template> +<div class="mk-app" :class="{ wallpaper }"> + <XSidebar ref="nav" class="sidebar"/> + + <div class="contents" ref="contents" :class="{ withHeader: $store.state.titlebar }" @contextmenu.stop="onContextmenu"> + <header v-if="$store.state.titlebar" class="header" ref="header" @click="onHeaderClick"> + <XHeader :info="pageInfo"/> + </header> + <main ref="main"> + <div class="content"> + <router-view v-slot="{ Component }"> + <transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition"> + <keep-alive :include="['timeline']"> + <component :is="Component" :ref="changePage"/> + </keep-alive> + </transition> + </router-view> + </div> + <div class="spacer"></div> + </main> + </div> + + <XSide v-if="isDesktop" class="side" ref="side"/> + + <div v-if="isDesktop" class="widgets" ref="widgets"> + <XWidgets @mounted="attachSticky"/> + </div> + + <div class="buttons" :class="{ navHidden }"> + <button class="button nav _button" @click="showNav" ref="navButton"><Fa :icon="faBars"/><i v-if="navIndicated"><Fa :icon="faCircle"/></i></button> + <button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><Fa :icon="faHome"/></button> + <button class="button notifications _button" @click="$router.push('/my/notifications')"><Fa :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></button> + <button class="button widget _button" @click="widgetsShowing = true"><Fa :icon="faLayerGroup"/></button> + <button class="button post _button" @click="post"><Fa :icon="faPencilAlt"/></button> + </div> + + <button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><Fa :icon="faLayerGroup"/></button> + + <transition name="tray-back"> + <div class="tray-back _modalBg" + v-if="widgetsShowing" + @click="widgetsShowing = false" + @touchstart.passive="widgetsShowing = false" + ></div> + </transition> + + <transition name="tray"> + <XWidgets v-if="widgetsShowing" class="tray"/> + </transition> + + <XCommon/> +</div> +</template> + +<script lang="ts"> +import { defineComponent, defineAsyncComponent } from 'vue'; +import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faBell } from '@fortawesome/free-regular-svg-icons'; +import { instanceName } from '@client/config'; +import { StickySidebar } from '@client/scripts/sticky-sidebar'; +import XSidebar from '@client/ui/_common_/sidebar.vue'; +import XCommon from './_common_/common.vue'; +import XHeader from './_common_/header.vue'; +import XSide from './default.side.vue'; +import * as os from '@client/os'; +import { sidebarDef } from '@client/sidebar'; +import * as symbols from '@client/symbols'; + +const DESKTOP_THRESHOLD = 1100; + +export default defineComponent({ + components: { + XCommon, + XSidebar, + XHeader, + XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')), + XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる + }, + + provide() { + return { + sideViewHook: this.isDesktop ? (url) => { + this.$refs.side.navigate(url); + } : null + }; + }, + + data() { + return { + pageInfo: null, + isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, + menuDef: sidebarDef, + navHidden: false, + widgetsShowing: false, + wallpaper: localStorage.getItem('wallpaper') != null, + faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, + }; + }, + + computed: { + navIndicated(): boolean { + for (const def in this.menuDef) { + if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから + if (this.menuDef[def].indicated) return true; + } + return false; + } + }, + + created() { + document.documentElement.style.overflowY = 'scroll'; + + if (this.$store.state.widgets.length === 0) { + this.$store.set('widgets', [{ + name: 'calendar', + id: 'a', place: 'right', data: {} + }, { + name: 'notifications', + id: 'b', place: 'right', data: {} + }, { + name: 'trends', + id: 'c', place: 'right', data: {} + }]); + } + }, + + mounted() { + this.adjustUI(); + + const ro = new ResizeObserver((entries, observer) => { + this.adjustUI(); + }); + + ro.observe(this.$refs.contents); + + window.addEventListener('resize', this.adjustUI, { passive: true }); + + if (!this.isDesktop) { + window.addEventListener('resize', () => { + if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true; + }, { passive: true }); + } + }, + + methods: { + changePage(page) { + if (page == null) return; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; + document.title = `${this.pageInfo.title} | ${instanceName}`; + } + }, + + adjustUI() { + const navWidth = this.$refs.nav.$el.offsetWidth; + this.navHidden = navWidth === 0; + if (this.$refs.contents == null) return; + const width = this.$refs.contents.offsetWidth; + if (this.$refs.header) this.$refs.header.style.width = `${width}px`; + }, + + showNav() { + this.$refs.nav.show(); + }, + + attachSticky(el) { + const sticky = new StickySidebar(this.$refs.widgets); + window.addEventListener('scroll', () => { + sticky.calc(window.scrollY); + }, { passive: true }); + }, + + post() { + os.post(); + }, + + top() { + window.scroll({ top: 0, behavior: 'smooth' }); + }, + + onTransition() { + if (window._scroll) window._scroll(); + }, + + onHeaderClick() { + window.scroll({ top: 0, behavior: 'smooth' }); + }, + + onContextmenu(e) { + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + }; + if (isLink(e.target)) return; + if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return; + if (window.getSelection().toString() !== '') return; + const path = this.$route.path; + os.contextMenu([{ + type: 'label', + text: path, + }, { + icon: faColumns, + text: this.$ts.openInSideView, + action: () => { + this.$refs.side.navigate(path); + } + }, { + icon: faWindowMaximize, + text: this.$ts.openInWindow, + action: () => { + os.pageWindow(path); + } + }], e); + }, + } +}); +</script> + +<style lang="scss" scoped> +.tray-enter-active, +.tray-leave-active { + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.tray-enter-from, +.tray-leave-active { + opacity: 0; + transform: translateX(240px); +} + +.tray-back-enter-active, +.tray-back-leave-active { + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.tray-back-enter-from, +.tray-back-leave-active { + opacity: 0; +} + +.mk-app { + $header-height: 58px; // TODO: どこかに集約したい + $ui-font-size: 1em; // TODO: どこかに集約したい + $widgets-hide-threshold: 1090px; + + // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + min-height: calc(var(--vh, 1vh) * 100); + box-sizing: border-box; + display: flex; + + &.wallpaper { + background: var(--wallpaperOverlay); + //backdrop-filter: blur(4px); + } + + > .contents { + width: 100%; + min-width: 0; + --stickyTop: #{$header-height}; + + &.withHeader { + padding-top: $header-height; + } + + > .header { + position: fixed; + z-index: 1000; + top: 0; + height: $header-height; + width: 100%; + line-height: $header-height; + text-align: center; + font-weight: bold; + //background-color: var(--panel); + -webkit-backdrop-filter: blur(32px); + backdrop-filter: blur(32px); + background-color: var(--header); + //border-bottom: solid 0.5px var(--divider); + user-select: none; + } + + > main { + min-width: 0; + + > .content { + > * { + // ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + min-height: calc((var(--vh, 1vh) * 100) - #{$header-height}); + } + } + + > .spacer { + height: 82px; + + @media (min-width: ($widgets-hide-threshold + 1px)) { + display: none; + } + } + } + } + + > .side { + min-width: 370px; + max-width: 370px; + border-left: solid 0.5px var(--divider); + } + + > .widgets { + padding: 0 var(--margin); + border-left: solid 0.5px var(--divider); + + @media (max-width: $widgets-hide-threshold) { + display: none; + } + } + + > .widgetButton { + display: block; + position: fixed; + z-index: 1000; + bottom: 32px; + right: 32px; + width: 64px; + height: 64px; + border-radius: 100%; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); + font-size: 22px; + background: var(--panel); + + &.navHidden { + display: none; + } + + @media (min-width: ($widgets-hide-threshold + 1px)) { + display: none; + } + } + + > .buttons { + position: fixed; + z-index: 1000; + bottom: 0; + padding: 16px; + display: flex; + width: 100%; + box-sizing: border-box; + -webkit-backdrop-filter: blur(32px); + backdrop-filter: blur(32px); + background-color: var(--header); + + &:not(.navHidden) { + display: none; + } + + > .button { + position: relative; + flex: 1; + padding: 0; + margin: auto; + height: 64px; + border-radius: 8px; + background: var(--panel); + color: var(--fg); + + &:not(:last-child) { + margin-right: 12px; + } + + @media (max-width: 400px) { + height: 60px; + + &:not(:last-child) { + margin-right: 8px; + } + } + + &:hover { + background: var(--X2); + } + + > i { + position: absolute; + top: 0; + left: 0; + color: var(--indicator); + font-size: 16px; + animation: blink 1s infinite; + } + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + + > * { + font-size: 22px; + } + + &:disabled { + cursor: default; + + > * { + opacity: 0.5; + } + } + } + } + + > .tray-back { + z-index: 1001; + } + + > .tray { + position: fixed; + top: 0; + right: 0; + z-index: 1001; + // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + height: calc(var(--vh, 1vh) * 100); + padding: var(--margin); + box-sizing: border-box; + overflow: auto; + background: var(--bg); + } +} +</style> + +<style lang="scss"> +</style> diff --git a/src/client/ui/universal.widgets.vue b/src/client/ui/universal.widgets.vue new file mode 100644 index 0000000000..35d3442bb2 --- /dev/null +++ b/src/client/ui/universal.widgets.vue @@ -0,0 +1,81 @@ +<template> +<div class="efzpzdvf"> + <XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> + + <button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button> + <button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> +</div> +</template> + +<script lang="ts"> +import { defineComponent, defineAsyncComponent } from 'vue'; +import { faPencilAlt, faPlus, faBars, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons'; +import XWidgets from '@client/components/widgets.vue'; +import * as os from '@client/os'; + +export default defineComponent({ + components: { + XWidgets + }, + + emits: ['mounted'], + + data() { + return { + editMode: false, + faPencilAlt, faPlus, faBars, faTimes, faCheck, + }; + }, + + mounted() { + this.$emit('mounted', this.$el); + }, + + methods: { + addWidget(widget) { + this.$store.set('widgets', [{ + ...widget, + place: null, + }, ...this.$store.state.widgets]); + }, + + removeWidget(widget) { + this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id)); + }, + + updateWidget({ id, data }) { + this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? { + ...w, + data: data + } : w)); + }, + + updateWidgets(widgets) { + this.$store.set('widgets', widgets); + } + } +}); +</script> + +<style lang="scss" scoped> +.efzpzdvf { + position: sticky; + height: min-content; + min-height: 100vh; + padding: var(--margin) 0; + box-sizing: border-box; + + > * { + margin: var(--margin) 0; + width: 300px; + + &:first-child { + margin-top: 0; + } + } + + > .add { + margin: 0 auto; + } +} +</style> diff --git a/src/client/ui/visitor/a.vue b/src/client/ui/visitor/a.vue index 2aed50100c..1b080c9e82 100644 --- a/src/client/ui/visitor/a.vue +++ b/src/client/ui/visitor/a.vue @@ -32,7 +32,7 @@ </main> <div class="powered-by"> <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small> + <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> </div> </div> </div> @@ -49,6 +49,7 @@ import MkPagination from '@client/components/ui/pagination.vue'; import MkButton from '@client/components/ui/button.vue'; import XHeader from './header.vue'; import { ColdDeviceStorage } from '@client/store'; +import * as symbols from '@client/symbols'; const DESKTOP_THRESHOLD = 1100; @@ -110,8 +111,8 @@ export default defineComponent({ changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; } }, diff --git a/src/client/ui/visitor/b.vue b/src/client/ui/visitor/b.vue index bd00773c57..da2ae45180 100644 --- a/src/client/ui/visitor/b.vue +++ b/src/client/ui/visitor/b.vue @@ -1,6 +1,6 @@ <template> <div class="mk-app"> - <a v-if="root" href="https://github.com/syuilo/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> + <a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> <div class="side" v-if="!narrow && !root"> <XKanban class="kanban" full/> @@ -20,7 +20,7 @@ </main> <div class="powered-by" v-if="!root"> <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small> + <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> </div> </div> </div> @@ -61,6 +61,7 @@ import MkButton from '@client/components/ui/button.vue'; import XHeader from './header.vue'; import XKanban from './kanban.vue'; import { ColdDeviceStorage } from '@client/store'; +import * as symbols from '@client/symbols'; const DESKTOP_THRESHOLD = 1100; @@ -124,8 +125,8 @@ export default defineComponent({ methods: { changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; } }, diff --git a/src/client/ui/visitor/kanban.vue b/src/client/ui/visitor/kanban.vue index 8317f24402..2ed8cf4bc0 100644 --- a/src/client/ui/visitor/kanban.vue +++ b/src/client/ui/visitor/kanban.vue @@ -28,7 +28,7 @@ </div> <div class="powered-by" v-if="poweredBy"> <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small> + <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> </div> </template> </div> diff --git a/src/client/ui/zen.vue b/src/client/ui/zen.vue index 9215a639b0..af3e53225e 100644 --- a/src/client/ui/zen.vue +++ b/src/client/ui/zen.vue @@ -28,6 +28,7 @@ import { faBell } from '@fortawesome/free-regular-svg-icons'; import { host } from '@client/config'; import XHeader from './_common_/header.vue'; import XCommon from './_common_/common.vue'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -50,8 +51,8 @@ export default defineComponent({ methods: { changePage(page) { if (page == null) return; - if (page.INFO) { - this.pageInfo = page.INFO; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; } }, @@ -94,7 +95,7 @@ export default defineComponent({ -webkit-backdrop-filter: blur(32px); backdrop-filter: blur(32px); background-color: var(--header); - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); } > main { diff --git a/src/client/widgets/aiscript.vue b/src/client/widgets/aiscript.vue index 84390866bd..f24e033593 100644 --- a/src/client/widgets/aiscript.vue +++ b/src/client/widgets/aiscript.vue @@ -122,7 +122,7 @@ export default defineComponent({ color: var(--fg); background: transparent; border: none; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); border-radius: 0; box-sizing: border-box; font: inherit; @@ -147,7 +147,7 @@ export default defineComponent({ } > .logs { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); text-align: left; padding: 16px; diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue index 6eb656ce05..f0a79a31a6 100644 --- a/src/client/widgets/federation.vue +++ b/src/client/widgets/federation.vue @@ -100,7 +100,7 @@ export default defineComponent({ display: flex; align-items: center; padding: 14px 16px; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); > img { display: block; diff --git a/src/client/widgets/job-queue.vue b/src/client/widgets/job-queue.vue index aaf2b2fc8f..0b560ca56e 100644 --- a/src/client/widgets/job-queue.vue +++ b/src/client/widgets/job-queue.vue @@ -136,7 +136,7 @@ export default defineComponent({ padding: 16px; &:not(:first-child) { - border-top: solid 1px var(--divider); + border-top: solid 0.5px var(--divider); } > .label { diff --git a/src/client/widgets/memo.vue b/src/client/widgets/memo.vue index 4a7786312a..c3ab2934a8 100644 --- a/src/client/widgets/memo.vue +++ b/src/client/widgets/memo.vue @@ -77,7 +77,7 @@ export default defineComponent({ color: var(--fg); background: transparent; border: none; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); border-radius: 0; box-sizing: border-box; font: inherit; diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue index b4c2201862..046556ef1c 100644 --- a/src/client/widgets/notifications.vue +++ b/src/client/widgets/notifications.vue @@ -3,7 +3,7 @@ <template #header><Fa :icon="faBell"/>{{ $ts.notifications }}</template> <template #func><button @click="configure()" class="_button"><Fa :icon="faCog"/></button></template> - <div> + <div class="_flat_"> <XNotifications :include-types="props.includingTypes"/> </div> </MkContainer> diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue index 300e3b31de..cd7202f1f8 100644 --- a/src/client/widgets/trends.vue +++ b/src/client/widgets/trends.vue @@ -79,7 +79,7 @@ export default defineComponent({ display: flex; align-items: center; padding: 14px 16px; - border-bottom: solid 1px var(--divider); + border-bottom: solid 0.5px var(--divider); > .tag { flex: 1; diff --git a/src/docs/ar-SA/theme.md b/src/docs/ar-SA/theme.md index 8e24a1cbc7..59b582fa29 100644 --- a/src/docs/ar-SA/theme.md +++ b/src/docs/ar-SA/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/cs-CZ/theme.md b/src/docs/cs-CZ/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/cs-CZ/theme.md +++ b/src/docs/cs-CZ/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/da-DK/theme.md b/src/docs/da-DK/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/da-DK/theme.md +++ b/src/docs/da-DK/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/de-DE/theme.md b/src/docs/de-DE/theme.md index fe1c4fc7bb..e8d95ca628 100644 --- a/src/docs/de-DE/theme.md +++ b/src/docs/de-DE/theme.md @@ -43,7 +43,7 @@ Themencodes werden im Format eines JSON5-Objekts gespeichert. Themen werden wie * `props` ... Definitionen der Themenoptionen.Diese werden im folgenden erläutert. ### Definition von Themenoptionen -Die Optionen des Themas werden in `props` definiert. Die Schlüssel werden zu CSS-Variablen, die Werte geben den Inhalt an. Zusätzlich werden die `props` des gewählten Basisthemas von diesem Thema geerbt. Ist die `base` dieses Themas auf `light` gesetzt, so werden sie aus [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5) kopiert, ist sie auf `dark` gesetzt, so werden sie aus [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5) kopiert. Beispielsweise wird, falls sich in den `props` dieses Themas keine Definition für den Schlüssel `panel` befindet, so wird der Wert von `panel` aus dem Basisthema verwendet. +Die Optionen des Themas werden in `props` definiert. Die Schlüssel werden zu CSS-Variablen, die Werte geben den Inhalt an. Zusätzlich werden die `props` des gewählten Basisthemas von diesem Thema geerbt. Ist die `base` dieses Themas auf `light` gesetzt, so werden sie aus [_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5) kopiert, ist sie auf `dark` gesetzt, so werden sie aus [_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5) kopiert. Beispielsweise wird, falls sich in den `props` dieses Themas keine Definition für den Schlüssel `panel` befindet, so wird der Wert von `panel` aus dem Basisthema verwendet. #### Syntax für Wertangaben * Hexadezimalfarben diff --git a/src/docs/en-US/theme.md b/src/docs/en-US/theme.md index 35c0197966..6deb75abe6 100644 --- a/src/docs/en-US/theme.md +++ b/src/docs/en-US/theme.md @@ -43,7 +43,7 @@ Theme codes are saved as a JSON5 object of theme options. Themes are composed of * `props` ... The style definitions of the theme.These will be explained in the following. ### Theme style definitions -Define the style of the theme within `props`. The keys will become CSS variables, and the value specifies the content. In addition, the default `props` options are inherited from the base theme. If this theme's `base` is `light`, they will be copied from [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5), if it is `dark` they will be copied from [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5). In other words, if there is for example no `panel` key contained in `props`, then the value of `panel` from the base theme will be used. +Define the style of the theme within `props`. The keys will become CSS variables, and the value specifies the content. In addition, the default `props` options are inherited from the base theme. If this theme's `base` is `light`, they will be copied from [_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5), if it is `dark` they will be copied from [_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5). In other words, if there is for example no `panel` key contained in `props`, then the value of `panel` from the base theme will be used. #### Syntax for values * Hex colors diff --git a/src/docs/es-ES/theme.md b/src/docs/es-ES/theme.md index b6490106d1..3e379ee5f5 100644 --- a/src/docs/es-ES/theme.md +++ b/src/docs/es-ES/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/fr-FR/theme.md b/src/docs/fr-FR/theme.md index cf15c921e4..ab721fe445 100644 --- a/src/docs/fr-FR/theme.md +++ b/src/docs/fr-FR/theme.md @@ -43,7 +43,7 @@ Le code des thèmes est écrit sous forme d'objets JSON5. Les thèmes comprennen * `props` ... Définir un style de thème.Voir les explications ci-après. ### Définir un style de thème -C'est dans `props` que vous définirez le style de thème. Les propriétés deviendront des variables CSS et les valeurs associées spécifieront le contenu de ces variables. Par ailleurs, les objets présents par défaut dans `props` sont hérités du thème de base. Ainsi, si le thème de `base` est clair `light` ce sera l'objet [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5) ; et s'il est sombre `dark` ce sera l'objet [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5). Cela signifie, par exemple, que s'il n'y pas de propriété `panel` définie dans les `props` du thème, alors ce sera la valeur `panel` du thème de base qui sera prise en compte. +C'est dans `props` que vous définirez le style de thème. Les propriétés deviendront des variables CSS et les valeurs associées spécifieront le contenu de ces variables. Par ailleurs, les objets présents par défaut dans `props` sont hérités du thème de base. Ainsi, si le thème de `base` est clair `light` ce sera l'objet [_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5) ; et s'il est sombre `dark` ce sera l'objet [_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5). Cela signifie, par exemple, que s'il n'y pas de propriété `panel` définie dans les `props` du thème, alors ce sera la valeur `panel` du thème de base qui sera prise en compte. #### Syntaxe des valeurs * Codes de couleur Hex diff --git a/src/docs/ht-HT/theme.md b/src/docs/ht-HT/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/ht-HT/theme.md +++ b/src/docs/ht-HT/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/id-ID/theme.md b/src/docs/id-ID/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/id-ID/theme.md +++ b/src/docs/id-ID/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/it-IT/theme.md b/src/docs/it-IT/theme.md index ad21299e89..7467bf340d 100644 --- a/src/docs/it-IT/theme.md +++ b/src/docs/it-IT/theme.md @@ -43,7 +43,7 @@ Il codice dei temi è scritto a forma di oggetti JSON5. I temi contengono gli og * `props` ... テーマのスタイル定義。これから説明します。 ### Impostare uno stile di tema -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### Sintassi dei valori * 16進数で表された色 diff --git a/src/docs/ja-JP/theme.md b/src/docs/ja-JP/theme.md index c9604da412..89b467120c 100644 --- a/src/docs/ja-JP/theme.md +++ b/src/docs/ja-JP/theme.md @@ -47,7 +47,7 @@ `props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 -ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 +ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 diff --git a/src/docs/ja-KS/theme.md b/src/docs/ja-KS/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/ja-KS/theme.md +++ b/src/docs/ja-KS/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/kab-KAB/theme.md b/src/docs/kab-KAB/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/kab-KAB/theme.md +++ b/src/docs/kab-KAB/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/kn-IN/theme.md b/src/docs/kn-IN/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/kn-IN/theme.md +++ b/src/docs/kn-IN/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/ko-KR/theme.md b/src/docs/ko-KR/theme.md index 856580e600..e01133b831 100644 --- a/src/docs/ko-KR/theme.md +++ b/src/docs/ko-KR/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/nl-NL/theme.md b/src/docs/nl-NL/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/nl-NL/theme.md +++ b/src/docs/nl-NL/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/no-NO/theme.md b/src/docs/no-NO/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/no-NO/theme.md +++ b/src/docs/no-NO/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/pl-PL/theme.md b/src/docs/pl-PL/theme.md index 36590b7437..7190c39dd7 100644 --- a/src/docs/pl-PL/theme.md +++ b/src/docs/pl-PL/theme.md @@ -43,7 +43,7 @@ Kod motywów jest zapisywany jako obiekt JSON5 z opcjami motywu. Motywy składaj * `props` ... Definicje stylów motywu.これから説明します。 ### Definicje stylów motywu. -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### Składnia wartości * Kolory Hex diff --git a/src/docs/pt-PT/theme.md b/src/docs/pt-PT/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/pt-PT/theme.md +++ b/src/docs/pt-PT/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/ru-RU/theme.md b/src/docs/ru-RU/theme.md index 6ab7b54665..e4661c1011 100644 --- a/src/docs/ru-RU/theme.md +++ b/src/docs/ru-RU/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/th-TH/theme.md b/src/docs/th-TH/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/th-TH/theme.md +++ b/src/docs/th-TH/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/ug-CN/theme.md b/src/docs/ug-CN/theme.md index 4e52ee8cd7..a406f3433c 100644 --- a/src/docs/ug-CN/theme.md +++ b/src/docs/ug-CN/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/uk-UA/theme.md b/src/docs/uk-UA/theme.md index df1e33a920..baa74c33e4 100644 --- a/src/docs/uk-UA/theme.md +++ b/src/docs/uk-UA/theme.md @@ -43,7 +43,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 16進数で表された色 diff --git a/src/docs/zh-CN/theme.md b/src/docs/zh-CN/theme.md index a0d0c10bcd..1f33c00b41 100644 --- a/src/docs/zh-CN/theme.md +++ b/src/docs/zh-CN/theme.md @@ -43,7 +43,7 @@ * `props` ... 关于主题样式的定义,下面是详细介绍。 ### 主题样式定义 -在 `props` 下,你可以定义主题的样式。 键是 CSS 变量名,值是指定的内容。 请注意,`props` 对象是从基础主题集继承的。 如果这个主题的 `base` 是 `light`,则基础主题为 [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5);如果 `dark`,则基础主题为 [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)。 换句话说,即使这个主题中的 `props` 中没有定义关键的 `panel`,也会继承在基础主题中所拥有 `panel`。 +在 `props` 下,你可以定义主题的样式。 键是 CSS 变量名,值是指定的内容。 请注意,`props` 对象是从基础主题集继承的。 如果这个主题的 `base` 是 `light`,则基础主题为 [_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5);如果 `dark`,则基础主题为 [_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)。 换句话说,即使这个主题中的 `props` 中没有定义关键的 `panel`,也会继承在基础主题中所拥有 `panel`。 #### 可以在值中使用的语法 * 以十六进制表示的颜色 diff --git a/src/docs/zh-TW/theme.md b/src/docs/zh-TW/theme.md index 1c41260339..6715dce72f 100644 --- a/src/docs/zh-TW/theme.md +++ b/src/docs/zh-TW/theme.md @@ -45,7 +45,7 @@ * `props` ... テーマのスタイル定義。これから説明します。 ### テーマのスタイル定義 -`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 +`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。 #### バリューで使える構文 * 以十六進位色碼標示 diff --git a/src/games/reversi/package.json b/src/games/reversi/package.json index 5e7fdcb58a..a4415ad141 100644 --- a/src/games/reversi/package.json +++ b/src/games/reversi/package.json @@ -7,8 +7,8 @@ ], "author": "syuilo <i@syuilo.com>", "license": "MIT", - "repository": "https://github.com/syuilo/misskey.git", - "bugs": "https://github.com/syuilo/misskey/issues", + "repository": "https://github.com/misskey-dev/misskey.git", + "bugs": "https://github.com/misskey-dev/misskey/issues", "main": "./built/core.js", "types": "./built/core.d.ts", "scripts": { diff --git a/src/mfm/from-html.ts b/src/mfm/from-html.ts index 0b4f9b8945..4c8e2dbec8 100644 --- a/src/mfm/from-html.ts +++ b/src/mfm/from-html.ts @@ -1,7 +1,9 @@ import * as parse5 from 'parse5'; import treeAdapter = require('parse5/lib/tree-adapters/default'); import { URL } from 'url'; -import { urlRegex, urlRegexFull } from './prelude'; + +const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; +const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; export function fromHtml(html: string, hashtagNames?: string[]): string { const dom = parse5.parseFragment(html); diff --git a/src/mfm/language.ts b/src/mfm/language.ts deleted file mode 100644 index bad7b10a0d..0000000000 --- a/src/mfm/language.ts +++ /dev/null @@ -1,191 +0,0 @@ -import * as P from 'parsimmon'; -import { createLeaf, createTree, urlRegex } from './prelude'; -import { takeWhile, cumulativeSum } from '../prelude/array'; -import parseAcct from '@/misc/acct/parse'; -import { toUnicode } from 'punycode'; -import { emojiRegex } from '@/misc/emoji-regex'; - -export function removeOrphanedBrackets(s: string): string { - const openBrackets = ['(', '「', '[']; - const closeBrackets = [')', '」', ']']; - const xs = cumulativeSum(s.split('').map(c => { - if (openBrackets.includes(c)) return 1; - if (closeBrackets.includes(c)) return -1; - return 0; - })); - const firstOrphanedCloseBracket = xs.findIndex(x => x < 0); - if (firstOrphanedCloseBracket !== -1) return s.substr(0, firstOrphanedCloseBracket); - const lastMatched = xs.lastIndexOf(0); - return s.substr(0, lastMatched + 1); -} - -export const mfmLanguage = P.createLanguage({ - root: r => P.alt(r.block, r.inline).atLeast(1), - plain: r => P.alt(r.emoji, r.text).atLeast(1), - block: r => P.alt( - r.quote, - r.search, - r.blockCode, - r.mathBlock, - r.center, - ), - startOfLine: () => P((input, i) => { - if (i === 0 || input[i] === '\n' || input[i - 1] === '\n') { - return P.makeSuccess(i, null); - } else { - return P.makeFailure(i, 'not newline'); - } - }), - quote: r => r.startOfLine.then(P((input, i) => { - const text = input.substr(i); - if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote'); - const quote = takeWhile(line => line.startsWith('>'), text.split('\n')); - const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, ''); - if (qInner === '') return P.makeFailure(i, 'not a quote'); - const contents = r.root.tryParse(qInner); - return P.makeSuccess(i + quote.join('\n').length + 1, createTree('quote', contents, {})); - })), - search: r => r.startOfLine.then(P((input, i) => { - const text = input.substr(i); - const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i); - if (!match) return P.makeFailure(i, 'not a search'); - return P.makeSuccess(i + match[0].length, createLeaf('search', { query: match[1], content: match[0].trim() })); - })), - blockCode: r => r.startOfLine.then(P((input, i) => { - const text = input.substr(i); - const match = text.match(/^```(.+?)?\n([\s\S]+?)\n```(\n|$)/i); - if (!match) return P.makeFailure(i, 'not a blockCode'); - return P.makeSuccess(i + match[0].length, createLeaf('blockCode', { code: match[2], lang: match[1] ? match[1].trim() : null })); - })), - inline: r => P.alt( - r.big, - r.bold, - r.small, - r.italic, - r.strike, - r.inlineCode, - r.mathInline, - r.mention, - r.hashtag, - r.url, - r.link, - r.emoji, - r.fn, - r.text - ), - // TODO: そのうち消す - big: r => P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1).map(x => createTree('fn', r.inline.atLeast(1).tryParse(x), { - name: 'tada', - args: {} - })), - bold: r => { - const asterisk = P.regexp(/\*\*([\s\S]+?)\*\*/, 1); - const underscore = P.regexp(/__([a-zA-Z0-9\s]+?)__/, 1); - return P.alt(asterisk, underscore).map(x => createTree('bold', r.inline.atLeast(1).tryParse(x), {})); - }, - small: r => P.regexp(/<small>([\s\S]+?)<\/small>/, 1).map(x => createTree('small', r.inline.atLeast(1).tryParse(x), {})), - italic: r => { - const xml = P.regexp(/<i>([\s\S]+?)<\/i>/, 1); - const underscore = P((input, i) => { - const text = input.substr(i); - const match = text.match(/^(\*|_)([a-zA-Z0-9]+?[\s\S]*?)\1/); - if (!match) return P.makeFailure(i, 'not a italic'); - if (input[i - 1] != null && input[i - 1] != ' ' && input[i - 1] != '\n') return P.makeFailure(i, 'not a italic'); - return P.makeSuccess(i + match[0].length, match[2]); - }); - - return P.alt(xml, underscore).map(x => createTree('italic', r.inline.atLeast(1).tryParse(x), {})); - }, - strike: r => P.regexp(/~~([^\n~]+?)~~/, 1).map(x => createTree('strike', r.inline.atLeast(1).tryParse(x), {})), - center: r => r.startOfLine.then(P.regexp(/<center>([\s\S]+?)<\/center>/, 1).map(x => createTree('center', r.inline.atLeast(1).tryParse(x), {}))), - inlineCode: () => P.regexp(/`([^´\n]+?)`/, 1).map(x => createLeaf('inlineCode', { code: x })), - mathBlock: r => r.startOfLine.then(P.regexp(/\\\[([\s\S]+?)\\\]/, 1).map(x => createLeaf('mathBlock', { formula: x.trim() }))), - mathInline: () => P.regexp(/\\\((.+?)\\\)/, 1).map(x => createLeaf('mathInline', { formula: x })), - mention: () => { - return P((input, i) => { - const text = input.substr(i); - const match = text.match(/^@\w([\w-]*\w)?(?:@[\w.\-]+\w)?/); - if (!match) return P.makeFailure(i, 'not a mention'); - if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a mention'); - return P.makeSuccess(i + match[0].length, match[0]); - }).map(x => { - const { username, host } = parseAcct(x.substr(1)); - const canonical = host != null ? `@${username}@${toUnicode(host)}` : x; - return createLeaf('mention', { canonical, username, host, acct: x }); - }); - }, - hashtag: () => P((input, i) => { - const text = input.substr(i); - const match = text.match(/^#([^\s.,!?'"#:\/\[\]【】]+)/i); - if (!match) return P.makeFailure(i, 'not a hashtag'); - let hashtag = match[1]; - hashtag = removeOrphanedBrackets(hashtag); - if (hashtag.match(/^(\u20e3|\ufe0f)/)) return P.makeFailure(i, 'not a hashtag'); - if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag'); - if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a hashtag'); - if (Array.from(hashtag || '').length > 128) return P.makeFailure(i, 'not a hashtag'); - return P.makeSuccess(i + ('#' + hashtag).length, createLeaf('hashtag', { hashtag: hashtag })); - }), - url: () => { - return P((input, i) => { - const text = input.substr(i); - const match = text.match(urlRegex); - let url: string; - if (!match) { - const match = text.match(/^<(https?:\/\/.*?)>/); - if (!match) { - return P.makeFailure(i, 'not a url'); - } - url = match[1]; - i += 2; - } else { - url = match[0]; - } - url = removeOrphanedBrackets(url); - url = url.replace(/[.,]*$/, ''); - return P.makeSuccess(i + url.length, url); - }).map(x => createLeaf('url', { url: x })); - }, - link: r => { - return P.seqObj( - ['silent', P.string('?').fallback(null).map(x => x != null)] as any, - P.string('['), ['text', P.regexp(/[^\n\[\]]+/)] as any, P.string(']'), - P.string('('), ['url', r.url] as any, P.string(')'), - ).map((x: any) => { - return createTree('link', r.inline.atLeast(1).tryParse(x.text), { - silent: x.silent, - url: x.url.node.props.url - }); - }); - }, - emoji: () => { - const name = P.regexp(/:([a-z0-9_+-]+):/i, 1).map(x => createLeaf('emoji', { name: x })); - const code = P.regexp(emojiRegex).map(x => createLeaf('emoji', { emoji: x })); - return P.alt(name, code); - }, - fn: r => { - return P.seqObj( - P.string('['), ['fn', P.regexp(/[^\s\n\[\]]+/)] as any, P.string(' '), P.optWhitespace, ['text', P.regexp(/[^\n\[\]]+/)] as any, P.string(']'), - ).map((x: any) => { - let name = x.fn; - const args = {}; - const separator = x.fn.indexOf('.'); - if (separator > -1) { - name = x.fn.substr(0, separator); - for (const arg of x.fn.substr(separator + 1).split(',')) { - const kv = arg.split('='); - if (kv.length === 1) { - args[kv[0]] = true; - } else { - args[kv[0]] = kv[1]; - } - } - } - return createTree('fn', r.inline.atLeast(1).tryParse(x.text), { - name, - args - }); - }); - }, - text: () => P.any.map(x => createLeaf('text', { text: x })) -}); diff --git a/src/mfm/normalize.ts b/src/mfm/normalize.ts deleted file mode 100644 index a0f0702096..0000000000 --- a/src/mfm/normalize.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as A from '../prelude/array'; -import * as S from '../prelude/string'; -import { MfmForest, MfmTree } from './prelude'; -import { createTree, createLeaf } from '../prelude/tree'; - -function isEmptyTextTree(t: MfmTree): boolean { - return t.node.type === 'text' && t.node.props.text === ''; -} - -function concatTextTrees(ts: MfmForest): MfmTree { - return createLeaf({ type: 'text', props: { text: S.concat(ts.map(x => x.node.props.text)) } }); -} - -function concatIfTextTrees(ts: MfmForest): MfmForest { - return ts[0].node.type === 'text' ? [concatTextTrees(ts)] : ts; -} - -function concatConsecutiveTextTrees(ts: MfmForest): MfmForest { - const us = A.concat(A.groupOn(t => t.node.type, ts).map(concatIfTextTrees)); - return us.map(t => createTree(t.node, concatConsecutiveTextTrees(t.children))); -} - -function removeEmptyTextNodes(ts: MfmForest): MfmForest { - return ts - .filter(t => !isEmptyTextTree(t)) - .map(t => createTree(t.node, removeEmptyTextNodes(t.children))); -} - -export function normalize(ts: MfmForest): MfmForest { - return removeEmptyTextNodes(concatConsecutiveTextTrees(ts)); -} diff --git a/src/mfm/parse.ts b/src/mfm/parse.ts deleted file mode 100644 index c628042f12..0000000000 --- a/src/mfm/parse.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { mfmLanguage } from './language'; -import { MfmForest } from './prelude'; -import { normalize } from './normalize'; - -export function parse(source: string | null): MfmForest | null { - if (source == null || source === '') { - return null; - } - - return normalize(mfmLanguage.root.tryParse(source)); -} - -export function parsePlain(source: string | null): MfmForest | null { - if (source == null || source === '') { - return null; - } - - return normalize(mfmLanguage.plain.tryParse(source)); -} diff --git a/src/mfm/prelude.ts b/src/mfm/prelude.ts deleted file mode 100644 index a8b52eb315..0000000000 --- a/src/mfm/prelude.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Tree } from '../prelude/tree'; -import * as T from '../prelude/tree'; - -type Node<T, P> = { type: T, props: P }; - -export type MentionNode = Node<'mention', { - canonical: string, - username: string, - host: string, - acct: string -}>; - -export type HashtagNode = Node<'hashtag', { - hashtag: string -}>; - -export type EmojiNode = Node<'emoji', { - name: string -}>; - -export type MfmNode = - MentionNode | - HashtagNode | - EmojiNode | - Node<string, any>; - -export type MfmTree = Tree<MfmNode>; - -export type MfmForest = MfmTree[]; - -export function createLeaf(type: string, props: any): MfmTree { - return T.createLeaf({ type, props }); -} - -export function createTree(type: string, children: MfmForest, props: any): MfmTree { - return T.createTree({ type, props }, children); -} - -export const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; -export const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; diff --git a/src/mfm/to-html.ts b/src/mfm/to-html.ts index 66015d539f..aa39443c64 100644 --- a/src/mfm/to-html.ts +++ b/src/mfm/to-html.ts @@ -1,12 +1,12 @@ import { JSDOM } from 'jsdom'; +import * as mfm from 'mfm-js'; import config from '@/config'; import { intersperse } from '../prelude/array'; -import { MfmForest, MfmTree } from './prelude'; import { IMentionedRemoteUsers } from '../models/entities/note'; import { wellKnownServices } from '../well-known-services'; -export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { - if (tokens == null) { +export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { + if (nodes == null) { return null; } @@ -14,95 +14,101 @@ export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentione const doc = window.document; - function appendChildren(children: MfmForest, targetElement: any): void { - for (const child of children.map(t => handlers[t.node.type](t))) targetElement.appendChild(child); + function appendChildren(children: mfm.MfmNode[], targetElement: any): void { + if (children) { + for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); + } } - const handlers: { [key: string]: (token: MfmTree) => any } = { - bold(token) { + const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { + bold(node) { const el = doc.createElement('b'); - appendChildren(token.children, el); + appendChildren(node.children, el); return el; }, - small(token) { + small(node) { const el = doc.createElement('small'); - appendChildren(token.children, el); + appendChildren(node.children, el); return el; }, - strike(token) { + strike(node) { const el = doc.createElement('del'); - appendChildren(token.children, el); + appendChildren(node.children, el); return el; }, - italic(token) { + italic(node) { const el = doc.createElement('i'); - appendChildren(token.children, el); + appendChildren(node.children, el); return el; }, - fn(token) { + fn(node) { const el = doc.createElement('i'); - appendChildren(token.children, el); + appendChildren(node.children, el); return el; }, - blockCode(token) { + blockCode(node) { const pre = doc.createElement('pre'); const inner = doc.createElement('code'); - inner.textContent = token.node.props.code; + inner.textContent = node.props.code; pre.appendChild(inner); return pre; }, - center(token) { + center(node) { const el = doc.createElement('div'); - appendChildren(token.children, el); + appendChildren(node.children, el); return el; }, - emoji(token) { - return doc.createTextNode(token.node.props.emoji ? token.node.props.emoji : `\u200B:${token.node.props.name}:\u200B`); + emojiCode(node) { + return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); + }, + + unicodeEmoji(node) { + return doc.createTextNode(node.props.emoji); }, - hashtag(token) { + hashtag(node) { const a = doc.createElement('a'); - a.href = `${config.url}/tags/${token.node.props.hashtag}`; - a.textContent = `#${token.node.props.hashtag}`; + a.href = `${config.url}/tags/${node.props.hashtag}`; + a.textContent = `#${node.props.hashtag}`; a.setAttribute('rel', 'tag'); return a; }, - inlineCode(token) { + inlineCode(node) { const el = doc.createElement('code'); - el.textContent = token.node.props.code; + el.textContent = node.props.code; return el; }, - mathInline(token) { + mathInline(node) { const el = doc.createElement('code'); - el.textContent = token.node.props.formula; + el.textContent = node.props.formula; return el; }, - mathBlock(token) { + mathBlock(node) { const el = doc.createElement('code'); - el.textContent = token.node.props.formula; + el.textContent = node.props.formula; return el; }, - link(token) { + link(node) { const a = doc.createElement('a'); - a.href = token.node.props.url; - appendChildren(token.children, a); + a.href = node.props.url; + appendChildren(node.children, a); return a; }, - mention(token) { + mention(node) { const a = doc.createElement('a'); - const { username, host, acct } = token.node.props; + const { username, host, acct } = node.props; const wellKnown = wellKnownServices.find(x => x[0] === host); if (wellKnown) { a.href = wellKnown[1](username); @@ -115,39 +121,39 @@ export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentione return a; }, - quote(token) { + quote(node) { const el = doc.createElement('blockquote'); - appendChildren(token.children, el); + appendChildren(node.children, el); return el; }, - text(token) { + text(node) { const el = doc.createElement('span'); - const nodes = (token.node.props.text as string).split(/\r\n|\r|\n/).map(x => doc.createTextNode(x) as Node); + const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); - for (const x of intersperse<Node | 'br'>('br', nodes)) { + for (const x of intersperse<FIXME | 'br'>('br', nodes)) { el.appendChild(x === 'br' ? doc.createElement('br') : x); } return el; }, - url(token) { + url(node) { const a = doc.createElement('a'); - a.href = token.node.props.url; - a.textContent = token.node.props.url; + a.href = node.props.url; + a.textContent = node.props.url; return a; }, - search(token) { + search(node) { const a = doc.createElement('a'); - a.href = `https://www.google.com/search?q=${token.node.props.query}`; - a.textContent = token.node.props.content; + a.href = `https://www.google.com/search?q=${node.props.query}`; + a.textContent = node.props.content; return a; } }; - appendChildren(tokens, doc.body); + appendChildren(nodes, doc.body); return `<p>${doc.body.innerHTML}</p>`; } diff --git a/src/mfm/to-string.ts b/src/mfm/to-string.ts deleted file mode 100644 index 347c94c247..0000000000 --- a/src/mfm/to-string.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { MfmForest, MfmTree } from './prelude'; -import { nyaize } from '@/misc/nyaize'; - -export type RestoreOptions = { - doNyaize?: boolean; -}; - -export function toString(tokens: MfmForest | null, opts?: RestoreOptions): string { - - if (tokens === null) return ''; - - function appendChildren(children: MfmForest, opts?: RestoreOptions): string { - return children.map(t => handlers[t.node.type](t, opts)).join(''); - } - - const handlers: { [key: string]: (token: MfmTree, opts?: RestoreOptions) => string } = { - bold(token, opts) { - return `**${appendChildren(token.children, opts)}**`; - }, - - small(token, opts) { - return `<small>${appendChildren(token.children, opts)}</small>`; - }, - - strike(token, opts) { - return `~~${appendChildren(token.children, opts)}~~`; - }, - - italic(token, opts) { - return `<i>${appendChildren(token.children, opts)}</i>`; - }, - - fn(token, opts) { - const name = token.node.props?.name; - const args = token.node.props?.args || {}; - const argsStr = Object.entries(args).map(([k, v]) => v === true ? k : `${k}=${v}`).join(','); - return `[${name}${argsStr !== '' ? '.' + argsStr : ''} ${appendChildren(token.children, opts)}]`; - }, - - blockCode(token) { - return `\`\`\`${token.node.props.lang || ''}\n${token.node.props.code}\n\`\`\`\n`; - }, - - center(token, opts) { - return `<center>${appendChildren(token.children, opts)}</center>`; - }, - - emoji(token) { - return (token.node.props.emoji ? token.node.props.emoji : `:${token.node.props.name}:`); - }, - - hashtag(token) { - return `#${token.node.props.hashtag}`; - }, - - inlineCode(token) { - return `\`${token.node.props.code}\``; - }, - - mathInline(token) { - return `\\(${token.node.props.formula}\\)`; - }, - - mathBlock(token) { - return `\\[${token.node.props.formula}\\]`; - }, - - link(token, opts) { - if (token.node.props.silent) { - return `?[${appendChildren(token.children, opts)}](${token.node.props.url})`; - } else { - return `[${appendChildren(token.children, opts)}](${token.node.props.url})`; - } - }, - - mention(token) { - return token.node.props.canonical; - }, - - quote(token) { - return `${appendChildren(token.children, {doNyaize: false}).replace(/^/gm,'>').trim()}\n`; - }, - - text(token, opts) { - return (opts && opts.doNyaize) ? nyaize(token.node.props.text) : token.node.props.text; - }, - - url(token) { - return `<${token.node.props.url}>`; - }, - - search(token, opts) { - const query = token.node.props.query; - return `${(opts && opts.doNyaize ? nyaize(query) : query)} [search]\n`; - } - }; - - return appendChildren(tokens, { doNyaize: (opts && opts.doNyaize) || false }).trim(); -} diff --git a/src/misc/convert-host.ts b/src/misc/convert-host.ts index f8957a64a8..7b0b0acf10 100644 --- a/src/misc/convert-host.ts +++ b/src/misc/convert-host.ts @@ -1,6 +1,6 @@ import { URL } from 'url'; import config from '@/config'; -import { toASCII } from 'punycode'; +import { toASCII } from 'punycode/'; export function getFullApAccount(username: string, host: string | null) { return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; diff --git a/src/misc/extract-custom-emojis-from-mfm.ts b/src/misc/extract-custom-emojis-from-mfm.ts new file mode 100644 index 0000000000..b29ce281b3 --- /dev/null +++ b/src/misc/extract-custom-emojis-from-mfm.ts @@ -0,0 +1,10 @@ +import * as mfm from 'mfm-js'; +import { unique } from '@/prelude/array'; + +export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { + const emojiNodes = mfm.extract(nodes, (node) => { + return (node.type === 'emojiCode' && node.props.name.length <= 100); + }); + + return unique(emojiNodes.map(x => x.props.name)); +} diff --git a/src/misc/extract-emojis.ts b/src/misc/extract-emojis.ts deleted file mode 100644 index 2c57e9a8aa..0000000000 --- a/src/misc/extract-emojis.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EmojiNode, MfmForest } from '../mfm/prelude'; -import { preorderF } from '../prelude/tree'; -import { unique } from '../prelude/array'; - -export default function(mfmForest: MfmForest): string[] { - const emojiNodes = preorderF(mfmForest).filter(x => x.type === 'emoji') as EmojiNode[]; - const emojis = emojiNodes.filter(x => x.props.name && x.props.name.length <= 100).map(x => x.props.name); - return unique(emojis); -} diff --git a/src/misc/extract-hashtags.ts b/src/misc/extract-hashtags.ts index 36b2296a76..b0a74df219 100644 --- a/src/misc/extract-hashtags.ts +++ b/src/misc/extract-hashtags.ts @@ -1,9 +1,9 @@ -import { HashtagNode, MfmForest } from '../mfm/prelude'; -import { preorderF } from '../prelude/tree'; -import { unique } from '../prelude/array'; +import * as mfm from 'mfm-js'; +import { unique } from '@/prelude/array'; -export default function(mfmForest: MfmForest): string[] { - const hashtagNodes = preorderF(mfmForest).filter(x => x.type === 'hashtag') as HashtagNode[]; - const hashtags = hashtagNodes.map(x => x.props.hashtag); - return unique(hashtags); +export function extractHashtags(nodes: mfm.MfmNode[]): string[] { + const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag'); + const hashtags = unique(hashtagNodes.map(x => x.props.hashtag)); + + return hashtags; } diff --git a/src/misc/extract-mentions.ts b/src/misc/extract-mentions.ts index 72330d31e1..cc19b161a8 100644 --- a/src/misc/extract-mentions.ts +++ b/src/misc/extract-mentions.ts @@ -1,10 +1,11 @@ // test is located in test/extract-mentions -import { MentionNode, MfmForest } from '../mfm/prelude'; -import { preorderF } from '../prelude/tree'; +import * as mfm from 'mfm-js'; -export default function(mfmForest: MfmForest): MentionNode['props'][] { +export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { // TODO: 重複を削除 - const mentionNodes = preorderF(mfmForest).filter(x => x.type === 'mention') as MentionNode[]; - return mentionNodes.map(x => x.props); + const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); + const mentions = mentionNodes.map(x => x.props); + + return mentions; } diff --git a/src/misc/extract-url-from-mfm.ts b/src/misc/extract-url-from-mfm.ts new file mode 100644 index 0000000000..e6d5d0104a --- /dev/null +++ b/src/misc/extract-url-from-mfm.ts @@ -0,0 +1,19 @@ +import * as mfm from 'mfm-js'; +import { unique } from '@/prelude/array'; + +// unique without hash +// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] +const removeHash = (x: string) => x.replace(/#[^#]*$/, ''); + +export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] { + const urlNodes = mfm.extract(nodes, (node) => { + return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent)); + }); + const urls: string[] = unique(urlNodes.map(x => x.props.url)); + + return urls.reduce((array, url) => { + const urlWithoutHash = removeHash(url); + if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url); + return array; + }, [] as string[]); +} diff --git a/src/misc/gen-avatar.ts b/src/misc/gen-avatar.ts index 14c01a9bd9..7ff28b2e88 100644 --- a/src/misc/gen-avatar.ts +++ b/src/misc/gen-avatar.ts @@ -8,7 +8,7 @@ import { WriteStream } from 'fs'; const size = 256; // px const n = 5; // resolution -const margin = (size / n) / 1.5; +const margin = (size / n); const colors = [ '#e57373', '#F06292', diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts index f013169f86..d0b6ee7f2b 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -321,14 +321,14 @@ export class Meta { @Column('varchar', { length: 512, - default: 'https://github.com/syuilo/misskey', + default: 'https://github.com/misskey-dev/misskey', nullable: false }) public repositoryUrl: string; @Column('varchar', { length: 512, - default: 'https://github.com/syuilo/misskey/issues/new', + default: 'https://github.com/misskey-dev/misskey/issues/new', nullable: true }) public feedbackUrl: string | null; diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 3642a03c2c..cdf4841918 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -1,12 +1,12 @@ import { EntityRepository, Repository, In } from 'typeorm'; +import * as mfm from 'mfm-js'; import { Note } from '../entities/note'; import { User } from '../entities/user'; import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..'; import { SchemaType } from '@/misc/schema'; +import { nyaize } from '@/misc/nyaize'; import { awaitAll } from '../../prelude/await-all'; import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib'; -import { toString } from '../../mfm/to-string'; -import { parse } from '../../mfm/parse'; import { NoteReaction } from '../entities/note-reaction'; import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis'; @@ -223,8 +223,13 @@ export class NoteRepository extends Repository<Note> { }); if (packed.user.isCat && packed.text) { - const tokens = packed.text ? parse(packed.text) : []; - packed.text = toString(tokens, { doNyaize: true }); + const tokens = packed.text ? mfm.parse(packed.text) : []; + mfm.inspect(tokens, node => { + if (node.type === 'text') { + node.props.text = nyaize(node.props.text); + } + }); + packed.text = mfm.toString(tokens); } if (!opts.skipHide) { diff --git a/src/prelude/tree.ts b/src/prelude/tree.ts deleted file mode 100644 index 519234a0b0..0000000000 --- a/src/prelude/tree.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { concat, sum } from './array'; - -export type Tree<T> = { - node: T, - children: Forest<T>; -}; - -export type Forest<T> = Tree<T>[]; - -export function createLeaf<T>(node: T): Tree<T> { - return { node, children: [] }; -} - -export function createTree<T>(node: T, children: Forest<T>): Tree<T> { - return { node, children }; -} - -export function hasChildren<T>(t: Tree<T>): boolean { - return t.children.length !== 0; -} - -export function preorder<T>(t: Tree<T>): T[] { - return [t.node, ...preorderF(t.children)]; -} - -export function preorderF<T>(ts: Forest<T>): T[] { - return concat(ts.map(preorder)); -} - -export function countNodes<T>(t: Tree<T>): number { - return preorder(t).length; -} - -export function countNodesF<T>(ts: Forest<T>): number { - return sum(ts.map(countNodes)); -} diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts index 6990a4ae5e..683860d9cc 100644 --- a/src/remote/activitypub/misc/get-note-html.ts +++ b/src/remote/activitypub/misc/get-note-html.ts @@ -1,9 +1,9 @@ +import * as mfm from 'mfm-js'; import { Note } from '../../../models/entities/note'; import { toHtml } from '../../../mfm/to-html'; -import { parse } from '../../../mfm/parse'; export default function(note: Note) { - let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers)); + let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null; if (html == null) html = '<p>.</p>'; return html; diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index e4e8f24f10..91b91bff92 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -1,10 +1,10 @@ import { URL } from 'url'; +import * as mfm from 'mfm-js'; import renderImage from './image'; import renderKey from './key'; import config from '@/config'; import { ILocalUser } from '../../../models/entities/user'; import { toHtml } from '../../../mfm/to-html'; -import { parse } from '../../../mfm/parse'; import { getEmojis } from './note'; import renderEmoji from './emoji'; import { IIdentifier } from '../models/identifier'; @@ -66,7 +66,7 @@ export async function renderPerson(user: ILocalUser) { url: `${config.url}/@${user.username}`, preferredUsername: user.username, name: user.name, - summary: toHtml(parse(profile.description)), + summary: profile.description ? toHtml(mfm.parse(profile.description)) : null, icon: avatar ? renderImage(avatar) : null, image: banner ? renderImage(banner) : null, tag, diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 0554fe76fb..c0ffd75e23 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -1,12 +1,12 @@ import $ from 'cafy'; +import * as mfm from 'mfm-js'; import { ID } from '@/misc/cafy-id'; import { publishMainStream, publishUserEvent } from '../../../../services/stream'; import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; import { publishToFollowers } from '../../../../services/i/update'; import define from '../../define'; -import { parse, parsePlain } from '../../../../mfm/parse'; -import extractEmojis from '@/misc/extract-emojis'; -import extractHashtags from '@/misc/extract-hashtags'; +import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm'; +import { extractHashtags } from '@/misc/extract-hashtags'; import * as langmap from 'langmap'; import { updateUsertags } from '../../../../services/update-hashtag'; import { ApiError } from '../../error'; @@ -291,13 +291,13 @@ export default define(meta, async (ps, _user, token) => { const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; if (newName != null) { - const tokens = parsePlain(newName); - emojis = emojis.concat(extractEmojis(tokens!)); + const tokens = mfm.parsePlain(newName); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); } if (newDescription != null) { - const tokens = parse(newDescription); - emojis = emojis.concat(extractEmojis(tokens!)); + const tokens = mfm.parse(newDescription); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); } diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 2a94ae35f6..3760c8b37b 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -71,12 +71,12 @@ export const meta = { repositoryUrl: { type: 'string' as const, optional: false as const, nullable: false as const, - default: 'https://github.com/syuilo/misskey' + default: 'https://github.com/misskey-dev/misskey' }, feedbackUrl: { type: 'string' as const, optional: false as const, nullable: false as const, - default: 'https://github.com/syuilo/misskey/issues/new' + default: 'https://github.com/misskey-dev/misskey/issues/new' }, secure: { type: 'boolean' as const, diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts index 221e0a32df..adac3bda2c 100644 --- a/src/server/api/openapi/gen-spec.ts +++ b/src/server/api/openapi/gen-spec.ts @@ -18,7 +18,7 @@ export function genOpenapiSpec(lang = 'ja-JP') { externalDocs: { description: 'Repository', - url: 'https://github.com/syuilo/misskey' + url: 'https://github.com/misskey-dev/misskey' }, servers: [{ @@ -120,7 +120,7 @@ export function genOpenapiSpec(lang = 'ja-JP') { description: desc, externalDocs: { description: 'Source code', - url: `https://github.com/syuilo/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts` + url: `https://github.com/misskey-dev/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts` }, ...(endpoint.meta.tags ? { tags: [endpoint.meta.tags[0]] diff --git a/src/server/web/views/info.pug b/src/server/web/views/info.pug index 323dce974f..61f9b7b9e2 100644 --- a/src/server/web/views/info.pug +++ b/src/server/web/views/info.pug @@ -144,4 +144,4 @@ html | :#{emoji.name}: = ' ' footer - p Misskey is open-source software. <a href="https://github.com/syuilo/misskey">View source</a> + p Misskey is open-source software. <a href="https://github.com/misskey-dev/misskey">View source</a> diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 64d5513ecc..b9b39d25e4 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -1,3 +1,4 @@ +import * as mfm from 'mfm-js'; import es from '../../db/elasticsearch'; import { publishMainStream, publishNotesStream } from '../stream'; import DeliverManager from '../../remote/activitypub/deliver-manager'; @@ -5,16 +6,15 @@ import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; import renderAnnounce from '../../remote/activitypub/renderer/announce'; import { renderActivity } from '../../remote/activitypub/renderer'; -import { parse } from '../../mfm/parse'; import { resolveUser } from '../../remote/resolve-user'; import config from '@/config'; import { updateHashtags } from '../update-hashtag'; import { concat } from '../../prelude/array'; import insertNoteUnread from './unread'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import extractMentions from '@/misc/extract-mentions'; -import extractEmojis from '@/misc/extract-emojis'; -import extractHashtags from '@/misc/extract-hashtags'; +import { extractMentions } from '@/misc/extract-mentions'; +import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm'; +import { extractHashtags } from '@/misc/extract-hashtags'; import { Note, IMentionedRemoteUsers } from '../../models/entities/note'; import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings } from '../../models'; import { DriveFile } from '../../models/entities/drive-file'; @@ -182,17 +182,17 @@ export default async (user: { id: User['id']; username: User['username']; host: // Parse MFM if needed if (!tags || !emojis || !mentionedUsers) { - const tokens = data.text ? parse(data.text)! : []; - const cwTokens = data.cw ? parse(data.cw)! : []; + const tokens = data.text ? mfm.parse(data.text)! : []; + const cwTokens = data.cw ? mfm.parse(data.cw)! : []; const choiceTokens = data.poll && data.poll.choices - ? concat(data.poll.choices.map(choice => parse(choice)!)) + ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) : []; const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); tags = data.apHashtags || extractHashtags(combinedTokens); - emojis = data.apEmojis || extractEmojis(combinedTokens); + emojis = data.apEmojis || extractCustomEmojisFromMfm(combinedTokens); mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens); } @@ -604,7 +604,7 @@ function incNotesCountOfUser(user: { id: User['id']; }) { .execute(); } -async function extractMentionedUsers(user: { host: User['host']; }, tokens: ReturnType<typeof parse>): Promise<User[]> { +async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise<User[]> { if (tokens == null) return []; const mentions = extractMentions(tokens); |