diff options
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/components/notes.vue | 14 | ||||
| -rw-r--r-- | src/client/components/notification.vue | 35 | ||||
| -rw-r--r-- | src/client/pages/apps.vue | 101 | ||||
| -rwxr-xr-x | src/client/pages/auth.vue | 8 | ||||
| -rw-r--r-- | src/client/pages/doc.vue | 25 | ||||
| -rw-r--r-- | src/client/pages/follow-requests.vue | 14 | ||||
| -rw-r--r-- | src/client/pages/messaging/index.vue | 14 | ||||
| -rw-r--r-- | src/client/pages/miauth.vue | 103 | ||||
| -rw-r--r-- | src/client/pages/my-settings/index.vue | 4 | ||||
| -rw-r--r-- | src/client/pages/preferences/theme.vue | 28 | ||||
| -rw-r--r-- | src/client/router.ts | 2 | ||||
| -rw-r--r-- | src/client/style.scss | 20 |
12 files changed, 303 insertions, 65 deletions
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 65dda17575..0cf4dee2dd 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -1,6 +1,6 @@ <template> <div class="mk-notes" v-size="[{ max: 500 }]"> - <div class="empty" v-if="empty"> + <div class="_fullinfo" v-if="empty"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noNotes') }}</div> </div> @@ -90,18 +90,6 @@ export default Vue.extend({ <style lang="scss" scoped> .mk-notes { - > .empty { - padding: 32px; - text-align: center; - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 16px; - border-radius: 16px; - } - } - > .notes { > ::v-deep *:not(:last-child) { margin-bottom: var(--marginFull); diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue index d768c0b074..f415887e76 100644 --- a/src/client/components/notification.vue +++ b/src/client/components/notification.vue @@ -1,22 +1,24 @@ <template> <div class="mk-notification" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]"> <div class="head"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="icon" :class="notification.type"> + <mk-avatar v-if="notification.user" class="icon" :user="notification.user"/> + <img v-else class="icon" :src="notification.icon" alt=""/> + <div class="sub-icon" :class="notification.type"> <fa :icon="faPlus" v-if="notification.type === 'follow'"/> - <fa :icon="faClock" v-if="notification.type === 'receiveFollowRequest'"/> - <fa :icon="faCheck" v-if="notification.type === 'followRequestAccepted'"/> - <fa :icon="faIdCardAlt" v-if="notification.type === 'groupInvited'"/> - <fa :icon="faRetweet" v-if="notification.type === 'renote'"/> - <fa :icon="faReply" v-if="notification.type === 'reply'"/> - <fa :icon="faAt" v-if="notification.type === 'mention'"/> - <fa :icon="faQuoteLeft" v-if="notification.type === 'quote'"/> - <x-reaction-icon v-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/> + <fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/> + <fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/> + <fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/> + <fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/> + <fa :icon="faReply" v-else-if="notification.type === 'reply'"/> + <fa :icon="faAt" v-else-if="notification.type === 'mention'"/> + <fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/> + <x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/> </div> </div> <div class="tail"> <header> - <router-link class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link> + <router-link v-if="notification.user" class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link> + <span v-else>{{ notification.header }}</span> <mk-time :time="notification.createdAt" v-if="withTime"/> </header> <router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> @@ -42,6 +44,9 @@ <span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span> <span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span> <span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span> + <span v-if="notification.type === 'app'" class="text"> + <mfm :text="notification.body" :nowrap="!full"/> + </span> </div> </div> </template> @@ -142,14 +147,14 @@ export default Vue.extend({ height: 42px; margin-right: 8px; - > .avatar { + > .icon { display: block; width: 100%; height: 100%; border-radius: 6px; } - > .icon { + > .sub-icon { position: absolute; z-index: 1; bottom: -2px; @@ -163,6 +168,10 @@ export default Vue.extend({ font-size: 12px; pointer-events: none; + &:empty { + display: none; + } + > * { color: #fff; width: 100%; diff --git a/src/client/pages/apps.vue b/src/client/pages/apps.vue new file mode 100644 index 0000000000..03c6707f95 --- /dev/null +++ b/src/client/pages/apps.vue @@ -0,0 +1,101 @@ +<template> +<div> + <portal to="icon"><fa :icon="faPlug"/></portal> + <portal to="title">{{ $t('installedApps') }}</portal> + + <mk-pagination :pagination="pagination" class="bfomjevm" ref="list"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ $t('nothing') }}</div> + </div> + </template> + <template #default="{items}"> + <div class="token _panel" v-for="token in items" :key="token.id"> + <img class="icon" :src="token.iconUrl" alt=""/> + <div class="body"> + <div class="name">{{ token.name }}</div> + <div class="description">{{ token.description }}</div> + <div class="_keyValue"> + <div>{{ $t('installedDate') }}:</div> + <div><mk-time :time="token.createdAt"/></div> + </div> + <div class="_keyValue"> + <div>{{ $t('lastUsedDate') }}:</div> + <div><mk-time :time="token.lastUsedAt"/></div> + </div> + <div class="actions"> + <button class="_button" @click="revoke(token)"><fa :icon="faTrashAlt"/></button> + </div> + </div> + </div> + </template> + </mk-pagination> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons'; +import MkPagination from '../components/ui/pagination.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('installedApps') as string + }; + }, + + components: { + MkPagination + }, + + data() { + return { + pagination: { + endpoint: 'i/apps', + limit: 100, + params: { + sort: '+lastUsedAt' + } + }, + faTrashAlt, faPlug + }; + }, + + methods: { + revoke(token) { + this.$root.api('i/revoke-token', { tokenId: token.id }).then(() => { + this.$refs.list.reload(); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.bfomjevm { + > .token { + display: flex; + padding: 16px; + + > .icon { + display: block; + flex-shrink: 0; + margin: 0 12px 0 0; + width: 50px; + height: 50px; + border-radius: 8px; + } + + > .body { + width: calc(100% - 62px); + position: relative; + + > .name { + font-weight: bold; + } + } + } +} +</style> diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue index 9f5b45f001..e025924fe0 100755 --- a/src/client/pages/auth.vue +++ b/src/client/pages/auth.vue @@ -12,20 +12,18 @@ @accepted="accepted" /> <div class="denied _panel" v-if="state == 'denied'"> - <h1>{{ $t('denied') }}</h1> - <p>{{ $t('denied-paragraph') }}</p> + <h1>{{ $t('_auth.denied') }}</h1> </div> <div class="accepted _panel" v-if="state == 'accepted'"> <h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1> - <p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p> - <p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p> + <p v-if="session.app.callbackUrl">{{ $t('_auth.callback') }}<mk-ellipsis/></p> + <p v-if="!session.app.callbackUrl">{{ $t('_auth.pleaseGoBack') }}</p> </div> <div class="error _panel" v-if="state == 'fetch-session-error'"> <p>{{ $t('error') }}</p> </div> </div> <div class="signin" v-else> - <h1>{{ $t('sign-in') }}</h1> <mk-signin @login="onLogin"/> </div> </template> diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue index e0db5a3746..7c4f7ebccf 100644 --- a/src/client/pages/doc.vue +++ b/src/client/pages/doc.vue @@ -18,6 +18,7 @@ import Vue from 'vue'; import { faFileAlt } from '@fortawesome/free-solid-svg-icons' import MarkdownIt from 'markdown-it'; +import MarkdownItAnchor from 'markdown-it-anchor'; import i18n from '../i18n'; import { url, lang } from '../config'; import MkLink from '../components/link.vue'; @@ -26,6 +27,10 @@ const markdown = MarkdownIt({ html: true }); +markdown.use(MarkdownItAnchor, { + slugify: (s) => encodeURIComponent(String(s).trim().replace(/\s+/g, '-')) +}); + export default Vue.extend({ i18n, @@ -72,6 +77,9 @@ export default Vue.extend({ }, parse(md: string) { + // 変数置換 + md = md.replace(/\{_URL_\}/g, url); + // markdown の全容をパースする const parsed = markdown.parse(md, {}); if (parsed.length === 0) return; @@ -115,6 +123,23 @@ export default Vue.extend({ margin-bottom: 0; } + ::v-deep a { + color: var(--link); + } + + ::v-deep blockquote { + display: block; + margin: 8px; + padding: 6px 0 6px 12px; + color: var(--fg); + border-left: solid 3px var(--fg); + opacity: 0.7; + + p { + margin: 0; + } + } + ::v-deep h2 { font-size: 1.25em; padding: 0 0 0.5em 0; diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue index a900bf735c..b310d9f581 100644 --- a/src/client/pages/follow-requests.vue +++ b/src/client/pages/follow-requests.vue @@ -5,7 +5,7 @@ <mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list"> <template #empty> - <div class="tkdrhpxr"> + <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noFollowRequests') }}</div> </div> @@ -75,18 +75,6 @@ export default Vue.extend({ <style lang="scss" scoped> .mk-follow-requests { - .tkdrhpxr { - padding: 32px; - text-align: center; - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 16px; - border-radius: 16px; - } - } - > .user { display: flex; padding: 16px; diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue index ed24f8ef54..7a55004cbf 100644 --- a/src/client/pages/messaging/index.vue +++ b/src/client/pages/messaging/index.vue @@ -31,7 +31,7 @@ </div> </router-link> </div> - <div class="no-history" v-if="!fetching && messages.length == 0"> + <div class="_fullinfo" v-if="!fetching && messages.length == 0"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noHistory') }}</div> </div> @@ -287,18 +287,6 @@ export default Vue.extend({ } } - > .no-history { - padding: 32px; - text-align: center; - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 16px; - border-radius: 16px; - } - } - @media (max-width: 400px) { > .history { > .message { diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue new file mode 100644 index 0000000000..2ee0f23479 --- /dev/null +++ b/src/client/pages/miauth.vue @@ -0,0 +1,103 @@ +<template> +<div v-if="$store.getters.isSignedIn"> + <div class="waiting _card" v-if="state == 'waiting'"> + <div class="_content"> + <mk-loading/> + </div> + </div> + <div class="denied _card" v-if="state == 'denied'"> + <div class="_content"> + <p>{{ $t('_auth.denied') }}</p> + </div> + </div> + <div class="accepted _card" v-else-if="state == 'accepted'"> + <div class="_content"> + <p v-if="callback">{{ $t('_auth.callback') }}<mk-ellipsis/></p> + <p v-else>{{ $t('_auth.pleaseGoBack') }}</p> + </div> + </div> + <div class="_card" v-else> + <div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div> + <div class="_title" v-else>{{ $t('_auth.shareAccessAsk') }}</div> + <div class="_content"> + <p>{{ $t('_auth.permissionAsk') }}</p> + <ul> + <template v-for="p in permission"> + <li :key="p">{{ $t(`_permissions.${p}`) }}</li> + </template> + </ul> + </div> + <div class="_footer"> + <mk-button @click="deny" inline>{{ $t('cancel') }}</mk-button> + <mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button> + </div> + </div> +</div> +<div class="signin" v-else> + <mk-signin @login="onLogin"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import MkSignin from '../components/signin.vue'; +import MkButton from '../components/ui/button.vue'; + +export default Vue.extend({ + i18n, + components: { + MkSignin, + MkButton, + }, + data() { + return { + state: null + }; + }, + computed: { + session(): string { + return this.$route.params.session; + }, + callback(): string { + return this.$route.query.callback; + }, + name(): string { + return this.$route.query.name; + }, + icon(): string { + return this.$route.query.icon; + }, + permission(): string { + return this.$route.query.permission; + }, + }, + methods: { + async accept() { + this.state = 'waiting'; + await this.$root.api('miauth/gen-token', { + session: this.session, + name: this.name, + iconUrl: this.icon, + permission: this.permission || [], + }); + + this.state = 'accepted'; + if (this.callback) { + location.href = `${this.callback}?session=${this.session}`; + } + }, + deny() { + this.state = 'denied'; + }, + onLogin(res) { + localStorage.setItem('i', res.i); + location.reload(); + } + } +}); +</script> + +<style lang="scss" scoped> + +</style> diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue index 4742793f2b..c3080e0f81 100644 --- a/src/client/pages/my-settings/index.vue +++ b/src/client/pages/my-settings/index.vue @@ -32,7 +32,9 @@ <x-integration/> <x-api/> - <mk-button @click="$root.signout()" primary style="margin: var(--margin) auto;">{{ $t('logout') }}</mk-button> + <router-link class="_panel _buttonPrimary" to="/my/apps" style="margin: var(--margin) auto;">{{ $t('installedApps') }}</router-link> + + <button class="_panel _buttonPrimary" @click="$root.signout()" style="margin: var(--margin) auto;">{{ $t('logout') }}</button> </div> </template> diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue index fcea457396..f35b5d6ed8 100644 --- a/src/client/pages/preferences/theme.vue +++ b/src/client/pages/preferences/theme.vue @@ -57,7 +57,8 @@ <mk-textarea v-model="installThemeCode"> <span>{{ $t('_theme.code') }}</span> </mk-textarea> - <mk-button @click="() => install(this.installThemeCode)" :disabled="installThemeCode == null"><fa :icon="faCheck"/> {{ $t('install') }}</mk-button> + <mk-button @click="() => install(this.installThemeCode)" :disabled="installThemeCode == null" primary inline><fa :icon="faCheck"/> {{ $t('install') }}</mk-button> + <mk-button @click="() => preview(this.installThemeCode)" :disabled="installThemeCode == null" inline><fa :icon="faEye"/> {{ $t('preview') }}</mk-button> </details> </div> <div class="_content"> @@ -79,7 +80,7 @@ <script lang="ts"> import Vue from 'vue'; -import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons'; import * as JSON5 from 'json5'; import MkInput from '../../components/ui/input.vue'; import MkButton from '../../components/ui/button.vue'; @@ -108,7 +109,7 @@ export default Vue.extend({ installThemeCode: null, selectedThemeId: null, wallpaper: localStorage.getItem('wallpaper'), - faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt + faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } }, @@ -196,8 +197,9 @@ export default Vue.extend({ }); }, - install(code) { + parseThemeCode(code) { let theme; + try { theme = JSON5.parse(code); } catch (e) { @@ -205,22 +207,34 @@ export default Vue.extend({ type: 'error', text: this.$t('_theme.invalid') }); - return; + return false; } if (!validateTheme(theme)) { this.$root.dialog({ type: 'error', text: this.$t('_theme.invalid') }); - return; + return false; } if (this.$store.state.device.themes.some(t => t.id === theme.id)) { this.$root.dialog({ type: 'info', text: this.$t('_theme.alreadyInstalled') }); - return; + return false; } + + return theme; + }, + + preview(code) { + const theme = this.parseThemeCode(code); + if (theme) applyTheme(theme, false); + }, + + install(code) { + const theme = this.parseThemeCode(code); + if (!theme) return; const themes = this.$store.state.device.themes.concat(theme); this.$store.commit('device/set', { key: 'themes', value: themes diff --git a/src/client/router.ts b/src/client/router.ts index 83445fea7e..9644ede55f 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -46,6 +46,7 @@ export const router = new VueRouter({ { path: '/my/groups', component: page('my-groups/index') }, { path: '/my/groups/:group', component: page('my-groups/group') }, { path: '/my/antennas', component: page('my-antennas/index') }, + { path: '/my/apps', component: page('apps') }, { path: '/preferences', component: page('preferences/index') }, { path: '/instance', component: page('instance/index') }, { path: '/instance/emojis', component: page('instance/emojis') }, @@ -58,6 +59,7 @@ export const router = new VueRouter({ { path: '/notes/:note', name: 'note', component: page('note') }, { path: '/tags/:tag', component: page('tag') }, { path: '/auth/:token', component: page('auth') }, + { path: '/miauth/:session', component: page('miauth') }, { path: '/authorize-follow', component: page('follow') }, { path: '/share', component: page('share') }, { path: '*', component: page('not-found') } diff --git a/src/client/style.scss b/src/client/style.scss index 7b509e5b51..57906d5ae7 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -412,6 +412,26 @@ main ._panel { } } +._fullinfo { + padding: 32px; + text-align: center; + + > img { + vertical-align: bottom; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; + } +} + +._keyValue { + display: flex; + + > div { + flex: 1; + } +} + ._link { color: var(--link); } |