diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-01-30 04:37:25 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-01-30 04:37:25 +0900 |
| commit | f6154dc0af1a0d65819e87240f4385f9573095cb (patch) | |
| tree | 699a5ca07d6727b7f8497d4769f25d6d62f94b5a /src/client/app/mobile | |
| parent | Add Event activity-type support (#5785) (diff) | |
| download | misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.tar.gz misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.tar.bz2 misskey-f6154dc0af1a0d65819e87240f4385f9573095cb.zip | |
v12 (#5712)
Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>
Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
Diffstat (limited to 'src/client/app/mobile')
56 files changed, 0 insertions, 7006 deletions
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts deleted file mode 100644 index 26ef740811..0000000000 --- a/src/client/app/mobile/script.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Mobile Client - */ - -import Vue from 'vue'; -import VueRouter from 'vue-router'; - -// Style -import './style.styl'; - -import init from '../init'; - -import MkIndex from './views/pages/index.vue'; -import MkSignup from './views/pages/signup.vue'; -import MkSelectDrive from './views/pages/selectdrive.vue'; -import MkDrive from './views/pages/drive.vue'; -import MkNotifications from './views/pages/notifications.vue'; -import MkMessaging from './views/pages/messaging.vue'; -import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkNote from './views/pages/note.vue'; -import MkSearch from './views/pages/search.vue'; -import UI from './views/pages/ui.vue'; -import MkReversi from './views/pages/games/reversi.vue'; -import MkTag from './views/pages/tag.vue'; -import MkShare from '../common/views/pages/share.vue'; -import MkFollow from '../common/views/pages/follow.vue'; -import MkNotFound from '../common/views/pages/not-found.vue'; -import DeckColumn from '../common/views/deck/deck.column-template.vue'; -import PostFormDialog from './views/components/post-form-dialog.vue'; - -import FileChooser from './views/components/drive-file-chooser.vue'; -import FolderChooser from './views/components/drive-folder-chooser.vue'; - -/** - * init - */ -init((launch, os) => { - Vue.mixin({ - data() { - return { - isMobile: true - }; - }, - - methods: { - $post(opts) { - const o = opts || {}; - - document.documentElement.style.overflow = 'hidden'; - - function recover() { - document.documentElement.style.overflow = 'auto'; - } - - const vm = this.$root.new(PostFormDialog, { - reply: o.reply, - mention: o.mention, - renote: o.renote, - initialText: o.initialText, - instant: o.instant, - initialNote: o.initialNote, - }); - vm.$once('cancel', recover); - vm.$once('posted', recover); - if (o.cb) vm.$once('closed', o.cb); - (vm as any).focus(); - }, - - $chooseDriveFile(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const vm = this.$root.new(FileChooser, { - title: o.title, - multiple: o.multiple, - initFolder: o.currentFolder - }); - vm.$once('selected', file => { - res(file); - }); - }); - }, - - $chooseDriveFolder(opts) { - return new Promise((res, rej) => { - const o = opts || {}; - const vm = this.$root.new(FolderChooser, { - title: o.title, - initFolder: o.currentFolder - }); - vm.$once('selected', folder => { - res(folder); - }); - }); - }, - - $notify(message) { - alert(message); - } - } - }); - - // Register directives - require('./views/directives'); - - // Register components - require('./views/components'); - require('./views/widgets'); - - // http://qiita.com/junya/items/3ff380878f26ca447f85 - document.body.setAttribute('ontouchstart', ''); - - // Init router - const router = new VueRouter({ - mode: 'history', - routes: [ - ...(os.store.state.device.inDeckMode - ? [{ path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [ - { path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [ - { path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) }, - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) }, - { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, - { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, - { path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) }, - { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, - { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, - { path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) }, - ]}] - : [ - { path: '/', name: 'index', component: MkIndex }, - ]), - { path: '/signup', name: 'signup', component: MkSignup }, - { path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, - { path: '/i/settings/:page', redirect: '/i/settings' }, - { path: '/i/favorites', name: 'favorites', component: UI, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'mobile' }) }, - { path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, - { path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, - { path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) }, - { path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, - { path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) }, - { path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, - { path: '/i/widgets', name: 'widgets', component: () => import('./views/pages/widgets.vue').then(m => m.default) }, - { path: '/i/notifications', name: 'notifications', component: MkNotifications }, - { path: '/i/messaging', name: 'messaging', component: MkMessaging }, - { path: '/i/messaging/group/:group', component: MkMessagingRoom }, - { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', name: 'drive', component: MkDrive }, - { path: '/i/drive/folder/:folder', component: MkDrive }, - { path: '/i/drive/file/:file', component: MkDrive }, - { path: '/i/pages/new', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }) }, - { path: '/i/pages/edit/:pageId', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initPageId: route.params.pageId }) }, - { path: '/selectdrive', component: MkSelectDrive }, - { path: '/search', component: MkSearch }, - { path: '/tags/:tag', component: MkTag }, - { path: '/featured', name: 'featured', component: UI, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'mobile' }) }, - { path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, - { path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, - { path: '/share', component: MkShare }, - { path: '/games/reversi/:game?', name: 'reversi', component: MkReversi }, - { path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [ - { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, - { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, - ]}, - { path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) }, - { path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) }, - { path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) }, - { path: '/notes/:note', component: MkNote }, - { path: '/authorize-follow', component: MkFollow }, - { path: '*', component: MkNotFound } - ] - }); - - // Launch the app - launch(router); -}, true); diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl deleted file mode 100644 index 3a4fc9c0c6..0000000000 --- a/src/client/app/mobile/style.styl +++ /dev/null @@ -1,23 +0,0 @@ -@import "../app" -@import "../reset" - -#wait - top auto - bottom 15px - left 15px - -html - height 100% - background var(--bg) - -main - width 100% - max-width 680px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - @media (min-width 600px) - padding 32px diff --git a/src/client/app/mobile/views/components/detail-notes.vue b/src/client/app/mobile/views/components/detail-notes.vue deleted file mode 100644 index bab7949534..0000000000 --- a/src/client/app/mobile/views/components/detail-notes.vue +++ /dev/null @@ -1,52 +0,0 @@ -<template> -<div class="fdcvngpy"> - <sequential-entrance animation="entranceFromTop" delay="25"> - <template v-for="note in notes"> - <mk-note-detail class="post" :note="note" :key="note.id"/> - </template> - </sequential-entrance> - <ui-button v-if="more" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - }), - ], - - props: { - pagination: { - required: true - }, - extract: { - required: false - } - }, - - computed: { - notes() { - return this.extract ? this.extract(this.items) : this.items; - } - } -}); -</script> - -<style lang="stylus" scoped> -.fdcvngpy - > * > .post - margin-bottom 8px - - @media (min-width 500px) - > * > .post - margin-bottom 16px - -</style> diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue deleted file mode 100644 index 8795102f97..0000000000 --- a/src/client/app/mobile/views/components/drive-file-chooser.vue +++ /dev/null @@ -1,106 +0,0 @@ -<template> -<div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb"> - <div class="body"> - <header> - <h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="close" @click="cancel"><fa icon="times"/></button> - <button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive class="drive" ref="browser" - :select-file="true" - :type="type" - :multiple="multiple" - @change-selection="onChangeSelection" - @selected="onSelected" - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive-file-chooser.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - props: ['type', 'multiple'], - data() { - return { - files: [] - }; - }, - methods: { - onChangeSelection(files) { - this.files = files; - }, - onSelected(file) { - this.$emit('selected', file); - this.destroyDom(); - }, - cancel() { - this.$emit('canceled'); - this.destroyDom(); - }, - ok() { - this.$emit('selected', this.files); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.cdxzvcfawjxdyxsekbxbfgtplebnoneb - position fixed - z-index 20000 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(#000, 0.2) - - > .body - width 100% - height 100% - background var(--faceHeader) - - > header - border-bottom solid 1px var(--faceDivider) - color var(--text) - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .count - margin-left 4px - opacity 0.5 - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - -</style> diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue deleted file mode 100644 index 250a7aca2c..0000000000 --- a/src/client/app/mobile/views/components/drive-folder-chooser.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> -<div class="mk-drive-folder-chooser"> - <div class="body"> - <header> - <h1>{{ $t('select-folder') }}</h1> - <button class="close" @click="cancel"><fa icon="times"/></button> - <button class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive ref="browser" - select-folder - /> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive-folder-chooser.vue'), - components: { - XDrive: () => import('./drive.vue').then(m => m.default), - }, - methods: { - cancel() { - this.$emit('canceled'); - this.destroyDom(); - }, - ok() { - this.$emit('selected', (this.$refs.browser as any).folder); - this.destroyDom(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-drive-folder-chooser - position fixed - z-index 2048 - top 0 - left 0 - width 100% - height 100% - padding 8px - background rgba(#000, 0.2) - - > .body - width 100% - height 100% - background #fff - - > header - border-bottom solid 1px #eee - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .close - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .mk-drive - height calc(100% - 42px) - overflow scroll - -webkit-overflow-scrolling touch - -</style> diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue deleted file mode 100644 index 328982a16b..0000000000 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ /dev/null @@ -1,253 +0,0 @@ -<template> -<div class="pyvicwrksnfyhpfgkjwqknuururpaztw"> - <div class="preview"> - <x-file-thumbnail class="preview" :file="file" :detail="true"/> - <template v-if="kind != 'image'"><fa icon="file"/></template> - <footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height"> - <span class="size"> - <span class="width">{{ file.properties.width }}</span> - <span class="time">×</span> - <span class="height">{{ file.properties.height }}</span> - <span class="px">px</span> - </span> - <span class="separator"></span> - <span class="aspect-ratio"> - <span class="width">{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}</span> - <span class="colon">:</span> - <span class="height">{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}</span> - </span> - </footer> - </div> - <div class="info"> - <div> - <span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span> - <span class="separator"></span> - <span class="data-size">{{ file.size | bytes }}</span> - <span class="separator"></span> - <span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> - <template v-if="file.isSensitive"> - <span class="separator"></span> - <span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> - </template> - </div> - </div> - <div class="menu"> - <div> - <ui-input readonly :value="file.url">URL</ui-input> - <ui-button link :href="dlUrl" :download="file.name"><fa icon="download"/> {{ $t('download') }}</ui-button> - <ui-button @click="rename"><fa icon="pencil-alt"/> {{ $t('rename') }}</ui-button> - <ui-button @click="move"><fa :icon="['far', 'folder-open']"/> {{ $t('move') }}</ui-button> - <ui-button @click="toggleSensitive" v-if="file.isSensitive"><fa :icon="['far', 'eye']"/> {{ $t('unmark-as-sensitive') }}</ui-button> - <ui-button @click="toggleSensitive" v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mark-as-sensitive') }}</ui-button> - <ui-button @click="del"><fa :icon="['far', 'trash-alt']"/> {{ $t('delete') }}</ui-button> - </div> - </div> - <div class="hash"> - <div> - <p> - <fa icon="hashtag"/>{{ $t('hash') }} - </p> - <code>{{ file.md5 }}</code> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { gcd } from '../../../../../prelude/math'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.file-detail.vue'), - props: ['file'], - - components: { - XFileThumbnail - }, - - data() { - return { - gcd, - exif: null - }; - }, - - computed: { - browser(): any { - return this.$parent; - }, - - kind(): string { - return this.file.type.split('/')[0]; - }, - - style(): any { - return this.file.properties.avgColor ? { - 'background-color': this.file.properties.avgColor - } : {}; - }, - - dlUrl(): string { - return this.file.url; - } - }, - - methods: { - rename() { - const name = window.prompt(this.$t('rename'), this.file.name); - if (name == null || name == '' || name == this.file.name) return; - this.$root.api('drive/files/update', { - fileId: this.file.id, - name: name - }).then(() => { - this.browser.cf(this.file, true); - }); - }, - - move() { - this.$chooseDriveFolder().then(folder => { - this.$root.api('drive/files/update', { - fileId: this.file.id, - folderId: folder == null ? null : folder.id - }).then(() => { - this.browser.cf(this.file, true); - }); - }); - }, - - del() { - this.$root.api('drive/files/delete', { - fileId: this.file.id - }).then(() => { - this.browser.cd(this.file.folderId); - }); - }, - - toggleSensitive() { - this.$root.api('drive/files/update', { - fileId: this.file.id, - isSensitive: !this.file.isSensitive - }); - - this.file.isSensitive = !this.file.isSensitive; - }, - - showCreatedAt() { - this.$root.dialog({ - type: 'info', - text: (new Date(this.file.createdAt)).toLocaleString() - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.pyvicwrksnfyhpfgkjwqknuururpaztw - > .preview - padding 8px - background var(--bg) - - > .preview - width fit-content - width -moz-fit-content - max-width 100% - margin 0 auto - box-shadow 1px 1px 4px rgba(#000, 0.2) - overflow hidden - color var(--driveFileIcon) - justify-content center - - > footer - padding 8px 8px 0 8px - text-align center - font-size 0.8em - color var(--text) - opacity 0.7 - - > .separator - display inline - padding 0 4px - - > .size - display inline - - .time - margin 0 2px - - .px - margin-left 4px - - > .aspect-ratio - display inline - opacity 0.7 - - &:before - content "(" - - &:after - content ")" - - > .info - padding 14px - font-size 0.8em - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - color var(--text) - - > .separator - padding 0 4px - - > .created-at - - > [data-icon] - margin-right 2px - - > .nsfw - color #bf4633 - - > .menu - padding 0 14px 14px 14px - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - - > .hash - padding 14px - border-top solid 1px var(--faceDivider) - - > div - max-width 500px - margin 0 auto - - > p - display block - margin 0 - padding 0 - color var(--text) - font-size 0.9em - - > [data-icon] - margin-right 4px - - > code - display block - width 100% - margin 6px 0 0 0 - padding 8px - white-space nowrap - overflow auto - font-size 0.8em - color #222 - border solid 1px #dfdfdf - border-radius 2px - background #f5f5f5 - -</style> diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue deleted file mode 100644 index ed95537f9c..0000000000 --- a/src/client/app/mobile/views/components/drive.file.vue +++ /dev/null @@ -1,155 +0,0 @@ -<template> -<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> - <div class="container"> - <x-file-thumbnail class="thumbnail" :file="file" fit="cover"/> - <div class="body"> - <p class="name"> - <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> - <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> - </p> - <footer> - <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> - <span class="separator"></span> - <span class="data-size">{{ file.size | bytes }}</span> - <span class="separator"></span> - <span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> - <template v-if="file.isSensitive"> - <span class="separator"></span> - <span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span> - </template> - </footer> - </div> - </div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.file.vue'), - props: ['file'], - components: { - XFileThumbnail - }, - data() { - return { - isSelected: false - }; - }, - computed: { - browser(): any { - return this.$parent; - } - }, - created() { - this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id) - - this.browser.$on('change-selection', this.onBrowserChangeSelection); - }, - beforeDestroy() { - this.browser.$off('change-selection', this.onBrowserChangeSelection); - }, - methods: { - onBrowserChangeSelection(selections) { - this.isSelected = selections.some(f => f.id == this.file.id); - }, - onClick() { - this.browser.chooseFile(this.file); - } - } -}); -</script> - -<style lang="stylus" scoped> -.vupkuhvjnjyqaqhsiogfbywvjxynrgsm - display block - text-decoration none !important - - * - user-select none - pointer-events none - - > .container - display grid - max-width 500px - margin 0 auto - padding 16px - grid-template-columns 64px 1fr - grid-column-gap 10px - - &:after - content "" - display block - clear both - - > .thumbnail - width 64px - height 64px - color var(--driveFileIcon) - - > .body - display block - word-break break-all - - > .name - display block - margin 0 - padding 0 - font-size 0.9em - font-weight bold - color var(--text) - word-break break-word - - > .ext - opacity 0.5 - - > .tags - display block - margin 4px 0 0 0 - padding 0 - list-style none - font-size 0.5em - - > .tag - display inline-block - margin 0 5px 0 0 - padding 1px 5px - border-radius 2px - - > footer - display block - margin 4px 0 0 0 - font-size 0.7em - color var(--text) - - > .separator - padding 0 4px - - > .type - opacity 0.7 - - > .mk-file-type-icon - margin-right 4px - - > .data-size - opacity 0.7 - - > .created-at - opacity 0.7 - - > [data-icon] - margin-right 2px - - > .nsfw - color #bf4633 - - &[data-is-selected] - background var(--primary) - - &, * - color var(--primaryForeground) !important - -</style> diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue deleted file mode 100644 index 0959c1e7d4..0000000000 --- a/src/client/app/mobile/views/components/drive.folder.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> -<a class="jvwxssxsytqlqvrpiymarjlzlsxskqsr" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`"> - <div class="container"> - <p class="name"><fa icon="folder"/>{{ folder.name }}</p><fa icon="angle-right"/> - </div> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['folder'], - computed: { - browser(): any { - return this.$parent; - } - }, - methods: { - onClick() { - this.browser.cd(this.folder); - } - } -}); -</script> - -<style lang="stylus" scoped> -.jvwxssxsytqlqvrpiymarjlzlsxskqsr - display block - color var(--text) - text-decoration none !important - - * - user-select none - pointer-events none - - > .container - max-width 500px - margin 0 auto - padding 16px - - > .name - display block - margin 0 - padding 0 - - > [data-icon] - margin-right 6px - - > [data-icon] - position absolute - top 0 - bottom 0 - right 20px - height 100% - -</style> diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue deleted file mode 100644 index fe193f311a..0000000000 --- a/src/client/app/mobile/views/components/drive.vue +++ /dev/null @@ -1,618 +0,0 @@ -<template> -<div class="kmmwchoexgckptowjmjgfsygeltxfeqs"> - <nav ref="nav"> - <a @click.prevent="goRoot()" href="/i/drive"><fa icon="cloud"/>{{ $t('@.drive') }}</a> - <template v-for="folder in hierarchyFolders"> - <span :key="folder.id + '>'"><fa icon="angle-right"/></span> - <a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a> - </template> - <template v-if="folder != null"> - <span><fa icon="angle-right"/></span> - <p>{{ folder.name }}</p> - </template> - <template v-if="file != null"> - <span><fa icon="angle-right"/></span> - <p>{{ file.name }}</p> - </template> - </nav> - <mk-uploader ref="uploader"/> - <div class="browser" :class="{ fetching }" v-if="file == null"> - <div class="info" v-if="info"> - <p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% {{ $t('used') }}</p> - <p v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)"> - <template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} {{ $t('folder-count') }}</template> - <template v-if="folder.foldersCount > 0 && folder.filesCount > 0">{{ $t('count-separator') }}</template> - <template v-if="folder.filesCount > 0">{{ folder.filesCount }} {{ $t('file-count') }}</template> - </p> - </div> - <div class="folders" v-if="folders.length > 0 || moreFolders"> - <x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/> - <p v-if="moreFolders">{{ $t('@.load-more') }}</p> - </div> - <div class="files" v-if="files.length > 0 || moreFiles"> - <x-file class="file" v-for="file in files" :key="file.id" :file="file"/> - <button class="more" v-if="moreFiles" @click="fetchMoreFiles"> - {{ fetchingMoreFiles ? this.$t('@.loading') : this.$t('@.load-more') }} - </button> - </div> - <div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching"> - <p v-if="folder == null">{{ $t('nothing-in-drive') }}</p> - <p v-if="folder != null">{{ $t('folder-is-empty') }}</p> - </div> - </div> - <div class="fetching" v-if="fetching && file == null && files.length == 0 && folders.length == 0"> - <div class="spinner"> - <div class="dot1"></div> - <div class="dot2"></div> - </div> - </div> - <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeLocalFile"/> - <x-file-detail v-if="file != null" :file="file"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XFolder from './drive.folder.vue'; -import XFile from './drive.file.vue'; -import XFileDetail from './drive.file-detail.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/drive.vue'), - components: { - XFolder, - XFile, - XFileDetail - }, - props: ['initFolder', 'initFile', 'selectFile', 'multiple', 'isNaked', 'top'], - data() { - return { - /** - * 現在の階層(フォルダ) - * * null でルートを表す - */ - folder: null, - - file: null, - - files: [], - folders: [], - moreFiles: false, - moreFolders: false, - hierarchyFolders: [], - selectedFiles: [], - info: null, - connection: null, - - fetching: true, - fetchingMoreFiles: false, - fetchingMoreFolders: false - }; - }, - computed: { - isFileSelectMode(): boolean { - return this.selectFile; - } - }, - watch: { - top() { - if (this.isNaked) { - (this.$refs.nav as any).style.top = `${this.top}px`; - } - } - }, - mounted() { - this.connection = this.$root.stream.useSharedConnection('drive'); - - this.connection.on('fileCreated', this.onStreamDriveFileCreated); - this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); - this.connection.on('fileDeleted', this.onStreamDriveFileDeleted); - this.connection.on('folderCreated', this.onStreamDriveFolderCreated); - this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated); - - if (this.initFolder) { - this.cd(this.initFolder, true); - } else if (this.initFile) { - this.cf(this.initFile, true); - } else { - this.fetch(); - } - - if (this.isNaked) { - (this.$refs.nav as any).style.top = `${this.top}px`; - } - }, - beforeDestroy() { - this.connection.dispose(); - }, - methods: { - onStreamDriveFileCreated(file) { - this.addFile(file, true); - }, - - onStreamDriveFileUpdated(file) { - const current = this.folder ? this.folder.id : null; - if (current != file.folderId) { - this.removeFile(file); - } else { - this.addFile(file, true); - } - }, - - onStreamDriveFileDeleted(fileId) { - this.removeFile(fileId); - }, - - onStreamDriveFolderCreated(folder) { - this.addFolder(folder, true); - }, - - onStreamDriveFolderUpdated(folder) { - const current = this.folder ? this.folder.id : null; - if (current != folder.parentId) { - this.removeFolder(folder); - } else { - this.addFolder(folder, true); - } - }, - - dive(folder) { - this.hierarchyFolders.unshift(folder); - if (folder.parent) this.dive(folder.parent); - }, - - cd(target, silent = false) { - if (target == null) { - this.goRoot(silent); - return; - } else if (typeof target == 'object') { - target = target.id; - } - - this.file = null; - this.fetching = true; - - this.$root.api('drive/folders/show', { - folderId: target - }).then(folder => { - this.folder = folder; - this.hierarchyFolders = []; - - if (folder.parent) this.dive(folder.parent); - - this.$emit('open-folder', this.folder, silent); - this.fetch(); - }); - }, - - addFolder(folder, unshift = false) { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断 - if (current != folder.parentId) return; - - // 追加しようとしているフォルダを既に所有してたら中断 - if (this.folders.some(f => f.id == folder.id)) return; - - if (unshift) { - this.folders.unshift(folder); - } else { - this.folders.push(folder); - } - }, - - addFile(file, unshift = false) { - const current = this.folder ? this.folder.id : null; - // 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断 - if (current != file.folderId) return; - - if (this.files.some(f => f.id == file.id)) { - const exist = this.files.map(f => f.id).indexOf(file.id); - Vue.set(this.files, exist, file); - return; - } - - if (unshift) { - this.files.unshift(file); - } else { - this.files.push(file); - } - }, - - removeFolder(folder) { - if (typeof folder == 'object') folder = folder.id; - this.folders = this.folders.filter(f => f.id != folder); - }, - - removeFile(file) { - if (typeof file == 'object') file = file.id; - this.files = this.files.filter(f => f.id != file); - }, - - appendFile(file) { - this.addFile(file); - }, - appendFolder(folder) { - this.addFolder(folder); - }, - prependFile(file) { - this.addFile(file, true); - }, - prependFolder(folder) { - this.addFolder(folder, true); - }, - - goRoot(silent = false) { - // すでにrootにいるなら何もしない - if (this.folder == null && this.file == null) return; - - this.file = null; - this.folder = null; - this.hierarchyFolders = []; - this.$emit('move-root', silent); - this.fetch(); - }, - - fetch() { - this.folders = []; - this.files = []; - this.moreFolders = false; - this.moreFiles = false; - this.fetching = true; - - this.$emit('begin-fetch'); - - let fetchedFolders = null; - let fetchedFiles = null; - - const foldersMax = 20; - const filesMax = 20; - - // フォルダ一覧取得 - this.$root.api('drive/folders', { - folderId: this.folder ? this.folder.id : null, - limit: foldersMax + 1 - }).then(folders => { - if (folders.length == foldersMax + 1) { - this.moreFolders = true; - folders.pop(); - } - fetchedFolders = folders; - complete(); - }); - - // ファイル一覧取得 - this.$root.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: filesMax + 1 - }).then(files => { - if (files.length == filesMax + 1) { - this.moreFiles = true; - files.pop(); - } - fetchedFiles = files; - complete(); - }); - - let flag = false; - const complete = () => { - if (flag) { - for (const x of fetchedFolders) this.appendFolder(x); - for (const x of fetchedFiles) this.appendFile(x); - this.fetching = false; - - // 一連の読み込みが完了したイベントを発行 - this.$emit('fetched'); - } else { - flag = true; - // 一連の読み込みが半分完了したイベントを発行 - this.$emit('fetch-mid'); - } - }; - - if (this.folder == null) { - // Fetch addtional drive info - this.$root.api('drive').then(info => { - this.info = info; - }); - } - }, - - fetchMoreFiles() { - this.fetching = true; - this.fetchingMoreFiles = true; - - const max = 30; - - // ファイル一覧取得 - this.$root.api('drive/files', { - folderId: this.folder ? this.folder.id : null, - limit: max + 1, - untilId: this.files[this.files.length - 1].id - }).then(files => { - if (files.length == max + 1) { - this.moreFiles = true; - files.pop(); - } else { - this.moreFiles = false; - } - for (const x of files) this.appendFile(x); - this.fetching = false; - this.fetchingMoreFiles = false; - }); - }, - - chooseFile(file) { - if (this.isFileSelectMode) { - if (this.multiple) { - if (this.selectedFiles.some(f => f.id == file.id)) { - this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id); - } else { - this.selectedFiles.push(file); - } - this.$emit('change-selection', this.selectedFiles); - } else { - this.$emit('selected', file); - } - } else { - this.cf(file); - } - }, - - cf(file, silent = false) { - if (typeof file == 'object') file = file.id; - - this.fetching = true; - - this.$root.api('drive/files/show', { - fileId: file - }).then(file => { - this.file = file; - this.folder = null; - this.hierarchyFolders = []; - - if (file.folder) this.dive(file.folder); - - this.fetching = false; - - this.$emit('open-file', this.file, silent); - }); - }, - - selectLocalFile() { - (this.$refs.file as any).click(); - }, - - createFolder() { - this.$root.dialog({ - title: this.$t('folder-name'), - input: { - default: this.folder.name - } - }).then(({ result: name }) => { - if (!name) { - this.$root.dialog({ - type: 'error', - text: this.$t('folder-name-cannot-empty') - }); - return; - } - this.$root.api('drive/folders/create', { - name: name, - parentId: this.folder ? this.folder.id : undefined - }).then(folder => { - this.addFolder(folder, true); - }); - }); - }, - - renameFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$root.dialog({ - title: this.$t('folder-name'), - input: { - default: this.folder.name - } - }).then(({ result: name }) => { - if (!name) { - this.$root.dialog({ - type: 'error', - text: this.$t('cannot-empty') - }); - return; - } - this.$root.api('drive/folders/update', { - name: name, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }, - - moveFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$chooseDriveFolder().then(folder => { - this.$root.api('drive/folders/update', { - parentId: folder ? folder.id : null, - folderId: this.folder.id - }).then(folder => { - this.cd(folder); - }); - }); - }, - - urlUpload() { - const url = window.prompt(this.$t('url-prompt')); - if (url == null || url == '') return; - this.$root.api('drive/files/upload_from_url', { - url: url, - folderId: this.folder ? this.folder.id : undefined - }); - this.$root.dialog({ - type: 'info', - text: this.$t('uploading') - }); - }, - - onChangeLocalFile() { - for (const f of Array.from((this.$refs.file as any).files)) { - (this.$refs.uploader as any).upload(f, this.folder); - } - }, - - deleteFolder() { - if (this.folder == null) { - this.$root.dialog({ - type: 'error', - text: this.$t('here-is-root') - }); - return; - } - this.$root.api('drive/folders/delete', { - folderId: this.folder.id - }).then(folder => { - this.cd(this.folder.parentId); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.kmmwchoexgckptowjmjgfsygeltxfeqs - background var(--face) - - > nav - display block - position sticky - position -webkit-sticky - top 0 - z-index 1 - width 100% - padding 10px 12px - overflow auto - white-space nowrap - font-size 0.9em - color var(--text) - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color var(--mobileDriveNavBg) - border-bottom solid 1px rgba(#000, 0.13) - - > p - > a - display inline - margin 0 - padding 0 - text-decoration none !important - color inherit - - &:last-child - font-weight bold - - > [data-icon] - margin-right 4px - - > span - margin 0 8px - opacity 0.5 - - > .browser - &.fetching - opacity 0.5 - - > .info - border-bottom solid 1px var(--faceDivider) - - &:empty - display none - - > p - display block - max-width 500px - margin 0 auto - padding 4px 16px - font-size 10px - color var(--text) - - > .folders - > .folder - border-bottom solid 1px var(--faceDivider) - - > .files - > .file - border-bottom solid 1px var(--faceDivider) - - > .more - display block - width 100% - padding 16px - font-size 16px - color #555 - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background rgba(#000, 0.2) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { - 100% { - transform: rotate(360deg); - } - } - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } - 50% { - transform: scale(1.0); - } - } - - > .file - display none - -</style> diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts deleted file mode 100644 index 4e10d80f92..0000000000 --- a/src/client/app/mobile/views/components/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Vue from 'vue'; - -import ui from './ui.vue'; -import note from './note.vue'; -import notes from './notes.vue'; -import mediaVideo from './media-video.vue'; -import notePreview from './note-preview.vue'; -import subNoteContent from './sub-note-content.vue'; -import noteCard from './note-card.vue'; -import noteDetail from './note-detail.vue'; -import notification from './notification.vue'; -import notifications from './notifications.vue'; -import notificationPreview from './notification-preview.vue'; -import userTimeline from './user-timeline.vue'; -import userListTimeline from './user-list-timeline.vue'; -import uiContainer from './ui-container.vue'; - -Vue.component('mk-ui', ui); -Vue.component('mk-note', note); -Vue.component('mk-notes', notes); -Vue.component('mk-media-video', mediaVideo); -Vue.component('mk-note-preview', notePreview); -Vue.component('mk-sub-note-content', subNoteContent); -Vue.component('mk-note-card', noteCard); -Vue.component('mk-note-detail', noteDetail); -Vue.component('mk-notification', notification); -Vue.component('mk-notifications', notifications); -Vue.component('mk-notification-preview', notificationPreview); -Vue.component('mk-user-timeline', userTimeline); -Vue.component('mk-user-list-timeline', userListTimeline); -Vue.component('ui-container', uiContainer); diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue deleted file mode 100644 index 044bb4c106..0000000000 --- a/src/client/app/mobile/views/components/media-video.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> -<div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> - <div> - <b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> - <span>{{ $t('click-to-show') }}</span> - </div> -</div> -<a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else - :href="video.url" - rel="nofollow noopener" - target="_blank" - :style="imageStyle" - :title="video.name" -> - <fa :icon="['far', 'play-circle']"/> -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/media-video.vue'), - props: { - video: { - type: Object, - required: true - } - }, - data() { - return { - hide: true - }; - }, - computed: { - imageStyle(): any { - return { - 'background-image': `url(${this.video.thumbnailUrl})` - }; - } - } -}); -</script> - -<style lang="stylus" scoped> -.kkjnbbplepmiyuadieoenjgutgcmtsvu - display flex - justify-content center - align-items center - - font-size 3.5em - overflow hidden - background-position center - background-size cover - width 100% - height 100% - -.icozogqfvdetwohsdglrbswgrejoxbdj - display flex - justify-content center - align-items center - background #111 - color #fff - - > div - display table-cell - text-align center - font-size 12px - - > b - display block - -</style> diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue deleted file mode 100644 index 2704820318..0000000000 --- a/src/client/app/mobile/views/components/note-card.vue +++ /dev/null @@ -1,93 +0,0 @@ -<template> -<div class="mk-note-card"> - <a :href="note | notePage"> - <header> - <img :src="avator" alt="avatar"/> - <h3><mk-user-name :user="note.user"/></h3> - </header> - <div> - {{ text }} - </div> - <mk-time :time="note.createdAt"/> - </a> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import summary from '../../../../../misc/get-note-summary'; -import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - props: ['note'], - computed: { - text(): string { - return summary(this.note); - }, - avator(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.note.user.avatarUrl) - : this.note.user.avatarUrl; - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-card - display inline-block - width 150px - //height 120px - font-size 12px - background var(--face) - border-radius 4px - box-shadow 0 2px 8px rgba(0, 0, 0, 0.2) - - > a - display block - color var(--noteText) - - &:hover - text-decoration none - - > header - > img - position absolute - top 8px - left 8px - width 28px - height 28px - border-radius 6px - - > h3 - display inline-block - overflow hidden - width calc(100% - 45px) - margin 8px 0 0 42px - line-height 28px - white-space nowrap - text-overflow ellipsis - font-size 12px - - > div - padding 2px 8px 8px 8px - height 60px - overflow hidden - white-space normal - - &:after - content "" - display block - position absolute - top 40px - left 0 - width 100% - height 20px - background linear-gradient(to bottom, transparent 0%, var(--face) 100%) - - > .mk-time - display inline-block - padding 8px - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue deleted file mode 100644 index 358b827a5c..0000000000 --- a/src/client/app/mobile/views/components/note-detail.vue +++ /dev/null @@ -1,351 +0,0 @@ -<template> -<div class="mk-note-detail" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <button - class="more" - v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0" - @click="fetchConversation" - :disabled="conversationFetching" - > - <template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template> - <template v-if="conversationFetching"><fa icon="spinner" pulse/></template> - </button> - <div class="conversation"> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - </div> - <div class="reply-to" v-if="appearNote.reply"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note" mini/> - <article> - <header> - <mk-avatar class="avatar" :user="appearNote.user"/> - <div> - <router-link class="name" :to="appearNote.user | userPage"><mk-user-name :user="appearNote.user"/></router-link> - <span class="username"><mk-acct :user="appearNote.user"/></span> - </div> - </header> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <span v-if="appearNote.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files" :raw="true"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="map" v-if="appearNote.geo" ref="map"></div> - <div class="renote" v-if="appearNote.renote"> - <mk-note-preview :note="appearNote.renote"/> - </div> - </div> - </div> - <router-link class="time" :to="appearNote | notePage"> - <mk-time :time="appearNote.createdAt" mode="detail"/> - </router-link> - <div class="visibility-info"> - <span class="visibility" v-if="appearNote.visibility != 'public'"> - <fa v-if="appearNote.visibility == 'home'" icon="home"/> - <fa v-if="appearNote.visibility == 'followers'" icon="unlock"/> - <fa v-if="appearNote.visibility == 'specified'" icon="envelope"/> - </span> - <span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span> - </div> - <footer> - <mk-reactions-viewer :note="appearNote"/> - <button @click="reply()" :title="$t('title')"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton"> - <fa icon="minus"/> - </button> - <button @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - </article> - <div class="replies" v-if="!compact"> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSub from './note.sub.vue'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; -import noteMixin from '../../../common/scripts/note-mixin'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/note-detail.vue'), - - components: { - XSub - }, - - mixins: [noteMixin(), noteSubscriber('note')], - - props: { - note: { - type: Object, - required: true - }, - compact: { - default: false - } - }, - - data() { - return { - conversation: [], - conversationFetching: false, - replies: [] - }; - }, - - watch: { - note() { - this.fetchReplies(); - } - }, - - mounted() { - this.fetchReplies(); - }, - - methods: { - fetchReplies() { - if (this.compact) return; - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - }, - - fetchConversation() { - this.conversationFetching = true; - - // Fetch conversation - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversationFetching = false; - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-note-detail - overflow hidden - width 100% - text-align left - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) - - > .fetching - padding 64px 0 - - > .more - display block - margin 0 - padding 10px 0 - width 100% - font-size 1em - text-align center - color var(--text) - cursor pointer - background var(--subNoteBg) - outline none - border none - border-bottom solid 1px var(--faceDivider) - border-radius 6px 6px 0 0 - box-shadow none - - &:hover - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) - - &:active - box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) - - > .conversation - > * - border-bottom 1px solid var(--faceDivider) - - > .renote + article - padding-top 8px - - > .reply-to - border-bottom 1px solid var(--faceDivider) - - > article - padding 14px 16px 9px 16px - - @media (min-width 500px) - padding 28px 32px 18px 32px - - > header - display flex - line-height 1.1em - - > .avatar - display block - margin 0 12px 0 0 - width 54px - height 54px - border-radius 8px - - @media (min-width 500px) - width 60px - height 60px - - > div - min-width 0 - - > .name - display inline-block - margin .4em 0 - color var(--noteHeaderName) - font-size 16px - font-weight bold - text-align left - text-decoration none - - &:hover - text-decoration underline - - > .username - display block - text-align left - margin 0 - color var(--noteHeaderAcct) - - > .body - padding 8px 0 - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 16px - color var(--noteText) - - @media (min-width 500px) - font-size 24px - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed 1px var(--quoteBorder) - border-radius 8px - - > .location - margin 4px 0 - font-size 12px - color var(--text) - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-url-preview - margin-top 8px - - > .files - > img - display block - max-width 100% - - > .time - font-size 16px - color var(--noteHeaderInfo) - - > .visibility-info - color var(--noteHeaderInfo) - - > .localOnly - margin-left 4px - - > footer - font-size 1.2em - - > button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color var(--noteActions) - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color var(--noteActionsHover) - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted - color var(--primary) - - > .replies - > * - border-top 1px solid var(--faceDivider) - -</style> diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue deleted file mode 100644 index 1dbbddaa62..0000000000 --- a/src/client/app/mobile/views/components/note-preview.vue +++ /dev/null @@ -1,114 +0,0 @@ -<template> -<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart' && !narrow"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <span class="text" v-if="note.cw != ''">{{ note.cw }}</span> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - } -}); -</script> - -<style lang="stylus" scoped> -.yohlumlkhizgfkvvscwfcrcggkotpvry - display flex - margin 0 - padding 0 - overflow hidden - font-size 10px - - &:not(.mini) - - @media (min-width 350px) - font-size 12px - - @media (min-width 500px) - font-size 14px - - > .avatar - - @media (min-width 350px) - margin 0 10px 0 0 - width 44px - height 44px - - @media (min-width 500px) - margin 0 12px 0 0 - width 48px - height 48px - - &.smart - > .main - width 100% - - > header - align-items center - - > .avatar - flex-shrink 0 - display block - margin 0 10px 0 0 - width 40px - height 40px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - cursor default - margin 0 - padding 0 - color var(--subNoteText) - -</style> diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue deleted file mode 100644 index b951947f2a..0000000000 --- a/src/client/app/mobile/views/components/note.sub.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }"> - <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> - <div class="main"> - <mk-note-header class="header" :note="note" :mini="true"/> - <div class="body"> - <p v-if="note.cw != null" class="cw"> - <mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> - <mk-cw-button v-model="showContent" :note="note"/> - </p> - <div class="content" v-show="note.cw == null || showContent"> - <mk-sub-note-content class="text" :note="note"/> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - note: { - type: Object, - required: true - }, - // TODO - truncate: { - type: Boolean, - default: true - } - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - showContent: false - }; - } -}); -</script> - -<style lang="stylus" scoped> -.zlrxdaqttccpwhpaagdmkawtzklsccam - display flex - padding 16px - font-size 10px - background var(--subNoteBg) - - &:not(.mini) - - @media (min-width 350px) - font-size 12px - - @media (min-width 500px) - font-size 14px - - @media (min-width 600px) - padding 24px 32px - - > .avatar - - @media (min-width 350px) - margin-right 10px - width 42px - height 42px - - @media (min-width 500px) - margin-right 14px - width 50px - height 50px - - &.smart - > .main - width 100% - - > header - align-items center - - > .avatar - flex-shrink 0 - display block - margin 0 8px 0 0 - width 38px - height 38px - border-radius 8px - - > .main - flex 1 - min-width 0 - - > .header - margin-bottom 2px - - > .body - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - > .text - margin 0 - padding 0 - color var(--subNoteText) - font-size calc(1em + var(--fontSize)) - - pre - max-height 120px - font-size 80% - -</style> diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue deleted file mode 100644 index 01514f05fc..0000000000 --- a/src/client/app/mobile/views/components/note.vue +++ /dev/null @@ -1,302 +0,0 @@ -<template> -<div - class="note" - v-show="appearNote.deletedAt == null && !hideThisNote" - :tabindex="appearNote.deletedAt == null ? '-1' : null" - :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart', mini: narrow }" - v-hotkey="keymap" -> - <x-sub v-for="note in conversation" :key="note.id" :note="note"/> - <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> - <x-sub :note="appearNote.reply"/> - </div> - <mk-renote class="renote" v-if="isRenote" :note="note"/> - <article class="article"> - <mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/> - <div class="main"> - <mk-note-header class="header" :note="appearNote" :mini="true"/> - <div class="body" v-if="appearNote.deletedAt == null"> - <p v-if="appearNote.cw != null" class="cw"> - <mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> - <mk-cw-button v-model="showContent" :note="appearNote"/> - </p> - <div class="content" v-show="appearNote.cw == null || showContent"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a> - <mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> - <a class="rp" v-if="appearNote.renote != null">RN:</a> - </div> - <div class="files" v-if="appearNote.files.length > 0"> - <mk-media-list :media-list="appearNote.files"/> - </div> - <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> - <mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true"/> - <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a> - <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div> - </div> - <span class="app" v-if="appearNote.app && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span> - </div> - <footer v-if="appearNote.deletedAt == null" class="footer"> - <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/> - <button @click="reply()" class="button"> - <template v-if="appearNote.reply"><fa icon="reply-all"/></template> - <template v-else><fa icon="reply"/></template> - <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> - </button> - <button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote" class="button"> - <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="button"> - <fa icon="ban"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction == null" class="button" @click="react()" ref="reactButton"> - <fa icon="plus"/> - </button> - <button v-if="!isMyNote && appearNote.myReaction != null" class="button reacted" @click="undoReact(appearNote)" ref="reactButton"> - <fa icon="minus"/> - </button> - <button class="button" @click="menu()" ref="menuButton"> - <fa icon="ellipsis-h"/> - </button> - </footer> - <div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div> - </div> - </article> - <x-sub v-for="note in replies" :key="note.id" :note="note"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -import XSub from './note.sub.vue'; -import noteMixin from '../../../common/scripts/note-mixin'; -import noteSubscriber from '../../../common/scripts/note-subscriber'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/note.vue'), - components: { - XSub - }, - - mixins: [ - noteMixin({ - mobile: true - }), - noteSubscriber('note') - ], - - props: { - note: { - type: Object, - required: true - }, - detail: { - type: Boolean, - required: false, - default: false - }, - }, - - inject: { - narrow: { - default: false - } - }, - - data() { - return { - conversation: [], - replies: [] - }; - }, - - created() { - if (this.detail) { - this.$root.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; - }); - - this.$root.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversation = conversation.reverse(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.note - overflow hidden - font-size 13px - border-bottom solid var(--lineWidth) var(--faceDivider) - - &:last-of-type - border-bottom none - - &:not(.mini) - - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - > .article - @media (min-width 600px) - padding 32px 32px 22px - - > .avatar - @media (min-width 350px) - width 48px - height 48px - border-radius 6px - - @media (min-width 500px) - margin-right 16px - width 58px - height 58px - border-radius 8px - - > .main - > .header - @media (min-width 500px) - margin-bottom 2px - - > .body - @media (min-width 700px) - font-size 1.1em - - &.smart - > .article - > .main - > header - align-items center - margin-bottom 4px - - > .renote + .article - padding-top 8px - - > .article - display flex - padding 16px 16px 9px - - > .avatar - flex-shrink 0 - display block - margin 0 10px 8px 0 - width 42px - height 42px - border-radius 6px - //position -webkit-sticky - //position sticky - //top 62px - - > .main - flex 1 - min-width 0 - - > .body - > .cw - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - - > .text - margin-right 8px - - > .content - - > .text - display block - margin 0 - padding 0 - overflow-wrap break-word - color var(--noteText) - font-size calc(1em + var(--fontSize)) - - > .reply - margin-right 8px - color var(--noteText) - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - .mk-url-preview - margin-top 8px - - > .files - > img - display block - max-width 100% - - > .location - margin 4px 0 - font-size 12px - color #ccc - - > .map - width 100% - height 200px - - &:empty - display none - - > .mk-poll - font-size 80% - - > .renote - margin 8px 0 - - > * - padding 16px - border dashed var(--lineWidth) var(--quoteBorder) - border-radius 8px - - > .app - font-size 12px - color #ccc - - > .footer - > .button - margin 0 - padding 8px - background transparent - border none - box-shadow none - font-size 1em - color var(--noteActions) - cursor pointer - - &:not(:last-child) - margin-right 28px - - &:hover - color var(--noteActionsHover) - - > .count - display inline - margin 0 0 0 8px - color var(--text) - opacity 0.7 - - &.reacted - color var(--primary) - - > .deleted - color var(--noteText) - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue deleted file mode 100644 index 1a0cd5cc24..0000000000 --- a/src/client/app/mobile/views/components/notes.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="ivaojijs" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div> - - <mk-error v-if="error" @retry="init()"/> - - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div"> - <template v-for="(note, i) in _notes"> - <mk-note :note="note" :key="note.id"/> - <p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date"> - <span><fa icon="angle-up"/>{{ note._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <footer v-if="more"> - <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - </button> - </footer> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import shouldMuteNote from '../../../common/scripts/should-mute-note'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - paging({ - captureWindowScroll: true, - - onQueueChanged: (self, x) => { - if (x.length > 0) { - self.$store.commit('indicate', true); - } else { - self.$store.commit('indicate', false); - } - }, - - onPrepend: (self, note) => { - // 弾く - if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false; - - // タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 - if (document.hidden || !self.isScrollTop()) { - self.$store.commit('pushBehindNote', note); - } - }, - - onInited: (self) => { - self.$emit('loaded'); - } - }), - ], - - props: { - pagination: { - required: true - }, - }, - - computed: { - _notes(): any[] { - return (this.items as any).map(item => { - const date = new Date(item.createdAt).getDate(); - const month = new Date(item.createdAt).getMonth() + 1; - item._date = date; - item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return item; - }); - } - }, -}); -</script> - -<style lang="stylus" scoped> -.ivaojijs - overflow hidden - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) - - > .empty - padding 16px - text-align center - color var(--text) - - .transition - .mk-notes-enter - .mk-notes-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.9em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .placeholder - padding 16px - opacity 0.3 - - @media (min-width 500px) - padding 32px - - > .empty - margin 0 auto - padding 32px - max-width 400px - text-align center - color var(--text) - - > footer - text-align center - border-top solid var(--lineWidth) var(--faceDivider) - - &:empty - display none - - > button - margin 0 - padding 16px - width 100% - color var(--text) - - @media (min-width 500px) - padding 20px - - &:disabled - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue deleted file mode 100644 index 8422c73420..0000000000 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ /dev/null @@ -1,137 +0,0 @@ -<template> -<div class="mk-notification-preview" :class="notification.type"> - <template v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><mk-reaction-icon :reaction="notification.reaction"/><mk-user-name :user="notification.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p> - </div> - </template> - - <template v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="retweet"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note.renote) }}<fa icon="quote-right"/></p> - </div> - </template> - - <template v-if="notification.type == 'quote'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="quote-left"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="user-plus"/><mk-user-name :user="notification.user"/></p> - </div> - </template> - - <template v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="user-clock"/><mk-user-name :user="notification.user"/></p> - </div> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="reply"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-avatar class="avatar" :user="notification.note.user"/> - <div class="text"> - <p><fa icon="at"/><mk-user-name :user="notification.note.user"/></p> - <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> - </div> - </template> - - <template v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div class="text"> - <p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p> - <p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p> - </div> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notification-preview - margin 0 - padding 8px - color #fff - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > .text - float right - width calc(100% - 36px) - padding-left 8px - - p - margin 0 - - [data-icon], mk-reaction-icon - margin-right 4px - - .note-ref - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.renote, &.quote - .text p [data-icon] - color #77B255 - - &.follow - .text p [data-icon] - color #53c7ce - - &.receiveFollowRequest - .text p [data-icon] - color #888 - - &.reply, &.mention - .text p [data-icon] - color #fff - -</style> - diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue deleted file mode 100644 index 2defef4777..0000000000 --- a/src/client/app/mobile/views/components/notification.vue +++ /dev/null @@ -1,199 +0,0 @@ -<template> -<div class="mk-notification"> - <div class="notification reaction" v-if="notification.type == 'reaction'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <mk-reaction-icon :reaction="notification.reaction"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification renote" v-if="notification.type == 'renote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="retweet"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <div class="notification follow" v-if="notification.type == 'follow'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-plus"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="user-clock"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - </div> - </div> - - <div class="notification pollVote" v-if="notification.type == 'pollVote'"> - <mk-avatar class="avatar" :user="notification.user"/> - <div> - <header> - <fa icon="chart-pie"/> - <router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link> - <mk-time :time="notification.createdAt"/> - </header> - <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> - <fa icon="quote-left"/> - <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/> - <fa icon="quote-right"/> - </router-link> - </div> - </div> - - <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import getNoteSummary from '../../../../../misc/get-note-summary'; - -export default Vue.extend({ - props: ['notification'], - data() { - return { - getNoteSummary - }; - }, -}); -</script> - -<style lang="stylus" scoped> -.mk-notification - - &.wide - > .notification - @media (min-width 350px) - font-size 14px - - @media (min-width 500px) - font-size 16px - - @media (min-width 600px) - padding 24px 32px - - > .avatar - @media (min-width 500px) - width 42px - height 42px - - > div - @media (min-width 500px) - width calc(100% - 42px) - - > .notification - padding 16px - font-size 12px - overflow-wrap break-word - - &:after - content "" - display block - clear both - - > .avatar - display block - float left - width 36px - height 36px - border-radius 6px - - > div - float right - width calc(100% - 36px) - padding-left 8px - - > header - display flex - align-items baseline - white-space nowrap - - [data-icon], .mk-reaction-icon - margin-right 4px - - > .name - text-overflow ellipsis - white-space nowrap - min-width 0 - overflow hidden - - > .mk-time - margin-left auto - color var(--noteHeaderInfo) - font-size 0.9em - - > .note-preview - color var(--noteText) - - > .note-ref - color var(--noteText) - display inline-block - width: 100% - overflow hidden - white-space nowrap - text-overflow ellipsis - - [data-icon] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px - - &.reaction - > div > header - align-items normal - - &.renote - > div > header [data-icon] - color #77B255 - - &.follow - > div > header [data-icon] - color #53c7ce - - &.receiveFollowRequest - > div > header [data-icon] - color #888 - -</style> diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue deleted file mode 100644 index ca6a8beca3..0000000000 --- a/src/client/app/mobile/views/components/notifications.vue +++ /dev/null @@ -1,167 +0,0 @@ -<template> -<div class="mk-notifications"> - <div class="placeholder" v-if="fetching"> - <template v-for="i in 10"> - <mk-note-skeleton :key="i"/> - </template> - </div> - - <!-- トランジションを有効にするとなぜかメモリリークする --> - <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div"> - <template v-for="(notification, i) in _notifications"> - <mk-notification :notification="notification" :key="notification.id" :class="{ wide: wide }"/> - <p class="date" :key="notification.id + '_date'" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date"> - <span><fa icon="angle-up"/>{{ notification._datetext }}</span> - <span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> - </component> - - <button class="more" v-if="more" @click="fetchMore" :disabled="moreFetching"> - <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> - {{ moreFetching ? $t('@.loading') : $t('@.load-more') }} - </button> - - <p class="empty" v-if="empty">{{ $t('empty') }}</p> - - <mk-error v-if="error" @retry="init()"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import paging from '../../../common/scripts/paging'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/notifications.vue'), - - mixins: [ - paging({ - beforeInit: (self) => { - self.$emit('beforeInit'); - }, - onInited: (self) => { - self.$emit('inited'); - } - }), - ], - - props: { - type: { - type: String, - required: false - }, - wide: { - type: Boolean, - required: false, - default: false - } - }, - - data() { - return { - connection: null, - pagination: { - endpoint: 'i/notifications', - limit: 15, - params: () => ({ - includeTypes: this.type ? [this.type] : undefined - }) - } - }; - }, - - computed: { - _notifications(): any[] { - return (this.items as any).map(notification => { - const date = new Date(notification.createdAt).getDate(); - const month = new Date(notification.createdAt).getMonth() + 1; - notification._date = date; - notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString()); - return notification; - }); - } - }, - - watch: { - type() { - this.reload(); - } - }, - - mounted() { - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('notification', this.onNotification); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.prepend(notification); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notifications - .transition - .mk-notifications-enter - .mk-notifications-leave-to - opacity 0 - transform translateY(-30px) - - > * - transition transform .3s ease, opacity .3s ease - - > .notifications - - > .mk-notification:not(:last-child) - border-bottom solid var(--lineWidth) var(--faceDivider) - - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color var(--dateDividerFg) - background var(--dateDividerBg) - border-bottom solid var(--lineWidth) var(--faceDivider) - - span - margin 0 16px - - [data-icon] - margin-right 8px - - > .more - display block - width 100% - padding 16px - color var(--text) - border-top solid var(--lineWidth) rgba(#000, 0.05) - - > [data-icon] - margin-right 4px - - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > .placeholder - padding 32px - opacity 0.3 - -</style> diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue deleted file mode 100644 index c6e1df0fde..0000000000 --- a/src/client/app/mobile/views/components/notify.vue +++ /dev/null @@ -1,73 +0,0 @@ -<template> -<div class="mk-notify" :class="pos"> - <div> - <mk-notification-preview :notification="notification"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; - -export default Vue.extend({ - props: ['notification'], - computed: { - pos() { - return this.$store.state.device.mobileNotificationPosition; - } - }, - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$el, - [this.pos]: '0px', - duration: 500, - easing: 'easeOutQuad' - }); - - setTimeout(() => { - anime({ - targets: this.$el, - [this.pos]: `-${this.$el.offsetHeight}px`, - duration: 500, - easing: 'easeOutQuad', - complete: () => this.destroyDom() - }); - }, 6000); - }); - } -}); -</script> - -<style lang="stylus" scoped> -.mk-notify - $height = 78px - - position fixed - z-index 10000 - left 0 - right 0 - width 100% - max-width 500px - height $height - margin 0 auto - padding 8px - pointer-events none - font-size 80% - - &.bottom - bottom -($height) - - &.top - top -($height) - - > div - height 100% - -webkit-backdrop-filter blur(2px) - backdrop-filter blur(2px) - background-color rgba(#000, 0.5) - border-radius 7px - overflow hidden - -</style> diff --git a/src/client/app/mobile/views/components/post-form-dialog.vue b/src/client/app/mobile/views/components/post-form-dialog.vue deleted file mode 100644 index 4ae79dbd7b..0000000000 --- a/src/client/app/mobile/views/components/post-form-dialog.vue +++ /dev/null @@ -1,120 +0,0 @@ -<template> -<ui-modal - ref="modal" - :close-on-bg-click="false" - :close-anime-duration="300" - @before-close="onBeforeClose"> - <div class="main" ref="main"> - <x-post-form ref="form" - :reply="reply" - :renote="renote" - :mention="mention" - :initial-text="initialText" - :initial-note="initialNote" - :instant="instant" - @posted="onPosted" - @cancel="onCanceled"/> - </div> -</ui-modal> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import anime from 'animejs'; -import XPostForm from './post-form.vue'; - -export default Vue.extend({ - components: { - XPostForm - }, - - props: { - reply: { - type: Object, - required: false - }, - renote: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - initialText: { - type: String, - required: false - }, - initialNote: { - type: Object, - required: false - }, - instant: { - type: Boolean, - required: false, - default: false - } - }, - - mounted() { - this.$nextTick(() => { - anime({ - targets: this.$refs.main, - opacity: 1, - translateY: [-16, 0], - duration: 300, - easing: 'easeOutQuad' - }); - }); - }, - - methods: { - focus() { - this.$refs.form.focus(); - }, - - onBeforeClose() { - (this.$refs.main as any).style.pointerEvents = 'none'; - - anime({ - targets: this.$refs.main, - opacity: 0, - translateY: 16, - duration: 300, - easing: 'easeOutQuad' - }); - }, - - close() { - (this.$refs.modal as any).close(); - }, - - onPosted() { - this.$emit('posted'); - this.close(); - }, - - onCanceled() { - this.$emit('cancel'); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> - -.main - display block - position fixed - z-index 10000 - top 0 - left 0 - right 0 - height 100% - overflow auto - margin 0 auto 0 auto - opacity 0 - transform translateY(-16px) - -</style> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue deleted file mode 100644 index 38c6a42dd5..0000000000 --- a/src/client/app/mobile/views/components/post-form.vue +++ /dev/null @@ -1,244 +0,0 @@ -<template> -<div class="gafaadew"> - <div class="form" - @dragover.stop="onDragover" - @dragenter="onDragenter" - @dragleave="onDragleave" - @drop.stop="onDrop" - > - <header> - <button class="cancel" @click="cancel"><fa icon="times"/></button> - <div> - <span class="text-count" :class="{ over: trimmedLength(text) > maxNoteTextLength }">{{ maxNoteTextLength - trimmedLength(text) }}</span> - <span class="geo" v-if="geo"><fa icon="map-marker-alt"/></span> - <button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button> - </div> - </header> - <div class="form"> - <mk-note-preview class="preview" v-if="reply" :note="reply"/> - <mk-note-preview class="preview" v-if="renote" :note="renote"/> - <div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> - <div v-if="visibility === 'specified'" class="to-specified"> - <fa icon="envelope"/> {{ $t('@.post-form.specified-recipient') }} - <div class="visibleUsers"> - <span v-for="u in visibleUsers"> - <mk-user-name :user="u"/> - <button @click="removeVisibleUser(u)"><fa icon="times"/></button> - </span> - <button @click="addVisibleUser">{{ $t('@.post-form.add-visible-user') }}</button> - </div> - </div> - <div class="local-only" v-if="localOnly === true"><fa icon="heart"/> {{ $t('@.post-form.local-only-message') }}</div> - <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('@.post-form.cw-placeholder')" v-autocomplete="{ model: 'cw' }"> - <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @paste="onPaste"></textarea> - <x-post-form-attaches class="attaches" :files="files"/> - <x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <footer> - <button class="upload" @click="chooseFile"><fa icon="upload"/></button> - <button class="drive" @click="chooseFileFromDrive"><fa icon="cloud"/></button> - <button class="kao" @click="kao"><fa :icon="['far', 'smile']"/></button> - <button class="poll" @click="poll = true"><fa icon="chart-pie"/></button> - <button class="poll" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button> - <button class="geo" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button> - <button class="visibility" @click="setVisibility" ref="visibilityButton"> - <span v-if="visibility === 'public'"><fa icon="globe"/></span> - <span v-if="visibility === 'home'"><fa icon="home"/></span> - <span v-if="visibility === 'followers'"><fa icon="unlock"/></span> - <span v-if="visibility === 'specified'"><fa icon="envelope"/></span> - </button> - </footer> - <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeFile"/> - </div> - </div> - <div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags"> - <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)">#{{ tag }}</a> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import form from '../../../common/scripts/post-form'; - -export default Vue.extend({ - i18n: i18n(), - - mixins: [ - form({ - mobile: true - }), - ], - - methods: { - cancel() { - this.$emit('cancel'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.gafaadew - max-width 500px - width calc(100% - 16px) - margin 8px auto - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > .form - box-shadow 0 8px 32px rgba(#000, 0.1) - - @media (min-width 600px) - margin 32px auto - - > .form - background var(--face) - border-radius 8px - box-shadow 0 0 2px rgba(#000, 0.1) - - > header - z-index 1000 - height 50px - box-shadow 0 1px 0 0 var(--mobilePostFormDivider) - - > .cancel - padding 0 - width 50px - line-height 50px - font-size 24px - color var(--text) - - > div - position absolute - top 0 - right 0 - color var(--text) - - > .text-count - line-height 50px - - > .geo - margin 0 8px - line-height 50px - - > .submit - margin 8px - padding 0 16px - line-height 34px - vertical-align bottom - color var(--primaryForeground) - background var(--primary) - border-radius 4px - - &:disabled - opacity 0.7 - - > .form - max-width 500px - margin 0 auto - - > .preview - padding 16px - - > .with-quote - margin 0 0 8px 0 - color var(--primary) - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .to-specified - margin 0 0 8px 0 - color var(--primary) - - > .visibleUsers - display inline - top -1px - font-size 14px - - > span - margin-left 14px - - > button - padding 4px 8px - color var(--primaryAlpha04) - - &:hover - color var(--primaryAlpha06) - - &:active - color var(--primaryDarken30) - - > .local-only - margin 0 0 8px 0 - color var(--primary) - - > input - z-index 1 - - > input - > textarea - display block - padding 12px - margin 0 - width 100% - font-size 16px - color var(--inputText) - background var(--mobilePostFormTextareaBg) - border none - border-radius 0 - box-shadow 0 1px 0 0 var(--mobilePostFormDivider) - - &:disabled - opacity 0.5 - - > textarea - max-width 100% - min-width 100% - min-height 80px - - > .mk-uploader - margin 8px 0 0 0 - padding 8px - - > .file - display none - - > footer - white-space nowrap - overflow auto - -webkit-overflow-scrolling touch - overflow-scrolling touch - - > * - display inline-block - padding 0 - margin 0 - width 48px - height 48px - font-size 20px - color var(--mobilePostFormButton) - background transparent - outline none - border none - border-radius 0 - box-shadow none - - > .hashtags - margin 8px - - > * - margin-right 8px - -</style> diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue deleted file mode 100644 index 66dbb90ebb..0000000000 --- a/src/client/app/mobile/views/components/sub-note-content.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<div class="mk-sub-note-content"> - <div class="body"> - <span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> - <span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span> - <a class="reply" v-if="note.replyId"><fa icon="reply"/></a> - <mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/> - <a class="rp" v-if="note.renoteId">RN: ...</a> - </div> - <details v-if="note.files.length > 0"> - <summary>({{ $t('media-count').replace('{}', note.files.length) }})</summary> - <mk-media-list :media-list="note.files"/> - </details> - <details v-if="note.poll"> - <summary>{{ $t('poll') }}</summary> - <mk-poll :note="note"/> - </details> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/components/sub-note-content.vue'), - props: ['note'] -}); -</script> - -<style lang="stylus" scoped> -.mk-sub-note-content - overflow-wrap break-word - - > .body - > .reply - margin-right 6px - color #717171 - - > .rp - margin-left 4px - font-style oblique - color var(--renoteText) - - mk-poll - font-size 80% - -</style> diff --git a/src/client/app/mobile/views/components/ui-container.vue b/src/client/app/mobile/views/components/ui-container.vue deleted file mode 100644 index 08af7035f9..0000000000 --- a/src/client/app/mobile/views/components/ui-container.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> -<div class="ukygtjoj" :class="{ naked, inNakedDeckColumn, hideHeader: !showHeader, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <header v-if="showHeader" @click="() => showBody = !showBody"> - <div class="title"><slot name="header"></slot></div> - <slot name="func"></slot> - <button v-if="bodyTogglable"> - <template v-if="showBody"><fa icon="angle-up"/></template> - <template v-else><fa icon="angle-down"/></template> - </button> - </header> - <div v-show="showBody"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: { - showHeader: { - type: Boolean, - default: true - }, - naked: { - type: Boolean, - default: false - }, - bodyTogglable: { - type: Boolean, - default: false - }, - expanded: { - type: Boolean, - default: true - }, - }, - inject: { - inNakedDeckColumn: { - default: false - } - }, - data() { - return { - showBody: this.expanded - }; - }, - methods: { - toggleContent(show: boolean) { - if (!this.bodyTogglable) return; - this.showBody = show; - } - } -}); -</script> - -<style lang="stylus" scoped> -.ukygtjoj - overflow hidden - - &:not(.inNakedDeckColumn) - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - & + .ukygtjoj - margin-top 16px - - @media (max-width 500px) - margin-top 8px - - &.naked - background transparent !important - box-shadow none !important - - > header - > .title - margin 0 - padding 8px 10px - font-size 15px - font-weight normal - color var(--faceHeaderText) - background var(--faceHeader) - - > [data-icon] - margin-right 6px - - &:empty - display none - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - height 100% - font-size 15px - color var(--faceTextButton) - - > div - color var(--text) - - &.inNakedDeckColumn - background var(--face) - - > header - margin 0 - padding 8px 16px - font-size 12px - color var(--text) - background var(--deckColumnBg) - - > button - position absolute - top 0 - right 8px - padding 8px 6px - font-size 14px - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue deleted file mode 100644 index f20f64e7ff..0000000000 --- a/src/client/app/mobile/views/components/ui.header.vue +++ /dev/null @@ -1,142 +0,0 @@ -<template> -<div class="header" ref="root" :class="{ shadow: $store.state.device.useShadow }"> - <div class="main" ref="main"> - <div class="backdrop"></div> - <div class="content" ref="mainContainer"> - <button class="nav" @click="$parent.isDrawerOpening = true"><fa icon="bars"/></button> - <i v-if="$parent.indicate" class="circle"><fa icon="circle"/></i> - <h1> - <slot>{{ $root.instanceName }}</slot> - </h1> - <slot name="func"></slot> - </div> - </div> - <div class="indicator" v-show="$store.state.indicate"></div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { env } from '../../../config'; - -export default Vue.extend({ - i18n: i18n(), - props: ['func'], - - data() { - return { - env: env - }; - }, - - mounted() { - this.$store.commit('setUiHeaderHeight', 48); - }, -}); -</script> - -<style lang="stylus" scoped> -.header - $height = 48px - - position fixed - top 0 - left -8px - z-index 1024 - width calc(100% + 16px) - padding 0 8px - - &.shadow - box-shadow 0 0 8px rgba(0, 0, 0, 0.25) - - &, * - user-select none - - > .indicator - height 3px - background var(--primary) - - > .warn - display block - margin 0 - padding 4px - text-align center - font-size 12px - background #f00 - color #fff - - > .main - color var(--mobileHeaderFg) - - > .backdrop - position absolute - top 0 - z-index 1000 - width 100% - height $height - -webkit-backdrop-filter blur(12px) - backdrop-filter blur(12px) - background-color var(--mobileHeaderBg) - - > .content - z-index 1001 - - > h1 - display block - margin 0 auto - padding 0 - width 100% - max-width calc(100% - 112px) - text-align center - font-size 1.1em - font-weight normal - line-height $height - white-space nowrap - overflow hidden - text-overflow ellipsis - - > img - display inline-block - vertical-align bottom - width ($height - 16px) - height ($height - 16px) - margin 8px - border-radius 6px - - > .nav - display block - position absolute - top 0 - left 0 - padding 0 - width $height - font-size 1.4em - line-height $height - border-right solid 1px rgba(#000, 0.1) - - > [data-icon] - transition all 0.2s ease - - > i.circle - position absolute - top 8px - left 8px - pointer-events none - font-size 10px - color var(--notificationIndicator) - - > button:last-child - display block - position absolute - top 0 - right 0 - padding 0 - width $height - text-align center - font-size 1.4em - color inherit - line-height $height - border-left solid 1px rgba(#000, 0.1) - -</style> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue deleted file mode 100644 index db250ec6f8..0000000000 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ /dev/null @@ -1,346 +0,0 @@ -<template> -<div class="fquwcbxs"> - <transition name="back"> - <div class="backdrop" - v-if="isOpen" - @click="$parent.isDrawerOpening = false" - @touchstart="$parent.isDrawerOpening = false" - ></div> - </transition> - <transition name="nav"> - <div class="body" :class="{ notifications: showNotifications }" v-if="isOpen"> - <div class="nav" v-show="!showNotifications"> - <router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`"> - <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> - <p class="name"><mk-user-name :user="$store.state.i"/></p> - </router-link> - <div class="links"> - <ul> - <li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.state.device.enableMobileQuickNotificationView"><p @click="showNotifications = true"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li> - <li v-else><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/follow-requests" :data-active="$route.name == 'follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/groups" :data-active="$route.name == 'user-groups'"><i><fa :icon="faUsers" fixed-width/></i>{{ $t('user-groups') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li> - <li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li> - </ul> - <ul> - <li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> - <li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('@.settings') }}<i><fa icon="angle-right"/></i></router-link></li> - <li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> - </ul> - <ul> - <li @click="toggleDeckMode"><p><i><fa :icon="$store.state.device.inDeckMode ? faHome : faColumns" fixed-width/></i><span>{{ $store.state.device.inDeckMode ? $t('@.home') : $t('@.deck') }}</span></p></li> - <li @click="dark"><p><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon" fixed-width/></i><span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span></p></li> - </ul> - </div> - <div class="announcements" v-if="announcements && announcements.length > 0"> - <article v-for="announcement in announcements"> - <span v-html="announcement.title" class="title"></span> - <div><mfm :text="announcement.text"/></div> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/> - </article> - </div> - <a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a> - </div> - <div class="notifications" v-if="showNotifications"> - <header> - <button @click="showNotifications = false"><fa icon="times"/></button> - <i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i> - </header> - <mk-notifications/> - </div> - </div> - </transition> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { lang } from '../../../config'; -import { faNewspaper, faHashtag, faHome, faColumns, faUsers } from '@fortawesome/free-solid-svg-icons'; -import { faMoon, faSun, faStickyNote, faBell } from '@fortawesome/free-regular-svg-icons'; -import { search } from '../../../common/scripts/search'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/ui.nav.vue'), - - props: ['isOpen'], - - provide: { - narrow: true - }, - - data() { - return { - hasGameInvitation: false, - connection: null, - aboutUrl: `/docs/${lang}/about`, - announcements: [], - searching: false, - showNotifications: false, - faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote, faUsers, faBell, - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - } - }, - - watch: { - isOpen() { - this.showNotifications = false; - } - }, - - mounted() { - this.$root.getMeta().then(meta => { - this.announcements = meta.announcements; - }); - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - search() { - if (this.searching) return; - - this.$root.dialog({ - title: this.$t('search'), - input: true - }).then(async ({ canceled, result: query }) => { - if (canceled) return; - - this.searching = true; - search(this, query).finally(() => { - this.searching = false; - }); - }); - }, - - onReversiInvited() { - this.hasGameInvitation = true; - }, - - onReversiNoInvites() { - this.hasGameInvitation = false; - }, - - dark() { - this.$store.commit('device/set', { - key: 'darkmode', - value: !this.$store.state.device.darkmode - }); - }, - - toggleDeckMode() { - this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode }); - location.replace('/'); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.fquwcbxs - $color = var(--text) - - .backdrop - position fixed - top 0 - left 0 - z-index 1025 - width 100% - height 100% - background var(--mobileNavBackdrop) - - .body - position fixed - top 0 - left 0 - z-index 1026 - width 240px - height 100% - overflow auto - -webkit-overflow-scrolling touch - background var(--secondary) - font-size 15px - - &.notifications - width 330px - - > .notifications - padding-top 42px - - > header - position fixed - top 0 - left 0 - z-index 1000 - width 330px - line-height 42px - background var(--secondary) - - > button - display block - padding 0 14px - font-size 20px - line-height 42px - color var(--text) - - > i - position absolute - top 0 - right 16px - font-size 12px - color var(--notificationIndicator) - - > .nav - - > .me - display block - margin 0 - padding 16px - - .avatar - display inline - max-width 64px - border-radius 32px - vertical-align middle - - .name - display block - margin 0 16px - position absolute - top 0 - left 80px - padding 0 - width calc(100% - 112px) - color $color - line-height 96px - overflow hidden - text-overflow ellipsis - white-space nowrap - - ul - display block - margin 16px 0 - padding 0 - list-style none - - &:first-child - margin-top 0 - - &:last-child - margin-bottom 0 - - > li - display block - font-size 1em - line-height 1em - - a, p - display block - margin 0 - padding 0 20px - line-height 3rem - line-height calc(1rem + 30px) - color $color - text-decoration none - - &[data-active] - color var(--primaryForeground) - background var(--primary) - - > i:last-child - color var(--primaryForeground) - - > i:first-child - margin-right 0.5em - width 20px - text-align center - - > i.circle - margin-left 6px - font-size 10px - color var(--notificationIndicator) - - > i:last-child - position absolute - top 0 - right 0 - padding 0 20px - font-size 1.2em - line-height calc(1rem + 30px) - color $color - opacity 0.5 - - .announcements - > article - background var(--mobileAnnouncement) - color var(--mobileAnnouncementFg) - padding 16px - margin 8px 0 - font-size 12px - - > .title - font-weight bold - - .about - margin 0 0 8px 0 - padding 1em 0 - text-align center - font-size 0.8em - color $color - opacity 0.5 - -.nav-enter-active, -.nav-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); -} -.nav-enter, -.nav-leave-active { - opacity: 0; - transform: translateX(-240px); -} - -.back-enter-active, -.back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.back-enter, -.back-leave-active { - opacity: 0; -} - -</style> diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue deleted file mode 100644 index 05c886a497..0000000000 --- a/src/client/app/mobile/views/components/ui.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> -<div class="mk-ui" :class="{ deck: $store.state.device.inDeckMode }"> - <x-header v-if="!$store.state.device.inDeckMode"> - <template #func><slot name="func"></slot></template> - <slot name="header"></slot> - </x-header> - <x-nav :is-open="isDrawerOpening"/> - <div class="content"> - <slot></slot> - </div> - <mk-stream-indicator v-if="$store.getters.isSignedIn"/> - <button class="nav button" v-if="$store.state.device.inDeckMode" @click="isDrawerOpening = !isDrawerOpening"><fa icon="bars"/><i v-if="indicate"><fa icon="circle"/></i></button> - <button class="post button" v-if="$store.state.device.inDeckMode" @click="$post()"><fa icon="pencil-alt"/></button> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import MkNotify from './notify.vue'; -import XHeader from './ui.header.vue'; -import XNav from './ui.nav.vue'; - -export default Vue.extend({ - components: { - XHeader, - XNav - }, - - props: ['title'], - - data() { - return { - hasGameInvitation: false, - isDrawerOpening: false, - connection: null - }; - }, - - computed: { - hasUnreadNotification(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; - }, - - hasUnreadMessagingMessage(): boolean { - return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage; - }, - - indicate(): boolean { - return this.hasUnreadNotification || this.hasUnreadMessagingMessage || this.hasGameInvitation; - } - }, - - watch: { - '$store.state.uiHeaderHeight'() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - } - }, - - mounted() { - this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; - - if (this.$store.getters.isSignedIn) { - this.connection = this.$root.stream.useSharedConnection('main'); - - this.connection.on('notification', this.onNotification); - this.connection.on('reversiInvited', this.onReversiInvited); - this.connection.on('reversiNoInvites', this.onReversiNoInvites); - } - }, - - beforeDestroy() { - if (this.$store.getters.isSignedIn) { - this.connection.dispose(); - } - }, - - methods: { - onNotification(notification) { - // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない - this.$root.stream.send('readNotification', { - id: notification.id - }); - - this.$root.new(MkNotify, { - notification - }); - }, - - onReversiInvited() { - this.hasGameInvitation = true; - }, - - onReversiNoInvites() { - this.hasGameInvitation = false; - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-ui - &:not(.deck) - padding-top 48px - - > .button - position fixed - z-index 1000 - bottom 28px - padding 0 - 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 24px - - &.nav - left 28px - background var(--secondary) - color var(--text) - - > i - position absolute - top 0 - left 0 - color var(--notificationIndicator) - font-size 16px - animation blink 1s infinite - - &.post - right 28px - background var(--primary) - color var(--primaryForeground) - -</style> diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue deleted file mode 100644 index d9aa1dad8a..0000000000 --- a/src/client/app/mobile/views/components/user-list-timeline.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['list'], - - data() { - return { - connection: null, - date: null, - pagination: { - endpoint: 'notes/user-list-timeline', - limit: 10, - params: init => ({ - listId: this.list.id, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }) - } - }; - }, - - watch: { - $route: 'init' - }, - - mounted() { - this.init(); - - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - - beforeDestroy() { - this.connection.dispose(); - }, - - methods: { - init() { - if (this.connection) this.connection.dispose(); - this.connection = this.$root.stream.connectToChannel('userList', { - listId: this.list.id - }); - this.connection.on('note', this.onNote); - this.connection.on('userAdded', this.onUserAdded); - this.connection.on('userRemoved', this.onUserRemoved); - }, - - onNote(note) { - // Prepend a note - (this.$refs.timeline as any).prepend(note); - }, - - onUserAdded() { - (this.$refs.timeline as any).reload(); - }, - - onUserRemoved() { - (this.$refs.timeline as any).reload(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue deleted file mode 100644 index 3b6baa76be..0000000000 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/components/user-timeline.vue'), - - props: ['user', 'withMedia'], - - data() { - return { - date: null, - pagination: { - endpoint: 'users/notes', - limit: 10, - params: init => ({ - userId: this.user.id, - withFiles: this.withMedia, - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - }) - } - }; - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - }); - }, - - methods: { - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/directives/index.ts b/src/client/app/mobile/views/directives/index.ts deleted file mode 100644 index 324e07596d..0000000000 --- a/src/client/app/mobile/views/directives/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Vue from 'vue'; - -import userPreview from './user-preview'; - -Vue.directive('userPreview', userPreview); -Vue.directive('user-preview', userPreview); diff --git a/src/client/app/mobile/views/directives/user-preview.ts b/src/client/app/mobile/views/directives/user-preview.ts deleted file mode 100644 index 1a54abc20d..0000000000 --- a/src/client/app/mobile/views/directives/user-preview.ts +++ /dev/null @@ -1,2 +0,0 @@ -// nope -export default {}; diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue deleted file mode 100644 index 05163c6ed9..0000000000 --- a/src/client/app/mobile/views/pages/drive.vue +++ /dev/null @@ -1,147 +0,0 @@ -<template> -<mk-ui> - <template #header> - <template v-if="folder"><span style="margin-right:4px;"><fa :icon="['far', 'folder-open']"/></span>{{ folder.name }}</template> - <template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template> - <template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template> - </template> - <template #func v-if="folder || (!folder && !file)"><button @click="openContextMenu" ref="contextSource"><fa icon="ellipsis-h"/></button></template> - <x-drive - ref="browser" - :init-folder="initFolder" - :init-file="initFile" - :is-naked="true" - :top="$store.state.uiHeaderHeight" - @begin-fetch="Progress.start()" - @fetched-mid="Progress.set(0.5)" - @fetched="Progress.done()" - @move-root="onMoveRoot" - @open-folder="onOpenFolder" - @open-file="onOpenFile" - /> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import XMenu from '../../../common/views/components/menu.vue'; -import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; -import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/drive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - Progress, - folder: null, - file: null, - initFolder: null, - initFile: null - }; - }, - created() { - this.initFolder = this.$route.params.folder; - this.initFile = this.$route.params.file; - - window.addEventListener('popstate', this.onPopState); - }, - mounted() { - document.title = `${this.$root.instanceName} Drive`; - }, - beforeDestroy() { - window.removeEventListener('popstate', this.onPopState); - }, - methods: { - onPopState() { - if (this.$route.params.folder) { - (this.$refs as any).browser.cd(this.$route.params.folder, true); - } else if (this.$route.params.file) { - (this.$refs as any).browser.cf(this.$route.params.file, true); - } else { - (this.$refs as any).browser.goRoot(true); - } - }, - onMoveRoot(silent) { - const title = `${this.$root.instanceName} Drive`; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, '/i/drive'); - } - - document.title = title; - - this.file = null; - this.folder = null; - }, - onOpenFolder(folder, silent) { - const title = `${folder.name} | ${this.$root.instanceName} Drive`; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, `/i/drive/folder/${folder.id}`); - } - - document.title = title; - - this.file = null; - this.folder = folder; - }, - onOpenFile(file, silent) { - const title = `${file.name} | ${this.$root.instanceName} Drive`; - - if (!silent) { - // Rewrite URL - history.pushState(null, title, `/i/drive/file/${file.id}`); - } - - document.title = title; - - this.file = file; - this.folder = null; - }, - openContextMenu() { - this.$root.new(XMenu, { - items: [{ - type: 'item', - text: this.$t('contextmenu.upload'), - icon: 'upload', - action: this.$refs.browser.selectLocalFile - }, { - type: 'item', - text: this.$t('contextmenu.url-upload'), - icon: faCloudUploadAlt, - action: this.$refs.browser.urlUpload - }, { - type: 'item', - text: this.$t('contextmenu.create-folder'), - icon: ['far', 'folder'], - action: this.$refs.browser.createFolder - }, ...(this.folder ? [{ - type: 'item', - text: this.$t('contextmenu.rename-folder'), - icon: 'i-cursor', - action: this.$refs.browser.renameFolder - }, { - type: 'item', - text: this.$t('contextmenu.move-folder'), - icon: ['far', 'folder-open'], - action: this.$refs.browser.moveFolder - }, { - type: 'item', - text: this.$t('contextmenu.delete-folder'), - icon: faTrashAlt, - action: this.$refs.browser.deleteFolder - }] : [])], - source: this.$refs.contextSource, - }); - } - } -}); -</script> - diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue deleted file mode 100644 index 69b7bdffb4..0000000000 --- a/src/client/app/mobile/views/pages/games/reversi.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="gamepad"/></span>{{ $t('reversi') }}</template> - <x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/games/reversi.vue'), - components: { - XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue').then(m => m.default) - }, - mounted() { - document.title = `${this.$root.instanceName} | ${this.$t('reversi')}`; - }, - methods: { - nav(game, actualNav) { - if (actualNav) { - this.$router.push(`/games/reversi/${game.id}`); - } else { - // TODO: https://github.com/vuejs/vue-router/issues/703 - this.$router.push(`/games/reversi/${game.id}`); - } - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue deleted file mode 100644 index f115458092..0000000000 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ /dev/null @@ -1,143 +0,0 @@ -<template> -<div> - <ui-container v-if="src == 'home' && alone" :show-header="false" style="margin-bottom:8px;"> - <div class="zrzngnxs"> - <p>{{ $t('@.empty-timeline-info.follow-users-to-make-your-timeline') }}</p> - <router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link> - </div> - </ui-container> - - <mk-notes ref="timeline" :pagination="pagination" @loaded="() => $emit('loaded')"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/home.timeline.vue'), - - props: { - src: { - type: String, - required: true - }, - tagTl: { - required: false - } - }, - - data() { - return { - streamManager: null, - connection: null, - unreadCount: 0, - date: null, - baseQuery: { - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes - }, - query: {}, - endpoint: null, - pagination: null - }; - }, - - computed: { - alone(): boolean { - return this.$store.state.i.followingCount == 0; - } - }, - - created() { - this.$root.$on('warp', this.warp); - this.$once('hook:beforeDestroy', () => { - this.$root.$off('warp', this.warp); - this.connection.dispose(); - }); - - const prepend = note => { - (this.$refs.timeline as any).prepend(note); - }; - - if (this.src == 'tag') { - this.endpoint = 'notes/search-by-tag'; - this.query = { - query: this.tagTl.query - }; - this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query }); - this.connection.on('note', prepend); - } else if (this.src == 'home') { - this.endpoint = 'notes/timeline'; - const onChangeFollowing = () => { - this.fetch(); - }; - this.connection = this.$root.stream.useSharedConnection('homeTimeline'); - this.connection.on('note', prepend); - this.connection.on('follow', onChangeFollowing); - this.connection.on('unfollow', onChangeFollowing); - } else if (this.src == 'local') { - this.endpoint = 'notes/local-timeline'; - this.connection = this.$root.stream.useSharedConnection('localTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'hybrid') { - this.endpoint = 'notes/hybrid-timeline'; - this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'global') { - this.endpoint = 'notes/global-timeline'; - this.connection = this.$root.stream.useSharedConnection('globalTimeline'); - this.connection.on('note', prepend); - } else if (this.src == 'mentions') { - this.endpoint = 'notes/mentions'; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', prepend); - } else if (this.src == 'messages') { - this.endpoint = 'notes/mentions'; - this.query = { - visibility: 'specified' - }; - const onNote = note => { - if (note.visibility == 'specified') { - prepend(note); - } - }; - this.connection = this.$root.stream.useSharedConnection('main'); - this.connection.on('mention', onNote); - } - - this.pagination = { - endpoint: this.endpoint, - limit: 10, - params: init => ({ - untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), - ...this.baseQuery, ...this.query - }) - }; - }, - - methods: { - focus() { - (this.$refs.timeline as any).focus(); - }, - - warp(date) { - this.date = date; - (this.$refs.timeline as any).reload(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.zrzngnxs - padding 16px - text-align center - font-size 14px - - > p - margin 0 0 8px 0 - -</style> diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue deleted file mode 100644 index 0d110bf2ee..0000000000 --- a/src/client/app/mobile/views/pages/home.vue +++ /dev/null @@ -1,249 +0,0 @@ -<template> -<mk-ui> - <template #header> - <span @click="showNav = true"> - <span :class="$style.title"> - <span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span> - <span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span> - <span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span> - <span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span> - <span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span> - <span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span> - <span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span> - <span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span> - </span> - <span style="margin-left:8px"> - <template v-if="!showNav"><fa icon="angle-down"/></template> - <template v-else><fa icon="angle-up"/></template> - </span> - <i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i> - </span> - </template> - - <template #func> - <button @click="fn"><fa icon="pencil-alt"/></button> - </template> - - <main> - <div class="nav" v-if="showNav"> - <div class="bg" @click="showNav = false"></div> - <div class="pointer"></div> - <div class="body"> - <div> - <span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> - <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> - <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> - <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> - <div class="hr"></div> - <span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span> - <span :data-active="src == 'messages'" @click="src = 'messages'"><fa :icon="['far', 'envelope']"/> {{ $t('messages') }}<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></span> - <template v-if="lists"> - <div class="hr" v-if="lists.length > 0"></div> - <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id"><fa icon="list"/> {{ l.name }}</span> - </template> - <div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div> - <span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id"><fa icon="hashtag"/> {{ tl.title }}</span> - </div> - </div> - </div> - - <div class="tl"> - <x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/> - <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> - <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> - <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> - <x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> - <x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> - <x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/> - <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> - </div> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import XTl from './home.timeline.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/home.vue'), - - components: { - XTl - }, - - data() { - return { - src: 'home', - list: null, - lists: null, - tagTl: null, - showNav: false, - enableLocalTimeline: false, - enableGlobalTimeline: false, - }; - }, - - watch: { - src() { - this.showNav = false; - this.saveSrc(); - }, - - list(x) { - this.showNav = false; - this.saveSrc(); - if (x != null) this.tagTl = null; - }, - - tagTl(x) { - this.showNav = false; - this.saveSrc(); - if (x != null) this.list = null; - }, - - showNav(v) { - if (v && this.lists === null) { - this.$root.api('users/lists/list').then(lists => { - this.lists = lists; - }); - } - } - }, - - created() { - this.$root.getMeta().then((meta: Record<string, any>) => { - if (!( - this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && this.src === 'global') this.src = 'local'; - if (!( - this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin - ) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; - }); - - if (this.$store.state.device.tl) { - this.src = this.$store.state.device.tl.src; - if (this.src == 'list') { - this.list = this.$store.state.device.tl.arg; - } else if (this.src == 'tag') { - this.tagTl = this.$store.state.device.tl.arg; - } - } - }, - - mounted() { - document.title = this.$root.instanceName; - - Progress.start(); - - (this.$refs.tl as any).$once('loaded', () => { - Progress.done(); - }); - }, - - methods: { - fn() { - this.$post(); - }, - - saveSrc() { - this.$store.commit('device/setTl', { - src: this.src, - arg: this.src == 'list' ? this.list : this.tagTl - }); - }, - - warp() { - - } - } -}); -</script> - -<style lang="stylus" scoped> -main - > .nav - > .pointer - position fixed - z-index 10002 - top 56px - left 0 - right 0 - - $size = 16px - - &:after - content "" - display block - position absolute - top -($size * 2) - left s('calc(50% - %s)', $size) - border-top solid $size transparent - border-left solid $size transparent - border-right solid $size transparent - border-bottom solid $size var(--popupBg) - - > .bg - position fixed - z-index 10000 - top 0 - left 0 - width 100% - height 100% - background rgba(#000, 0.5) - - > .body - position fixed - z-index 10001 - top 56px - left 0 - right 0 - width 300px - max-height calc(100% - 70px) - margin 0 auto - overflow auto - -webkit-overflow-scrolling touch - background var(--popupBg) - border-radius 8px - box-shadow 0 0 16px rgba(#000, 0.1) - - > div - padding 8px 0 - - > .hr - margin 8px 0 - border-top solid 1px var(--faceDivider) - - > *:not(.hr) - display block - padding 8px 16px - color var(--text) - - &[data-active] - color var(--primaryForeground) - background var(--primary) - - &:not([data-active]):hover - background var(--mobileHomeTlItemHover) - - > .badge - margin-left 6px - font-size 10px - color var(--notificationIndicator) - -</style> - -<style lang="stylus" module> -.title - [data-icon] - margin-right 4px - -.badge - margin-left 6px - font-size 10px - color var(--notificationIndicator) - vertical-align middle - -</style> diff --git a/src/client/app/mobile/views/pages/index.vue b/src/client/app/mobile/views/pages/index.vue deleted file mode 100644 index 5d11fc5423..0000000000 --- a/src/client/app/mobile/views/pages/index.vue +++ /dev/null @@ -1,16 +0,0 @@ -<template> -<component :is="$store.getters.isSignedIn ? 'home' : 'welcome'"></component> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import Home from './home.vue'; -import Welcome from './welcome.vue'; - -export default Vue.extend({ - components: { - Home, - Welcome - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue deleted file mode 100644 index 7872847127..0000000000 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<mk-ui> - <template #header> - <template v-if="user"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span><mk-user-name :user="user"/></template> - <template v-else-if="group"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ group.name }}</template> - <template v-else><mk-ellipsis/></template> - </template> - <x-messaging-room v-if="!fetching" :user="user" :group="group" :is-naked="true"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import parseAcct from '../../../../../misc/acct/parse'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) - }, - data() { - return { - fetching: true, - user: null, - group: null, - unwatchDarkmode: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - const applyBg = v => - document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important'); - - applyBg(this.$store.state.device.darkmode); - - this.unwatchDarkmode = this.$store.watch(s => { - return s.device.darkmode; - }, applyBg); - - this.fetch(); - }, - beforeDestroy() { - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); // for safari's bug - this.unwatchDarkmode(); - }, - methods: { - fetch() { - this.fetching = true; - if (this.$route.params.user) { - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; - }); - } else { - this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { - this.group = group; - this.fetching = false; - - document.title = this.$t('@.messaging') + ': ' + this.group.name; - }); - } - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue deleted file mode 100644 index ff66ae06e6..0000000000 --- a/src/client/app/mobile/views/pages/messaging.vue +++ /dev/null @@ -1,30 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</template> - <x-messaging @navigate="navigate" @navigateGroup="navigateGroup" :header-top="48"/> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import getAcct from '../../../../../misc/acct/render'; - -export default Vue.extend({ - i18n: i18n(), - components: { - XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default) - }, - mounted() { - document.title = `${this.$root.instanceName} ${this.$t('@.messaging')}`; - }, - methods: { - navigate(user) { - (this as any).$router.push(`/i/messaging/${getAcct(user)}`); - }, - navigateGroup(group) { - (this as any).$router.push(`/i/messaging/group/${group.id}`); - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue deleted file mode 100644 index 090851fc4e..0000000000 --- a/src/client/app/mobile/views/pages/note.vue +++ /dev/null @@ -1,67 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa :icon="['far', 'sticky-note']"/></span>{{ $t('title') }}</template> - <main v-if="!fetching"> - <div> - <mk-note-detail :note="note" :key="note.id"/> - </div> - <footer> - <router-link v-if="note.prev" :to="note.prev"><fa icon="angle-left"/> {{ $t('prev') }}</router-link> - <router-link v-if="note.next" :to="note.next">{{ $t('next') }} <fa icon="angle-right"/></router-link> - </footer> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/note.vue'), - data() { - return { - fetching: true, - note: null - }; - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - fetch() { - Progress.start(); - this.fetching = true; - - this.$root.api('notes/show', { - noteId: this.$route.params.note - }).then(note => { - this.note = note; - this.fetching = false; - - Progress.done(); - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - text-align center - - > footer - margin-top 16px - - > a - display inline-block - margin 0 16px - -</style> diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue deleted file mode 100644 index 24f8f79ccc..0000000000 --- a/src/client/app/mobile/views/pages/notifications.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<mk-ui> - <template #header><fa :icon="faBell"/> {{ $t('notifications') }}</template> - <template #func> - <button @click="filter()"><fa icon="cog"/></button> - </template> - - <main> - <mk-notifications @before-init="beforeInit()" @inited="inited()" :type="type === 'all' ? null : type" :wide="true" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import { faBell } from '@fortawesome/free-regular-svg-icons'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/notifications.vue'), - data() { - return { - type: 'all', - faBell, - }; - }, - mounted() { - document.title = this.$root.instanceName; - }, - methods: { - beforeInit() { - Progress.start(); - }, - inited() { - Progress.done(); - }, - filter() { - this.$root.dialog({ - title: this.$t('@.notification-type'), - type: null, - select: { - items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ - value: x, text: this.$t('@.notification-types.' + x) - })) - default: this.type, - }, - showCancelButton: true - }).then(({ canceled, result: type }) => { - if (canceled) return; - this.type = type; - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -main > * - overflow hidden - background var(--face) - - &.round - border-radius 8px - - &.shadow - box-shadow 0 4px 16px rgba(#000, 0.1) - - @media (min-width 500px) - box-shadow 0 8px 32px rgba(#000, 0.1) -</style> diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue deleted file mode 100644 index dca1ffd40a..0000000000 --- a/src/client/app/mobile/views/pages/search.vue +++ /dev/null @@ -1,47 +0,0 @@ -<template> -<mk-ui> - <template #header><fa icon="search"/> {{ q }}</template> - - <main> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; -import { genSearchQuery } from '../../../common/scripts/gen-search-query'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/search.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search', - limit: 20, - params: () => genSearchQuery(this, this.q) - } - }; - }, - computed: { - q(): string { - return this.$route.query.q; - } - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - mounted() { - document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`; - }, - methods: { - inited() { - Progress.done(); - }, - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue deleted file mode 100644 index 095c19cf2c..0000000000 --- a/src/client/app/mobile/views/pages/selectdrive.vue +++ /dev/null @@ -1,101 +0,0 @@ -<template> -<div class="mk-selectdrive"> - <header> - <h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> - <button class="upload" @click="upload"><fa icon="upload"/></button> - <button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button> - </header> - <x-drive ref="browser" select-file :multiple="multiple" is-naked :top="$store.state.uiHeaderHeight"/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/selectdrive.vue'), - components: { - XDrive: () => import('../components/drive.vue').then(m => m.default), - }, - data() { - return { - files: [] - }; - }, - computed: { - multiple(): boolean { - const q = (new URL(location.toString())).searchParams; - return q.get('multiple') == 'true'; - } - }, - mounted() { - document.title = this.$t('title'); - }, - methods: { - onSelected(file) { - this.files = [file]; - this.ok(); - }, - onChangeSelection(files) { - this.files = files; - }, - upload() { - (this.$refs.browser as any).selectLocalFile(); - }, - close() { - window.close(); - }, - ok() { - window.opener.cb(this.multiple ? this.files : this.files[0]); - this.close(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-selectdrive - width 100% - height 100% - background #fff - - > header - position fixed - top 0 - left 0 - width 100% - z-index 1000 - background #fff - box-shadow 0 1px rgba(#000, 0.1) - - > h1 - margin 0 - padding 0 - text-align center - line-height 42px - font-size 1em - font-weight normal - - > .count - margin-left 4px - opacity 0.5 - - > .upload - position absolute - top 0 - left 0 - line-height 42px - width 42px - - > .ok - position absolute - top 0 - right 0 - line-height 42px - width 42px - - > .mk-drive - top 42px - -</style> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue deleted file mode 100644 index c24a56be7b..0000000000 --- a/src/client/app/mobile/views/pages/settings.vue +++ /dev/null @@ -1,86 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('@.settings') }}</template> - <main> - <div class="signed-in-as" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <mfm :text="$t('signed-in-as').replace('{}', name)" :plain="true" :custom-emojis="$store.state.i.emojis"/> - </div> - - <x-settings/> - - <div class="signout" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" @click="signout">{{ $t('@.signout') }}</div> - - <footer> - <small>ver {{ version }} ({{ codename }})</small> - </footer> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import XSettings from '../../../common/views/components/settings/settings.vue'; -import { version, codename } from '../../../config'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/settings.vue'), - components: { - XSettings, - }, - data() { - return { - version, - codename, - }; - }, - computed: { - name(): string { - return Vue.filter('userName')(this.$store.state.i); - }, - }, - methods: { - signout() { - this.$root.signout(); - }, - } -}); -</script> - -<style lang="stylus" scoped> -main - - > .signed-in-as - margin 16px - padding 16px - text-align center - color var(--mobileSignedInAsFg) - background var(--mobileSignedInAsBg) - font-weight bold - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > .signout - margin 16px - padding 16px - text-align center - color var(--mobileSignedInAsFg) - background var(--mobileSignedInAsBg) - - &.round - border-radius 6px - - &.shadow - box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12) - - > footer - margin 16px - text-align center - color var(--text) - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue deleted file mode 100644 index 81d2741ae5..0000000000 --- a/src/client/app/mobile/views/pages/signup.vue +++ /dev/null @@ -1,29 +0,0 @@ -<template> -<div class="signup"> - <h1>{{ $t('lets-start') }}</h1> - <mk-signup/> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/pages/signup.vue') -}); -</script> - -<style lang="stylus" scoped> -.signup - padding 32px - margin 0 auto - max-width 500px - - h1 - margin 0 - padding 8px 0 0 0 - font-size 1.5em - font-weight bold - color var(--text) - -</style> diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue deleted file mode 100644 index 19482ec382..0000000000 --- a/src/client/app/mobile/views/pages/tag.vue +++ /dev/null @@ -1,40 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template> - - <main> - <mk-notes ref="timeline" :pagination="pagination" @inited="inited"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import Progress from '../../../common/scripts/loading'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/tag.vue'), - data() { - return { - pagination: { - endpoint: 'notes/search-by-tag', - limit: 20, - params: { - tag: this.$route.params.tag - } - } - }; - }, - watch: { - $route() { - this.$refs.timeline.reload(); - } - }, - methods: { - inited() { - Progress.done(); - }, - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/ui.vue b/src/client/app/mobile/views/pages/ui.vue deleted file mode 100644 index 397ba5df07..0000000000 --- a/src/client/app/mobile/views/pages/ui.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;" v-if="icon"><fa :icon="icon"/></span>{{ title }}</template> - - <main> - <component :is="component" @init="init" v-bind="$attrs"/> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - component: { - required: true - } - }, - - data() { - return { - title: null, - icon: null, - }; - }, - - mounted() { - }, - - methods: { - init(v) { - this.title = v.title; - this.icon = v.icon; - } - } -}); -</script> diff --git a/src/client/app/mobile/views/pages/user/home.notes.vue b/src/client/app/mobile/views/pages/user/home.notes.vue deleted file mode 100644 index 9abe5b893c..0000000000 --- a/src/client/app/mobile/views/pages/user/home.notes.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> -<div class="root notes"> - <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div v-if="!fetching && notes.length > 0"> - <mk-note-card v-for="note in notes" :key="note.id" :note="note"/> - </div> - <p class="empty" v-if="!fetching && notes.length == 0">{{ $t('@.no-notes') }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.notes.vue'), - props: ['user'], - data() { - return { - fetching: true, - notes: [] - }; - }, - mounted() { - this.$root.api('users/notes', { - userId: this.user.id, - }).then(notes => { - this.notes = notes; - this.fetching = false; - }); - } -}); -</script> - -<style lang="stylus" scoped> -.root.notes - - > div - overflow-x scroll - -webkit-overflow-scrolling touch - white-space nowrap - padding 8px - - > * - vertical-align top - - &:not(:last-child) - margin-right 8px - - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > i - margin-right 4px - -</style> diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue deleted file mode 100644 index b5547c916f..0000000000 --- a/src/client/app/mobile/views/pages/user/home.photos.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> -<div class="root photos"> - <p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> - <div class="stream" v-if="!fetching && images.length > 0"> - <a v-for="(image, i) in images" :key="i" - class="img" - :style="`background-image: url(${thumbnail(image.file)})`" - :href="image.note | notePage" - ></a> - </div> - <p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.photos.vue'), - props: ['user'], - data() { - return { - fetching: true, - images: [] - }; - }, - mounted() { - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - this.$root.api('users/notes', { - userId: this.user.id, - fileType: image, - excludeNsfw: !this.$store.state.device.alwaysShowNsfw, - limit: 9, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - if (this.images.length < 9) { - this.images.push({ - note, - file - }); - } - } - } - this.fetching = false; - }); - }, - methods: { - thumbnail(image: any): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) - : image.thumbnailUrl; - }, - }, -}); -</script> - -<style lang="stylus" scoped> -.root.photos - - > .stream - display -webkit-flex - display -moz-flex - display -ms-flex - display flex - justify-content center - flex-wrap wrap - padding 8px - - > .img - flex 1 1 33% - width 33% - height 90px - background-position center center - background-size cover - background-clip content-box - border solid 2px transparent - border-radius 4px - - > .initializing - > .empty - margin 0 - padding 16px - text-align center - color var(--text) - - > i - margin-right 4px - -</style> - diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue deleted file mode 100644 index 316b2a12fe..0000000000 --- a/src/client/app/mobile/views/pages/user/home.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<div class="wojmldye"> - <x-page class="page" v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/> - <mk-note-detail class="note" v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> - <ui-container :body-togglable="true"> - <template #header><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</template> - <div> - <x-notes :user="user"/> - </div> - </ui-container> - <ui-container :body-togglable="true"> - <template #header><fa icon="image"/>{{ $t('images') }}</template> - <div> - <x-photos :user="user"/> - </div> - </ui-container> - <ui-container :body-togglable="true"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <div style="padding:8px;"> - <x-activity :user="user"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import XNotes from './home.notes.vue'; -import XPhotos from './home.photos.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user/home.vue'), - components: { - XNotes, - XPhotos, - XPage: () => import('../../../../common/views/components/page/page.vue').then(m => m.default), - XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default) - }, - props: ['user'], - data() { - return { - makeFrequentlyRepliedUsersPromise: () => this.$root.api('users/get_frequently_replied_users', { - userId: this.user.id - }).then(res => res.map(x => x.user)), - makeFollowersYouKnowPromise: () => this.$root.api('users/followers', { - userId: this.user.id, - iknow: true, - limit: 30 - }).then(res => res.users), - }; - } -}); -</script> - -<style lang="stylus" scoped> -.wojmldye - > .page - margin 0 0 8px 0 - - @media (min-width 500px) - margin 0 0 16px 0 - - > .note - margin 0 0 8px 0 - - @media (min-width 500px) - margin 0 0 16px 0 - -</style> diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue deleted file mode 100644 index b8a79a6b34..0000000000 --- a/src/client/app/mobile/views/pages/user/index.vue +++ /dev/null @@ -1,349 +0,0 @@ -<template> -<mk-ui> - <template #header v-if="!fetching"> - <img :src="avator" alt=""><mk-user-name :user="user" :key="user.id"/> - </template> - <div class="wwtwuxyh" v-if="!fetching"> - <div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div> - <div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div> - <header> - <div class="banner" :style="style"></div> - <div class="body"> - <div class="top"> - <a class="avatar"> - <img :src="avator" alt="avatar"/> - </a> - <button class="menu" ref="menu" @click="menu"><fa icon="ellipsis-h"/></button> - <mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> - </div> - <div class="title"> - <h1><mk-user-name :user="user" :key="user.id" :nowrap="false"/></h1> - <span class="username"><mk-acct :user="user" :detail="true" :key="user.id"/></span> - <span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span> - </div> - <div class="description"> - <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :key="user.id"/> - <x-integrations :user="user" style="margin:20px 0;"/> - </div> - <div class="fields" v-if="user.fields" :key="user.id"> - <dl class="field" v-for="(field, i) in user.fields" :key="i"> - <dt class="name"> - <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/> - </dt> - <dd class="value"> - <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> - </dd> - </dl> - </div> - <div class="info"> - <p class="location" v-if="user.host === null && user.location"> - <fa icon="map-marker"/>{{ user.location }} - </p> - <p class="birthday" v-if="user.host === null && user.birthday"> - <fa icon="birthday-cake"/>{{ user.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }}) - </p> - </div> - <div class="status"> - <router-link :to="user | userPage()"> - <b>{{ user.notesCount | number }}</b> - <i>{{ $t('notes') }}</i> - </router-link> - <router-link :to="user | userPage('following')"> - <b>{{ user.followingCount | number }}</b> - <i>{{ $t('following') }}</i> - </router-link> - <router-link :to="user | userPage('followers')"> - <b>{{ user.followersCount | number }}</b> - <i>{{ $t('followers') }}</i> - </router-link> - </div> - </div> - </header> - <nav v-if="$route.name == 'user'" :class="{ shadow: $store.state.device.useShadow }"> - <div class="nav-container"> - <a :data-active="page == 'home'" @click="page = 'home'"><fa icon="home"/> {{ $t('overview') }}</a> - <a :data-active="page == 'notes'" @click="page = 'notes'"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</a> - <a :data-active="page == 'media'" @click="page = 'media'"><fa icon="image"/> {{ $t('media') }}</a> - </div> - </nav> - <main> - <template v-if="$route.name == 'user'"> - <x-home v-if="page == 'home'" :user="user" :key="user.id"/> - <mk-user-timeline v-if="page == 'notes'" :user="user" :key="`tl:${user.id}`"/> - <mk-user-timeline v-if="page == 'media'" :user="user" :with-media="true" :key="`media:${user.id}`"/> - </template> - <router-view :user="user"></router-view> - </main> - </div> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../../i18n'; -import * as age from 's-age'; -import parseAcct from '../../../../../../misc/acct/parse'; -import Progress from '../../../../common/scripts/loading'; -import XUserMenu from '../../../../common/views/components/user-menu.vue'; -import XHome from './home.vue'; -import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url'; -import XIntegrations from '../../../../common/views/components/integrations.vue'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/user.vue'), - components: { - XHome, - XIntegrations - }, - data() { - return { - fetching: true, - user: null, - page: this.$route.name == 'user' ? 'home' : null - }; - }, - computed: { - age(): number { - return age(this.user.birthday); - }, - avator(): string { - return this.$store.state.device.disableShowingAnimatedImages - ? getStaticImageUrl(this.user.avatarUrl) - : this.user.avatarUrl; - }, - style(): any { - if (this.user.bannerUrl == null) return {}; - return { - backgroundColor: this.user.bannerColor, - backgroundImage: `url(${ this.user.bannerUrl })` - }; - } - }, - watch: { - $route: 'fetch' - }, - created() { - this.fetch(); - }, - methods: { - fetch() { - Progress.start(); - - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; - - Progress.done(); - document.title = `${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; - }); - }, - - menu() { - this.$root.new(XUserMenu, { - source: this.$refs.menu, - user: this.user - }); - }, - } -}); -</script> - -<style lang="stylus" scoped> -.wwtwuxyh - $bg = var(--face) - - > .is-suspended - > .is-remote - &.is-suspended - color #570808 - background #ffdbdb - - &.is-remote - color #573c08 - background #fff0db - - > p - margin 0 auto - padding 14px - max-width 600px - font-size 14px - - > a - font-weight bold - - @media (max-width 500px) - padding 12px - font-size 12px - - > header - background $bg - - > .banner - padding-bottom 33.3% - background-color rgba(0, 0, 0, 0.1) - background-size cover - background-position center - - > .body - padding 12px - margin 0 auto - max-width 600px - - > .top - display flex - - > .avatar - display block - width 25% - height 40px - - > img - display block - position absolute - left -2px - bottom -2px - width 100% - background $bg - border 3px solid $bg - border-radius 6px - - @media (min-width 500px) - left -4px - bottom -4px - border 4px solid $bg - border-radius 12px - - > .menu - margin 0 0 0 auto - padding 8px - margin-right 8px - font-size 18px - color var(--text) - - > .title - margin 8px 0 - - > h1 - margin 0 - line-height 22px - font-size 20px - color var(--mobileUserPageName) - - > .username - display inline-block - line-height 20px - font-size 16px - font-weight bold - color var(--mobileUserPageAcct) - - > .followed - margin-left 8px - padding 2px 4px - font-size 12px - color var(--mobileUserPageFollowedFg) - background var(--mobileUserPageFollowedBg) - border-radius 4px - - > .description - margin 8px 0 - color var(--mobileUserPageDescription) - - @media (max-width 450px) - font-size 15px - - > .fields - margin 8px 0 - - > .field - display flex - padding 0 - margin 0 - align-items center - - > .name - padding 4px - margin 4px - width 30% - overflow hidden - white-space nowrap - text-overflow ellipsis - font-weight bold - color var(--mobileUserPageStatusHighlight) - - > .value - padding 4px - margin 4px - width 70% - overflow hidden - white-space nowrap - text-overflow ellipsis - color var(--mobileUserPageStatusHighlight) - - > .info - margin 8px 0 - - @media (max-width 450px) - font-size 15px - - > p - display inline - margin 0 16px 0 0 - color var(--text) - - > i - margin-right 4px - - > .status - > a - color var(--text) - - &:not(:last-child) - margin-right 16px - - > b - margin-right 4px - font-size 16px - color var(--mobileUserPageStatusHighlight) - - > i - font-size 14px - - > button - color var(--text) - - > nav - position -webkit-sticky - position sticky - top 47px - background-color $bg - z-index 2 - - &.shadow - box-shadow 0 4px 4px var(--mobileUserPageHeaderShadow) - - > .nav-container - display flex - justify-content center - margin 0 auto - max-width 616px - - > a - display block - flex 1 1 - text-align center - line-height 48px - font-size 12px - text-decoration none - color var(--text) - border-bottom solid 2px transparent - - @media (min-width 400px) - line-height 52px - font-size 14px - - &[data-active] - font-weight bold - color var(--primary) - border-color var(--primary) - -</style> diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue deleted file mode 100644 index 6cf4a36f90..0000000000 --- a/src/client/app/mobile/views/pages/welcome.vue +++ /dev/null @@ -1,310 +0,0 @@ -<template> -<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes"> - <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> - - <div> - <img svg-inline src="../../../../assets/title.svg" alt="Misskey"> - <p class="host">{{ host }}</p> - <div class="about"> - <h2>{{ name || 'Misskey' }}</h2> - <p v-html="description || this.$t('@.about')"></p> - <router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link> - </div> - <div class="signin"> - <a href="/signin" @click.prevent="signin()">{{ $t('@.signin') }}</a> - </div> - <div class="tl"> - <mk-welcome-timeline/> - </div> - <div class="hashtags"> - <mk-tag-cloud/> - </div> - <div class="photos"> - <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> - </div> - <div class="stats" v-if="stats"> - <span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span> - <span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span> - </div> - <div class="announcements" v-if="announcements && announcements.length > 0"> - <article v-for="announcement in announcements"> - <span class="title" v-html="announcement.title"></span> - <mfm :text="announcement.text"/> - <img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/> - </article> - </div> - <article class="about-misskey"> - <h1>{{ $t('@.intro.title') }}</h1> - <p v-html="this.$t('@.intro.about')"></p> - <section> - <h2>{{ $t('@.intro.features') }}</h2> - <section> - <h3>{{ $t('@.intro.rich-contents') }}</h3> - <div class="image"><img src="/assets/about/post.png" alt=""></div> - <p v-html="this.$t('@.intro.rich-contents-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.reaction') }}</h3> - <div class="image"><img src="/assets/about/reaction.png" alt=""></div> - <p v-html="this.$t('@.intro.reaction-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.ui') }}</h3> - <div class="image"><img src="/assets/about/ui.png" alt=""></div> - <p v-html="this.$t('@.intro.ui-desc')"></p> - </section> - <section> - <h3>{{ $t('@.intro.drive') }}</h3> - <div class="image"><img src="/assets/about/drive.png" alt=""></div> - <p v-html="this.$t('@.intro.drive-desc')"></p> - </section> - </section> - <p v-html="this.$t('@.intro.outro')"></p> - </article> - <div class="info" v-if="meta"> - <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> - </div> - <footer> - <small>{{ copyright }}</small> - </footer> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import { copyright, host } from '../../../config'; -import { concat } from '../../../../../prelude/array'; -import { toUnicode } from 'punycode'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/welcome.vue'), - data() { - return { - meta: null, - copyright, - stats: null, - banner: null, - host: toUnicode(host), - name: null, - description: '', - photos: [], - announcements: [] - }; - }, - created() { - this.$root.getMeta().then(meta => { - this.meta = meta; - this.name = meta.name; - this.description = meta.description; - this.announcements = meta.announcements; - this.banner = meta.bannerUrl; - }); - - this.$root.api('stats').then(stats => { - this.stats = stats; - }); - - const image = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/apng', - 'image/vnd.mozilla.apng', - ]; - - this.$root.api('notes/local-timeline', { - fileType: image, - excludeNsfw: true, - limit: 6 - }).then((notes: any[]) => { - const files = concat(notes.map((n: any): any[] => n.files)); - this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); - }); - }, - methods: { - signin() { - this.$root.dialog({ - type: 'signin' - }); - } - } -}); -</script> - -<style lang="stylus" scoped> -.wgwfgvvimdjvhjfwxropcwksnzftjqes - text-align center - - > .banner - position absolute - top 0 - left 0 - width 100% - height 300px - background-position center - background-size cover - opacity 0.7 - - &:after - content "" - display block - position absolute - bottom 0 - left 0 - width 100% - height 100px - background linear-gradient(transparent, var(--bg)) - - > div:not(.banner) - padding 32px - margin 0 auto - max-width 500px - - > svg - display block - width 200px - height 50px - margin 0 auto - - > .host - display block - text-align center - padding 6px 12px - line-height 32px - font-weight bold - color #333 - background rgba(#000, 0.035) - border-radius 6px - - > .about - margin-top 16px - padding 16px - color var(--text) - background var(--face) - border-radius 6px - - > h2 - margin 0 - - > p - margin 8px - - > .signup - font-weight bold - - > .signin - margin 16px 0 - - > .tl - margin 16px 0 - - > * - max-height 300px - border-radius 6px - overflow auto - -webkit-overflow-scrolling touch - - > .hashtags - padding 0 8px - height 200px - - > .photos - display grid - grid-template-rows 1fr 1fr 1fr - grid-template-columns 1fr 1fr - gap 8px - height 300px - margin-top 16px - - > div - border-radius 4px - background-position center center - background-size cover - - > .stats - margin 16px 0 - padding 8px - font-size 14px - color var(--text) - background rgba(#000, 0.1) - border-radius 6px - - > * - margin 0 8px - - > .announcements - margin 16px 0 - - > article - background var(--mobileAnnouncement) - border-radius 6px - color var(--mobileAnnouncementFg) - padding 16px - margin 8px 0 - font-size 12px - - > .title - font-weight bold - - > .about-misskey - margin 16px 0 - padding 32px - font-size 14px - background var(--face) - border-radius 6px - overflow hidden - color var(--text) - - > h1 - margin 0 - - & + p - margin-top 8px - - > p:last-child - margin-bottom 0 - - > section - > h2 - border-bottom 1px solid var(--faceDivider) - - > section - margin-bottom 16px - padding-bottom 16px - border-bottom 1px solid var(--faceDivider) - - > h3 - margin-bottom 8px - - > p - margin-bottom 0 - - > .image - > img - display block - width 100% - height 120px - object-fit cover - - > .info - padding 16px 0 - border solid 2px rgba(0, 0, 0, 0.1) - border-radius 8px - color var(--text) - - > * - margin 0 16px - - > footer - text-align center - color var(--text) - - > small - display block - margin 16px 0 0 0 - opacity 0.7 - -</style> diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue deleted file mode 100644 index 19df613b3a..0000000000 --- a/src/client/app/mobile/views/pages/widgets.vue +++ /dev/null @@ -1,192 +0,0 @@ -<template> -<mk-ui> - <template #header><span style="margin-right:4px;"><fa icon="home"/></span>{{ $t('dashboard') }}</template> - <template #func> - <button @click="customizing = !customizing"><fa icon="cog"/></button> - </template> - <main> - <template v-if="customizing"> - <header> - <select v-model="widgetAdderSelected"> - <option value="profile">{{ $t('@.widgets.profile') }}</option> - <option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option> - <option value="calendar">{{ $t('@.widgets.calendar') }}</option> - <option value="activity">{{ $t('@.widgets.activity') }}</option> - <option value="rss">{{ $t('@.widgets.rss') }}</option> - <option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option> - <option value="slideshow">{{ $t('@.widgets.slideshow') }}</option> - <option value="hashtags">{{ $t('@.widgets.hashtags') }}</option> - <option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option> - <option value="version">{{ $t('@.widgets.version') }}</option> - <option value="server">{{ $t('@.widgets.server') }}</option> - <option value="queue">{{ $t('@.widgets.queue') }}</option> - <option value="memo">{{ $t('@.widgets.memo') }}</option> - <option value="nav">{{ $t('@.widgets.nav') }}</option> - <option value="tips">{{ $t('@.widgets.tips') }}</option> - </select> - <button @click="addWidget">{{ $t('add-widget') }}</button> - <p><a @click="hint">{{ $t('customization-tips') }}</a></p> - </header> - <x-draggable - :list="widgets" - handle=".handle" - animation="150" - @sort="onWidgetSort" - > - <div v-for="widget in widgets" class="customize-container" :key="widget.id"> - <header> - <span class="handle"><fa icon="bars"/></span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)"><fa icon="times"/></button> - </header> - <div @click="widgetFunc(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="mobile"/> - </div> - </div> - </x-draggable> - </template> - <template v-else> - <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="mobile"/> - </template> - </main> -</mk-ui> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../../i18n'; -import * as XDraggable from 'vuedraggable'; -import { v4 as uuid } from 'uuid'; - -export default Vue.extend({ - i18n: i18n('mobile/views/pages/widgets.vue'), - components: { - XDraggable - }, - - data() { - return { - showNav: false, - customizing: false, - widgetAdderSelected: null - }; - }, - - computed: { - widgets(): any[] { - return this.$store.getters.mobileHome || []; - } - }, - - created() { - if (this.widgets.length == 0) { - this.$store.commit('setMobileHome', [{ - name: 'calendar', - id: 'a', data: {} - }, { - name: 'activity', - id: 'b', data: {} - }, { - name: 'rss', - id: 'c', data: {} - }, { - name: 'photo-stream', - id: 'd', data: {} - }, { - name: 'nav', - id: 'f', data: {} - }, { - name: 'version', - id: 'g', data: {} - }]); - } - }, - - mounted() { - document.title = this.$root.instanceName; - }, - - methods: { - hint() { - this.$root.dialog({ - type: 'info', - text: this.$t('widgets-hints') - }); - }, - - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - - onWidgetSort() { - this.saveHome(); - }, - - addWidget() { - if(this.widgetAdderSelected == null) return; - - this.$store.commit('addMobileHomeWidget', { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - }); - }, - - removeWidget(widget) { - this.$store.commit('removeMobileHomeWidget', widget); - }, - - saveHome() { - this.$store.commit('setMobileHome', this.widgets); - } - } -}); -</script> - -<style lang="stylus" scoped> -main - margin 0 auto - padding 8px - max-width 500px - width 100% - - @media (min-width 500px) - padding 16px 8px - - @media (min-width 600px) - padding 32px 8px - - > header - padding 8px - background #fff - - .widget - margin-bottom 8px - - @media (min-width 600px) - margin-bottom 16px - - .customize-container - margin 8px - background #fff - - > header - line-height 32px - background #eee - - > .handle - padding 0 8px - - > .remove - position absolute - top 0 - right 0 - padding 0 8px - line-height 32px - - > div - padding 8px - - > * - pointer-events none - -</style> diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue deleted file mode 100644 index 047784deac..0000000000 --- a/src/client/app/mobile/views/widgets/activity.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<div class="mkw-activity"> - <ui-container :show-header="!props.compact"> - <template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template> - <div :class="$style.body"> - <x-activity :user="$store.state.i"/> - </div> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; -import i18n from '../../../i18n'; - -export default define({ - name: 'activity', - props: () => ({ - compact: false - }) -}).extend({ - i18n: i18n(), - components: { - XActivity: () => import('../../../common/views/components/activity.vue').then(m => m.default) - }, - methods: { - func() { - this.props.compact = !this.props.compact; - this.save(); - } - } -}); -</script> - -<style lang="stylus" module> -.body - padding 8px -</style> diff --git a/src/client/app/mobile/views/widgets/index.ts b/src/client/app/mobile/views/widgets/index.ts deleted file mode 100644 index 4de912b64c..0000000000 --- a/src/client/app/mobile/views/widgets/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Vue from 'vue'; - -import wActivity from './activity.vue'; -import wProfile from './profile.vue'; - -Vue.component('mkw-activity', wActivity); -Vue.component('mkw-profile', wProfile); diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue deleted file mode 100644 index d4ccc87e57..0000000000 --- a/src/client/app/mobile/views/widgets/profile.vue +++ /dev/null @@ -1,65 +0,0 @@ -<template> -<div class="mkw-profile"> - <ui-container> - <div :class="$style.banner" - :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" - ></div> - <img :class="$style.avatar" - :src="$store.state.i.avatarUrl" - alt="avatar" - /> - <router-link :class="$style.name" :to="$store.state.i | userPage"> - <mk-user-name :user="$store.state.i"/> - </router-link> - </ui-container> -</div> -</template> - -<script lang="ts"> -import define from '../../../common/define-widget'; - -export default define({ - name: 'profile' -}); -</script> - -<style lang="stylus" module> -.banner - height 100px - background-color #f5f5f5 - background-size cover - background-position center - cursor pointer - -.banner:before - content "" - display block - width 100% - height 100% - background rgba(#000, 0.5) - -.avatar - display block - position absolute - width 58px - height 58px - margin 0 - vertical-align bottom - top ((100px - 58px) / 2) - left ((100px - 58px) / 2) - border none - border-radius 100% - box-shadow 0 0 16px rgba(#000, 0.5) - -.name - display block - position absolute - top 0 - left 92px - margin 0 - line-height 100px - color #fff - font-weight bold - text-shadow 0 0 8px rgba(#000, 0.5) - -</style> |