summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2018-10-08 15:37:24 +0900
committerGitHub <noreply@github.com>2018-10-08 15:37:24 +0900
commit9c170c426be01773afb15a9868ff3c278e09409c (patch)
tree0229bb52dd9197308d193f4e41bbc11d3dcb95a1 /src/client
parentNew translations ja-JP.yml (Norwegian) (diff)
parentfix(package): update @types/mongodb to version 3.1.10 (#2849) (diff)
downloadmisskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.gz
misskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.bz2
misskey-9c170c426be01773afb15a9868ff3c278e09409c.zip
Merge branch 'develop' into l10n_develop
Diffstat (limited to 'src/client')
-rw-r--r--src/client/app/app.styl12
-rw-r--r--src/client/app/app.vue31
-rw-r--r--src/client/app/auth/views/index.vue2
-rw-r--r--src/client/app/base.pug3
-rw-r--r--src/client/app/boot.js28
-rw-r--r--src/client/app/common/hotkey.ts110
-rw-r--r--src/client/app/common/keycode.ts33
-rw-r--r--src/client/app/common/scripts/check-for-update.ts2
-rw-r--r--src/client/app/common/scripts/compose-notification.ts6
-rw-r--r--src/client/app/common/scripts/fuck-ad-block.ts4
-rw-r--r--src/client/app/common/scripts/gcd.ts2
-rw-r--r--src/client/app/common/scripts/get-md5.ts8
-rw-r--r--src/client/app/common/scripts/note-subscriber.ts116
-rw-r--r--src/client/app/common/scripts/parse-search-query.ts53
-rw-r--r--src/client/app/common/scripts/stream.ts318
-rw-r--r--src/client/app/common/scripts/streaming/drive.ts34
-rw-r--r--src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts11
-rw-r--r--src/client/app/common/scripts/streaming/games/reversi/reversi.ts31
-rw-r--r--src/client/app/common/scripts/streaming/global-timeline.ts34
-rw-r--r--src/client/app/common/scripts/streaming/home.ts102
-rw-r--r--src/client/app/common/scripts/streaming/hybrid-timeline.ts34
-rw-r--r--src/client/app/common/scripts/streaming/local-timeline.ts34
-rw-r--r--src/client/app/common/scripts/streaming/messaging-index.ts34
-rw-r--r--src/client/app/common/scripts/streaming/messaging.ts20
-rw-r--r--src/client/app/common/scripts/streaming/notes-stats.ts30
-rw-r--r--src/client/app/common/scripts/streaming/server-stats.ts30
-rw-r--r--src/client/app/common/scripts/streaming/stream-manager.ts108
-rw-r--r--src/client/app/common/scripts/streaming/stream.ts137
-rw-r--r--src/client/app/common/scripts/streaming/user-list.ts17
-rw-r--r--src/client/app/common/views/components/acct.vue12
-rw-r--r--src/client/app/common/views/components/autocomplete.vue32
-rw-r--r--src/client/app/common/views/components/avatar.vue36
-rw-r--r--src/client/app/common/views/components/connect-failed.troubleshooter.vue2
-rw-r--r--src/client/app/common/views/components/connect-failed.vue12
-rw-r--r--src/client/app/common/views/components/cw-button.vue38
-rw-r--r--src/client/app/common/views/components/forkit.vue6
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.game.vue54
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.gameroom.vue8
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.index.vue50
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.room.vue58
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.vue22
-rw-r--r--src/client/app/common/views/components/google.vue20
-rw-r--r--src/client/app/common/views/components/index.ts12
-rw-r--r--src/client/app/common/views/components/instance.vue51
-rw-r--r--src/client/app/common/views/components/media-banner.vue85
-rw-r--r--src/client/app/common/views/components/media-list.vue121
-rw-r--r--src/client/app/common/views/components/menu.vue32
-rw-r--r--src/client/app/common/views/components/messaging-room.form.vue22
-rw-r--r--src/client/app/common/views/components/messaging-room.message.vue22
-rw-r--r--src/client/app/common/views/components/messaging-room.vue77
-rw-r--r--src/client/app/common/views/components/messaging.vue70
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.ts70
-rw-r--r--src/client/app/common/views/components/nav.vue2
-rw-r--r--src/client/app/common/views/components/note-header.vue26
-rw-r--r--src/client/app/common/views/components/note-menu.vue64
-rw-r--r--src/client/app/common/views/components/poll-editor.vue37
-rw-r--r--src/client/app/common/views/components/poll.vue22
-rw-r--r--src/client/app/common/views/components/reaction-icon.vue22
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue137
-rw-r--r--src/client/app/common/views/components/reactions-viewer.vue15
-rw-r--r--src/client/app/common/views/components/signin.vue12
-rw-r--r--src/client/app/common/views/components/signup.vue14
-rw-r--r--src/client/app/common/views/components/stream-indicator.vue6
-rw-r--r--src/client/app/common/views/components/switch.vue199
-rw-r--r--src/client/app/common/views/components/tag-cloud.vue84
-rw-r--r--src/client/app/common/views/components/theme.vue308
-rw-r--r--src/client/app/common/views/components/trends.chart.vue (renamed from src/client/app/common/views/widgets/hashtags.chart.vue)0
-rw-r--r--src/client/app/common/views/components/trends.vue98
-rw-r--r--src/client/app/common/views/components/ui/button.vue122
-rw-r--r--src/client/app/common/views/components/ui/card.vue38
-rw-r--r--src/client/app/common/views/components/ui/form.vue2
-rw-r--r--src/client/app/common/views/components/ui/form/button.vue40
-rw-r--r--src/client/app/common/views/components/ui/form/radio.vue20
-rw-r--r--src/client/app/common/views/components/ui/input.vue46
-rw-r--r--src/client/app/common/views/components/ui/radio.vue18
-rw-r--r--src/client/app/common/views/components/ui/select.vue40
-rw-r--r--src/client/app/common/views/components/ui/switch.vue32
-rw-r--r--src/client/app/common/views/components/ui/textarea.vue28
-rw-r--r--src/client/app/common/views/components/uploader.vue123
-rw-r--r--src/client/app/common/views/components/url-preview.vue49
-rw-r--r--src/client/app/common/views/components/url.vue9
-rw-r--r--src/client/app/common/views/components/visibility-chooser.vue35
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue166
-rw-r--r--src/client/app/common/views/directives/autocomplete.ts6
-rw-r--r--src/client/app/common/views/filters/note.ts2
-rw-r--r--src/client/app/common/views/filters/user.ts2
-rw-r--r--src/client/app/common/views/pages/follow.vue45
-rw-r--r--src/client/app/common/views/widgets/analog-clock.vue19
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue76
-rw-r--r--src/client/app/common/views/widgets/calendar.vue19
-rw-r--r--src/client/app/common/views/widgets/donation.vue72
-rw-r--r--src/client/app/common/views/widgets/hashtags.vue94
-rw-r--r--src/client/app/common/views/widgets/memo.vue24
-rw-r--r--src/client/app/common/views/widgets/nav.vue16
-rw-r--r--src/client/app/common/views/widgets/photo-stream.vue11
-rw-r--r--src/client/app/common/views/widgets/posts-monitor.vue20
-rw-r--r--src/client/app/common/views/widgets/rss.vue16
-rw-r--r--src/client/app/common/views/widgets/server.cpu-memory.vue10
-rw-r--r--src/client/app/common/views/widgets/server.cpu.vue10
-rw-r--r--src/client/app/common/views/widgets/server.disk.vue10
-rw-r--r--src/client/app/common/views/widgets/server.memory.vue10
-rw-r--r--src/client/app/common/views/widgets/server.pie.vue10
-rw-r--r--src/client/app/common/views/widgets/server.vue8
-rw-r--r--src/client/app/config.ts4
-rw-r--r--src/client/app/desktop/api/update-avatar.ts2
-rw-r--r--src/client/app/desktop/api/update-banner.ts2
-rw-r--r--src/client/app/desktop/assets/header-icon.light.svg150
-rw-r--r--src/client/app/desktop/assets/header-icon.svg (renamed from src/client/app/desktop/assets/header-icon.dark.svg)8
-rw-r--r--src/client/app/desktop/script.ts97
-rw-r--r--src/client/app/desktop/style.styl29
-rw-r--r--src/client/app/desktop/ui.styl181
-rw-r--r--src/client/app/desktop/views/components/calendar.vue63
-rw-r--r--src/client/app/desktop/views/components/charts.vue111
-rw-r--r--src/client/app/desktop/views/components/choose-file-from-drive-window.vue34
-rw-r--r--src/client/app/desktop/views/components/choose-folder-from-drive-window.vue22
-rw-r--r--src/client/app/desktop/views/components/context-menu.menu.vue26
-rw-r--r--src/client/app/desktop/views/components/context-menu.vue12
-rw-r--r--src/client/app/desktop/views/components/crop-window.vue24
-rw-r--r--src/client/app/desktop/views/components/dialog.vue12
-rw-r--r--src/client/app/desktop/views/components/drive-window.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.file.vue22
-rw-r--r--src/client/app/desktop/views/components/drive.folder.vue24
-rw-r--r--src/client/app/desktop/views/components/drive.vue40
-rw-r--r--src/client/app/desktop/views/components/follow-button.vue59
-rw-r--r--src/client/app/desktop/views/components/followers-window.vue2
-rw-r--r--src/client/app/desktop/views/components/following-window.vue2
-rw-r--r--src/client/app/desktop/views/components/friends-maker.vue2
-rw-r--r--src/client/app/desktop/views/components/game-window.vue2
-rw-r--r--src/client/app/desktop/views/components/home.vue44
-rw-r--r--src/client/app/desktop/views/components/input-dialog.vue32
-rw-r--r--src/client/app/desktop/views/components/media-image-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-image.vue15
-rw-r--r--src/client/app/desktop/views/components/media-video-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-video.vue22
-rw-r--r--src/client/app/desktop/views/components/messaging-room-window.vue2
-rw-r--r--src/client/app/desktop/views/components/messaging-window.vue2
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue163
-rw-r--r--src/client/app/desktop/views/components/note-preview.vue37
-rw-r--r--src/client/app/desktop/views/components/notes.note.sub.vue52
-rw-r--r--src/client/app/desktop/views/components/notes.note.vue295
-rw-r--r--src/client/app/desktop/views/components/notes.vue38
-rw-r--r--src/client/app/desktop/views/components/notifications.vue42
-rw-r--r--src/client/app/desktop/views/components/post-form-window.vue43
-rw-r--r--src/client/app/desktop/views/components/post-form.vue155
-rw-r--r--src/client/app/desktop/views/components/progress-dialog.vue24
-rw-r--r--src/client/app/desktop/views/components/received-follow-requests-window.vue15
-rw-r--r--src/client/app/desktop/views/components/renote-form-window.vue48
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue23
-rw-r--r--src/client/app/desktop/views/components/settings-window.vue10
-rw-r--r--src/client/app/desktop/views/components/settings.drive.vue1
-rw-r--r--src/client/app/desktop/views/components/settings.profile.vue26
-rw-r--r--src/client/app/desktop/views/components/settings.signins.vue12
-rw-r--r--src/client/app/desktop/views/components/settings.tags.vue58
-rw-r--r--src/client/app/desktop/views/components/settings.vue413
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue8
-rw-r--r--src/client/app/desktop/views/components/taskmanager.vue219
-rw-r--r--src/client/app/desktop/views/components/timeline.core.vue132
-rw-r--r--src/client/app/desktop/views/components/timeline.vue186
-rw-r--r--src/client/app/desktop/views/components/ui-notification.vue16
-rw-r--r--src/client/app/desktop/views/components/ui.header.account.vue42
-rw-r--r--src/client/app/desktop/views/components/ui.header.clock.vue2
-rw-r--r--src/client/app/desktop/views/components/ui.header.nav.vue30
-rw-r--r--src/client/app/desktop/views/components/ui.header.notifications.vue32
-rw-r--r--src/client/app/desktop/views/components/ui.header.post.vue10
-rw-r--r--src/client/app/desktop/views/components/ui.header.search.vue21
-rw-r--r--src/client/app/desktop/views/components/ui.header.vue122
-rw-r--r--src/client/app/desktop/views/components/ui.vue71
-rw-r--r--src/client/app/desktop/views/components/user-list-timeline.vue1
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue15
-rw-r--r--src/client/app/desktop/views/components/user-preview.vue29
-rw-r--r--src/client/app/desktop/views/components/users-list.item.vue126
-rw-r--r--src/client/app/desktop/views/components/users-list.vue32
-rw-r--r--src/client/app/desktop/views/components/widget-container.vue42
-rw-r--r--src/client/app/desktop/views/components/window.vue59
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.announcements.vue52
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue10
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.dashboard.vue65
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.hashtags.vue45
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.suspend-user.vue24
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue23
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.unverify-user.vue24
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.verify-user.vue24
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.vue25
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column-core.vue9
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column.vue75
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.direct-column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.direct.vue93
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue116
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.list-tl.vue7
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.mentions-column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.mentions.vue89
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.note.sub.vue12
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.note.vue148
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notes.vue26
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notification.vue14
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notifications.vue34
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl-column.vue11
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl.vue27
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.vue56
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.widgets-column.vue13
-rw-r--r--src/client/app/desktop/views/pages/drive.vue2
-rw-r--r--src/client/app/desktop/views/pages/games/reversi.vue4
-rw-r--r--src/client/app/desktop/views/pages/home.vue12
-rw-r--r--src/client/app/desktop/views/pages/messaging-room.vue2
-rw-r--r--src/client/app/desktop/views/pages/selectdrive.vue32
-rw-r--r--src/client/app/desktop/views/pages/stats/stats.vue9
-rw-r--r--src/client/app/desktop/views/pages/user-list.users.vue125
-rw-r--r--src/client/app/desktop/views/pages/user-list.vue71
-rw-r--r--src/client/app/desktop/views/pages/user/user.followers-you-know.vue12
-rw-r--r--src/client/app/desktop/views/pages/user/user.friends.vue28
-rw-r--r--src/client/app/desktop/views/pages/user/user.header.vue28
-rw-r--r--src/client/app/desktop/views/pages/user/user.photos.vue29
-rw-r--r--src/client/app/desktop/views/pages/user/user.profile.vue18
-rw-r--r--src/client/app/desktop/views/pages/user/user.timeline.vue31
-rw-r--r--src/client/app/desktop/views/pages/user/user.vue46
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue585
-rw-r--r--src/client/app/desktop/views/widgets/polls.vue12
-rw-r--r--src/client/app/desktop/views/widgets/post-form.vue10
-rw-r--r--src/client/app/desktop/views/widgets/profile.vue61
-rw-r--r--src/client/app/desktop/views/widgets/trends.vue10
-rw-r--r--src/client/app/desktop/views/widgets/users.vue15
-rw-r--r--src/client/app/init.css6
-rw-r--r--src/client/app/init.ts100
-rw-r--r--src/client/app/mios.ts140
-rw-r--r--src/client/app/mobile/api/post.ts7
-rw-r--r--src/client/app/mobile/script.ts1
-rw-r--r--src/client/app/mobile/style.styl10
-rw-r--r--src/client/app/mobile/views/components/dialog.vue12
-rw-r--r--src/client/app/mobile/views/components/drive-file-chooser.vue21
-rw-r--r--src/client/app/mobile/views/components/drive-folder-chooser.vue4
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue73
-rw-r--r--src/client/app/mobile/views/components/drive.file.vue28
-rw-r--r--src/client/app/mobile/views/components/drive.folder.vue6
-rw-r--r--src/client/app/mobile/views/components/drive.vue42
-rw-r--r--src/client/app/mobile/views/components/follow-button.vue39
-rw-r--r--src/client/app/mobile/views/components/friends-maker.vue2
-rw-r--r--src/client/app/mobile/views/components/media-image.vue13
-rw-r--r--src/client/app/mobile/views/components/media-video.vue20
-rw-r--r--src/client/app/mobile/views/components/mute-button.vue24
-rw-r--r--src/client/app/mobile/views/components/note-card.vue15
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue161
-rw-r--r--src/client/app/mobile/views/components/note-preview.vue43
-rw-r--r--src/client/app/mobile/views/components/note.sub.vue45
-rw-r--r--src/client/app/mobile/views/components/note.vue164
-rw-r--r--src/client/app/mobile/views/components/notes.vue28
-rw-r--r--src/client/app/mobile/views/components/notification.vue14
-rw-r--r--src/client/app/mobile/views/components/notifications.vue41
-rw-r--r--src/client/app/mobile/views/components/notify.vue46
-rw-r--r--src/client/app/mobile/views/components/post-form-dialog.vue126
-rw-r--r--src/client/app/mobile/views/components/post-form.vue94
-rw-r--r--src/client/app/mobile/views/components/sub-note-content.vue8
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue115
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue69
-rw-r--r--src/client/app/mobile/views/components/ui.vue24
-rw-r--r--src/client/app/mobile/views/components/user-list-timeline.vue1
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue4
-rw-r--r--src/client/app/mobile/views/components/users-list.vue6
-rw-r--r--src/client/app/mobile/views/components/widget-container.vue17
-rw-r--r--src/client/app/mobile/views/pages/drive.vue13
-rw-r--r--src/client/app/mobile/views/pages/favorites.vue4
-rw-r--r--src/client/app/mobile/views/pages/followers.vue2
-rw-r--r--src/client/app/mobile/views/pages/following.vue2
-rw-r--r--src/client/app/mobile/views/pages/games/reversi.vue7
-rw-r--r--src/client/app/mobile/views/pages/home.timeline.vue119
-rw-r--r--src/client/app/mobile/views/pages/home.vue119
-rw-r--r--src/client/app/mobile/views/pages/messaging-room.vue2
-rw-r--r--src/client/app/mobile/views/pages/messaging.vue2
-rw-r--r--src/client/app/mobile/views/pages/note.vue2
-rw-r--r--src/client/app/mobile/views/pages/notifications.vue4
-rw-r--r--src/client/app/mobile/views/pages/received-follow-requests.vue4
-rw-r--r--src/client/app/mobile/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/mobile/views/pages/settings.vue309
-rw-r--r--src/client/app/mobile/views/pages/settings/settings.profile.vue101
-rw-r--r--src/client/app/mobile/views/pages/tag.vue2
-rw-r--r--src/client/app/mobile/views/pages/user-list.vue2
-rw-r--r--src/client/app/mobile/views/pages/user-lists.vue4
-rw-r--r--src/client/app/mobile/views/pages/user.vue44
-rw-r--r--src/client/app/mobile/views/pages/user/home.photos.vue4
-rw-r--r--src/client/app/mobile/views/pages/user/home.vue18
-rw-r--r--src/client/app/mobile/views/pages/welcome.vue195
-rw-r--r--src/client/app/mobile/views/pages/widgets.vue2
-rw-r--r--src/client/app/safe.js10
-rw-r--r--src/client/app/store.ts23
-rw-r--r--src/client/app/sw.js4
-rw-r--r--src/client/app/theme.ts104
-rw-r--r--src/client/app/tsconfig.json3
-rw-r--r--src/client/assets/code-highlight.css93
-rw-r--r--src/client/assets/pointer.pngbin237627 -> 280910 bytes
-rw-r--r--src/client/assets/reactions/angry.pngbin4606 -> 0 bytes
-rw-r--r--src/client/assets/reactions/confused.pngbin6058 -> 0 bytes
-rw-r--r--src/client/assets/reactions/congrats.pngbin10000 -> 0 bytes
-rw-r--r--src/client/assets/reactions/hmm.pngbin5874 -> 0 bytes
-rw-r--r--src/client/assets/reactions/laugh.pngbin6933 -> 0 bytes
-rw-r--r--src/client/assets/reactions/like.pngbin4822 -> 0 bytes
-rw-r--r--src/client/assets/reactions/love.pngbin2805 -> 0 bytes
-rw-r--r--src/client/assets/reactions/pudding.pngbin7595 -> 0 bytes
-rw-r--r--src/client/assets/reactions/rip.pngbin6793 -> 0 bytes
-rw-r--r--src/client/assets/reactions/surprise.pngbin3806 -> 0 bytes
-rw-r--r--src/client/assets/reactions/sushi.pngbin8651 -> 0 bytes
-rw-r--r--src/client/assets/title.light.svg140
-rw-r--r--src/client/assets/title.svg (renamed from src/client/assets/title.dark.svg)16
-rw-r--r--src/client/const.styl4
-rw-r--r--src/client/element.scss12
-rw-r--r--src/client/style.styl14
-rw-r--r--src/client/theme/black.json520
-rw-r--r--src/client/theme/dark.json5209
-rw-r--r--src/client/theme/halloween.json521
-rw-r--r--src/client/theme/light.json5209
-rw-r--r--src/client/theme/pink.json520
309 files changed, 7352 insertions, 6615 deletions
diff --git a/src/client/app/app.styl b/src/client/app/app.styl
index 431b9daa65..2f0095944c 100644
--- a/src/client/app/app.styl
+++ b/src/client/app/app.styl
@@ -6,6 +6,10 @@ html
&, *
cursor progress !important
+html
+ // iOSのため
+ overflow auto
+
body
overflow-wrap break-word
@@ -23,7 +27,7 @@ body
z-index 65536
.bar
- background $theme-color
+ background var(--primary)
position fixed
z-index 65537
@@ -40,7 +44,7 @@ body
right 0px
width 100px
height 100%
- box-shadow 0 0 10px $theme-color, 0 0 5px $theme-color
+ box-shadow 0 0 10px var(--primary), 0 0 5px var(--primary)
opacity 1
transform rotate(3deg) translate(0px, -4px)
@@ -60,8 +64,8 @@ body
box-sizing border-box
border solid 2px transparent
- border-top-color $theme-color
- border-left-color $theme-color
+ border-top-color var(--primary)
+ border-left-color var(--primary)
border-radius 50%
animation progress-spinner 400ms linear infinite
diff --git a/src/client/app/app.vue b/src/client/app/app.vue
index 7a46e7dea0..e639c9f9ac 100644
--- a/src/client/app/app.vue
+++ b/src/client/app/app.vue
@@ -1,3 +1,32 @@
<template>
-<router-view id="app"></router-view>
+<router-view id="app" v-hotkey.global="keymap"></router-view>
</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { url, lang } from './config';
+
+export default Vue.extend({
+ computed: {
+ keymap(): any {
+ return {
+ 'h|slash': this.help,
+ 'd': this.dark
+ };
+ }
+ },
+
+ methods: {
+ help() {
+ window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank');
+ },
+
+ dark() {
+ this.$store.commit('device/set', {
+ key: 'darkmode',
+ value: !this.$store.state.device.darkmode
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
index 609e758994..ba7df911e5 100644
--- a/src/client/app/auth/views/index.vue
+++ b/src/client/app/auth/views/index.vue
@@ -80,7 +80,7 @@ export default Vue.extend({
accepted() {
this.state = 'accepted';
if (this.session.app.callbackUrl) {
- location.href = this.session.app.callbackUrl + '?token=' + this.session.token;
+ location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
}
}
}
diff --git a/src/client/app/base.pug b/src/client/app/base.pug
index 11b150bc67..ee9d4b6f6d 100644
--- a/src/client/app/base.pug
+++ b/src/client/app/base.pug
@@ -34,9 +34,6 @@ html
//- FontAwesome style
style #{facss}
- //- highlight.js style
- style #{hljscss}
-
body
noscript: p
| JavaScriptを有効にしてください
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 54397c98c6..6e06a88aa3 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -18,6 +18,17 @@
return;
}
+ const langs = LANGS;
+
+ //#region Apply theme
+ const theme = localStorage.getItem('theme');
+ if (theme) {
+ Object.entries(JSON.parse(theme)).forEach(([k, v]) => {
+ document.documentElement.style.setProperty(`--${k}`, v.toString());
+ });
+ }
+ //#endregion
+
//#region Load settings
let settings = null;
const vuex = localStorage.getItem('vuex');
@@ -40,10 +51,10 @@
//#region Detect the user language
let lang = null;
- if (LANGS.includes(navigator.language)) {
+ if (langs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = LANGS.find(x => x.split('-')[0] == navigator.language);
+ lang = langs.find(x => x.split('-')[0] == navigator.language);
if (lang == null) {
// Fallback
@@ -52,7 +63,7 @@
}
if (settings && settings.device.lang &&
- LANGS.includes(settings.device.lang)) {
+ langs.includes(settings.device.lang)) {
lang = settings.device.lang;
}
//#endregion
@@ -82,19 +93,12 @@
app = isMobile ? 'mobile' : 'desktop';
}
- // Dark/Light
- if (settings) {
- if (settings.device.darkmode) {
- document.documentElement.setAttribute('data-darkmode', 'true');
- }
- }
-
// Script version
const ver = localStorage.getItem('v') || VERSION;
// Get salt query
const salt = localStorage.getItem('salt')
- ? '?salt=' + localStorage.getItem('salt')
+ ? `?salt=${localStorage.getItem('salt')}`
: '';
// Load an app script
@@ -140,7 +144,7 @@
// Random
localStorage.setItem('salt', Math.random().toString());
- // Clear cache (serive worker)
+ // Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');
diff --git a/src/client/app/common/hotkey.ts b/src/client/app/common/hotkey.ts
new file mode 100644
index 0000000000..dc1a34338a
--- /dev/null
+++ b/src/client/app/common/hotkey.ts
@@ -0,0 +1,110 @@
+import keyCode from './keycode';
+import { concat } from '../../../prelude/array';
+
+type pattern = {
+ which: string[];
+ ctrl?: boolean;
+ shift?: boolean;
+ alt?: boolean;
+};
+
+type action = {
+ patterns: pattern[];
+
+ callback: Function;
+};
+
+const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
+ const result = {
+ patterns: [],
+ callback: callback
+ } as action;
+
+ result.patterns = patterns.split('|').map(part => {
+ const pattern = {
+ which: [],
+ ctrl: false,
+ alt: false,
+ shift: false
+ } as pattern;
+
+ part.trim().split('+').forEach(key => {
+ key = key.trim().toLowerCase();
+ switch (key) {
+ case 'ctrl': pattern.ctrl = true; break;
+ case 'alt': pattern.alt = true; break;
+ case 'shift': pattern.shift = true; break;
+ default: pattern.which = keyCode(key).map(k => k.toLowerCase());
+ }
+ });
+
+ return pattern;
+ });
+
+ return result;
+});
+
+const ignoreElemens = ['input', 'textarea'];
+
+export default {
+ install(Vue) {
+ Vue.directive('hotkey', {
+ bind(el, binding) {
+ el._hotkey_global = binding.modifiers.global === true;
+
+ const actions = getKeyMap(binding.value);
+
+ // flatten
+ const reservedKeys = concat(concat(actions.map(a => a.patterns.map(p => p.which))));
+
+ el.dataset.reservedKeys = reservedKeys.map(key => `'${key}'`).join(' ');
+
+ el._keyHandler = (e: KeyboardEvent) => {
+ const key = e.code.toLowerCase();
+
+ const targetReservedKeys = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeys || '' : '';
+ if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
+
+ for (const action of actions) {
+ if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
+
+ const matched = action.patterns.some(pattern => {
+ const matched = pattern.which.includes(key) &&
+ pattern.ctrl == e.ctrlKey &&
+ pattern.shift == e.shiftKey &&
+ pattern.alt == e.altKey &&
+ e.metaKey == false;
+
+ if (matched) {
+ e.preventDefault();
+ e.stopPropagation();
+ action.callback(e);
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ if (matched) {
+ break;
+ }
+ }
+ };
+
+ if (el._hotkey_global) {
+ document.addEventListener('keydown', el._keyHandler);
+ } else {
+ el.addEventListener('keydown', el._keyHandler);
+ }
+ },
+
+ unbind(el) {
+ if (el._hotkey_global) {
+ document.removeEventListener('keydown', el._keyHandler);
+ } else {
+ el.removeEventListener('keydown', el._keyHandler);
+ }
+ }
+ });
+ }
+};
diff --git a/src/client/app/common/keycode.ts b/src/client/app/common/keycode.ts
new file mode 100644
index 0000000000..5786c1dc0a
--- /dev/null
+++ b/src/client/app/common/keycode.ts
@@ -0,0 +1,33 @@
+export default (input: string): string[] => {
+ if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
+ const codes = aliases[input];
+ return Array.isArray(codes) ? codes : [codes];
+ } else {
+ return [input];
+ }
+};
+
+export const aliases = {
+ 'esc': 'Escape',
+ 'enter': ['Enter', 'NumpadEnter'],
+ 'up': 'ArrowUp',
+ 'down': 'ArrowDown',
+ 'left': 'ArrowLeft',
+ 'right': 'ArrowRight',
+ 'plus': ['NumpadAdd', 'Semicolon'],
+};
+
+/*!
+* Programatically add the following
+*/
+
+// lower case chars
+for (let i = 97; i < 123; i++) {
+ const char = String.fromCharCode(i);
+ aliases[char] = `Key${char.toUpperCase()}`;
+}
+
+// numbers
+for (let i = 0; i < 10; i++) {
+ aliases[i] = [`Numpad${i}`, `Digit${i}`];
+}
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 4445eefc39..91b165b45d 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
localStorage.setItem('should-refresh', 'true');
localStorage.setItem('v', newer);
- // Clear cache (serive worker)
+ // Clear cache (service worker)
try {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear');
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index f42af94370..65087cc98e 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -13,21 +13,21 @@ type Notification = {
export default function(type, data): Notification {
switch (type) {
- case 'drive_file_created':
+ case 'driveFileCreated':
return {
title: '%i18n:common.notification.file-uploaded%',
body: data.name,
icon: data.url
};
- case 'unread_messaging_message':
+ case 'unreadMessagingMessage':
return {
title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] ,
body: data.text, // TODO: getMessagingMessageSummary(data),
icon: data.user.avatarUrl
};
- case 'reversi_invited':
+ case 'reversiInvited':
return {
title: '%i18n:common.notification.reversi-invited%',
body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1],
diff --git a/src/client/app/common/scripts/fuck-ad-block.ts b/src/client/app/common/scripts/fuck-ad-block.ts
index ed0904aeb3..0c802f1648 100644
--- a/src/client/app/common/scripts/fuck-ad-block.ts
+++ b/src/client/app/common/scripts/fuck-ad-block.ts
@@ -1,8 +1,8 @@
-require('fuckadblock');
-
declare const fuckAdBlock: any;
export default (os) => {
+ require('fuckadblock');
+
function adBlockDetected() {
os.apis.dialog({
title: '%fa:exclamation-triangle%%i18n:common.adblock.detected%',
diff --git a/src/client/app/common/scripts/gcd.ts b/src/client/app/common/scripts/gcd.ts
deleted file mode 100644
index 9a19f9da66..0000000000
--- a/src/client/app/common/scripts/gcd.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-const gcd = (a, b) => !b ? a : gcd(b, a % b);
-export default gcd;
diff --git a/src/client/app/common/scripts/get-md5.ts b/src/client/app/common/scripts/get-md5.ts
new file mode 100644
index 0000000000..24ac04c1ad
--- /dev/null
+++ b/src/client/app/common/scripts/get-md5.ts
@@ -0,0 +1,8 @@
+const crypto = require('crypto');
+
+export default (data: ArrayBuffer) => {
+ const buf = new Buffer(data);
+ const hash = crypto.createHash("md5");
+ hash.update(buf);
+ return hash.digest("hex");
+}; \ No newline at end of file
diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts
new file mode 100644
index 0000000000..c41897e70f
--- /dev/null
+++ b/src/client/app/common/scripts/note-subscriber.ts
@@ -0,0 +1,116 @@
+import Vue from 'vue';
+
+export default prop => ({
+ data() {
+ return {
+ connection: null
+ };
+ },
+
+ computed: {
+ $_ns_note_(): any {
+ return this[prop];
+ },
+
+ $_ns_isRenote(): boolean {
+ return (this.$_ns_note_.renote &&
+ this.$_ns_note_.text == null &&
+ this.$_ns_note_.fileIds.length == 0 &&
+ this.$_ns_note_.poll == null);
+ },
+
+ $_ns_target(): any {
+ return this._ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_;
+ },
+ },
+
+ created() {
+ if (this.$store.getters.isSignedIn) {
+ this.connection = (this as any).os.stream;
+ }
+ },
+
+ mounted() {
+ this.capture(true);
+
+ if (this.$store.getters.isSignedIn) {
+ this.connection.on('_connected_', this.onStreamConnected);
+ }
+ },
+
+ beforeDestroy() {
+ this.decapture(true);
+
+ if (this.$store.getters.isSignedIn) {
+ this.connection.off('_connected_', this.onStreamConnected);
+ }
+ },
+
+ methods: {
+ capture(withHandler = false) {
+ if (this.$store.getters.isSignedIn) {
+ const data = {
+ id: this.$_ns_target.id
+ } as any;
+
+ if (
+ (this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) ||
+ (this.$_ns_target.mentions || []).includes(this.$store.state.i.id)
+ ) {
+ data.read = true;
+ }
+
+ this.connection.send('sn', data);
+ if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
+ }
+ },
+
+ decapture(withHandler = false) {
+ if (this.$store.getters.isSignedIn) {
+ this.connection.send('un', {
+ id: this.$_ns_target.id
+ });
+ if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
+ }
+ },
+
+ onStreamConnected() {
+ this.capture();
+ },
+
+ onStreamNoteUpdated(data) {
+ const { type, id, body } = data;
+
+ if (id !== this.$_ns_target.id) return;
+
+ switch (type) {
+ case 'reacted': {
+ const reaction = body.reaction;
+ if (this.$_ns_target.reactionCounts == null) Vue.set(this.$_ns_target, 'reactionCounts', {});
+ this.$_ns_target.reactionCounts[reaction] = (this.$_ns_target.reactionCounts[reaction] || 0) + 1;
+ break;
+ }
+
+ case 'pollVoted': {
+ if (body.userId == this.$store.state.i.id) return;
+ const choice = body.choice;
+ this.$_ns_target.poll.choices.find(c => c.id === choice).votes++;
+ break;
+ }
+
+ case 'deleted': {
+ Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt);
+ this.$_ns_target.text = null;
+ this.$_ns_target.tags = [];
+ this.$_ns_target.fileIds = [];
+ this.$_ns_target.poll = null;
+ this.$_ns_target.geo = null;
+ this.$_ns_target.cw = null;
+ break;
+ }
+ }
+
+ this.$emit(`update:${prop}`, this.$_ns_note_);
+ },
+ }
+});
diff --git a/src/client/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts
deleted file mode 100644
index 5f6ae3320a..0000000000
--- a/src/client/app/common/scripts/parse-search-query.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-export default function(qs: string) {
- const q = {
- text: ''
- };
-
- qs.split(' ').forEach(x => {
- if (/^([a-z_]+?):(.+?)$/.test(x)) {
- const [key, value] = x.split(':');
- switch (key) {
- case 'user':
- q['includeUserUsernames'] = value.split(',');
- break;
- case 'exclude_user':
- q['excludeUserUsernames'] = value.split(',');
- break;
- case 'follow':
- q['following'] = value == 'null' ? null : value == 'true';
- break;
- case 'reply':
- q['reply'] = value == 'null' ? null : value == 'true';
- break;
- case 'renote':
- q['renote'] = value == 'null' ? null : value == 'true';
- break;
- case 'media':
- q['media'] = value == 'null' ? null : value == 'true';
- break;
- case 'poll':
- q['poll'] = value == 'null' ? null : value == 'true';
- break;
- case 'until':
- case 'since':
- // YYYY-MM-DD
- if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
- const [yyyy, mm, dd] = value.split('-');
- q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
- }
- break;
- default:
- q[key] = value;
- break;
- }
- } else {
- q.text += x + ' ';
- }
- });
-
- if (q.text) {
- q.text = q.text.trim();
- }
-
- return q;
-}
diff --git a/src/client/app/common/scripts/stream.ts b/src/client/app/common/scripts/stream.ts
new file mode 100644
index 0000000000..3b1a94adf9
--- /dev/null
+++ b/src/client/app/common/scripts/stream.ts
@@ -0,0 +1,318 @@
+import autobind from 'autobind-decorator';
+import { EventEmitter } from 'eventemitter3';
+import ReconnectingWebsocket from 'reconnecting-websocket';
+import { wsUrl } from '../../config';
+import MiOS from '../../mios';
+
+/**
+ * Misskey stream connection
+ */
+export default class Stream extends EventEmitter {
+ private stream: ReconnectingWebsocket;
+ private state: string;
+ private buffer: any[];
+ private sharedConnections: SharedConnection[] = [];
+ private nonSharedConnections: NonSharedConnection[] = [];
+
+ constructor(os: MiOS) {
+ super();
+
+ this.state = 'initializing';
+ this.buffer = [];
+
+ const user = os.store.state.i;
+
+ this.stream = new ReconnectingWebsocket(wsUrl + (user ? `?i=${user.token}` : ''));
+ this.stream.addEventListener('open', this.onOpen);
+ this.stream.addEventListener('close', this.onClose);
+ this.stream.addEventListener('message', this.onMessage);
+
+ if (user) {
+ const main = this.useSharedConnection('main');
+
+ // 自分の情報が更新されたとき
+ main.on('meUpdated', i => {
+ os.store.dispatch('mergeMe', i);
+ });
+
+ main.on('readAllNotifications', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadNotification: false
+ });
+ });
+
+ main.on('unreadNotification', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadNotification: true
+ });
+ });
+
+ main.on('readAllMessagingMessages', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadMessagingMessage: false
+ });
+ });
+
+ main.on('unreadMessagingMessage', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadMessagingMessage: true
+ });
+ });
+
+ main.on('unreadMention', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadMentions: true
+ });
+ });
+
+ main.on('readAllUnreadMentions', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadMentions: false
+ });
+ });
+
+ main.on('unreadSpecifiedNote', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadSpecifiedNotes: true
+ });
+ });
+
+ main.on('readAllUnreadSpecifiedNotes', () => {
+ os.store.dispatch('mergeMe', {
+ hasUnreadSpecifiedNotes: false
+ });
+ });
+
+ main.on('clientSettingUpdated', x => {
+ os.store.commit('settings/set', {
+ key: x.key,
+ value: x.value
+ });
+ });
+
+ main.on('homeUpdated', x => {
+ os.store.commit('settings/setHome', x);
+ });
+
+ main.on('mobileHomeUpdated', x => {
+ os.store.commit('settings/setMobileHome', x);
+ });
+
+ main.on('widgetUpdated', x => {
+ os.store.commit('settings/setWidget', {
+ id: x.id,
+ data: x.data
+ });
+ });
+
+ // トークンが再生成されたとき
+ // このままではMisskeyが利用できないので強制的にサインアウトさせる
+ main.on('myTokenRegenerated', () => {
+ alert('%i18n:common.my-token-regenerated%');
+ os.signout();
+ });
+ }
+ }
+
+ public useSharedConnection = (channel: string): SharedConnection => {
+ const existConnection = this.sharedConnections.find(c => c.channel === channel);
+
+ if (existConnection) {
+ existConnection.use();
+ return existConnection;
+ } else {
+ const connection = new SharedConnection(this, channel);
+ connection.use();
+ this.sharedConnections.push(connection);
+ return connection;
+ }
+ }
+
+ @autobind
+ public removeSharedConnection(connection: SharedConnection) {
+ this.sharedConnections = this.sharedConnections.filter(c => c.id !== connection.id);
+ }
+
+ public connectToChannel = (channel: string, params?: any): NonSharedConnection => {
+ const connection = new NonSharedConnection(this, channel, params);
+ this.nonSharedConnections.push(connection);
+ return connection;
+ }
+
+ @autobind
+ public disconnectToChannel(connection: NonSharedConnection) {
+ this.nonSharedConnections = this.nonSharedConnections.filter(c => c.id !== connection.id);
+ }
+
+ /**
+ * Callback of when open connection
+ */
+ @autobind
+ private onOpen() {
+ const isReconnect = this.state == 'reconnecting';
+
+ this.state = 'connected';
+ this.emit('_connected_');
+
+ // バッファーを処理
+ const _buffer = [].concat(this.buffer); // Shallow copy
+ this.buffer = []; // Clear buffer
+ _buffer.forEach(data => {
+ this.send(data); // Resend each buffered messages
+ });
+
+ // チャンネル再接続
+ if (isReconnect) {
+ this.sharedConnections.forEach(c => {
+ c.connect();
+ });
+ this.nonSharedConnections.forEach(c => {
+ c.connect();
+ });
+ }
+ }
+
+ /**
+ * Callback of when close connection
+ */
+ @autobind
+ private onClose() {
+ this.state = 'reconnecting';
+ this.emit('_disconnected_');
+ }
+
+ /**
+ * Callback of when received a message from connection
+ */
+ @autobind
+ private onMessage(message) {
+ const { type, body } = JSON.parse(message.data);
+
+ if (type == 'channel') {
+ const id = body.id;
+ const connection = this.sharedConnections.find(c => c.id === id) || this.nonSharedConnections.find(c => c.id === id);
+ connection.emit(body.type, body.body);
+ } else {
+ this.emit(type, body);
+ }
+ }
+
+ /**
+ * Send a message to connection
+ */
+ @autobind
+ public send(typeOrPayload, payload?) {
+ const data = payload === undefined ? typeOrPayload : {
+ type: typeOrPayload,
+ body: payload
+ };
+
+ // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
+ if (this.state != 'connected') {
+ this.buffer.push(data);
+ return;
+ }
+
+ this.stream.send(JSON.stringify(data));
+ }
+
+ /**
+ * Close this connection
+ */
+ @autobind
+ public close() {
+ this.stream.removeEventListener('open', this.onOpen);
+ this.stream.removeEventListener('message', this.onMessage);
+ }
+}
+
+abstract class Connection extends EventEmitter {
+ public channel: string;
+ public id: string;
+ protected params: any;
+ protected stream: Stream;
+
+ constructor(stream: Stream, channel: string, params?: any) {
+ super();
+
+ this.stream = stream;
+ this.channel = channel;
+ this.params = params;
+ this.id = Math.random().toString();
+ this.connect();
+ }
+
+ @autobind
+ public connect() {
+ this.stream.send('connect', {
+ channel: this.channel,
+ id: this.id,
+ params: this.params
+ });
+ }
+
+ @autobind
+ public send(typeOrPayload, payload?) {
+ const data = payload === undefined ? typeOrPayload : {
+ type: typeOrPayload,
+ body: payload
+ };
+
+ this.stream.send('channel', {
+ id: this.id,
+ body: data
+ });
+ }
+
+ public abstract dispose: () => void;
+}
+
+class SharedConnection extends Connection {
+ private users = 0;
+ private disposeTimerId: any;
+
+ constructor(stream: Stream, channel: string) {
+ super(stream, channel);
+ }
+
+ @autobind
+ public use() {
+ this.users++;
+
+ // タイマー解除
+ if (this.disposeTimerId) {
+ clearTimeout(this.disposeTimerId);
+ this.disposeTimerId = null;
+ }
+ }
+
+ @autobind
+ public dispose() {
+ this.users--;
+
+ // そのコネクションの利用者が誰もいなくなったら
+ if (this.users === 0) {
+ // また直ぐに再利用される可能性があるので、一定時間待ち、
+ // 新たな利用者が現れなければコネクションを切断する
+ this.disposeTimerId = setTimeout(() => {
+ this.disposeTimerId = null;
+ this.removeAllListeners();
+ this.stream.send('disconnect', { id: this.id });
+ this.stream.removeSharedConnection(this);
+ }, 3000);
+ }
+ }
+}
+
+class NonSharedConnection extends Connection {
+ constructor(stream: Stream, channel: string, params?: any) {
+ super(stream, channel, params);
+ }
+
+ @autobind
+ public dispose() {
+ this.removeAllListeners();
+ this.stream.send('disconnect', { id: this.id });
+ this.stream.disconnectToChannel(this);
+ }
+}
diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts
deleted file mode 100644
index 50fff05737..0000000000
--- a/src/client/app/common/scripts/streaming/drive.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Drive stream connection
- */
-export class DriveStream extends Stream {
- constructor(os: MiOS, me) {
- super(os, 'drive', {
- i: me.token
- });
- }
-}
-
-export class DriveStreamManager extends StreamManager<DriveStream> {
- private me;
- private os: MiOS;
-
- constructor(os: MiOS, me) {
- super();
-
- this.me = me;
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new DriveStream(this.os, this.me);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts
deleted file mode 100644
index e6b02fcfdb..0000000000
--- a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import Stream from '../../stream';
-import MiOS from '../../../../../mios';
-
-export class ReversiGameStream extends Stream {
- constructor(os: MiOS, me, game) {
- super(os, 'games/reversi-game', {
- i: me ? me.token : null,
- game: game.id
- });
- }
-}
diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi.ts
deleted file mode 100644
index 1f4fd8c63e..0000000000
--- a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import StreamManager from '../../stream-manager';
-import Stream from '../../stream';
-import MiOS from '../../../../../mios';
-
-export class ReversiStream extends Stream {
- constructor(os: MiOS, me) {
- super(os, 'games/reversi', {
- i: me.token
- });
- }
-}
-
-export class ReversiStreamManager extends StreamManager<ReversiStream> {
- private me;
- private os: MiOS;
-
- constructor(os: MiOS, me) {
- super();
-
- this.me = me;
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new ReversiStream(this.os, this.me);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/global-timeline.ts b/src/client/app/common/scripts/streaming/global-timeline.ts
deleted file mode 100644
index a639f1595c..0000000000
--- a/src/client/app/common/scripts/streaming/global-timeline.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Global timeline stream connection
- */
-export class GlobalTimelineStream extends Stream {
- constructor(os: MiOS, me) {
- super(os, 'global-timeline', {
- i: me.token
- });
- }
-}
-
-export class GlobalTimelineStreamManager extends StreamManager<GlobalTimelineStream> {
- private me;
- private os: MiOS;
-
- constructor(os: MiOS, me) {
- super();
-
- this.me = me;
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new GlobalTimelineStream(this.os, this.me);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
deleted file mode 100644
index dd18c70d70..0000000000
--- a/src/client/app/common/scripts/streaming/home.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Home stream connection
- */
-export class HomeStream extends Stream {
- constructor(os: MiOS, me) {
- super(os, '', {
- i: me.token
- });
-
- // 最終利用日時を更新するため定期的にaliveメッセージを送信
- setInterval(() => {
- this.send({ type: 'alive' });
- me.lastUsedAt = new Date();
- }, 1000 * 60);
-
- // 自分の情報が更新されたとき
- this.on('meUpdated', i => {
- if (os.debug) {
- console.log('I updated:', i);
- }
-
- os.store.dispatch('mergeMe', i);
- });
-
- this.on('read_all_notifications', () => {
- os.store.dispatch('mergeMe', {
- hasUnreadNotification: false
- });
- });
-
- this.on('unread_notification', () => {
- os.store.dispatch('mergeMe', {
- hasUnreadNotification: true
- });
- });
-
- this.on('read_all_messaging_messages', () => {
- os.store.dispatch('mergeMe', {
- hasUnreadMessagingMessage: false
- });
- });
-
- this.on('unread_messaging_message', () => {
- os.store.dispatch('mergeMe', {
- hasUnreadMessagingMessage: true
- });
- });
-
- this.on('clientSettingUpdated', x => {
- os.store.commit('settings/set', {
- key: x.key,
- value: x.value
- });
- });
-
- this.on('home_updated', x => {
- os.store.commit('settings/setHome', x);
- });
-
- this.on('mobile_home_updated', x => {
- os.store.commit('settings/setMobileHome', x);
- });
-
- this.on('widgetUpdated', x => {
- os.store.commit('settings/setWidget', {
- id: x.id,
- data: x.data
- });
- });
-
- // トークンが再生成されたとき
- // このままではMisskeyが利用できないので強制的にサインアウトさせる
- this.on('my_token_regenerated', () => {
- alert('%i18n:common.my-token-regenerated%');
- os.signout();
- });
- }
-}
-
-export class HomeStreamManager extends StreamManager<HomeStream> {
- private me;
- private os: MiOS;
-
- constructor(os: MiOS, me) {
- super();
-
- this.me = me;
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new HomeStream(this.os, this.me);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/hybrid-timeline.ts b/src/client/app/common/scripts/streaming/hybrid-timeline.ts
deleted file mode 100644
index cd290797c4..0000000000
--- a/src/client/app/common/scripts/streaming/hybrid-timeline.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Hybrid timeline stream connection
- */
-export class HybridTimelineStream extends Stream {
- constructor(os: MiOS, me) {
- super(os, 'hybrid-timeline', {
- i: me.token
- });
- }
-}
-
-export class HybridTimelineStreamManager extends StreamManager<HybridTimelineStream> {
- private me;
- private os: MiOS;
-
- constructor(os: MiOS, me) {
- super();
-
- this.me = me;
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new HybridTimelineStream(this.os, this.me);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts
deleted file mode 100644
index 2834262bdc..0000000000
--- a/src/client/app/common/scripts/streaming/local-timeline.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Local timeline stream connection
- */
-export class LocalTimelineStream extends Stream {
- constructor(os: MiOS, me) {
- super(os, 'local-timeline', {
- i: me.token
- });
- }
-}
-
-export class LocalTimelineStreamManager extends StreamManager<LocalTimelineStream> {
- private me;
- private os: MiOS;
-
- constructor(os: MiOS, me) {
- super();
-
- this.me = me;
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new LocalTimelineStream(this.os, this.me);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts
deleted file mode 100644
index addcccb952..0000000000
--- a/src/client/app/common/scripts/streaming/messaging-index.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Messaging index stream connection
- */
-export class MessagingIndexStream extends Stream {
- constructor(os: MiOS, me) {
- super(os, 'messaging-index', {
- i: me.token
- });
- }
-}
-
-export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> {
- private me;
- private os: MiOS;
-
- constructor(os: MiOS, me) {
- super();
-
- this.me = me;
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new MessagingIndexStream(this.os, this.me);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts
deleted file mode 100644
index a59377d867..0000000000
--- a/src/client/app/common/scripts/streaming/messaging.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import Stream from './stream';
-import MiOS from '../../../mios';
-
-/**
- * Messaging stream connection
- */
-export class MessagingStream extends Stream {
- constructor(os: MiOS, me, otherparty) {
- super(os, 'messaging', {
- i: me.token,
- otherparty
- });
-
- (this as any).on('_connected_', () => {
- this.send({
- i: me.token
- });
- });
- }
-}
diff --git a/src/client/app/common/scripts/streaming/notes-stats.ts b/src/client/app/common/scripts/streaming/notes-stats.ts
deleted file mode 100644
index 9e3e78a709..0000000000
--- a/src/client/app/common/scripts/streaming/notes-stats.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Notes stats stream connection
- */
-export class NotesStatsStream extends Stream {
- constructor(os: MiOS) {
- super(os, 'notes-stats');
- }
-}
-
-export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> {
- private os: MiOS;
-
- constructor(os: MiOS) {
- super();
-
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new NotesStatsStream(this.os);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/server-stats.ts b/src/client/app/common/scripts/streaming/server-stats.ts
deleted file mode 100644
index 9983dfcaf0..0000000000
--- a/src/client/app/common/scripts/streaming/server-stats.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Server stats stream connection
- */
-export class ServerStatsStream extends Stream {
- constructor(os: MiOS) {
- super(os, 'server-stats');
- }
-}
-
-export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> {
- private os: MiOS;
-
- constructor(os: MiOS) {
- super();
-
- this.os = os;
- }
-
- public getConnection() {
- if (this.connection == null) {
- this.connection = new ServerStatsStream(this.os);
- }
-
- return this.connection;
- }
-}
diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts
deleted file mode 100644
index 568b8b0372..0000000000
--- a/src/client/app/common/scripts/streaming/stream-manager.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { EventEmitter } from 'eventemitter3';
-import * as uuid from 'uuid';
-import Connection from './stream';
-
-/**
- * ストリーム接続を管理するクラス
- * 複数の場所から同じストリームを利用する際、接続をまとめたりする
- */
-export default abstract class StreamManager<T extends Connection> extends EventEmitter {
- private _connection: T = null;
-
- private disposeTimerId: any;
-
- /**
- * コネクションを必要としているユーザー
- */
- private users = [];
-
- protected set connection(connection: T) {
- this._connection = connection;
-
- if (this._connection == null) {
- this.emit('disconnected');
- } else {
- this.emit('connected', this._connection);
-
- this._connection.on('_connected_', () => {
- this.emit('_connected_');
- });
-
- this._connection.on('_disconnected_', () => {
- this.emit('_disconnected_');
- });
-
- this._connection.user = 'Managed';
- }
- }
-
- protected get connection() {
- return this._connection;
- }
-
- /**
- * コネクションを持っているか否か
- */
- public get hasConnection() {
- return this._connection != null;
- }
-
- public get state(): string {
- if (!this.hasConnection) return 'no-connection';
- return this._connection.state;
- }
-
- /**
- * コネクションを要求します
- */
- public abstract getConnection(): T;
-
- /**
- * 現在接続しているコネクションを取得します
- */
- public borrow() {
- return this._connection;
- }
-
- /**
- * コネクションを要求するためのユーザーIDを発行します
- */
- public use() {
- // タイマー解除
- if (this.disposeTimerId) {
- clearTimeout(this.disposeTimerId);
- this.disposeTimerId = null;
- }
-
- // ユーザーID生成
- const userId = uuid();
-
- this.users.push(userId);
-
- this._connection.user = `Managed (${ this.users.length })`;
-
- return userId;
- }
-
- /**
- * コネクションを利用し終わってもう必要ないことを通知します
- * @param userId use で発行したユーザーID
- */
- public dispose(userId) {
- this.users = this.users.filter(id => id != userId);
-
- this._connection.user = `Managed (${ this.users.length })`;
-
- // 誰もコネクションの利用者がいなくなったら
- if (this.users.length == 0) {
- // また直ぐに再利用される可能性があるので、一定時間待ち、
- // 新たな利用者が現れなければコネクションを切断する
- this.disposeTimerId = setTimeout(() => {
- this.disposeTimerId = null;
-
- this.connection.close();
- this.connection = null;
- }, 3000);
- }
- }
-}
diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts
deleted file mode 100644
index fefa8e5ced..0000000000
--- a/src/client/app/common/scripts/streaming/stream.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { EventEmitter } from 'eventemitter3';
-import * as uuid from 'uuid';
-import * as ReconnectingWebsocket from 'reconnecting-websocket';
-import { wsUrl } from '../../../config';
-import MiOS from '../../../mios';
-
-/**
- * Misskey stream connection
- */
-export default class Connection extends EventEmitter {
- public state: string;
- private buffer: any[];
- public socket: ReconnectingWebsocket;
- public name: string;
- public connectedAt: Date;
- public user: string = null;
- public in: number = 0;
- public out: number = 0;
- public inout: Array<{
- type: 'in' | 'out',
- at: Date,
- data: string
- }> = [];
- public id: string;
- public isSuspended = false;
- private os: MiOS;
-
- constructor(os: MiOS, endpoint, params?) {
- super();
-
- //#region BIND
- this.onOpen = this.onOpen.bind(this);
- this.onClose = this.onClose.bind(this);
- this.onMessage = this.onMessage.bind(this);
- this.send = this.send.bind(this);
- this.close = this.close.bind(this);
- //#endregion
-
- this.id = uuid();
- this.os = os;
- this.name = endpoint;
- this.state = 'initializing';
- this.buffer = [];
-
- const query = params
- ? Object.keys(params)
- .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
- .join('&')
- : null;
-
- this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`);
- this.socket.addEventListener('open', this.onOpen);
- this.socket.addEventListener('close', this.onClose);
- this.socket.addEventListener('message', this.onMessage);
-
- // Register this connection for debugging
- this.os.registerStreamConnection(this);
- }
-
- /**
- * Callback of when open connection
- */
- private onOpen() {
- this.state = 'connected';
- this.emit('_connected_');
-
- this.connectedAt = new Date();
-
- // バッファーを処理
- const _buffer = [].concat(this.buffer); // Shallow copy
- this.buffer = []; // Clear buffer
- _buffer.forEach(data => {
- this.send(data); // Resend each buffered messages
-
- if (this.os.debug) {
- this.out++;
- this.inout.push({ type: 'out', at: new Date(), data });
- }
- });
- }
-
- /**
- * Callback of when close connection
- */
- private onClose() {
- this.state = 'reconnecting';
- this.emit('_disconnected_');
- }
-
- /**
- * Callback of when received a message from connection
- */
- private onMessage(message) {
- if (this.isSuspended) return;
-
- if (this.os.debug) {
- this.in++;
- this.inout.push({ type: 'in', at: new Date(), data: message.data });
- }
-
- try {
- const msg = JSON.parse(message.data);
- if (msg.type) this.emit(msg.type, msg.body);
- } catch (e) {
- // noop
- }
- }
-
- /**
- * Send a message to connection
- */
- public send(data) {
- if (this.isSuspended) return;
-
- // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
- if (this.state != 'connected') {
- this.buffer.push(data);
- return;
- }
-
- if (this.os.debug) {
- this.out++;
- this.inout.push({ type: 'out', at: new Date(), data });
- }
-
- this.socket.send(JSON.stringify(data));
- }
-
- /**
- * Close this connection
- */
- public close() {
- this.os.unregisterStreamConnection(this);
- this.socket.removeEventListener('open', this.onOpen);
- this.socket.removeEventListener('message', this.onMessage);
- }
-}
diff --git a/src/client/app/common/scripts/streaming/user-list.ts b/src/client/app/common/scripts/streaming/user-list.ts
deleted file mode 100644
index 30a52b98dd..0000000000
--- a/src/client/app/common/scripts/streaming/user-list.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import Stream from './stream';
-import MiOS from '../../mios';
-
-export class UserListStream extends Stream {
- constructor(os: MiOS, me, listId) {
- super(os, 'user-list', {
- i: me.token,
- listId
- });
-
- (this as any).on('_connected_', () => {
- this.send({
- i: me.token
- });
- });
- }
-}
diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue
index 1ad222afdd..542fbb4296 100644
--- a/src/client/app/common/views/components/acct.vue
+++ b/src/client/app/common/views/components/acct.vue
@@ -1,19 +1,25 @@
<template>
<span class="mk-acct">
<span class="name">@{{ user.username }}</span>
- <span class="host" v-if="user.host">@{{ user.host }}</span>
+ <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
+import { host } from '../../../config';
export default Vue.extend({
- props: ['user']
+ props: ['user', 'detail'],
+ data() {
+ return {
+ host
+ };
+ }
});
</script>
<style lang="stylus" scoped>
.mk-acct
- > .host
+ > .host.fade
opacity 0.5
</style>
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index b274eaa0a0..bc0120c9ab 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -125,7 +125,7 @@ export default Vue.extend({
}
if (this.type == 'user') {
- const cacheKey = 'autocomplete:user:' + this.q;
+ const cacheKey = `autocomplete:user:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const users = JSON.parse(cache);
@@ -148,7 +148,7 @@ export default Vue.extend({
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
} else {
- const cacheKey = 'autocomplete:hashtag:' + this.q;
+ const cacheKey = `autocomplete:hashtag:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
@@ -259,15 +259,13 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-autocomplete
position fixed
z-index 65535
max-width 100%
margin-top calc(1em + 8px)
overflow hidden
- background isDark ? #313543 : #fff
+ background var(--faceHeader)
border solid 1px rgba(#000, 0.1)
border-radius 4px
transition top 0.1s ease, left 0.1s ease
@@ -299,16 +297,16 @@ root(isDark)
text-overflow ellipsis
&:hover
- background isDark ? rgba(#fff, 0.1) : rgba(#000, 0.1)
+ background var(--autocompleteItemHoverBg)
&[data-selected='true']
- background $theme-color
+ background var(--primary)
&, *
color #fff !important
&:active
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
&, *
color #fff !important
@@ -325,15 +323,15 @@ root(isDark)
.name
margin 0 8px 0 0
- color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
+ color var(--autocompleteItemText)
.username
- color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
+ color var(--autocompleteItemTextSub)
> .hashtags > li
.name
- color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
+ color var(--autocompleteItemText)
> .emojis > li
@@ -343,15 +341,9 @@ root(isDark)
width 24px
.name
- color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
+ color var(--autocompleteItemText)
.alias
margin 0 0 0 8px
- color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
-
-.mk-autocomplete[data-darkmode]
- root(true)
-
-.mk-autocomplete:not([data-darkmode])
- root(false)
+ color var(--autocompleteItemTextSub)
</style>
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index c5ac74e537..ac018abcfc 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -1,15 +1,15 @@
<template>
- <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
- <span class="inner" :style="style"></span>
+ <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
+ <span class="inner" :style="icon"></span>
</span>
- <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
- <span class="inner" :style="style"></span>
+ <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
+ <span class="inner" :style="icon"></span>
</span>
- <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
- <span class="inner" :style="style"></span>
+ <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
+ <span class="inner" :style="icon"></span>
</router-link>
- <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
- <span class="inner" :style="style"></span>
+ <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
+ <span class="inner" :style="icon"></span>
</router-link>
</template>
@@ -43,6 +43,11 @@ export default Vue.extend({
},
style(): any {
return {
+ borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
+ };
+ },
+ icon(): any {
+ return {
backgroundColor: this.lightmode
? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
: this.user.avatarColor && this.user.avatarColor.length == 3
@@ -53,6 +58,11 @@ export default Vue.extend({
};
}
},
+ mounted() {
+ if (this.user.avatarColor) {
+ this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`;
+ }
+ },
methods: {
onClick(e) {
this.$emit('click', e);
@@ -62,8 +72,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-
-root(isDark)
+.mk-avatar
display inline-block
vertical-align bottom
@@ -74,7 +83,7 @@ root(isDark)
&.cat::before,
&.cat::after
background #df548f
- border solid 4px isDark ? #e0eefd : #202224
+ border solid 4px currentColor
box-sizing border-box
content ''
display inline-block
@@ -100,9 +109,4 @@ root(isDark)
transition border-radius 1s ease
z-index 1
-.mk-avatar[data-darkmode]
- root(true)
-
-.mk-avatar:not([data-darkmode])
- root(false)
</style>
diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
index 6c23cc7969..f64cae6b4b 100644
--- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
@@ -57,7 +57,7 @@ export default Vue.extend({
}
// Check internet connection
- fetch('https://google.com?rand=' + Math.random(), {
+ fetch(`https://google.com?rand=${Math.random()}`, {
mode: 'no-cors'
}).then(() => {
this.internet = true;
diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue
index 0f686926b0..36cae05665 100644
--- a/src/client/app/common/views/components/connect-failed.vue
+++ b/src/client/app/common/views/components/connect-failed.vue
@@ -39,7 +39,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-connect-failed
width 100%
@@ -70,17 +70,17 @@ export default Vue.extend({
display block
margin 1em auto 0 auto
padding 8px 10px
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
&:focus
- outline solid 3px rgba($theme-color, 0.3)
+ outline solid 3px var(--primaryAlpha03)
&:hover
- background lighten($theme-color, 10%)
+ background var(--primaryLighten10)
&:active
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
> .thanks
display block
diff --git a/src/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue
new file mode 100644
index 0000000000..79917f82ab
--- /dev/null
+++ b/src/client/app/common/views/components/cw-button.vue
@@ -0,0 +1,38 @@
+<template>
+<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? '%i18n:@hide%' : '%i18n:@show%' }}</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ value: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ methods: {
+ toggle() {
+ this.$emit('input', !this.value);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.nrvgflfuaxwgkxoynpnumyookecqrrvh
+ display inline-block
+ padding 4px 8px
+ font-size 0.7em
+ color var(--cwButtonFg)
+ background var(--cwButtonBg)
+ border-radius 2px
+ cursor pointer
+ user-select none
+
+ &:hover
+ background var(--cwButtonHoverBg)
+
+</style>
diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue
index de627181ef..b303b48b79 100644
--- a/src/client/app/common/views/components/forkit.vue
+++ b/src/client/app/common/views/components/forkit.vue
@@ -9,7 +9,7 @@
</template>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.a
display block
@@ -18,8 +18,8 @@
display block
//fill #151513
//color #fff
- fill $theme-color
- color $theme-color-foreground
+ fill var(--primary)
+ color var(--primaryForeground)
.octo-arm
transform-origin 130px 106px
diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue
index b432a2308d..751abe2ecd 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.game.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue
@@ -50,15 +50,15 @@
</div>
<div class="player" v-if="game.isEnded">
- <el-button-group>
- <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button>
- <el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button>
- </el-button-group>
+ <div>
+ <button @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</button>
+ <button @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</button>
+ </div>
<span>{{ logPos }} / {{ logs.length }}</span>
- <el-button-group>
- <el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button>
- <el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button>
- </el-button-group>
+ <div>
+ <button @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</button>
+ <button @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</button>
+ </div>
</div>
<div class="info">
@@ -159,11 +159,9 @@ export default Vue.extend({
canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard
});
- this.logs.forEach((log, i) => {
- if (i < v) {
- this.o.put(log.color, log.pos);
- }
- });
+ for (const log of this.logs.slice(0, v)) {
+ this.o.put(log.color, log.pos);
+ }
this.$forceUpdate();
}
},
@@ -306,9 +304,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.xqnhankfuuilcwvhgsopeqncafzsquya
text-align center
> .go-index
@@ -321,7 +317,7 @@ root(isDark)
> header
padding 8px
- border-bottom dashed 1px isDark ? #4c5761 : #c4cdd4
+ border-bottom dashed 1px var(--reversiGameHeaderLine)
a
color inherit
@@ -388,30 +384,30 @@ root(isDark)
user-select none
&.empty
- border solid 2px isDark ? #51595f : #eee
+ border solid 2px var(--reversiGameEmptyCell)
&.empty.can
- background isDark ? #51595f : #eee
+ background var(--reversiGameEmptyCell)
&.empty.myTurn
- border-color isDark ? #6a767f : #ddd
+ border-color var(--reversiGameEmptyCellMyTurn)
&.can
- background isDark ? #51595f : #eee
+ background var(--reversiGameEmptyCellCanPut)
cursor pointer
&:hover
- border-color darken($theme-color, 10%)
- background $theme-color
+ border-color var(--primaryDarken10)
+ background var(--primary)
&:active
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
&.prev
- box-shadow 0 0 0 4px rgba($theme-color, 0.7)
+ box-shadow 0 0 0 4px var(--primaryAlpha07)
&.isEnded
- border-color isDark ? #6a767f : #ddd
+ border-color var(--reversiGameEmptyCellMyTurn)
&.none
border-color transparent !important
@@ -460,10 +456,4 @@ root(isDark)
margin 0 8px
min-width 70px
-.xqnhankfuuilcwvhgsopeqncafzsquya[data-darkmode]
- root(true)
-
-.xqnhankfuuilcwvhgsopeqncafzsquya:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
index 1539c88de0..0a18e0b19a 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
@@ -9,7 +9,6 @@
import Vue from 'vue';
import XGame from './reversi.game.vue';
import XRoom from './reversi.room.vue';
-import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game';
export default Vue.extend({
components: {
@@ -34,12 +33,13 @@ export default Vue.extend({
},
created() {
this.g = this.game;
- this.connection = new ReversiGameStream((this as any).os, this.$store.state.i, this.game);
+ this.connection = (this as any).os.stream.connectToChannel('gamesReversiGame', {
+ gameId: this.game.id
+ });
this.connection.on('started', this.onStarted);
},
beforeDestroy() {
- this.connection.off('started', this.onStarted);
- this.connection.close();
+ this.connection.dispose();
},
methods: {
onStarted(game) {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue
index fa88aeaaf4..a040162802 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.index.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue
@@ -3,7 +3,6 @@
<h1>%i18n:@title%</h1>
<p>%i18n:@sub-title%</p>
<div class="play">
- <!--<el-button round>フリーマッチ(準備中)</el-button>-->
<form-button primary round @click="match">%i18n:@invite%</form-button>
<details>
<summary>%i18n:@rule%</summary>
@@ -60,15 +59,13 @@ export default Vue.extend({
myGames: [],
matching: null,
invitations: [],
- connection: null,
- connectionId: null
+ connection: null
};
},
mounted() {
if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.streams.reversiStream.getConnection();
- this.connectionId = (this as any).os.streams.reversiStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
this.connection.on('invited', this.onInvited);
@@ -91,8 +88,7 @@ export default Vue.extend({
beforeDestroy() {
if (this.connection) {
- this.connection.off('invited', this.onInvited);
- (this as any).os.streams.reversiStream.dispose(this.connectionId);
+ this.connection.dispose();
}
},
@@ -139,9 +135,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx
> h1
margin 0
padding 24px
@@ -149,7 +143,7 @@ root(isDark)
text-align center
font-weight normal
color #fff
- background linear-gradient(to bottom, isDark ? #45730e : #8bca3e, isDark ? #464300 : #d6cf31)
+ background linear-gradient(to bottom, var(--reversiBannerGradientStart), var(--reversiBannerGradientEnd))
& + p
margin 0
@@ -157,7 +151,7 @@ root(isDark)
margin-bottom 12px
text-align center
font-size 14px
- border-bottom solid 1px isDark ? #535f65 : #d3d9dc
+ border-bottom solid 1px var(--faceDivider)
> .play
margin 0 auto
@@ -172,14 +166,14 @@ root(isDark)
padding 16px
font-size 14px
text-align left
- background isDark ? #282c37 : #f5f5f5
+ background var(--reversiDescBg)
border-radius 8px
> section
margin 0 auto
padding 0 16px 16px 16px
max-width 500px
- border-top solid 1px isDark ? #535f65 : #d3d9dc
+ border-top solid 1px var(--faceDivider)
> h2
margin 0
@@ -190,9 +184,9 @@ root(isDark)
.invitation
margin 8px 0
padding 8px
- color isDark ? #fff : #677f84
- background isDark ? #282c37 : #fff
- box-shadow 0 2px 16px rgba(#000, isDark ? 0.7 : 0.15)
+ color var(--text)
+ background var(--face)
+ box-shadow 0 2px 16px var(--reversiListItemShadow)
border-radius 6px
cursor pointer
@@ -201,13 +195,13 @@ root(isDark)
user-select none
&:focus
- border-color $theme-color
+ border-color var(--primary)
&:hover
- background isDark ? #313543 : #f5f5f5
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
- background isDark ? #1e222b : #eee
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
> .avatar
width 32px
@@ -222,9 +216,9 @@ root(isDark)
display block
margin 8px 0
padding 8px
- color isDark ? #fff : #677f84
- background isDark ? #282c37 : #fff
- box-shadow 0 2px 16px rgba(#000, isDark ? 0.7 : 0.15)
+ color var(--text)
+ background var(--face)
+ box-shadow 0 2px 16px var(--reversiListItemShadow)
border-radius 6px
cursor pointer
@@ -233,10 +227,10 @@ root(isDark)
user-select none
&:hover
- background isDark ? #313543 : #f5f5f5
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
- background isDark ? #1e222b : #eee
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
> .avatar
width 32px
@@ -247,10 +241,4 @@ root(isDark)
margin 0 8px
line-height 32px
-.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx[data-darkmode]
- root(true)
-
-.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue
index aed8718dd0..9f0d9c23fb 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.room.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue
@@ -47,9 +47,9 @@
</header>
<div>
- <mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="%i18n:@is-llotheo%"/>
- <mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="%i18n:@looped-map%"/>
- <mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="%i18n:@can-put-everywhere%"/>
+ <ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">%i18n:@is-llotheo%</ui-switch>
+ <ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">%i18n:@looped-map%</ui-switch>
+ <ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">%i18n:@can-put-everywhere%</ui-switch>
</div>
</div>
@@ -59,13 +59,8 @@
</header>
<div>
- <el-alert v-for="message in messages"
- :title="message.text"
- :type="message.type"
- :key="message.id"/>
-
<template v-for="item in form">
- <mk-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</mk-switch>
+ <ui-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</ui-switch>
<div class="card" v-if="item.type == 'radio'" :key="item.id">
<header>
@@ -93,7 +88,7 @@
</header>
<div>
- <el-input v-model="item.value" @change="onChangeForm(item)"/>
+ <input v-model="item.value" @change="onChangeForm(item)"/>
</div>
</div>
</template>
@@ -257,11 +252,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.urbixznjwwuukfsckrwzwsqzsxornqij
text-align center
- background isDark ? #191b22 : #f9f9f9
+ background var(--bg)
> header
padding 8px
@@ -278,10 +271,10 @@ root(isDark)
> select
width 100%
padding 12px 14px
- background isDark ? #282C37 : #fff
- border 1px solid isDark ? #6a707d : #dcdfe6
+ background var(--face)
+ border 1px solid var(--reversiMapSelectBorder)
border-radius 4px
- color isDark ? #fff : #606266
+ color var(--text)
cursor pointer
transition border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)
-webkit-appearance none
@@ -289,17 +282,18 @@ root(isDark)
appearance none
&:hover
- border-color isDark ? #a7aebd : #c0c4cc
+ border-color var(--reversiMapSelectHoverBorder)
&:focus
&:active
- border-color $theme-color
+ border-color var(--primary)
> div
> .random
padding 32px 0
font-size 64px
- color isDark ? #4e5961 : #d8d8d8
+ color var(--text)
+ opacity 0.7
> .board
display grid
@@ -307,11 +301,11 @@ root(isDark)
width 300px
height 300px
margin 0 auto
- color isDark ? #fff : #444
+ color var(--text)
> div
background transparent
- border solid 2px isDark ? #6a767f : #ddd
+ border solid 2px var(--faceDivider)
border-radius 6px
overflow hidden
cursor pointer
@@ -336,32 +330,26 @@ root(isDark)
.card
max-width 400px
border-radius 4px
- background isDark ? #282C37 : #fff
- color isDark ? #fff : #303133
- box-shadow 0 2px 12px 0 rgba(#000, isDark ? 0.7 : 0.1)
+ background var(--face)
+ color var(--text)
+ box-shadow 0 2px 12px 0 var(--reversiRoomFormShadow)
> header
padding 18px 20px
- border-bottom 1px solid isDark ? #1c2023 : #ebeef5
+ border-bottom 1px solid var(--faceDivider)
> div
padding 20px
- color isDark ? #fff : #606266
+ color var(--text)
> footer
position sticky
bottom 0
padding 16px
- background rgba(isDark ? #191b22 : #fff, 0.9)
- border-top solid 1px isDark ? #606266 : #c4cdd4
+ background var(--reversiRoomFooterBg)
+ border-top solid 1px var(--faceDivider)
> .status
margin 0 0 16px 0
-.urbixznjwwuukfsckrwzwsqzsxornqij[data-darkmode]
- root(true)
-
-.urbixznjwwuukfsckrwzwsqzsxornqij:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue
index 223ec4597a..f2156bc41b 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.vue
@@ -47,7 +47,6 @@ export default Vue.extend({
game: null,
matching: null,
connection: null,
- connectionId: null,
pingClock: null
};
},
@@ -66,8 +65,7 @@ export default Vue.extend({
this.fetch();
if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.streams.reversiStream.getConnection();
- this.connectionId = (this as any).os.streams.reversiStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
this.connection.on('matched', this.onMatched);
@@ -84,9 +82,7 @@ export default Vue.extend({
beforeDestroy() {
if (this.connection) {
- this.connection.off('matched', this.onMatched);
- (this as any).os.streams.reversiStream.dispose(this.connectionId);
-
+ this.connection.dispose();
clearInterval(this.pingClock);
}
},
@@ -156,11 +152,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- color isDark ? #fff : #677f84
- background isDark ? #191b22 : #fff
+.vchtoekanapleubgzioubdtmlkribzfd
+ color var(--text)
+ background var(--bg)
> .matching
> h1
@@ -177,10 +171,4 @@ root(isDark)
text-align center
border-top dashed 1px #c4cdd4
-.vchtoekanapleubgzioubdtmlkribzfd[data-darkmode]
- root(true)
-
-.vchtoekanapleubgzioubdtmlkribzfd:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue
index 8272961ef2..ac71a5e56d 100644
--- a/src/client/app/common/views/components/google.vue
+++ b/src/client/app/common/views/components/google.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mk-google
display flex
margin 8px 0
@@ -37,31 +37,25 @@ root(isDark)
height 40px
font-family sans-serif
font-size 16px
- color isDark ? #dee4e8 : #55595c
- background isDark ? #191b22 : #fff
- border solid 1px isDark ? #495156 : #dadada
+ color var(--googleSearchFg)
+ background var(--googleSearchBg)
+ border solid 1px var(--googleSearchBorder)
border-radius 4px 0 0 4px
&:hover
- border-color isDark ? #777c86 : #b0b0b0
+ border-color var(--googleSearchHoverBorder)
> button
flex-shrink 0
padding 0 16px
- border solid 1px isDark ? #495156 : #dadada
+ border solid 1px var(--googleSearchBorder)
border-left none
border-radius 0 4px 4px 0
&:hover
- background-color isDark ? #2e3440 : #eee
+ background-color var(--googleSearchHoverButton)
&:active
box-shadow 0 2px 4px rgba(#000, 0.15) inset
-.mk-google[data-darkmode]
- root(true)
-
-.mk-google:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 422a3da050..0dea38a7a1 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -1,5 +1,10 @@
import Vue from 'vue';
+import theme from './theme.vue';
+import instance from './instance.vue';
+import cwButton from './cw-button.vue';
+import tagCloud from './tag-cloud.vue';
+import trends from './trends.vue';
import analogClock from './analog-clock.vue';
import menu from './menu.vue';
import noteHeader from './note-header.vue';
@@ -26,7 +31,6 @@ import messagingRoom from './messaging-room.vue';
import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue';
import fileTypeIcon from './file-type-icon.vue';
-import Switch from './switch.vue';
import Reversi from './games/reversi/reversi.vue';
import welcomeTimeline from './welcome-timeline.vue';
import uiInput from './ui/input.vue';
@@ -40,6 +44,11 @@ import uiSelect from './ui/select.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
+Vue.component('mk-theme', theme);
+Vue.component('mk-instance', instance);
+Vue.component('mk-cw-button', cwButton);
+Vue.component('mk-tag-cloud', tagCloud);
+Vue.component('mk-trends', trends);
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
Vue.component('mk-note-header', noteHeader);
@@ -66,7 +75,6 @@ Vue.component('mk-messaging-room', messagingRoom);
Vue.component('mk-url-preview', urlPreview);
Vue.component('mk-twitter-setting', twitterSetting);
Vue.component('mk-file-type-icon', fileTypeIcon);
-Vue.component('mk-switch', Switch);
Vue.component('mk-reversi', Reversi);
Vue.component('mk-welcome-timeline', welcomeTimeline);
Vue.component('ui-input', uiInput);
diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue
new file mode 100644
index 0000000000..c3935cce0e
--- /dev/null
+++ b/src/client/app/common/views/components/instance.vue
@@ -0,0 +1,51 @@
+<template>
+<div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta">
+ <div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div>
+
+ <h1>{{ meta.name }}</h1>
+ <p v-html="meta.description || '%i18n:common.about%'"></p>
+ <router-link to="/">%i18n:@start%</router-link>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ data() {
+ return {
+ meta: null
+ }
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.meta = meta;
+ });
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.nhasjydimbopojusarffqjyktglcuxjy
+ color var(--text)
+ background var(--face)
+ text-align center
+
+ > .banner
+ height 100px
+ background-position center
+ background-size cover
+
+ > h1
+ margin 16px
+ font-size 16px
+
+ > p
+ margin 16px
+ font-size 14px
+
+ > a
+ display block
+ padding-bottom 16px
+
+</style>
diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/app/common/views/components/media-banner.vue
new file mode 100644
index 0000000000..0f5981d3c4
--- /dev/null
+++ b/src/client/app/common/views/components/media-banner.vue
@@ -0,0 +1,85 @@
+<template>
+<div class="mk-media-banner">
+ <div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
+ <span class="icon">%fa:exclamation-triangle%</span>
+ <b>%i18n:@sensitive%</b>
+ <span>%i18n:@click-to-show%</span>
+ </div>
+ <div class="audio" v-else-if="media.type.startsWith('audio')">
+ <audio class="audio"
+ :src="media.url"
+ :title="media.name"
+ controls
+ ref="audio"
+ preload="metadata" />
+ </div>
+ <a class="download" v-else
+ :href="media.url"
+ :title="media.name"
+ :download="media.name"
+ >
+ <span class="icon">%fa:download%</span>
+ <b>{{ media.name }}</b>
+ </a>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ media: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ hide: true
+ };
+ }
+})
+</script>
+
+<style lang="stylus" scoped>
+.mk-media-banner
+ width 100%
+ border-radius 4px
+ margin-top 4px
+ overflow hidden
+
+ > .download,
+ > .sensitive
+ display flex
+ align-items center
+ font-size 12px
+ padding 8px 12px
+ white-space nowrap
+
+ > *
+ display block
+
+ > b
+ overflow hidden
+ text-overflow ellipsis
+
+ > *:not(:last-child)
+ margin-right .2em
+
+ > .icon
+ font-size 1.6em
+
+ > .download
+ background var(--noteAttachedFile)
+
+ > .sensitive
+ background #111
+ color #fff
+
+ > .audio
+ .audio
+ display block
+ width 100%
+
+</style>
diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
index cdfc2c8d3c..d83d6f85cd 100644
--- a/src/client/app/common/views/components/media-list.vue
+++ b/src/client/app/common/views/components/media-list.vue
@@ -1,18 +1,27 @@
<template>
<div class="mk-media-list">
- <div :data-count="mediaList.length" ref="grid">
- <template v-for="media in mediaList">
- <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
- <mk-media-image :image="media" :key="media.id" v-else :raw="raw"/>
- </template>
+ <template v-for="media in mediaList.filter(media => !previewable(media))">
+ <x-banner :media="media" :key="media.id"/>
+ </template>
+ <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
+ <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
+ <template v-for="media in mediaList">
+ <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
+ <mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
+ </template>
+ </div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+import XBanner from './media-banner.vue';
export default Vue.extend({
+ components: {
+ XBanner
+ },
props: {
mediaList: {
required: true
@@ -22,70 +31,80 @@ export default Vue.extend({
}
},
mounted() {
- // for Safari bug
- this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
+ //#region for Safari bug
+ if (this.$refs.grid) {
+ this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
+ }
+ //#endregion
+ },
+ methods: {
+ previewable(file) {
+ return file.type.startsWith('video') || file.type.startsWith('image');
+ }
}
});
</script>
<style lang="stylus" scoped>
.mk-media-list
- width 100%
+ > .gird-container
+ width 100%
+ margin-top 4px
- &:before
- content ''
- display block
- padding-top 56.25% // 16:9
+ &:before
+ content ''
+ display block
+ padding-top 56.25% // 16:9
- > div
- position absolute
- top 0
- right 0
- bottom 0
- left 0
- display grid
- grid-gap 4px
+ > div
+ position absolute
+ top 0
+ right 0
+ bottom 0
+ left 0
+ display grid
+ grid-gap 4px
- > *
- overflow hidden
- border-radius 4px
+ > *
+ overflow hidden
+ border-radius 4px
- &[data-count="1"]
- grid-template-rows 1fr
+ &[data-count="1"]
+ grid-template-rows 1fr
- &[data-count="2"]
- grid-template-columns 1fr 1fr
- grid-template-rows 1fr
+ &[data-count="2"]
+ grid-template-columns 1fr 1fr
+ grid-template-rows 1fr
- &[data-count="3"]
- grid-template-columns 1fr 0.5fr
- grid-template-rows 1fr 1fr
+ &[data-count="3"]
+ grid-template-columns 1fr 0.5fr
+ grid-template-rows 1fr 1fr
- > *:nth-child(1)
- grid-row 1 / 3
+ > *:nth-child(1)
+ grid-row 1 / 3
- > *:nth-child(3)
- grid-column 2 / 3
- grid-row 2 / 3
+ > *:nth-child(3)
+ grid-column 2 / 3
+ grid-row 2 / 3
- &[data-count="4"]
- grid-template-columns 1fr 1fr
- grid-template-rows 1fr 1fr
+ &[data-count="4"]
+ grid-template-columns 1fr 1fr
+ grid-template-rows 1fr 1fr
- > *:nth-child(1)
- grid-column 1 / 2
- grid-row 1 / 2
+ > *:nth-child(1)
+ grid-column 1 / 2
+ grid-row 1 / 2
- > *:nth-child(2)
- grid-column 2 / 3
- grid-row 1 / 2
+ > *:nth-child(2)
+ grid-column 2 / 3
+ grid-row 1 / 2
- > *:nth-child(3)
- grid-column 1 / 2
- grid-row 2 / 3
+ > *:nth-child(3)
+ grid-column 1 / 2
+ grid-row 2 / 3
- > *:nth-child(4)
- grid-column 2 / 3
- grid-row 2 / 3
+ > *:nth-child(4)
+ grid-column 2 / 3
+ grid-row 2 / 3
</style>
diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue
index 9b16732b9a..be2c03f54c 100644
--- a/src/client/app/common/views/components/menu.vue
+++ b/src/client/app/common/views/components/menu.vue
@@ -1,10 +1,10 @@
<template>
-<div class="mk-menu">
+<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover">
- <template v-for="item in items">
+ <template v-for="item, i in items">
<div v-if="item === null"></div>
- <button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text"></button>
+ <button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text" :tabindex="i"></button>
</template>
</div>
</div>
@@ -108,7 +108,7 @@ export default Vue.extend({
easing: 'easeInBack',
complete: () => {
this.$emit('closed');
- this.$destroy();
+ this.destroyDom();
}
});
}
@@ -117,11 +117,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+.onchrpzrvnoruiaenfcqvccjfuupzzwv
+ $bg-color = var(--popupBg)
+ $border-color = rgba(27, 31, 35, 0.15)
-$border-color = rgba(27, 31, 35, 0.15)
-
-.mk-menu
position initial
> .backdrop
@@ -131,14 +130,14 @@ $border-color = rgba(27, 31, 35, 0.15)
z-index 10000
width 100%
height 100%
- background rgba(#000, 0.1)
+ background var(--modalBackdrop)
opacity 0
> .popover
position absolute
z-index 10001
padding 8px 0
- background #fff
+ background $bg-color
border 1px solid $border-color
border-radius 4px
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
@@ -172,25 +171,26 @@ $border-color = rgba(27, 31, 35, 0.15)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
- border-bottom solid $balloon-size #fff
+ border-bottom solid $balloon-size $bg-color
> button
display block
padding 8px 16px
width 100%
+ color var(--popupFg)
&:hover
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
text-decoration none
&:active
- color $theme-color-foreground
- background darken($theme-color, 10%)
+ color var(--primaryForeground)
+ background var(--primaryDarken10)
> div
margin 8px 0
height 1px
- background #eee
+ background var(--faceDivider)
</style>
diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue
index f183749fad..c93fd7f78d 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -195,9 +195,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-messaging-form
> textarea
cursor auto
display block
@@ -209,10 +207,10 @@ root(isDark)
padding 8px
resize none
font-size 1em
- color isDark ? #fff : #000
+ color var(--inputText)
outline none
border none
- border-top solid 1px isDark ? #4b5056 : #eee
+ border-top solid 1px var(--faceDivider)
border-radius 0
box-shadow none
background transparent
@@ -234,10 +232,10 @@ root(isDark)
transition color 0.1s ease
&:hover
- color $theme-color
+ color var(--primary)
&:active
- color darken($theme-color, 10%)
+ color var(--primaryDarken10)
transition color 0s ease
.files
@@ -293,19 +291,13 @@ root(isDark)
transition color 0.1s ease
&:hover
- color $theme-color
+ color var(--primary)
&:active
- color darken($theme-color, 10%)
+ color var(--primaryDarken10)
transition color 0s ease
input[type=file]
display none
-.mk-messaging-form[data-darkmode]
- root(true)
-
-.mk-messaging-form:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 648d0eee18..77bf55c52c 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -59,10 +59,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- $me-balloon-color = $theme-color
+.message
+ $me-balloon-color = var(--primary)
padding 10px 12px 10px 12px
background-color transparent
@@ -179,7 +177,7 @@ root(isDark)
display block
margin 2px 0 0 0
font-size 10px
- color isDark ? rgba(#fff, 0.4) : rgba(#000, 0.4)
+ color var(--messagingRoomMessageInfo)
> [data-fa]
margin-left 4px
@@ -192,7 +190,7 @@ root(isDark)
padding-left 66px
> .balloon
- $color = isDark ? #2d3338 : #eee
+ $color = var(--messagingRoomMessageBg)
float left
background $color
@@ -208,8 +206,7 @@ root(isDark)
> .content
> .text
- if isDark
- color #fff
+ color var(--messagingRoomMessageFg)
> footer
text-align left
@@ -250,18 +247,9 @@ root(isDark)
> .read
user-select none
- margin 0 4px 0 0
- color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5)
- font-size 11px
&[data-is-deleted]
> .balloon
opacity 0.5
-.message[data-darkmode]
- root(true)
-
-.message:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index 30143b4f1d..488dff528f 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -3,7 +3,7 @@
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
- <div class="stream">
+ <div class="body">
<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p>
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@no-history%</p>
@@ -30,7 +30,6 @@
<script lang="ts">
import Vue from 'vue';
-import { MessagingStream } from '../../scripts/streaming/messaging';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import { url } from '../../../config';
@@ -72,11 +71,17 @@ export default Vue.extend({
},
mounted() {
- this.connection = new MessagingStream((this as any).os, this.$store.state.i, this.user.id);
+ this.connection =((this as any).os.stream.connectToChannel('messaging', { otherparty: this.user.id });
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
+ if (this.isNaked) {
+ window.addEventListener('scroll', this.onScroll, { passive: true });
+ } else {
+ this.$el.addEventListener('scroll', this.onScroll, { passive: true });
+ }
+
document.addEventListener('visibilitychange', this.onVisibilitychange);
this.fetchMessages().then(() => {
@@ -86,9 +91,13 @@ export default Vue.extend({
},
beforeDestroy() {
- this.connection.off('message', this.onMessage);
- this.connection.off('read', this.onRead);
- this.connection.close();
+ this.connection.dispose();
+
+ if (this.isNaked) {
+ window.removeEventListener('scroll', this.onScroll);
+ } else {
+ this.$el.removeEventListener('scroll', this.onScroll);
+ }
document.removeEventListener('visibilitychange', this.onVisibilitychange);
},
@@ -226,6 +235,14 @@ export default Vue.extend({
}, 4000);
},
+ onScroll() {
+ const el = this.isNaked ? window.document.documentElement : this.$el;
+ const current = el.scrollTop + el.clientHeight;
+ if (current > el.scrollHeight - 1) {
+ this.showIndicator = false;
+ }
+ },
+
onVisibilitychange() {
if (document.hidden) return;
this.messages.forEach(message => {
@@ -242,39 +259,28 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-messaging-room
display flex
flex 1
flex-direction column
height 100%
- background isDark ? #191b22 : #fff
+ background var(--messagingRoomBg)
- > .stream
+ > .body
width 100%
max-width 600px
margin 0 auto
flex 1
- > .init
- width 100%
- margin 0
- padding 16px 8px 8px 8px
- text-align center
- font-size 0.8em
- color rgba(isDark ? #fff : #000, 0.4)
-
- [data-fa]
- margin-right 4px
-
+ > .init,
> .empty
width 100%
margin 0
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
- color rgba(isDark ? #fff : #000, 0.4)
+ color var(--messagingRoomInfo)
+ opacity 0.5
[data-fa]
margin-right 4px
@@ -285,7 +291,8 @@ root(isDark)
padding 16px
text-align center
font-size 0.8em
- color rgba(isDark ? #fff : #000, 0.4)
+ color var(--messagingRoomInfo)
+ opacity 0.5
[data-fa]
margin-right 4px
@@ -329,7 +336,7 @@ root(isDark)
left 0
right 0
margin 0 auto
- background rgba(isDark ? #fff : #000, 0.1)
+ background var(--messagingRoomDateDividerLine)
> span
display inline-block
@@ -337,8 +344,8 @@ root(isDark)
padding 0 16px
//font-weight bold
line-height 32px
- color rgba(isDark ? #fff : #000, 0.3)
- background isDark ? #191b22 : #fff
+ color var(--messagingRoomDateDividerText)
+ background var(--messagingRoomBg)
> footer
position -webkit-sticky
@@ -349,7 +356,7 @@ root(isDark)
max-width 600px
margin 0 auto
padding 0
- background rgba(isDark ? #282c37 : #fff, 0.95)
+ //background rgba(var(--face), 0.95)
background-clip content-box
> .new-message
@@ -366,15 +373,15 @@ root(isDark)
cursor pointer
line-height 32px
font-size 12px
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
border-radius 16px
&:hover
- background lighten($theme-color, 10%)
+ background var(--primaryLighten10)
&:active
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
> [data-fa]
position absolute
@@ -390,10 +397,4 @@ root(isDark)
transition opacity 0.5s
opacity 0
-.mk-messaging-room[data-darkmode]
- root(true)
-
-.mk-messaging-room:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 6abfc92dca..f5b5e232f6 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -71,13 +71,11 @@ export default Vue.extend({
messages: [],
q: null,
result: [],
- connection: null,
- connectionId: null
+ connection: null
};
},
mounted() {
- this.connection = (this as any).os.streams.messagingIndexStream.getConnection();
- this.connectionId = (this as any).os.streams.messagingIndexStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('messagingIndex');
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
@@ -88,9 +86,7 @@ export default Vue.extend({
});
},
beforeDestroy() {
- this.connection.off('message', this.onMessage);
- this.connection.off('read', this.onRead);
- (this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
getAcct,
@@ -167,9 +163,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-messaging
&[data-compact]
font-size 0.8em
@@ -204,12 +198,10 @@ root(isDark)
left 0
z-index 1
width 100%
- background #fff
box-shadow 0 0px 2px rgba(#000, 0.2)
> .form
- padding 8px
- background isDark ? #282c37 : #f7f7f7
+ background rgba(0, 0, 0, 0.02)
> label
display block
@@ -229,32 +221,22 @@ root(isDark)
bottom 0
left 0
width 1em
- line-height 56px
+ line-height 48px
margin auto
color #555
> input
margin 0
- padding 0 0 0 32px
+ padding 0 0 0 42px
width 100%
font-size 1em
- line-height 38px
- color #000
+ line-height 48px
+ color var(--faceText)
outline none
- background isDark ? #191b22 : #fff
- border solid 1px isDark ? #495156 : #eee
+ background transparent
+ border none
border-radius 5px
box-shadow none
- transition color 0.5s ease, border 0.5s ease
-
- &:hover
- border solid 1px isDark ? #b0b0b0 : #ddd
- transition border 0.2s ease
-
- &:focus
- color darken($theme-color, 20%)
- border solid 1px $theme-color
- transition color 0, border 0
> .result
display block
@@ -287,7 +269,7 @@ root(isDark)
&:hover
&:focus
color #fff
- background $theme-color
+ background var(--primary)
.name
color #fff
@@ -297,7 +279,7 @@ root(isDark)
&:active
color #fff
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
.name
color #fff
@@ -329,21 +311,21 @@ root(isDark)
> a
display block
text-decoration none
- background isDark ? #282c37 : #fff
- border-bottom solid 1px isDark ? #1c2023 : #eee
+ background var(--face)
+ border-bottom solid 1px var(--faceDivider)
*
pointer-events none
user-select none
&:hover
- background isDark ? #1e2129 : #fafafa
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
- > .avatar
+ .avatar
filter saturate(200%)
&:active
- background isDark ? #14161b : #eee
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
&[data-is-read]
&[data-is-me]
@@ -383,17 +365,17 @@ root(isDark)
overflow hidden
text-overflow ellipsis
font-size 1em
- color isDark ? #fff : rgba(#000, 0.9)
+ color var(--noteHeaderName)
font-weight bold
transition all 0.1s ease
> .username
margin 0 8px
- color isDark ? #606984 : rgba(#000, 0.5)
+ color var(--noteHeaderAcct)
> .mk-time
margin 0 0 0 auto
- color isDark ? #606984 : rgba(#000, 0.5)
+ color var(--noteHeaderInfo)
font-size 80%
> .avatar
@@ -413,10 +395,10 @@ root(isDark)
overflow hidden
overflow-wrap break-word
font-size 1.1em
- color isDark ? #fff : rgba(#000, 0.8)
+ color var(--faceText)
.me
- color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.4)
+ opacity 0.7
> .image
display block
@@ -461,10 +443,4 @@ root(isDark)
> .avatar
margin 0 12px 0 0
-.mk-messaging[data-darkmode]
- root(true)
-
-.mk-messaging:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts
index e97da4302c..224bd6f5de 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { VNode } from 'vue';
import * as emojilib from 'emojilib';
import { length } from 'stringz';
import parse from '../../../../../mfm/parse';
@@ -6,10 +6,7 @@ import getAcct from '../../../../../misc/acct/render';
import { url } from '../../../config';
import MkUrl from './url.vue';
import MkGoogle from './google.vue';
-
-const flatten = list => list.reduce(
- (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
-);
+import { concat } from '../../../../../prelude/array';
export default Vue.component('misskey-flavored-markdown', {
props: {
@@ -32,20 +29,20 @@ export default Vue.component('misskey-flavored-markdown', {
},
render(createElement) {
- let ast;
+ let ast: any[];
if (this.ast == null) {
// Parse text to ast
ast = parse(this.text);
} else {
- ast = this.ast;
+ ast = this.ast as any[];
}
let bigCount = 0;
let motionCount = 0;
// Parse ast to DOM
- const els = flatten(ast.map(token => {
+ const els = concat(ast.map((token): VNode[] => {
switch (token.type) {
case 'text': {
const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
@@ -56,12 +53,12 @@ export default Vue.component('misskey-flavored-markdown', {
x[x.length - 1].pop();
return x;
} else {
- return createElement('span', text.replace(/\n/g, ' '));
+ return [createElement('span', text.replace(/\n/g, ' '))];
}
}
case 'bold': {
- return createElement('b', token.bold);
+ return [createElement('b', token.bold)];
}
case 'big': {
@@ -95,23 +92,23 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'url': {
- return createElement(MkUrl, {
+ return [createElement(MkUrl, {
props: {
url: token.content,
target: '_blank'
}
- });
+ })];
}
case 'link': {
- return createElement('a', {
+ return [createElement('a', {
attrs: {
class: 'link',
href: token.url,
target: '_blank',
title: token.url
}
- }, token.title);
+ }, token.title)];
}
case 'mention': {
@@ -129,16 +126,16 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'hashtag': {
- return createElement('a', {
+ return [createElement('a', {
attrs: {
href: `${url}/tags/${encodeURIComponent(token.hashtag)}`,
target: '_blank'
}
- }, token.content);
+ }, token.content)];
}
case 'code': {
- return createElement('pre', {
+ return [createElement('pre', {
class: 'code'
}, [
createElement('code', {
@@ -146,15 +143,15 @@ export default Vue.component('misskey-flavored-markdown', {
innerHTML: token.html
}
})
- ]);
+ ])];
}
case 'inline-code': {
- return createElement('code', {
+ return [createElement('code', {
domProps: {
innerHTML: token.html
}
- });
+ })];
}
case 'quote': {
@@ -164,58 +161,51 @@ export default Vue.component('misskey-flavored-markdown', {
const x = text2.split('\n')
.map(t => [createElement('span', t), createElement('br')]);
x[x.length - 1].pop();
- return createElement('div', {
+ return [createElement('div', {
attrs: {
class: 'quote'
}
- }, x);
+ }, x)];
} else {
- return createElement('span', {
+ return [createElement('span', {
attrs: {
class: 'quote'
}
- }, text2.replace(/\n/g, ' '));
+ }, text2.replace(/\n/g, ' '))];
}
}
case 'title': {
- return createElement('div', {
+ return [createElement('div', {
attrs: {
class: 'title'
}
- }, token.title);
+ }, token.title)];
}
case 'emoji': {
const emoji = emojilib.lib[token.emoji];
- return createElement('span', emoji ? emoji.char : token.content);
+ return [createElement('span', emoji ? emoji.char : token.content)];
}
case 'search': {
- return createElement(MkGoogle, {
+ return [createElement(MkGoogle, {
props: {
q: token.query
}
- });
+ })];
}
default: {
console.log('unknown ast type:', token.type);
- }
- }
- }));
- const _els = [];
- els.forEach((el, i) => {
- if (el.tag == 'br') {
- if (!['div', 'pre'].includes(els[i - 1].tag)) {
- _els.push(el);
+ return [];
}
- } else {
- _els.push(el);
}
- });
+ }));
+ // el.tag === 'br' のとき i !== 0 が保証されるため、短絡評価により els[i - 1] は配列外参照しない
+ const _els = els.filter((el, i) => !(el.tag === 'br' && ['div', 'pre'].includes(els[i - 1].tag)));
return createElement('span', _els);
}
});
diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue
index 27e66358e4..d52c8e27a4 100644
--- a/src/client/app/common/views/components/nav.vue
+++ b/src/client/app/common/views/components/nav.vue
@@ -2,6 +2,8 @@
<span class="mk-nav">
<a :href="aboutUrl">%i18n:@about%</a>
<i>・</i>
+ <a href="/stats">%i18n:@stats%</a>
+ <i>・</i>
<a :href="repositoryUrl">%i18n:@repository%</a>
<i>・</i>
<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>
diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue
index d25bd430f2..8192d88412 100644
--- a/src/client/app/common/views/components/note-header.vue
+++ b/src/client/app/common/views/components/note-header.vue
@@ -42,9 +42,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.bvonvjxbwzaiskogyhbwgyxvcgserpmu
display flex
align-items baseline
white-space nowrap
@@ -61,7 +59,7 @@ root(isDark)
margin 0 .5em 0 0
padding 0
overflow hidden
- color isDark ? #fff : #627079
+ color var(--noteHeaderName)
font-size 1em
font-weight bold
text-decoration none
@@ -82,19 +80,19 @@ root(isDark)
margin 0 .5em 0 0
padding 1px 6px
font-size 80%
- color isDark ? #758188 : #aaa
- border solid 1px isDark ? #57616f : #ddd
+ color var(--noteHeaderBadgeFg)
+ background var(--noteHeaderBadgeBg)
border-radius 3px
&.is-admin
- border-color isDark ? #d42c41 : #f56a7b
- color isDark ? #d42c41 : #f56a7b
+ background var(--noteHeaderAdminBg)
+ color var(--noteHeaderAdminFg)
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
- color isDark ? #606984 : #ccc
+ color var(--noteHeaderAcct)
flex-shrink 2147483647
> .info
@@ -102,7 +100,7 @@ root(isDark)
font-size 0.9em
> *
- color isDark ? #606984 : #c0c0c0
+ color var(--noteHeaderInfo)
> .mobile
margin-right 8px
@@ -110,15 +108,9 @@ root(isDark)
> .app
margin-right 8px
padding-right 8px
- border-right solid 1px isDark ? #1c2023 : #eaeaea
+ border-right solid 1px var(--faceDivider)
> .visibility
margin-left 8px
-.bvonvjxbwzaiskogyhbwgyxvcgserpmu[data-darkmode]
- root(true)
-
-.bvonvjxbwzaiskogyhbwgyxvcgserpmu:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 27a49a6536..c8ed1225cc 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -6,29 +6,51 @@
<script lang="ts">
import Vue from 'vue';
+import { url } from '../../../config';
+import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
export default Vue.extend({
props: ['note', 'source', 'compact'],
computed: {
items() {
- const items = [];
- items.push({
+ const items = [{
+ icon: '%fa:info-circle%',
+ text: '%i18n:@detail%',
+ action: this.detail
+ }, {
+ icon: '%fa:link%',
+ text: '%i18n:@copy-link%',
+ action: this.copyLink
+ }, null, {
icon: '%fa:star%',
text: '%i18n:@favorite%',
action: this.favorite
- });
+ }];
+
if (this.note.userId == this.$store.state.i.id) {
- items.push({
- icon: '%fa:thumbtack%',
- text: '%i18n:@pin%',
- action: this.pin
- });
+ if ((this.$store.state.i.pinnedNoteIds || []).includes(this.note.id)) {
+ items.push({
+ icon: '%fa:thumbtack%',
+ text: '%i18n:@unpin%',
+ action: this.unpin
+ });
+ } else {
+ items.push({
+ icon: '%fa:thumbtack%',
+ text: '%i18n:@pin%',
+ action: this.pin
+ });
+ }
+ }
+
+ if (this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin) {
items.push({
icon: '%fa:trash-alt R%',
text: '%i18n:@delete%',
action: this.del
});
}
+
if (this.note.uri) {
items.push({
icon: '%fa:external-link-square-alt%',
@@ -38,15 +60,33 @@ export default Vue.extend({
}
});
}
+
return items;
}
},
+
methods: {
+ detail() {
+ this.$router.push(`/notes/${ this.note.id }`);
+ },
+
+ copyLink() {
+ copyToClipboard(`${url}/notes/${ this.note.id }`);
+ },
+
pin() {
(this as any).api('i/pin', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
+ });
+ },
+
+ unpin() {
+ (this as any).api('i/unpin', {
+ noteId: this.note.id
+ }).then(() => {
+ this.destroyDom();
});
},
@@ -55,7 +95,7 @@ export default Vue.extend({
(this as any).api('notes/delete', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
@@ -63,13 +103,13 @@ export default Vue.extend({
(this as any).api('notes/favorites/create', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
closed() {
this.$nextTick(() => {
- this.$destroy();
+ this.destroyDom();
});
}
}
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 115c934c8b..b5c57d48a5 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -20,6 +20,7 @@
<script lang="ts">
import Vue from 'vue';
+import { erase } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
@@ -53,7 +54,7 @@ export default Vue.extend({
get() {
return {
- choices: this.choices.filter(choice => choice != '')
+ choices: erase('', this.choices)
}
},
@@ -67,9 +68,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-poll-editor
padding 8px
> .caution
@@ -102,49 +101,43 @@ root(isDark)
padding 6px 8px
width 300px
font-size 14px
- color isDark ? #fff : #000
- background isDark ? #191b22 : #fff
- border solid 1px rgba($theme-color, 0.1)
+ color var(--inputText)
+ background var(--pollEditorInputBg)
+ border solid 1px var(--primaryAlpha01)
border-radius 4px
&:hover
- border-color rgba($theme-color, 0.2)
+ border-color var(--primaryAlpha02)
&:focus
- border-color rgba($theme-color, 0.5)
+ border-color var(--primaryAlpha05)
> button
padding 4px 8px
- color rgba($theme-color, 0.4)
+ color var(--primaryAlpha04)
&:hover
- color rgba($theme-color, 0.6)
+ color var(--primaryAlpha06)
&:active
- color darken($theme-color, 30%)
+ color var(--primaryDarken30)
> .add
margin 8px 0 0 0
vertical-align top
- color $theme-color
+ color var(--primary)
> .destroy
position absolute
top 0
right 0
padding 4px 8px
- color rgba($theme-color, 0.4)
+ color var(--primaryAlpha04)
&:hover
- color rgba($theme-color, 0.6)
+ color var(--primaryAlpha06)
&:active
- color darken($theme-color, 30%)
-
-.mk-poll-editor[data-darkmode]
- root(true)
-
-.mk-poll-editor:not([data-darkmode])
- root(false)
+ color var(--primaryDarken30)
</style>
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 660247edbc..0dc2622f9b 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -21,6 +21,7 @@
<script lang="ts">
import Vue from 'vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
props: ['note'],
data() {
@@ -33,7 +34,7 @@ export default Vue.extend({
return this.note.poll;
},
total(): number {
- return this.poll.choices.reduce((a, b) => a + b.votes, 0);
+ return sum(this.poll.choices.map(x => x.votes));
},
isVoted(): boolean {
return this.poll.choices.some(c => c.isVoted);
@@ -66,10 +67,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
-
+.mk-poll
> ul
display block
margin 0
@@ -81,8 +79,8 @@ root(isDark)
margin 4px 0
padding 4px 8px
width 100%
- color isDark ? #fff : #000
- border solid 1px isDark ? #5e636f : #eee
+ color var(--pollChoiceText)
+ border solid 1px var(--pollChoiceBorder)
border-radius 4px
overflow hidden
cursor pointer
@@ -98,7 +96,7 @@ root(isDark)
top 0
left 0
height 100%
- background $theme-color
+ background var(--primary)
transition width 1s ease
> span
@@ -109,7 +107,7 @@ root(isDark)
margin-left 4px
> p
- color isDark ? #a3aebf : #000
+ color var(--text)
a
color inherit
@@ -124,10 +122,4 @@ root(isDark)
&:active
background transparent
-.mk-poll[data-darkmode]
- root(true)
-
-.mk-poll:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue
index 46886b8ab2..c668efac6b 100644
--- a/src/client/app/common/views/components/reaction-icon.vue
+++ b/src/client/app/common/views/components/reaction-icon.vue
@@ -1,17 +1,17 @@
<template>
<span class="mk-reaction-icon">
- <img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
- <img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
- <img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
- <img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%">
- <img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%">
- <img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
- <img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
- <img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
- <img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%">
+ <img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%">
+ <img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%">
+ <img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%">
+ <img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%">
+ <img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%">
+ <img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%">
+ <img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%">
+ <img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%">
+ <img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%">
<template v-if="reaction == 'pudding'">
- <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%">
- <img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
+ <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%">
+ <img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%">
</template>
</span>
</template>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index a455afbf7d..13e8cf1f07 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -1,9 +1,9 @@
<template>
-<div class="mk-reaction-picker">
+<div class="mk-reaction-picker" v-hotkey.global="keymap">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact, big }" ref="popover">
<p v-if="!compact">{{ title }}</p>
- <div>
+ <div ref="buttons" :class="{ showFocus }">
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
@@ -31,30 +31,84 @@ export default Vue.extend({
type: Object,
required: true
},
+
source: {
required: true
},
+
compact: {
type: Boolean,
required: false,
default: false
},
+
cb: {
required: false
},
+
big: {
type: Boolean,
required: false,
default: false
+ },
+
+ showFocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+
+ animation: {
+ type: Boolean,
+ required: false,
+ default: true
}
},
+
data() {
return {
- title: placeholder
+ title: placeholder,
+ focus: null
};
},
+
+ computed: {
+ keymap(): any {
+ return {
+ 'esc': this.close,
+ 'enter|space|plus': this.choose,
+ 'up|k': this.focusUp,
+ 'left|h|shift+tab': this.focusLeft,
+ 'right|l|tab': this.focusRight,
+ 'down|j': this.focusDown,
+ '1': () => this.react('like'),
+ '2': () => this.react('love'),
+ '3': () => this.react('laugh'),
+ '4': () => this.react('hmm'),
+ '5': () => this.react('surprise'),
+ '6': () => this.react('congrats'),
+ '7': () => this.react('angry'),
+ '8': () => this.react('confused'),
+ '9': () => this.react('rip'),
+ '0': () => this.react('pudding'),
+ };
+ }
+ },
+
+ watch: {
+ focus(i) {
+ this.$refs.buttons.children[i].focus();
+
+ if (this.showFocus) {
+ this.title = this.$refs.buttons.children[i].title;
+ }
+ }
+ },
+
mounted() {
this.$nextTick(() => {
+ this.focus = 0;
+
const popover = this.$refs.popover as any;
const rect = this.source.getBoundingClientRect();
@@ -76,7 +130,7 @@ export default Vue.extend({
anime({
targets: this.$refs.backdrop,
opacity: 1,
- duration: 100,
+ duration: this.animation ? 100 : 0,
easing: 'linear'
});
@@ -84,10 +138,11 @@ export default Vue.extend({
targets: this.$refs.popover,
opacity: 1,
scale: [0.5, 1],
- duration: 500
+ duration: this.animation ? 500 : 0
});
});
},
+
methods: {
react(reaction) {
(this as any).api('notes/reactions/create', {
@@ -95,21 +150,25 @@ export default Vue.extend({
reaction: reaction
}).then(() => {
if (this.cb) this.cb();
- this.$destroy();
+ this.$emit('closed');
+ this.destroyDom();
});
},
+
onMouseover(e) {
this.title = e.target.title;
},
+
onMouseout(e) {
this.title = placeholder;
},
+
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({
targets: this.$refs.backdrop,
opacity: 0,
- duration: 200,
+ duration: this.animation ? 200 : 0,
easing: 'linear'
});
@@ -118,21 +177,42 @@ export default Vue.extend({
targets: this.$refs.popover,
opacity: 0,
scale: 0.5,
- duration: 200,
+ duration: this.animation ? 200 : 0,
easing: 'easeInBack',
- complete: () => this.$destroy()
+ complete: () => {
+ this.$emit('closed');
+ this.destroyDom();
+ }
});
+ },
+
+ focusUp() {
+ this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
+ },
+
+ focusDown() {
+ this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
+ },
+
+ focusRight() {
+ this.focus = this.focus == 9 ? 0 : (this.focus + 1);
+ },
+
+ focusLeft() {
+ this.focus = this.focus == 0 ? 9 : (this.focus - 1);
+ },
+
+ choose() {
+ this.$refs.buttons.childNodes[this.focus].click();
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
$border-color = rgba(27, 31, 35, 0.15)
-root(isDark)
+.mk-reaction-picker
position initial
> .backdrop
@@ -142,11 +222,11 @@ root(isDark)
z-index 10000
width 100%
height 100%
- background isDark ? rgba(#000, 0.4) : rgba(#000, 0.1)
+ background var(--modalBackdrop)
opacity 0
> .popover
- $bgcolor = isDark ? #2c303c : #fff
+ $bgcolor = var(--popupBg)
position absolute
z-index 10001
background $bgcolor
@@ -199,14 +279,29 @@ root(isDark)
margin 0
padding 8px 10px
font-size 14px
- color isDark ? #d6dce2 : #586069
- border-bottom solid 1px isDark ? #1c2023 : #e1e4e8
+ color var(--popupFg)
+ border-bottom solid 1px var(--faceDivider)
> div
padding 4px
width 240px
text-align center
+ &.showFocus
+ > button:focus
+ z-index 1
+
+ &:after
+ content ""
+ pointer-events none
+ position absolute
+ top 0
+ right 0
+ bottom 0
+ left 0
+ border 2px solid var(--primaryAlpha03)
+ border-radius 4px
+
> button
padding 0
width 40px
@@ -215,16 +310,10 @@ root(isDark)
border-radius 2px
&:hover
- background isDark ? #252731 : #eee
+ background var(--reactionPickerButtonHoverBg)
&:active
- background $theme-color
+ background var(--primary)
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
-.mk-reaction-picker[data-darkmode]
- root(true)
-
-.mk-reaction-picker:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue
index c30fa2a1dc..9212a84b31 100644
--- a/src/client/app/common/views/components/reactions-viewer.vue
+++ b/src/client/app/common/views/components/reactions-viewer.vue
@@ -39,10 +39,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
- $borderColor = isDark ? #5e6673 : #eee
- border-top dashed 1px $borderColor
- border-bottom dashed 1px $borderColor
+.mk-reactions-viewer
+ border-top dashed 1px var(--reactionViewerBorder)
+ border-bottom dashed 1px var(--reactionViewerBorder)
margin 4px 0
&:empty
@@ -60,12 +59,6 @@ root(isDark)
> span
margin-left 4px
font-size 1.2em
- color isDark ? #d1d5dc : #444
-
-.mk-reactions-viewer[data-darkmode]
- root(true)
-
-.mk-reactions-viewer:not([data-darkmode])
- root(false)
+ color var(--text)
</style>
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 5230ac371a..9224f82cb9 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -1,16 +1,16 @@
<template>
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
- <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
+ <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange" styl="fill">
<span>%i18n:@username%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
</ui-input>
- <ui-input v-model="password" type="password" required>
+ <ui-input v-model="password" type="password" required styl="fill">
<span>%i18n:@password%</span>
<span slot="prefix">%fa:lock%</span>
</ui-input>
- <ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
+ <ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
</form>
@@ -56,7 +56,7 @@ export default Vue.extend({
username: this.username,
password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
- }).then(() => {
+ }, true).then(() => {
location.reload();
}).catch(() => {
alert('%i18n:@login-failed%');
@@ -68,7 +68,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-signin
color #555
@@ -78,7 +78,7 @@ export default Vue.extend({
cursor wait !important
> .avatar
- margin 16px auto 0 auto
+ margin 0 auto 0 auto
width 64px
height 64px
background #ddd
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index f603b9545c..8e06b13491 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -1,12 +1,12 @@
<template>
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<template v-if="meta">
- <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
+ <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill">
<span>%i18n:@invitation-code%</span>
<span slot="prefix">%fa:id-card-alt%</span>
<p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p>
</ui-input>
- <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
+ <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill">
<span>%i18n:@username%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
@@ -18,7 +18,7 @@
<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p>
<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p>
</ui-input>
- <ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true">
+ <ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill">
<span>%i18n:@password%</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">
@@ -27,7 +27,7 @@
<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
</div>
</ui-input>
- <ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
+ <ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill">
<span>%i18n:@password% (%i18n:@retype%)</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">
@@ -131,11 +131,11 @@ export default Vue.extend({
password: this.password,
invitationCode: this.invitationCode,
'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
- }).then(() => {
+ }, true).then(() => {
(this as any).api('signin', {
username: this.username,
password: this.password
- }).then(() => {
+ }, true).then(() => {
location.href = '/';
});
}).catch(() => {
@@ -151,7 +151,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-signup
min-width 302px
diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue
index d573db32e6..12bf78f130 100644
--- a/src/client/app/common/views/components/stream-indicator.vue
+++ b/src/client/app/common/views/components/stream-indicator.vue
@@ -1,14 +1,14 @@
<template>
<div class="mk-stream-indicator">
- <p v-if=" stream.state == 'initializing' ">
+ <p v-if="stream.state == 'initializing'">
%fa:spinner .pulse%
<span>%i18n:@connecting%<mk-ellipsis/></span>
</p>
- <p v-if=" stream.state == 'reconnecting' ">
+ <p v-if="stream.state == 'reconnecting'">
%fa:spinner .pulse%
<span>%i18n:@reconnecting%<mk-ellipsis/></span>
</p>
- <p v-if=" stream.state == 'connected' ">
+ <p v-if="stream.state == 'connected'">
%fa:check%
<span>%i18n:@connected%</span>
</p>
diff --git a/src/client/app/common/views/components/switch.vue b/src/client/app/common/views/components/switch.vue
deleted file mode 100644
index 32caab638a..0000000000
--- a/src/client/app/common/views/components/switch.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-<template>
-<div
- class="mk-switch"
- :class="{ disabled, checked }"
- role="switch"
- :aria-checked="checked"
- :aria-disabled="disabled"
- @click="switchValue"
- @mouseover="mouseenter"
->
- <input
- type="checkbox"
- @change="handleChange"
- ref="input"
- :disabled="disabled"
- @keydown.enter="switchValue"
- >
- <span class="button">
- <span :style="{ transform }"></span>
- </span>
- <span class="label">
- <span :aria-hidden="!checked">{{ text }}</span>
- <p :aria-hidden="!checked">
- <slot></slot>
- </p>
- </span>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
- props: {
- value: {
- type: Boolean,
- default: false
- },
- disabled: {
- type: Boolean,
- default: false
- },
- text: String
- },/*
- created() {
- if (!~[true, false].indexOf(this.value)) {
- this.$emit('input', false);
- }
- },*/
- computed: {
- checked(): boolean {
- return this.value;
- },
- transform(): string {
- return this.checked ? 'translate3d(20px, 0, 0)' : '';
- }
- },
- watch: {
- value() {
- (this.$el).style.transition = 'all 0.3s';
- (this.$refs.input as any).checked = this.checked;
- }
- },
- mounted() {
- (this.$refs.input as any).checked = this.checked;
- },
- methods: {
- mouseenter() {
- (this.$el).style.transition = 'all 0s';
- },
- handleChange() {
- (this.$el).style.transition = 'all 0.3s';
- this.$emit('input', !this.checked);
- this.$emit('change', !this.checked);
- this.$nextTick(() => {
- // set input's checked property
- // in case parent refuses to change component's value
- (this.$refs.input as any).checked = this.checked;
- });
- },
- switchValue() {
- !this.disabled && this.handleChange();
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- display flex
- margin 12px 0
- cursor pointer
- transition all 0.3s
-
- > *
- user-select none
-
- &.disabled
- opacity 0.6
- cursor not-allowed
-
- &.checked
- > .button
- background-color $theme-color
- border-color $theme-color
-
- > .label
- > span
- color $theme-color
-
- &:hover
- > .label
- > span
- color darken($theme-color, 10%)
-
- > .button
- background darken($theme-color, 10%)
- border-color darken($theme-color, 10%)
-
- &:hover
- > .label
- > span
- color isDark ? #fff : #2e3338
-
- > .button
- $color = isDark ? #15181d : #ced2da
- background $color
- border-color $color
-
- > input
- position absolute
- width 0
- height 0
- opacity 0
- margin 0
-
- &:focus + .button
- &:after
- content ""
- pointer-events none
- position absolute
- top -5px
- right -5px
- bottom -5px
- left -5px
- border 2px solid rgba($theme-color, 0.3)
- border-radius 14px
-
- > .button
- $color = isDark ? #1c1f25 : #dcdfe6
-
- display inline-block
- margin 0
- width 40px
- min-width 40px
- height 20px
- min-height 20px
- background $color
- border 1px solid $color
- outline none
- border-radius 10px
- transition inherit
-
- > *
- position absolute
- top 1px
- left 1px
- border-radius 100%
- transition transform 0.3s
- width 16px
- height 16px
- background-color #fff
-
- > .label
- margin-left 8px
- display block
- font-size 15px
- cursor pointer
- transition inherit
-
- > span
- display block
- line-height 20px
- color isDark ? #c4ccd2 : #4a535a
- transition inherit
-
- > p
- margin 0
- //font-size 90%
- color isDark ? #78858e : #9daab3
-
-.mk-switch[data-darkmode]
- root(true)
-
-.mk-switch:not([data-darkmode])
- root(false)
-
-</style>
diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue
new file mode 100644
index 0000000000..5cc828082f
--- /dev/null
+++ b/src/client/app/common/views/components/tag-cloud.vue
@@ -0,0 +1,84 @@
+<template>
+<div class="jtivnzhfwquxpsfidertopbmwmchmnmo">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <p class="empty" v-else-if="tags.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
+ <div v-else>
+ <vue-word-cloud
+ :words="tags.slice(0, 20).map(x => [x.name, x.count])"
+ :color="color"
+ :spacing="1">
+ <template slot-scope="{word, text, weight}">
+ <div style="cursor: pointer;" :title="weight">
+ {{ text }}
+ </div>
+ </template>
+ </vue-word-cloud>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as VueWordCloud from 'vuewordcloud';
+
+export default Vue.extend({
+ components: {
+ [VueWordCloud.name]: VueWordCloud
+ },
+ data() {
+ return {
+ tags: [],
+ fetching: true,
+ clock: null
+ };
+ },
+ mounted() {
+ this.fetch();
+ this.clock = setInterval(this.fetch, 1000 * 60);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ fetch() {
+ (this as any).api('aggregation/hashtags').then(tags => {
+ this.tags = tags;
+ this.fetching = false;
+ });
+ },
+ color([, weight]) {
+ const peak = Math.max.apply(null, this.tags.map(x => x.count));
+ const w = weight / peak;
+
+ if (w > 0.9) {
+ return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69';
+ } else if (w > 0.5) {
+ return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7';
+ } else {
+ return this.$store.state.device.darkmode ? '#fff' : '#555';
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.jtivnzhfwquxpsfidertopbmwmchmnmo
+ height 100%
+ width 100%
+
+ > .fetching
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [data-fa]
+ margin-right 4px
+
+ > div
+ height 100%
+ width 100%
+
+</style>
diff --git a/src/client/app/common/views/components/theme.vue b/src/client/app/common/views/components/theme.vue
new file mode 100644
index 0000000000..9eda3c5796
--- /dev/null
+++ b/src/client/app/common/views/components/theme.vue
@@ -0,0 +1,308 @@
+<template>
+<div class="nicnklzforebnpfgasiypmpdaaglujqm">
+ <label>
+ <span>%i18n:@light-theme%</span>
+ <ui-select v-model="light" placeholder="%i18n:@light-theme%">
+ <optgroup label="%i18n:@light-themes%">
+ <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ <optgroup label="%i18n:@dark-themes%">
+ <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ </ui-select>
+ </label>
+
+ <label>
+ <span>%i18n:@dark-theme%</span>
+ <ui-select v-model="dark" placeholder="%i18n:@dark-theme%">
+ <optgroup label="%i18n:@dark-themes%">
+ <option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ <optgroup label="%i18n:@light-themes%">
+ <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </optgroup>
+ </ui-select>
+ </label>
+
+ <details class="creator">
+ <summary>%fa:palette% %i18n:@create-a-theme%</summary>
+ <div>
+ <span>%i18n:@base-theme%:</span>
+ <ui-radio v-model="myThemeBase" value="light">%i18n:@base-theme-light%</ui-radio>
+ <ui-radio v-model="myThemeBase" value="dark">%i18n:@base-theme-dark%</ui-radio>
+ </div>
+ <div>
+ <ui-input v-model="myThemeName">
+ <span>%i18n:@theme-name%</span>
+ </ui-input>
+ <ui-textarea v-model="myThemeDesc">
+ <span>%i18n:@desc%</span>
+ </ui-textarea>
+ </div>
+ <div>
+ <div style="padding-bottom:8px;">%i18n:@primary-color%:</div>
+ <color-picker v-model="myThemePrimary"/>
+ </div>
+ <div>
+ <div style="padding-bottom:8px;">%i18n:@secondary-color%:</div>
+ <color-picker v-model="myThemeSecondary"/>
+ </div>
+ <div>
+ <div style="padding-bottom:8px;">%i18n:@text-color%:</div>
+ <color-picker v-model="myThemeText"/>
+ </div>
+ <ui-button @click="preview()">%fa:eye% %i18n:@preview-created-theme%</ui-button>
+ <ui-button primary @click="gen()">%fa:save R% %i18n:@save-created-theme%</ui-button>
+ </details>
+
+ <details>
+ <summary>%fa:download% %i18n:@install-a-theme%</summary>
+ <ui-button @click="import_()">%fa:file-import% %i18n:@import%</ui-button>
+ <input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/>
+ <p>%i18n:@import-by-code%:</p>
+ <ui-textarea v-model="installThemeCode">
+ <span>%i18n:@theme-code%</span>
+ </ui-textarea>
+ <ui-button @click="() => install(this.installThemeCode)">%fa:check% %i18n:@install%</ui-button>
+ </details>
+
+ <details>
+ <summary>%fa:folder-open% %i18n:@installed-themes%</summary>
+ <ui-select v-model="selectedInstalledThemeId" placeholder="%i18n:@select-theme%">
+ <option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </ui-select>
+ <template v-if="selectedInstalledTheme">
+ <ui-input readonly :value="selectedInstalledTheme.author">
+ <span>%i18n:@author%</span>
+ </ui-input>
+ <ui-textarea v-if="selectedInstalledTheme.desc" readonly :value="selectedInstalledTheme.desc">
+ <span>%i18n:@desc%</span>
+ </ui-textarea>
+ <ui-textarea readonly :value="selectedInstalledThemeCode">
+ <span>%i18n:@theme-code%</span>
+ </ui-textarea>
+ <ui-button @click="export_()" link :download="`${selectedInstalledTheme.name}.misskeytheme`" ref="export">%fa:box% %i18n:@export%</ui-button>
+ <ui-button @click="uninstall()">%fa:trash-alt R% %i18n:@uninstall%</ui-button>
+ </template>
+ </details>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../theme';
+import { Chrome } from 'vue-color';
+import * as uuid from 'uuid';
+import * as tinycolor from 'tinycolor2';
+import * as JSON5 from 'json5';
+
+// 後方互換性のため
+function convertOldThemedefinition(t) {
+ const t2 = {
+ id: t.meta.id,
+ name: t.meta.name,
+ author: t.meta.author,
+ base: t.meta.base,
+ vars: t.meta.vars,
+ props: t
+ };
+ delete t2.props.meta;
+ return t2;
+}
+
+export default Vue.extend({
+ components: {
+ ColorPicker: Chrome
+ },
+
+ data() {
+ return {
+ installThemeCode: null,
+ selectedInstalledThemeId: null,
+ myThemeBase: 'light',
+ myThemeName: '',
+ myThemeDesc: '',
+ myThemePrimary: lightTheme.vars.primary,
+ myThemeSecondary: lightTheme.vars.secondary,
+ myThemeText: lightTheme.vars.text
+ };
+ },
+
+ computed: {
+ themes(): Theme[] {
+ return builtinThemes.concat(this.$store.state.device.themes);
+ },
+
+ darkThemes(): Theme[] {
+ return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
+ },
+
+ lightThemes(): Theme[] {
+ return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
+ },
+
+ installedThemes(): Theme[] {
+ return this.$store.state.device.themes;
+ },
+
+ light: {
+ get() { return this.$store.state.device.lightTheme; },
+ set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); }
+ },
+
+ dark: {
+ get() { return this.$store.state.device.darkTheme; },
+ set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
+ },
+
+ selectedInstalledTheme() {
+ if (this.selectedInstalledThemeId == null) return null;
+ return this.installedThemes.find(x => x.id == this.selectedInstalledThemeId);
+ },
+
+ selectedInstalledThemeCode() {
+ if (this.selectedInstalledTheme == null) return null;
+ return JSON5.stringify(this.selectedInstalledTheme, null, '\t');
+ },
+
+ myTheme(): any {
+ return {
+ name: this.myThemeName,
+ author: this.$store.state.i.username,
+ desc: this.myThemeDesc,
+ base: this.myThemeBase,
+ vars: {
+ primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
+ secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
+ text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
+ }
+ };
+ }
+ },
+
+ watch: {
+ myThemeBase(v) {
+ const theme = v == 'light' ? lightTheme : darkTheme;
+ this.myThemePrimary = theme.vars.primary;
+ this.myThemeSecondary = theme.vars.secondary;
+ this.myThemeText = theme.vars.text;
+ }
+ },
+
+ beforeCreate() {
+ // migrate old theme definitions
+ // 後方互換性のため
+ this.$store.commit('device/set', {
+ key: 'themes', value: this.$store.state.device.themes.map(t => {
+ if (t.id == null) {
+ return convertOldThemedefinition(t);
+ } else {
+ return t;
+ }
+ })
+ });
+ },
+
+ methods: {
+ install(code) {
+ let theme;
+
+ try {
+ theme = JSON5.parse(code);
+ } catch (e) {
+ alert('%i18n:@invalid-theme%');
+ return;
+ }
+
+ // 後方互換性のため
+ if (theme.id == null && theme.meta != null) {
+ theme = convertOldThemedefinition(theme);
+ }
+
+ if (theme.id == null) {
+ alert('%i18n:@invalid-theme%');
+ return;
+ }
+
+ if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
+ alert('%i18n:@already-installed%');
+ return;
+ }
+
+ const themes = this.$store.state.device.themes.concat(theme);
+ this.$store.commit('device/set', {
+ key: 'themes', value: themes
+ });
+
+ alert('%i18n:@installed%'.replace('{}', theme.name));
+ },
+
+ uninstall() {
+ const theme = this.selectedInstalledTheme;
+ const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
+ this.$store.commit('device/set', {
+ key: 'themes', value: themes
+ });
+ alert('%i18n:@uninstalled%'.replace('{}', theme.name));
+ },
+
+ import_() {
+ (this.$refs.file as any).click();
+ }
+
+ export_() {
+ const blob = new Blob([this.selectedInstalledThemeCode], {
+ type: 'application/json5'
+ });
+ this.$refs.export.$el.href = window.URL.createObjectURL(blob);
+ },
+
+ onUpdateImportFile() {
+ const f = (this.$refs.file as any).files[0];
+
+ const reader = new FileReader();
+
+ reader.onload = e => {
+ this.install(e.target.result);
+ };
+
+ reader.readAsText(f);
+ },
+
+ preview() {
+ applyTheme(this.myTheme, false);
+ },
+
+ gen() {
+ const theme = this.myTheme;
+ if (theme.name == null || theme.name.trim() == '') {
+ alert('%i18n:@theme-name-required%');
+ return;
+ }
+ theme.id = uuid();
+ const themes = this.$store.state.device.themes.concat(theme);
+ this.$store.commit('device/set', {
+ key: 'themes', value: themes
+ });
+ alert('%i18n:@saved%');
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.nicnklzforebnpfgasiypmpdaaglujqm
+ > details
+ border-top solid 1px var(--faceDivider)
+
+ > summary
+ padding 16px 0
+
+ > *:last-child
+ margin-bottom 16px
+
+ > .creator
+ > div
+ padding 16px 0
+ border-bottom solid 1px var(--faceDivider)
+</style>
diff --git a/src/client/app/common/views/widgets/hashtags.chart.vue b/src/client/app/common/views/components/trends.chart.vue
index 723a3947f8..723a3947f8 100644
--- a/src/client/app/common/views/widgets/hashtags.chart.vue
+++ b/src/client/app/common/views/components/trends.chart.vue
diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue
new file mode 100644
index 0000000000..3d36d7449c
--- /dev/null
+++ b/src/client/app/common/views/components/trends.vue
@@ -0,0 +1,98 @@
+<template>
+<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
+ <!-- トランジションを有効にするとなぜかメモリリークする -->
+ <transition-group v-else tag="div" name="chart">
+ <div v-for="stat in stats" :key="stat.tag">
+ <div class="tag">
+ <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
+ <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
+ </div>
+ <x-chart class="chart" :src="stat.chart"/>
+ </div>
+ </transition-group>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XChart from './trends.chart.vue';
+
+export default Vue.extend({
+ components: {
+ XChart
+ },
+ data() {
+ return {
+ stats: [],
+ fetching: true,
+ clock: null
+ };
+ },
+ mounted() {
+ this.fetch();
+ this.clock = setInterval(this.fetch, 1000 * 60);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ fetch() {
+ (this as any).api('hashtags/trend').then(stats => {
+ this.stats = stats;
+ this.fetching = false;
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.csqvmxybqbycalfhkxvyfrgbrdalkaoc
+ > .fetching
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color var(--text)
+ opacity 0.7
+
+ > [data-fa]
+ margin-right 4px
+
+ > div
+ .chart-move
+ transition transform 1s ease
+
+ > div
+ display flex
+ align-items center
+ padding 14px 16px
+
+ &:not(:last-child)
+ border-bottom solid 1px var(--faceDivider)
+
+ > .tag
+ flex 1
+ overflow hidden
+ font-size 14px
+ color var(--text)
+
+ > a
+ display block
+ width 100%
+ white-space nowrap
+ overflow hidden
+ text-overflow ellipsis
+ color inherit
+
+ > p
+ margin 0
+ font-size 75%
+ opacity 0.7
+
+ > .chart
+ height 30px
+
+</style>
diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue
index e778750354..a509632520 100644
--- a/src/client/app/common/views/components/ui/button.vue
+++ b/src/client/app/common/views/components/ui/button.vue
@@ -1,9 +1,7 @@
<template>
-<div class="ui-button" :class="[styl]">
- <button :type="type" @click="$emit('click')">
- <slot></slot>
- </button>
-</div>
+<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr" :is="link ? 'a' : 'button'" :class="[styl, { inline, primary }]" :type="type" @click="$emit('click')">
+ <slot></slot>
+</component>
</template>
<script lang="ts">
@@ -13,70 +11,100 @@ export default Vue.extend({
type: {
type: String,
required: false
+ },
+ primary: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ inline: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ link: {
+ type: Boolean,
+ required: false,
+ default: false
}
},
data() {
return {
styl: 'fill'
};
- },
- inject: {
- isCardChild: { default: false }
- },
- created() {
- if (this.isCardChild) {
- this.styl = 'line';
- }
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+.dmtdnykelhudezerjlfpbhgovrgnqqgr
+ display block
+ width 100%
+ margin 0
+ padding 8px
+ text-align center
+ font-weight normal
+ font-size 16px
+ border none
+ border-radius 6px
+ outline none
+ box-shadow none
+ text-decoration none
+ user-select none
+
+ *
+ pointer-events none
-root(isDark, fill)
- > button
- display block
- width 100%
- margin 0
- padding 0
+ &:focus
+ &:after
+ content ""
+ pointer-events none
+ position absolute
+ top -5px
+ right -5px
+ bottom -5px
+ left -5px
+ border 2px solid var(--primaryAlpha03)
+ border-radius 10px
+
+ &:not(.inline) + .dmtdnykelhudezerjlfpbhgovrgnqqgr
+ margin-top 16px
+
+ &.inline
+ display inline-block
+ width auto
+
+ &.primary
font-weight bold
- font-size 16px
- line-height 44px
- border none
- border-radius 6px
- outline none
- box-shadow none
- if fill
- color $theme-color-foreground
- background $theme-color
+ &.fill
+ color var(--text)
+ background var(--buttonBg)
- &:hover
- background lighten($theme-color, 5%)
+ &:hover
+ background var(--buttonHoverBg)
- &:active
- background darken($theme-color, 5%)
- else
- color $theme-color
- background none
+ &:active
+ background var(--buttonActiveBg)
+
+ &.primary
+ color var(--primaryForeground)
+ background var(--primary)
&:hover
- color darken($theme-color, 5%)
+ background var(--primaryLighten5)
&:active
- background rgba($theme-color, 0.3)
+ background var(--primaryDarken5)
-.ui-button[data-darkmode]
- &.fill
- root(true, true)
&:not(.fill)
- root(true, false)
+ color var(--primary)
+ background none
-.ui-button:not([data-darkmode])
- &.fill
- root(false, true)
- &:not(.fill)
- root(false, false)
+ &:hover
+ color var(--primaryDarken5)
+
+ &:active
+ background var(--primaryAlpha03)
</style>
diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue
index 05c51bca6b..a37a38d340 100644
--- a/src/client/app/common/views/components/ui/card.vue
+++ b/src/client/app/common/views/components/ui/card.vue
@@ -20,27 +20,33 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.ui-card
margin 16px
- padding 16px
- color isDark ? #fff : #000
- background isDark ? #282C37 : #fff
+ color var(--faceText)
+ background var(--face)
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)
- @media (min-width 500px)
- padding 32px
-
> header
- font-weight normal
- font-size 24px
- color isDark ? #fff : #444
+ padding 16px
+ font-weight bold
+ font-size 20px
+ color var(--faceText)
+
+ @media (min-width 500px)
+ padding 24px 32px
+
+ > section
+ padding 20px 16px
+ border-top solid 1px var(--faceDivider)
-.ui-card[data-darkmode]
- root(true)
+ @media (min-width 500px)
+ padding 32px
-.ui-card:not([data-darkmode])
- root(false)
+ &.fit-top
+ padding-top 0
+ > header
+ margin-bottom 16px
+ font-weight bold
+ color var(--faceText)
</style>
diff --git a/src/client/app/common/views/components/ui/form.vue b/src/client/app/common/views/components/ui/form.vue
index fc8fdad9c4..5c5bbd7256 100644
--- a/src/client/app/common/views/components/ui/form.vue
+++ b/src/client/app/common/views/components/ui/form.vue
@@ -19,7 +19,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.ui-form
> fieldset
diff --git a/src/client/app/common/views/components/ui/form/button.vue b/src/client/app/common/views/components/ui/form/button.vue
index 9c37b3118b..3fd7b47629 100644
--- a/src/client/app/common/views/components/ui/form/button.vue
+++ b/src/client/app/common/views/components/ui/form/button.vue
@@ -25,9 +25,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg
display inline-block
& + .nvemkhtwcnnpkdrwfcbzuwhfulejhmzg
@@ -38,11 +36,11 @@ root(isDark)
margin 0
padding 12px 20px
font-size 14px
- border 1px solid isDark ? #6d727d : #dcdfe6
+ border 1px solid var(--formButtonBorder)
border-radius 4px
outline none
box-shadow none
- color isDark ? #fff : #606266
+ color var(--text)
transition 0.1s
*
@@ -50,40 +48,34 @@ root(isDark)
&:hover
&:focus
- color $theme-color
- background rgba($theme-color, isDark ? 0.2 : 0.12)
- border-color rgba($theme-color, isDark ? 0.5 : 0.3)
+ color var(--primary)
+ background var(--formButtonHoverBg)
+ border-color var(--formButtonHoverBorder)
&:active
- color darken($theme-color, 20%)
- background rgba($theme-color, 0.12)
- border-color $theme-color
+ color var(--primaryDarken20)
+ background var(--formButtonActiveBg)
+ border-color var(--primary)
transition all 0s
&.primary
> button
- border 1px solid $theme-color
- background $theme-color
- color $theme-color-foreground
+ border 1px solid var(--primary)
+ background var(--primary)
+ color var(--primaryForeground)
&:hover
&:focus
- background lighten($theme-color, 20%)
- border-color lighten($theme-color, 20%)
+ background var(--primaryLighten20)
+ border-color var(--primaryLighten20)
&:active
- background darken($theme-color, 20%)
- border-color darken($theme-color, 20%)
+ background var(--primaryDarken20)
+ border-color var(--primaryDarken20)
transition all 0s
&.round
> button
border-radius 64px
-.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg[data-darkmode]
- root(true)
-
-.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/ui/form/radio.vue b/src/client/app/common/views/components/ui/form/radio.vue
index 831981bb3e..396b2997e5 100644
--- a/src/client/app/common/views/components/ui/form/radio.vue
+++ b/src/client/app/common/views/components/ui/form/radio.vue
@@ -49,9 +49,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.uywduthvrdnlpsvsjkqigicixgyfctto
display inline-flex
margin 0 16px 0 0
cursor pointer
@@ -62,7 +60,7 @@ root(isDark)
&:hover
> .button
- border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ border solid 2px var(--inputLabel)
&.disabled
opacity 0.6
@@ -70,15 +68,15 @@ root(isDark)
&.checked
> .button
- border-color $theme-color
+ border-color var(--primary)
&:after
- background-color $theme-color
+ background-color var(--primary)
transform scale(1)
opacity 1
> .label
- color $theme-color
+ color var(--primary)
> input
position absolute
@@ -93,7 +91,7 @@ root(isDark)
width 20px
height 20px
background none
- border solid 2px isDark ? rgba(#fff, 0.6) : rgba(#000, 0.4)
+ border solid 2px var(--radioBorder)
border-radius 100%
transition inherit
@@ -117,10 +115,4 @@ root(isDark)
line-height 20px
cursor pointer
-.uywduthvrdnlpsvsjkqigicixgyfctto[data-darkmode]
- root(true)
-
-.uywduthvrdnlpsvsjkqigicixgyfctto:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue
index ce28bfb12a..abbd5a2feb 100644
--- a/src/client/app/common/views/components/ui/input.vue
+++ b/src/client/app/common/views/components/ui/input.vue
@@ -71,14 +71,18 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
+ },
+ styl: {
+ type: String,
+ required: false,
+ default: 'line'
}
},
data() {
return {
v: this.value,
focused: false,
- passwordStrength: '',
- styl: 'fill'
+ passwordStrength: ''
};
},
computed: {
@@ -117,14 +121,6 @@ export default Vue.extend({
}
}
},
- inject: {
- isCardChild: { default: false }
- },
- created() {
- if (this.isCardChild) {
- this.styl = 'line';
- }
- },
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
@@ -155,9 +151,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark, fill)
+root(fill)
margin 32px 0
> .icon
@@ -167,7 +161,7 @@ root(isDark, fill)
width 24px
text-align center
line-height 32px
- color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ color var(--inputLabel)
&:not(:empty) + .input
margin-left 28px
@@ -183,7 +177,7 @@ root(isDark, fill)
left 0
right 0
height 1px
- background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
+ background var(--inputBorder)
&:after
content ''
@@ -193,7 +187,7 @@ root(isDark, fill)
left 0
right 0
height 2px
- background $theme-color
+ background var(--primary)
opacity 0
transform scaleX(0.12)
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
@@ -242,7 +236,7 @@ root(isDark, fill)
transition-duration 0.3s
font-size 16px
line-height 32px
- color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ color var(--inputLabel)
pointer-events none
//will-change transform
transform-origin top left
@@ -257,7 +251,7 @@ root(isDark, fill)
font-weight fill ? bold : normal
font-size 16px
line-height 32px
- color isDark ? #fff : #000
+ color var(--inputText)
background transparent
border none
border-radius 0
@@ -280,7 +274,7 @@ root(isDark, fill)
top 0
font-size 16px
line-height fill ? 44px : 32px
- color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ color var(--inputLabel)
pointer-events none
&:empty
@@ -325,7 +319,7 @@ root(isDark, fill)
transform scaleX(1)
> .label
- color $theme-color
+ color var(--primary)
&.focused
&.filled
@@ -335,16 +329,10 @@ root(isDark, fill)
left 0 !important
transform scale(0.75)
-.ui-input[data-darkmode]
- &.fill
- root(true, true)
- &:not(.fill)
- root(true, false)
-
-.ui-input:not([data-darkmode])
+.ui-input
&.fill
- root(false, true)
+ root(true)
&:not(.fill)
- root(false, false)
+ root(false)
</style>
diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue
index 04a46c5a96..868a339aa4 100644
--- a/src/client/app/common/views/components/ui/radio.vue
+++ b/src/client/app/common/views/components/ui/radio.vue
@@ -51,11 +51,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.ui-radio
display inline-block
- margin 32px 32px 32px 0
+ margin 0 32px 0 0
cursor pointer
transition all 0.3s
@@ -68,10 +66,10 @@ root(isDark)
&.checked
> .button
- border-color $theme-color
+ border-color var(--primary)
&:after
- background-color $theme-color
+ background-color var(--primary)
transform scale(1)
opacity 1
@@ -87,7 +85,7 @@ root(isDark)
width 20px
height 20px
background none
- border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ border solid 2px var(--inputLabel)
border-radius 100%
transition inherit
@@ -111,10 +109,4 @@ root(isDark)
line-height 20px
cursor pointer
-.ui-radio[data-darkmode]
- root(true)
-
-.ui-radio:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/ui/select.vue b/src/client/app/common/views/components/ui/select.vue
index 4273a4a0de..da6f9696b5 100644
--- a/src/client/app/common/views/components/ui/select.vue
+++ b/src/client/app/common/views/components/ui/select.vue
@@ -29,13 +29,17 @@ export default Vue.extend({
required: {
type: Boolean,
required: false
+ },
+ styl: {
+ type: String,
+ required: false,
+ default: 'line'
}
},
data() {
return {
v: this.value,
- focused: false,
- styl: 'fill'
+ focused: false
};
},
computed: {
@@ -48,14 +52,6 @@ export default Vue.extend({
this.v = v;
}
},
- inject: {
- isCardChild: { default: false }
- },
- created() {
- if (this.isCardChild) {
- this.styl = 'line';
- }
- },
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
@@ -70,9 +66,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark, fill)
+root(fill)
margin 32px 0
> .icon
@@ -103,7 +97,7 @@ root(isDark, fill)
left 0
right 0
height 1px
- background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
+ background var(--inputBorder)
&:after
content ''
@@ -113,7 +107,7 @@ root(isDark, fill)
left 0
right 0
height 2px
- background $theme-color
+ background var(--primary)
opacity 0
transform scaleX(0.12)
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
@@ -143,7 +137,7 @@ root(isDark, fill)
font-weight fill ? bold : normal
font-size 16px
height 32px
- color isDark ? #fff : #000
+ color var(--inputText)
background transparent
border none
border-radius 0
@@ -190,7 +184,7 @@ root(isDark, fill)
transform scaleX(1)
> .label
- color $theme-color
+ color var(--primary)
&.focused
&.filled
@@ -200,16 +194,10 @@ root(isDark, fill)
left 0 !important
transform scale(0.75)
-.ui-select[data-darkmode]
- &.fill
- root(true, true)
- &:not(.fill)
- root(true, false)
-
-.ui-select:not([data-darkmode])
+.ui-select
&.fill
- root(false, true)
+ root(true)
&:not(.fill)
- root(false, false)
+ root(false)
</style>
diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue
index a9e00d73d2..935f219833 100644
--- a/src/client/app/common/views/components/ui/switch.vue
+++ b/src/client/app/common/views/components/ui/switch.vue
@@ -19,7 +19,7 @@
<span class="label">
<span :aria-hidden="!checked"><slot></slot></span>
<p :aria-hidden="!checked">
- <slot name="text"></slot>
+ <slot name="desc"></slot>
</p>
</span>
</div>
@@ -56,14 +56,18 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.ui-switch
display flex
margin 32px 0
cursor pointer
transition all 0.3s
+ &:first-child
+ margin-top 0
+
+ &:last-child
+ margin-bottom 0
+
> *
user-select none
@@ -73,11 +77,11 @@ root(isDark)
&.checked
> .button
- background-color rgba($theme-color, 0.4)
- border-color rgba($theme-color, 0.4)
+ background-color var(--primaryAlpha04)
+ border-color var(--primaryAlpha04)
> *
- background-color $theme-color
+ background-color var(--primary)
transform translateX(14px)
> input
@@ -89,10 +93,11 @@ root(isDark)
> .button
display inline-block
+ flex-shrink 0
margin 3px 0 0 0
width 34px
height 14px
- background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25)
+ background var(--switchTrack)
outline none
border-radius 14px
transition inherit
@@ -118,18 +123,11 @@ root(isDark)
> span
display block
line-height 20px
- color isDark ? #c4ccd2 : rgba(#000, 0.75)
+ color currentColor
transition inherit
> p
margin 0
- //font-size 90%
- color isDark ? #78858e : #9daab3
-
-.ui-switch[data-darkmode]
- root(true)
-
-.ui-switch:not([data-darkmode])
- root(false)
+ opacity 0.7
</style>
diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue
index 60fe1cdd82..67898ee059 100644
--- a/src/client/app/common/views/components/ui/textarea.vue
+++ b/src/client/app/common/views/components/ui/textarea.vue
@@ -63,9 +63,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark, fill)
+root(fill)
margin 42px 0 32px 0
> .input
@@ -84,7 +82,7 @@ root(isDark, fill)
left 0
right 0
background none
- border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
+ border solid 1px var(--inputBorder)
border-radius 3px
pointer-events none
@@ -97,7 +95,7 @@ root(isDark, fill)
left 0
right 0
background none
- border solid 2px $theme-color
+ border solid 2px var(--primary)
border-radius 3px
opacity 0
transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
@@ -112,7 +110,7 @@ root(isDark, fill)
transition-duration 0.3s
font-size 16px
line-height 32px
- color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ color var(--inputLabel)
pointer-events none
//will-change transform
transform-origin top left
@@ -126,7 +124,7 @@ root(isDark, fill)
font inherit
font-weight fill ? bold : normal
font-size 16px
- color isDark ? #fff : #000
+ color var(--inputText)
background transparent
border none
border-radius 0
@@ -149,7 +147,7 @@ root(isDark, fill)
opacity 1
> .label
- color $theme-color
+ color var(--primary)
&.focused
&.filled
@@ -159,16 +157,10 @@ root(isDark, fill)
left 0 !important
transform scale(0.75)
-.ui-textarea[data-darkmode]
- &.fill
- root(true, true)
- &:not(.fill)
- root(true, false)
+.ui-textarea.fill
+ root(true)
-.ui-textarea:not([data-darkmode])
- &.fill
- root(false, true)
- &:not(.fill)
- root(false, false)
+.ui-textarea:not(.fill)
+ root(false)
</style>
diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
index f4797d89f7..b812064bbb 100644
--- a/src/client/app/common/views/components/uploader.vue
+++ b/src/client/app/common/views/components/uploader.vue
@@ -20,6 +20,7 @@
<script lang="ts">
import Vue from 'vue';
import { apiUrl } from '../../../config';
+import getMD5 from '../../scripts/get-md5';
export default Vue.extend({
data() {
@@ -28,61 +29,83 @@ export default Vue.extend({
};
},
methods: {
- upload(file, folder) {
- if (folder && typeof folder == 'object') folder = folder.id;
+ checkExistence(fileData: ArrayBuffer): Promise<any> {
+ return new Promise((resolve, reject) => {
+ const data = new FormData();
+ data.append('md5', getMD5(fileData));
- const id = Math.random();
+ (this as any).api('drive/files/check_existence', {
+ md5: getMD5(fileData)
+ }).then(resp => {
+ resolve(resp.file);
+ });
+ });
+ },
- const ctx = {
- id: id,
- name: file.name || 'untitled',
- progress: undefined,
- img: undefined
- };
+ upload(file: File, folder: any) {
+ if (folder && typeof folder == 'object') folder = folder.id;
- this.uploads.push(ctx);
- this.$emit('change', this.uploads);
+ const id = Math.random();
const reader = new FileReader();
reader.onload = (e: any) => {
- ctx.img = e.target.result;
- };
- reader.readAsDataURL(file);
+ this.checkExistence(e.target.result).then(result => {
+ if (result !== null) {
+ this.$emit('uploaded', result);
+ return;
+ }
- const data = new FormData();
- data.append('i', this.$store.state.i.token);
- data.append('file', file);
+ // Upload if the file didn't exist yet
+ const buf = new Uint8Array(e.target.result);
+ let bin = '';
+ // We use for-of loop instead of apply() to avoid RangeError
+ // SEE: https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string
+ for (const byte of buf) bin += String.fromCharCode(byte);
+ const ctx = {
+ id: id,
+ name: file.name || 'untitled',
+ progress: undefined,
+ img: 'data:*/*;base64,' + btoa(bin)
+ };
- if (folder) data.append('folderId', folder);
+ this.uploads.push(ctx);
+ this.$emit('change', this.uploads);
- const xhr = new XMLHttpRequest();
- xhr.open('POST', apiUrl + '/drive/files/create', true);
- xhr.onload = (e: any) => {
- const driveFile = JSON.parse(e.target.response);
+ const data = new FormData();
+ data.append('i', this.$store.state.i.token);
+ data.append('file', file);
- this.$emit('uploaded', driveFile);
+ if (folder) data.append('folderId', folder);
- this.uploads = this.uploads.filter(x => x.id != id);
- this.$emit('change', this.uploads);
- };
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', apiUrl + '/drive/files/create', true);
+ xhr.onload = (e: any) => {
+ const driveFile = JSON.parse(e.target.response);
- xhr.upload.onprogress = e => {
- if (e.lengthComputable) {
- if (ctx.progress == undefined) ctx.progress = {};
- ctx.progress.max = e.total;
- ctx.progress.value = e.loaded;
- }
- };
+ this.$emit('uploaded', driveFile);
- xhr.send(data);
+ this.uploads = this.uploads.filter(x => x.id != id);
+ this.$emit('change', this.uploads);
+ };
+
+ xhr.upload.onprogress = e => {
+ if (e.lengthComputable) {
+ if (ctx.progress == undefined) ctx.progress = {};
+ ctx.progress.max = e.total;
+ ctx.progress.value = e.loaded;
+ }
+ };
+
+ xhr.send(data);
+ })
+ }
+ reader.readAsArrayBuffer(file);
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
.mk-uploader
overflow auto
@@ -100,7 +123,7 @@ export default Vue.extend({
margin 8px 0 0 0
padding 0
height 36px
- box-shadow 0 -1px 0 rgba($theme-color, 0.1)
+ box-shadow 0 -1px 0 var(--primaryAlpha01)
border-top solid 8px transparent
&:first-child
@@ -127,7 +150,7 @@ export default Vue.extend({
padding 0
max-width 256px
font-size 0.8em
- color rgba($theme-color, 0.7)
+ color var(--primaryAlpha07)
white-space nowrap
text-overflow ellipsis
overflow hidden
@@ -145,17 +168,17 @@ export default Vue.extend({
font-size 0.8em
> .initing
- color rgba($theme-color, 0.5)
+ color var(--primaryAlpha05)
> .kb
- color rgba($theme-color, 0.5)
+ color var(--primaryAlpha05)
> .percentage
display inline-block
width 48px
text-align right
- color rgba($theme-color, 0.7)
+ color var(--primaryAlpha07)
&:after
content '%'
@@ -174,10 +197,10 @@ export default Vue.extend({
overflow hidden
&::-webkit-progress-value
- background $theme-color
+ background var(--primary)
&::-webkit-progress-bar
- background rgba($theme-color, 0.1)
+ background var(--primaryAlpha01)
> .progress
display block
@@ -191,13 +214,13 @@ export default Vue.extend({
border-radius 4px
background linear-gradient(
45deg,
- lighten($theme-color, 30%) 25%,
- $theme-color 25%,
- $theme-color 50%,
- lighten($theme-color, 30%) 50%,
- lighten($theme-color, 30%) 75%,
- $theme-color 75%,
- $theme-color
+ var(--primaryLighten30) 25%,
+ var(--primary) 25%,
+ var(--primary) 50%,
+ var(--primaryLighten30) 50%,
+ var(--primaryLighten30) 75%,
+ var(--primary) 75%,
+ var(--primary)
)
background-size 32px 32px
animation bg 1.5s linear infinite
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 242d9ba5c6..86489cf8be 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -8,13 +8,13 @@
</blockquote>
</div>
<div v-else class="mk-url-preview">
- <a :href="url" target="_blank" :title="url" v-if="!fetching">
+ <a :class="{ mini }" :href="url" target="_blank" :title="url" v-if="!fetching">
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
<article>
<header>
<h1>{{ title }}</h1>
</header>
- <p>{{ description }}</p>
+ <p>{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
<footer>
<img class="icon" v-if="icon" :src="icon"/>
<p>{{ sitename }}</p>
@@ -118,6 +118,12 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
+ },
+
+ mini: {
+ type: Boolean,
+ required: false,
+ default: false
}
},
@@ -164,7 +170,7 @@ export default Vue.extend({
return;
}
- fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
+ fetch(`/url?url=${encodeURIComponent(this.url)}`).then(res => {
res.json().then(info => {
if (info.url == null) return;
this.title = info.title;
@@ -194,17 +200,17 @@ export default Vue.extend({
top 0
width 100%
-root(isDark)
+.mk-url-preview
> a
display block
font-size 14px
- border solid 1px isDark ? #191b1f : #eee
+ border solid 1px var(--urlPreviewBorder)
border-radius 4px
overflow hidden
&:hover
text-decoration none
- border-color isDark ? #4f5561 : #ddd
+ border-color var(--urlPreviewBorderHover)
> article > header > h1
text-decoration underline
@@ -229,11 +235,11 @@ root(isDark)
> h1
margin 0
font-size 1em
- color isDark ? #d6dae0 : #555
+ color var(--urlPreviewTitle)
> p
margin 0
- color isDark ? #a4aab3 : #777
+ color var(--urlPreviewText)
font-size 0.8em
> footer
@@ -250,7 +256,7 @@ root(isDark)
> p
display inline-block
margin 0
- color isDark ? #b0b4bf : #666
+ color var(--urlPreviewInfo)
font-size 0.8em
line-height 16px
vertical-align top
@@ -293,10 +299,27 @@ root(isDark)
width 12px
height 12px
-.mk-url-preview[data-darkmode]
- root(true)
+ &.mini
+ font-size 10px
+
+ > .thumbnail
+ position relative
+ width 100%
+ height 60px
-.mk-url-preview:not([data-darkmode])
- root(false)
+ > article
+ left 0
+ width 100%
+ padding 8px
+
+ > header
+ margin-bottom 4px
+
+ > footer
+ margin-top 4px
+
+ > img
+ width 12px
+ height 12px
</style>
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
index e6ffe4466d..04a1f30135 100644
--- a/src/client/app/common/views/components/url.vue
+++ b/src/client/app/common/views/components/url.vue
@@ -12,6 +12,7 @@
<script lang="ts">
import Vue from 'vue';
+import { toUnicode as decodePunycode } from 'punycode';
export default Vue.extend({
props: ['url', 'target'],
data() {
@@ -27,11 +28,11 @@ export default Vue.extend({
created() {
const url = new URL(this.url);
this.schema = url.protocol;
- this.hostname = url.hostname;
+ this.hostname = decodePunycode(url.hostname);
this.port = url.port;
- this.pathname = url.pathname;
- this.query = url.search;
- this.hash = url.hash;
+ this.pathname = decodeURIComponent(url.pathname);
+ this.query = decodeURIComponent(url.search);
+ this.hash = decodeURIComponent(url.hash);
}
});
</script>
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 4691604e57..02f33bfbc0 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
props: ['source', 'compact'],
data() {
return {
- v: this.$store.state.device.visibility || 'public'
+ v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility
}
},
mounted() {
@@ -97,9 +97,11 @@ export default Vue.extend({
},
methods: {
choose(visibility) {
- this.$store.commit('device/setVisibility', visibility);
+ if (this.$store.state.settings.rememberNoteVisibility) {
+ this.$store.commit('device/setVisibility', visibility);
+ }
this.$emit('chosen', visibility);
- this.$destroy();
+ this.destroyDom();
},
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
@@ -117,7 +119,7 @@ export default Vue.extend({
scale: 0.5,
duration: 200,
easing: 'easeInBack',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
@@ -125,11 +127,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
$border-color = rgba(27, 31, 35, 0.15)
-root(isDark)
+.mk-visibility-chooser
position initial
> .backdrop
@@ -139,11 +139,11 @@ root(isDark)
z-index 10000
width 100%
height 100%
- background isDark ? rgba(#000, 0.4) : rgba(#000, 0.1)
+ background var(--modalBackdrop)
opacity 0
> .popover
- $bgcolor = isDark ? #2c303c : #fff
+ $bgcolor = var(--popupBg)
position absolute
z-index 10001
width 240px
@@ -187,18 +187,18 @@ root(isDark)
display flex
padding 8px 14px
font-size 12px
- color isDark ? #fff : #666
+ color var(--popupFg)
cursor pointer
&:hover
- background isDark ? #252731 : #eee
+ background var(--faceClearButtonHover)
&:active
- background isDark ? #21242b : #ddd
+ background var(--faceClearButtonActive)
&.active
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
> *
user-select none
@@ -220,11 +220,4 @@ root(isDark)
> span:last-child:not(:first-child)
opacity 0.6
-
-.mk-visibility-chooser[data-darkmode]
- root(true)
-
-.mk-visibility-chooser:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 5a8b9df476..4a66db57b8 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,22 +1,24 @@
<template>
<div class="mk-welcome-timeline">
- <div v-for="note in notes">
- <mk-avatar class="avatar" :user="note.user" target="_blank"/>
- <div class="body">
- <header>
- <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- <div class="info">
- <router-link class="created-at" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
+ <transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div">
+ <div v-for="note in notes" :key="note.id">
+ <mk-avatar class="avatar" :user="note.user" target="_blank"/>
+ <div class="body">
+ <header>
+ <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
+ <span class="username">@{{ note.user | acct }}</span>
+ <div class="info">
+ <router-link class="created-at" :to="note | notePage">
+ <mk-time :time="note.createdAt"/>
+ </router-link>
+ </div>
+ </header>
+ <div class="text">
+ <misskey-flavored-markdown v-if="note.text" :text="note.text"/>
</div>
- </header>
- <div class="text">
- <misskey-flavored-markdown v-if="note.text" :text="note.text"/>
</div>
</div>
- </div>
+ </transition-group>
</div>
</template>
@@ -31,15 +33,27 @@ export default Vue.extend({
default: undefined
}
},
+
data() {
return {
fetching: true,
- notes: []
+ notes: [],
+ connection: null
};
},
+
mounted() {
this.fetch();
+
+ this.connection = (this as any).os.stream.useSharedConnection('localTimeline');
+
+ this.connection.on('note', this.onNote);
+ },
+
+ beforeDestroy() {
+ this.connection.dispose();
},
+
methods: {
fetch(cb?) {
this.fetching = true;
@@ -48,82 +62,92 @@ export default Vue.extend({
local: true,
reply: false,
renote: false,
- media: false,
- poll: false,
- bot: false
+ file: false,
+ poll: false
}).then(notes => {
this.notes = notes;
this.fetching = false;
});
- }
+ },
+
+ onNote(note) {
+ if (note.replyId != null) return;
+ if (note.renoteId != null) return;
+ if (note.poll != null) return;
+
+ this.notes.unshift(note);
+ },
}
});
</script>
<style lang="stylus" scoped>
-root(isDark)
- background isDark ? #282C37 : #fff
+.ldzpakcixzickvggyixyrhqwjaefknon-enter
+.ldzpakcixzickvggyixyrhqwjaefknon-leave-to
+ opacity 0
+ transform translateY(-30px)
- > div
- padding 16px
- overflow-wrap break-word
- font-size .9em
- color isDark ? #fff : #4C4C4C
- border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
+.mk-welcome-timeline
+ background var(--face)
- &:after
- content ""
- display block
- clear both
+ > div
+ > *
+ transition transform .3s ease, opacity .3s ease
- > .avatar
- display block
- float left
- position -webkit-sticky
- position sticky
- top 16px
- width 42px
- height 42px
- border-radius 6px
+ > div
+ padding 16px
+ overflow-wrap break-word
+ font-size .9em
+ color var(--noteText)
+ border-bottom 1px solid var(--faceDivider)
- > .body
- float right
- width calc(100% - 42px)
- padding-left 12px
+ &:after
+ content ""
+ display block
+ clear both
- > header
- display flex
- align-items center
- margin-bottom 4px
- white-space nowrap
+ > .avatar
+ display block
+ float left
+ position -webkit-sticky
+ position sticky
+ top 16px
+ width 42px
+ height 42px
+ border-radius 6px
- > .name
- display block
- margin 0 .5em 0 0
- padding 0
- overflow hidden
- font-weight bold
- text-overflow ellipsis
- color isDark ? #fff : #627079
+ > .body
+ float right
+ width calc(100% - 42px)
+ padding-left 12px
- > .username
- margin 0 .5em 0 0
- color isDark ? #606984 : #ccc
+ > header
+ display flex
+ align-items center
+ margin-bottom 4px
+ white-space nowrap
- > .info
- margin-left auto
- font-size 0.9em
+ > .name
+ display block
+ margin 0 .5em 0 0
+ padding 0
+ overflow hidden
+ font-weight bold
+ text-overflow ellipsis
+ color var(--noteHeaderName)
- > .created-at
- color isDark ? #606984 : #c0c0c0
+ > .username
+ margin 0 .5em 0 0
+ color var(--noteHeaderAcct)
- > .text
- text-align left
+ > .info
+ margin-left auto
+ font-size 0.9em
-.mk-welcome-timeline[data-darkmode]
- root(true)
+ > .created-at
+ color var(--noteHeaderInfo)
-.mk-welcome-timeline:not([data-darkmode])
- root(false)
+ > .text
+ text-align left
</style>
diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts
index b252cf5c1f..f7f8e9bf16 100644
--- a/src/client/app/common/views/directives/autocomplete.ts
+++ b/src/client/app/common/views/directives/autocomplete.ts
@@ -167,7 +167,7 @@ class Autocomplete {
private close() {
if (this.suggestion == null) return;
- this.suggestion.$destroy();
+ this.suggestion.destroyDom();
this.suggestion = null;
this.textarea.focus();
@@ -191,7 +191,7 @@ class Autocomplete {
const acct = renderAcct(value);
// 挿入
- this.text = trimmedBefore + '@' + acct + ' ' + after;
+ this.text = `${trimmedBefore}@${acct} ${after}`;
// キャレットを戻す
this.vm.$nextTick(() => {
@@ -207,7 +207,7 @@ class Autocomplete {
const after = source.substr(caret);
// 挿入
- this.text = trimmedBefore + '#' + value + ' ' + after;
+ this.text = `${trimmedBefore}#${value} ${after}`;
// キャレットを戻す
this.vm.$nextTick(() => {
diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts
index a611dc8685..3c9c8b7485 100644
--- a/src/client/app/common/views/filters/note.ts
+++ b/src/client/app/common/views/filters/note.ts
@@ -1,5 +1,5 @@
import Vue from 'vue';
Vue.filter('notePage', note => {
- return '/notes/' + note.id;
+ return `/notes/${note.id}`;
});
diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts
index ca0910fc53..e5220229b7 100644
--- a/src/client/app/common/views/filters/user.ts
+++ b/src/client/app/common/views/filters/user.ts
@@ -11,5 +11,5 @@ Vue.filter('userName', user => {
});
Vue.filter('userPage', (user, path?) => {
- return '/@' + Vue.filter('acct')(user) + (path ? '/' + path : '');
+ return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
});
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
index 13d855d20a..92f24fb538 100644
--- a/src/client/app/common/views/pages/follow.vue
+++ b/src/client/app/common/views/pages/follow.vue
@@ -1,6 +1,6 @@
<template>
-<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
- <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + myName + '</b>')"></div>
+<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching">
+ <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${myName}`)"></div>
<main>
<div class="banner" :style="bannerStyle"></div>
@@ -19,7 +19,8 @@
@click="onClick"
:disabled="followWait">
<template v-if="!followWait">
- <template v-if="user.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template>
+ <template v-if="user.hasPendingFollowRequestFromYou && user.isLocked">%fa:hourglass-half% %i18n:@request-pending%</template>
+ <template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked">%fa:hourglass-start% %i18n:@follow-processing%</template>
<template v-else-if="user.isFollowing">%fa:minus% %i18n:@following%</template>
<template v-else-if="!user.isFollowing && user.isLocked">%fa:plus% %i18n:@follow-request%</template>
<template v-else-if="!user.isFollowing && !user.isLocked">%fa:plus% %i18n:@follow%</template>
@@ -32,7 +33,6 @@
<script lang="ts">
import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse';
-import getUserName from '../../../../../misc/get-user-name';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
@@ -83,7 +83,7 @@ export default Vue.extend({
userId: this.user.id
});
} else {
- if (this.user.isLocked && this.user.hasPendingFollowRequestFromYou) {
+ if (this.user.hasPendingFollowRequestFromYou) {
this.user = await (this as any).api('following/requests/cancel', {
userId: this.user.id
});
@@ -108,16 +108,14 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.syxhndwprovvuqhmyvveewmbqayniwkv
padding 32px
max-width 500px
margin 0 auto
text-align center
- color isDark ? #9baec8 : #868c8c
+ color var(--text)
- $bg = isDark ? #282C37 : #fff
+ $bg = var(--face)
@media (max-width 400px)
padding 16px
@@ -125,7 +123,6 @@ root(isDark)
> .signed-in-as
margin-bottom 16px
font-size 14px
- color isDark ? #9baec8 : #9daab3
> main
margin-bottom 16px
@@ -174,29 +171,29 @@ root(isDark)
min-width 150px
font-size 14px
font-weight bold
- color $theme-color
+ color var(--primary)
background transparent
outline none
- border solid 1px $theme-color
+ border solid 1px var(--primary)
border-radius 36px
&:hover
- background rgba($theme-color, 0.1)
+ background var(--primaryAlpha01)
&:active
- background rgba($theme-color, 0.2)
+ background var(--primaryAlpha02)
&.active
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
&:hover
- background lighten($theme-color, 10%)
- border-color lighten($theme-color, 10%)
+ background var(--primaryLighten10)
+ border-color var(--primaryLighten10)
&:active
- background darken($theme-color, 10%)
- border-color darken($theme-color, 10%)
+ background var(--primaryDarken10)
+ border-color var(--primaryDarken10)
&.wait
cursor wait !important
@@ -205,10 +202,4 @@ root(isDark)
*
pointer-events none
-.syxhndwprovvuqhmyvveewmbqayniwkv[data-darkmode]
- root(true)
-
-.syxhndwprovvuqhmyvveewmbqayniwkv:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue
index 0de30228b3..cfcdd5a1b6 100644
--- a/src/client/app/common/views/widgets/analog-clock.vue
+++ b/src/client/app/common/views/widgets/analog-clock.vue
@@ -1,8 +1,8 @@
<template>
<div class="mkw-analog-clock">
- <mk-widget-container :naked="!(props.design % 2)" :show-header="false">
+ <mk-widget-container :naked="props.style % 2 === 0" :show-header="false">
<div class="mkw-analog-clock--body">
- <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/>
+ <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/>
</div>
</mk-widget-container>
</div>
@@ -13,13 +13,12 @@ import define from '../../../common/define-widget';
export default define({
name: 'analog-clock',
props: () => ({
- design: -1
+ style: 0
})
}).extend({
methods: {
func() {
- if (++this.props.design > 2)
- this.props.design = -1;
+ this.props.style = (this.props.style + 1) % 4;
this.save();
}
}
@@ -27,16 +26,8 @@ export default define({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mkw-analog-clock
.mkw-analog-clock--body
padding 8px
-.mkw-analog-clock[data-darkmode]
- root(true)
-
-.mkw-analog-clock:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 69b2a54fe9..620b09ff0e 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -1,31 +1,34 @@
<template>
-<div class="mkw-broadcast"
- :data-found="broadcasts.length != 0"
- :data-melt="props.design == 1"
- :data-mobile="platform == 'mobile'"
->
- <div class="icon">
- <svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
- <path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path>
- <path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path>
- <path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path>
- <path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path>
- <path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
- </svg>
- </div>
- <p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
- <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1>
- <p v-if="!fetching">
- <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
- <template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template>
- </p>
- <a v-if="broadcasts.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
+<div class="anltbovirfeutcigvwgmgxipejaeozxi">
+ <mk-widget-container :show-header="false" :naked="props.design == 1">
+ <div class="anltbovirfeutcigvwgmgxipejaeozxi-body"
+ :data-found="announcements && announcements.length != 0"
+ :data-melt="props.design == 1"
+ :data-mobile="platform == 'mobile'"
+ >
+ <div class="icon">
+ <svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
+ <path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path>
+ <path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path>
+ <path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path>
+ <path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path>
+ <path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
+ </svg>
+ </div>
+ <p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
+ <h1 v-if="!fetching">{{ announcements.length == 0 ? '%i18n:@no-broadcasts%' : announcements[i].title }}</h1>
+ <p v-if="!fetching">
+ <span v-if="announcements.length != 0" v-html="announcements[i].text"></span>
+ <template v-if="announcements.length == 0">%i18n:@have-a-nice-day%</template>
+ </p>
+ <a v-if="announcements.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
+ </div>
+ </mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
-import { lang } from '../../../config';
export default define({
name: 'broadcast',
@@ -37,26 +40,18 @@ export default define({
return {
i: 0,
fetching: true,
- broadcasts: []
+ announcements: []
};
},
mounted() {
(this as any).os.getMeta().then(meta => {
- let broadcasts = [];
- if (meta.broadcasts) {
- meta.broadcasts.forEach(broadcast => {
- if (broadcast[lang]) {
- broadcasts.push(broadcast[lang]);
- }
- });
- }
- this.broadcasts = broadcasts;
+ this.announcements = meta.broadcasts;
this.fetching = false;
});
},
methods: {
next() {
- if (this.i == this.broadcasts.length - 1) {
+ if (this.i == this.announcements.length - 1) {
this.i = 0;
} else {
this.i++;
@@ -75,13 +70,12 @@ export default define({
</script>
<style lang="stylus" scoped>
-.mkw-broadcast
+.anltbovirfeutcigvwgmgxipejaeozxi-body
padding 10px
- border solid 1px #4078c0
- border-radius 6px
+ background var(--announcementsBg)
&[data-melt]
- border none
+ background transparent
&[data-found]
padding-left 50px
@@ -135,22 +129,18 @@ export default define({
margin 0
font-size 0.95em
font-weight normal
- color #4078c0
+ color var(--announcementsTitle)
> p
display block
z-index 1
margin 0
font-size 0.7em
- color #555
+ color var(--announcementsText)
&.fetching
text-align center
- a
- color #555
- text-decoration underline
-
> a
display block
font-size 0.7em
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index eb15030370..308d73bad8 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -116,15 +116,13 @@ export default define({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mkw-calendar
&[data-special='on-new-years-day']
border-color #ef95a0
.mkw-calendar--body
padding 16px 0
- color isDark ? #c5ced6 : #777
+ color var(--calendarDay)
&:after
content ""
@@ -169,7 +167,8 @@ root(isDark)
margin 0 0 2px 0
font-size 12px
line-height 18px
- color isDark ? #7a8692 : #888
+ color var(--text)
+ opacity 0.8
> b
margin-left 2px
@@ -177,12 +176,12 @@ root(isDark)
> .meter
width 100%
overflow hidden
- background isDark ? #1c1f25 : #eee
+ background var(--materBg)
border-radius 8px
> .val
height 4px
- background $theme-color
+ background var(--primary)
transition width .3s cubic-bezier(0.23, 1, 0.32, 1)
&:nth-child(1)
@@ -197,10 +196,4 @@ root(isDark)
> .meter > .val
background #41ddde
-.mkw-calendar[data-darkmode]
- root(true)
-
-.mkw-calendar:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index 544ca1bd9d..b025b41e7d 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -1,13 +1,15 @@
<template>
-<div class="mkw-donation" :data-mobile="platform == 'mobile'">
- <article>
- <h1>%fa:heart%%i18n:@title%</h1>
- <p v-if="meta">
- {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
- <a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a>
- {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
- </p>
- </article>
+<div>
+ <mk-widget-container :show-header="false">
+ <article class="dolfvtibguprpxxhfndqaosjitixjohx">
+ <h1>%fa:heart%%i18n:@title%</h1>
+ <p v-if="meta">
+ {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
+ <a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a>
+ {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
+ </p>
+ </article>
+ </mk-widget-container>
</div>
</template>
@@ -30,46 +32,22 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
- background isDark ? #282c37 : #fff
- border solid 1px isDark ? #c3831c : #ead8bb
- border-radius 6px
+.dolfvtibguprpxxhfndqaosjitixjohx
+ padding 20px
+ background var(--donationBg)
+ color var(--donationFg)
- > article
- padding 20px
+ > h1
+ margin 0 0 5px 0
+ font-size 1em
- > h1
- margin 0 0 5px 0
- font-size 1em
- color isDark ? #b2bac1 : #888
+ > [data-fa]
+ margin-right 0.25em
- > [data-fa]
- margin-right 0.25em
-
- > p
- display block
- z-index 1
- margin 0
- font-size 0.8em
- color isDark ? #a1a6ab : #999
-
- &[data-mobile]
- border none
- background #ead8bb
- border-radius 8px
- box-shadow 0 0 0 1px rgba(#000, 0.2)
-
- > article
- > h1
- color #7b8871
-
- > p
- color #777d71
-
-.mkw-donation[data-darkmode]
- root(true)
-
-.mkw-donation:not([data-darkmode])
- root(false)
+ > p
+ display block
+ z-index 1
+ margin 0
+ font-size 0.8em
</style>
diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue
index 56520400b6..0cb6b2df10 100644
--- a/src/client/app/common/views/widgets/hashtags.vue
+++ b/src/client/app/common/views/widgets/hashtags.vue
@@ -4,20 +4,7 @@
<template slot="header">%fa:hashtag%%i18n:@title%</template>
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
- <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
- <!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group v-else tag="div" name="chart"> -->
- <div>
- <div v-for="stat in stats" :key="stat.tag">
- <div class="tag">
- <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
- <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
- </div>
- <x-chart class="chart" :src="stat.chart"/>
- </div>
- </div>
- <!-- </transition-group> -->
+ <mk-trends/>
</div>
</mk-widget-container>
</div>
@@ -25,7 +12,6 @@
<script lang="ts">
import define from '../../../common/define-widget';
-import XChart from './hashtags.chart.vue';
export default define({
name: 'hashtags',
@@ -33,89 +19,11 @@ export default define({
compact: false
})
}).extend({
- components: {
- XChart
- },
- data() {
- return {
- stats: [],
- fetching: true,
- clock: null
- };
- },
- mounted() {
- this.fetch();
- this.clock = setInterval(this.fetch, 1000 * 60);
- },
- beforeDestroy() {
- clearInterval(this.clock);
- },
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
- },
- fetch() {
- (this as any).api('hashtags/trend').then(stats => {
- this.stats = stats;
- this.fetching = false;
- });
}
}
});
</script>
-
-<style lang="stylus" scoped>
-root(isDark)
- .mkw-hashtags--body
- > .fetching
- > .empty
- margin 0
- padding 16px
- text-align center
- color #aaa
-
- > [data-fa]
- margin-right 4px
-
- > div
- .chart-move
- transition transform 1s ease
-
- > div
- display flex
- align-items center
- padding 14px 16px
-
- &:not(:last-child)
- border-bottom solid 1px isDark ? #393f4f : #eee
-
- > .tag
- flex 1
- overflow hidden
- font-size 14px
- color isDark ? #9baec8 : #65727b
-
- > a
- display block
- width 100%
- white-space nowrap
- overflow hidden
- text-overflow ellipsis
- color inherit
-
- > p
- margin 0
- font-size 75%
- opacity 0.7
-
- > .chart
- height 30px
-
-.mkw-hashtags[data-darkmode]
- root(true)
-
-.mkw-hashtags:not([data-darkmode])
- root(false)
-
-</style>
diff --git a/src/client/app/common/views/widgets/memo.vue b/src/client/app/common/views/widgets/memo.vue
index 30f0d3b009..be8b18a4e9 100644
--- a/src/client/app/common/views/widgets/memo.vue
+++ b/src/client/app/common/views/widgets/memo.vue
@@ -57,9 +57,7 @@ export default define({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mkw-memo
.mkw-memo--body
padding-bottom 28px + 16px
@@ -69,10 +67,10 @@ root(isDark)
max-width 100%
min-width 100%
padding 16px
- color isDark ? #fff : #222
- background isDark ? #282c37 : #fff
+ color var(--inputText)
+ background var(--face)
border none
- border-bottom solid 1px isDark ? #1c2023 : #eee
+ border-bottom solid 1px var(--faceDivider)
border-radius 0
> button
@@ -83,8 +81,8 @@ root(isDark)
margin 0
padding 0 10px
height 28px
- color $theme-color-foreground
- background $theme-color !important
+ color var(--primaryForeground)
+ background var(--primary) !important
outline none
border none
border-radius 4px
@@ -92,20 +90,14 @@ root(isDark)
cursor pointer
&:hover
- background lighten($theme-color, 10%) !important
+ background var(--primaryLighten10) !important
&:active
- background darken($theme-color, 10%) !important
+ background var(--primaryDarken10) !important
transition background 0s ease
&:disabled
opacity 0.7
cursor default
-.mkw-memo[data-darkmode]
- root(true)
-
-.mkw-memo:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/nav.vue b/src/client/app/common/views/widgets/nav.vue
index 0cbf7c158e..12003db3f2 100644
--- a/src/client/app/common/views/widgets/nav.vue
+++ b/src/client/app/common/views/widgets/nav.vue
@@ -16,23 +16,17 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mkw-nav
.mkw-nav--body
padding 16px
font-size 12px
- color isDark ? #9aa4b3 : #aaa
- background isDark ? #282c37 : #fff
+ color var(--text)
+ background var(--face)
a
- color isDark ? #9aa4b3 : #999
+ color var(--text)
i
- color isDark ? #9aa4b3 : #ccc
-
-.mkw-nav[data-darkmode]
- root(true)
-
-.mkw-nav:not([data-darkmode])
- root(false)
+ color var(--text)
</style>
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
index 3e24c58e8e..047b01df4f 100644
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ b/src/client/app/common/views/widgets/photo-stream.vue
@@ -24,15 +24,13 @@ export default define({
return {
images: [],
fetching: true,
- connection: null,
- connectionId: null
+ connection: null
};
},
mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
- this.connection.on('drive_file_created', this.onDriveFileCreated);
+ this.connection.on('driveFileCreated', this.onDriveFileCreated);
(this as any).api('drive/stream', {
type: 'image/*',
@@ -43,8 +41,7 @@ export default define({
});
},
beforeDestroy() {
- this.connection.off('drive_file_created', this.onDriveFileCreated);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
onDriveFileCreated(file) {
diff --git a/src/client/app/common/views/widgets/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue
index 801307be54..1c70e6dbc4 100644
--- a/src/client/app/common/views/widgets/posts-monitor.vue
+++ b/src/client/app/common/views/widgets/posts-monitor.vue
@@ -4,7 +4,7 @@
<template slot="header">%fa:chart-line%%i18n:@title%</template>
<button slot="func" @click="toggle" title="%i18n:@toggle%">%fa:sort%</button>
- <div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }" :data-darkmode="$store.state.device.darkmode">
+ <div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2">
<defs>
<linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0">
@@ -82,7 +82,6 @@ export default define({
data() {
return {
connection: null,
- connectionId: null,
viewBoxY: 30,
stats: [],
fediGradientId: uuid(),
@@ -110,8 +109,7 @@ export default define({
}
},
mounted() {
- this.connection = (this as any).os.streams.notesStatsStream.getConnection();
- this.connectionId = (this as any).os.streams.notesStatsStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('notesStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
@@ -121,9 +119,7 @@ export default define({
});
},
beforeDestroy() {
- this.connection.off('stats', this.onStats);
- this.connection.off('statsLog', this.onStatsLog);
- (this as any).os.streams.notesStatsStream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
toggle() {
@@ -173,7 +169,7 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.qpdmibaztplkylerhdbllwcokyrfxeyj
&.dual
> svg
width 50%
@@ -192,7 +188,7 @@ root(isDark)
> text
font-size 5px
- fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
+ fill var(--chartCaption)
> tspan
opacity 0.5
@@ -202,10 +198,4 @@ root(isDark)
display block
clear both
-.qpdmibaztplkylerhdbllwcokyrfxeyj[data-darkmode]
- root(true)
-
-.qpdmibaztplkylerhdbllwcokyrfxeyj:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index a777388cdb..448eee9fb6 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -65,7 +65,7 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mkw-rss
.mkw-rss--body
.feed
padding 12px 16px
@@ -74,8 +74,8 @@ root(isDark)
> a
display block
padding 4px 0
- color isDark ? #9aa4b3 : #666
- border-bottom dashed 1px isDark ? #1c2023 : #eee
+ color var(--text)
+ border-bottom dashed 1px var(--faceDivider)
&:last-child
border-bottom none
@@ -90,7 +90,7 @@ root(isDark)
margin-right 4px
&[data-mobile]
- background isDark ? #21242f : #f3f3f3
+ background var(--face)
.feed
padding 0
@@ -100,12 +100,6 @@ root(isDark)
border-bottom none
&:nth-child(even)
- background isDark ? rgba(#000, 0.05) : rgba(#fff, 0.7)
-
-.mkw-rss[data-darkmode]
- root(true)
-
-.mkw-rss:not([data-darkmode])
- root(false)
+ background rgba(#000, 0.05)
</style>
diff --git a/src/client/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue
index b0421d6150..55aa1ea895 100644
--- a/src/client/app/common/views/widgets/server.cpu-memory.vue
+++ b/src/client/app/common/views/widgets/server.cpu-memory.vue
@@ -129,7 +129,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.cpu-memory
> svg
display block
padding 10px
@@ -144,7 +144,7 @@ root(isDark)
> text
font-size 5px
- fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
+ fill var(--chartCaption)
> tspan
opacity 0.5
@@ -154,10 +154,4 @@ root(isDark)
display block
clear both
-.cpu-memory[data-darkmode]
- root(true)
-
-.cpu-memory:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue
index b9748bdf7c..2034aee0eb 100644
--- a/src/client/app/common/views/widgets/server.cpu.vue
+++ b/src/client/app/common/views/widgets/server.cpu.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.cpu
> .pie
padding 10px
height 100px
@@ -52,7 +52,7 @@ root(isDark)
> p
margin 0
font-size 12px
- color isDark ? #a8b4bd : #505050
+ color var(--chartCaption)
&:first-child
font-weight bold
@@ -65,10 +65,4 @@ root(isDark)
display block
clear both
-.cpu[data-darkmode]
- root(true)
-
-.cpu:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue
index 99ce624051..667576ab76 100644
--- a/src/client/app/common/views/widgets/server.disk.vue
+++ b/src/client/app/common/views/widgets/server.disk.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.disk
> .pie
padding 10px
height 100px
@@ -60,7 +60,7 @@ root(isDark)
> p
margin 0
font-size 12px
- color isDark ? #a8b4bd : #505050
+ color var(--chartCaption)
&:first-child
font-weight bold
@@ -73,10 +73,4 @@ root(isDark)
display block
clear both
-.disk[data-darkmode]
- root(true)
-
-.disk:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/server.memory.vue b/src/client/app/common/views/widgets/server.memory.vue
index 8a60621343..9e12884cf9 100644
--- a/src/client/app/common/views/widgets/server.memory.vue
+++ b/src/client/app/common/views/widgets/server.memory.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.memory
> .pie
padding 10px
height 100px
@@ -60,7 +60,7 @@ root(isDark)
> p
margin 0
font-size 12px
- color isDark ? #a8b4bd : #505050
+ color var(--chartCaption)
&:first-child
font-weight bold
@@ -73,10 +73,4 @@ root(isDark)
display block
clear both
-.memory[data-darkmode]
- root(true)
-
-.memory:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue
index d557c52ea5..ce342fd41b 100644
--- a/src/client/app/common/views/widgets/server.pie.vue
+++ b/src/client/app/common/views/widgets/server.pie.vue
@@ -45,7 +45,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+svg
display block
height 100%
@@ -56,12 +56,6 @@ root(isDark)
> text
font-size 0.15px
- fill isDark ? rgba(#fff, 0.6) : rgba(#000, 0.6)
-
-svg[data-darkmode]
- root(true)
-
-svg:not([data-darkmode])
- root(false)
+ fill var(--chartCaption)
</style>
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
index d796a3ae05..62d75e2bf6 100644
--- a/src/client/app/common/views/widgets/server.vue
+++ b/src/client/app/common/views/widgets/server.vue
@@ -45,8 +45,7 @@ export default define({
return {
fetching: true,
meta: null,
- connection: null,
- connectionId: null
+ connection: null
};
},
mounted() {
@@ -55,11 +54,10 @@ export default define({
this.fetching = false;
});
- this.connection = (this as any).os.streams.serverStatsStream.getConnection();
- this.connectionId = (this as any).os.streams.serverStatsStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('serverStats');
},
beforeDestroy() {
- (this as any).os.streams.serverStatsStream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
toggle() {
diff --git a/src/client/app/config.ts b/src/client/app/config.ts
index 74b9ea21c8..c3bc427eab 100644
--- a/src/client/app/config.ts
+++ b/src/client/app/config.ts
@@ -4,6 +4,7 @@ declare const _THEME_COLOR_: string;
declare const _COPYRIGHT_: string;
declare const _VERSION_: string;
declare const _CODENAME_: string;
+declare const _ENV_: string;
const address = new URL(location.href);
@@ -11,10 +12,11 @@ export const host = address.host;
export const hostname = address.hostname;
export const url = address.origin;
export const apiUrl = url + '/api';
-export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://');
+export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
export const lang = _LANG_;
export const langs = _LANGS_;
export const themeColor = _THEME_COLOR_;
export const copyright = _COPYRIGHT_;
export const version = _VERSION_;
export const codename = _CODENAME_;
+export const env = _ENV_;
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
index e9d92d1eb1..f08e8a2b4e 100644
--- a/src/client/app/desktop/api/update-avatar.ts
+++ b/src/client/app/desktop/api/update-avatar.ts
@@ -16,7 +16,7 @@ export default (os: OS) => {
text: '%i18n:common.got-it%'
}]
});
- reject();
+ return reject('invalid-filetype');
}
const w = os.new(CropWindow, {
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index e8fa35149b..42c9d69349 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -16,7 +16,7 @@ export default (os: OS) => {
text: '%i18n:common.got-it%'
}]
});
- reject();
+ return reject('invalid-filetype');
}
const w = os.new(CropWindow, {
diff --git a/src/client/app/desktop/assets/header-icon.light.svg b/src/client/app/desktop/assets/header-icon.light.svg
deleted file mode 100644
index 61e2026243..0000000000
--- a/src/client/app/desktop/assets/header-icon.light.svg
+++ /dev/null
@@ -1,150 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="512"
- height="512"
- viewBox="0 0 135.46667 135.46667"
- version="1.1"
- id="svg8"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="header-icon.light.svg"
- inkscape:export-filename="C:\Users\syuilo\projects\misskey\assets\favicon\32.png"
- inkscape:export-xdpi="6"
- inkscape:export-ydpi="6">
- <defs
- id="defs2">
- <inkscape:path-effect
- effect="simplify"
- id="path-effect5115"
- is_visible="true"
- steps="1"
- threshold="0.000408163"
- smooth_angles="360"
- helper_size="0"
- simplify_individual_paths="false"
- simplify_just_coalesce="false"
- simplifyindividualpaths="false"
- simplifyJustCoalesce="false" />
- <inkscape:path-effect
- effect="simplify"
- id="path-effect5111"
- is_visible="true"
- steps="1"
- threshold="0.000408163"
- smooth_angles="360"
- helper_size="0"
- simplify_individual_paths="false"
- simplify_just_coalesce="false"
- simplifyindividualpaths="false"
- simplifyJustCoalesce="false" />
- <inkscape:path-effect
- effect="simplify"
- id="path-effect5104"
- is_visible="true"
- steps="1"
- threshold="0.000408163"
- smooth_angles="360"
- helper_size="0"
- simplify_individual_paths="false"
- simplify_just_coalesce="false"
- simplifyindividualpaths="false"
- simplifyJustCoalesce="false" />
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0.0"
- inkscape:pageshadow="2"
- inkscape:zoom="1.4142136"
- inkscape:cx="114.309"
- inkscape:cy="251.50613"
- inkscape:document-units="px"
- inkscape:current-layer="g4502"
- showgrid="true"
- units="px"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="false"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-center="true"
- inkscape:snap-page="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="1072"
- inkscape:window-maximized="1"
- inkscape:snap-object-midpoints="true"
- inkscape:snap-midpoints="true"
- inkscape:object-paths="true"
- fit-margin-top="0"
- fit-margin-left="0"
- fit-margin-right="0"
- fit-margin-bottom="0"
- objecttolerance="1"
- guidetolerance="1"
- inkscape:snap-nodes="false"
- inkscape:snap-others="false">
- <inkscape:grid
- type="xygrid"
- id="grid4504"
- spacingx="4.2333334"
- spacingy="4.2333334"
- empcolor="#ff3fff"
- empopacity="0.25098039"
- empspacing="4" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="レイヤー 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(-30.809093,-111.78601)">
- <g
- id="g4502"
- transform="matrix(1.096096,0,0,1.096096,-2.960633,-44.023579)">
- <g
- style="fill:#000000;fill-opacity:1"
- transform="translate(-1.3333333e-6,-1.3439941e-6)"
- id="g5125">
- <g
- transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)"
- id="text4489"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- aria-label="Mi">
- <path
- sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz"
- inkscape:connector-curvature="0"
- id="path5210"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#000000;fill-opacity:1;stroke-width:0.28950602px"
- d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" />
- <path
- inkscape:connector-curvature="0"
- id="path5212"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#000000;fill-opacity:1;stroke-width:0.28950602px"
- d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" />
- </g>
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/client/app/desktop/assets/header-icon.dark.svg b/src/client/app/desktop/assets/header-icon.svg
index fa42856fa5..d677d2d163 100644
--- a/src/client/app/desktop/assets/header-icon.dark.svg
+++ b/src/client/app/desktop/assets/header-icon.svg
@@ -124,24 +124,24 @@
id="g4502"
transform="matrix(1.096096,0,0,1.096096,-2.960633,-44.023579)">
<g
- style="fill:#ffffff;fill-opacity:1"
+ style="fill-opacity:1"
transform="translate(-1.3333333e-6,-1.3439941e-6)"
id="g5125">
<g
transform="matrix(0.91391326,0,0,0.91391326,7.9719907,17.595761)"
id="text4489"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.28950602px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
aria-label="Mi">
<path
sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz"
inkscape:connector-curvature="0"
id="path5210"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#ffffff;fill-opacity:1;stroke-width:0.28950602px"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px"
d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" />
<path
inkscape:connector-curvature="0"
id="path5212"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#ffffff;fill-opacity:1;stroke-width:0.28950602px"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.28950602px"
d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" />
</g>
</g>
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index f0e8a42662..85c81d73a2 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -6,11 +6,9 @@ import VueRouter from 'vue-router';
// Style
import './style.styl';
-import '../../element.scss';
import init from '../init';
import fuckAdBlock from '../common/scripts/fuck-ad-block';
-import { HomeStreamManager } from '../common/scripts/streaming/home';
import composeNotification from '../common/scripts/compose-notification';
import chooseDriveFolder from './api/choose-drive-folder';
@@ -30,7 +28,6 @@ import MkUser from './views/pages/user/user.vue';
import MkFavorites from './views/pages/favorites.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
-import MkUserList from './views/pages/user-list.vue';
import MkHomeCustomize from './views/pages/home-customize.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
@@ -39,6 +36,7 @@ import MkTag from './views/pages/tag.vue';
import MkReversi from './views/pages/games/reversi.vue';
import MkShare from './views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
+import MiOS from '../mios';
/**
* init
@@ -64,7 +62,6 @@ init(async (launch) => {
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },
- { path: '/i/lists/:list', component: MkUserList },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
@@ -88,10 +85,12 @@ init(async (launch) => {
updateBanner: updateBanner(os)
}));
- /**
- * Fuck AD Block
- */
- fuckAdBlock(os);
+ if (os.store.getters.isSignedIn) {
+ /**
+ * Fuck AD Block
+ */
+ fuckAdBlock(os);
+ }
/**
* Init Notification
@@ -103,62 +102,56 @@ init(async (launch) => {
}
if ((Notification as any).permission == 'granted') {
- registerNotifications(os.stream);
+ registerNotifications(os);
}
}
}, true);
-function registerNotifications(stream: HomeStreamManager) {
- if (stream == null) return;
+function registerNotifications(os: MiOS) {
+ const stream = os.stream;
- if (stream.hasConnection) {
- attach(stream.borrow());
- }
+ if (stream == null) return;
- stream.on('connected', connection => {
- attach(connection);
- });
+ const connection = stream.useSharedConnection('main');
- function attach(connection) {
- connection.on('notification', notification => {
- const _n = composeNotification('notification', notification);
- const n = new Notification(_n.title, {
- body: _n.body,
- icon: _n.icon
- });
- setTimeout(n.close.bind(n), 6000);
+ connection.on('notification', notification => {
+ const _n = composeNotification('notification', notification);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
});
+ setTimeout(n.close.bind(n), 6000);
+ });
- connection.on('drive_file_created', file => {
- const _n = composeNotification('drive_file_created', file);
- const n = new Notification(_n.title, {
- body: _n.body,
- icon: _n.icon
- });
- setTimeout(n.close.bind(n), 5000);
+ connection.on('driveFileCreated', file => {
+ const _n = composeNotification('driveFileCreated', file);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
});
+ setTimeout(n.close.bind(n), 5000);
+ });
- connection.on('unread_messaging_message', message => {
- const _n = composeNotification('unread_messaging_message', message);
- const n = new Notification(_n.title, {
- body: _n.body,
- icon: _n.icon
- });
- n.onclick = () => {
- n.close();
- /*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
- user: message.user
- });*/
- };
- setTimeout(n.close.bind(n), 7000);
+ connection.on('unreadMessagingMessage', message => {
+ const _n = composeNotification('unreadMessagingMessage', message);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
});
+ n.onclick = () => {
+ n.close();
+ /*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
+ user: message.user
+ });*/
+ };
+ setTimeout(n.close.bind(n), 7000);
+ });
- connection.on('reversi_invited', matching => {
- const _n = composeNotification('reversi_invited', matching);
- const n = new Notification(_n.title, {
- body: _n.body,
- icon: _n.icon
- });
+ connection.on('reversiInvited', matching => {
+ const _n = composeNotification('reversiInvited', matching);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
});
- }
+ });
}
diff --git a/src/client/app/desktop/style.styl b/src/client/app/desktop/style.styl
index 3cd36482e4..96481a9808 100644
--- a/src/client/app/desktop/style.styl
+++ b/src/client/app/desktop/style.styl
@@ -1,8 +1,6 @@
@import "../app"
@import "../reset"
-@import "./ui"
-
*::input-placeholder
color #D8CBC5
@@ -11,34 +9,21 @@
html
height 100%
- background #f7f7f7
+ background var(--bg)
&, *
&::-webkit-scrollbar
width 6px
height 6px
+ &::-webkit-scrollbar-track
+ background var(--scrollbarTrack)
+
&::-webkit-scrollbar-thumb
- background rgba(0, 0, 0, 0.2)
+ background var(--scrollbarHandle)
&:hover
- background rgba(0, 0, 0, 0.4)
+ background var(--scrollbarHandleHover)
&:active
- background $theme-color
-
- &[data-darkmode]
- background #191B22
-
- &, *
- &::-webkit-scrollbar-track
- background-color #282C37
-
- &::-webkit-scrollbar-thumb
- background-color #454954
-
- &:hover
- background-color #535660
-
- &:active
- background-color $theme-color
+ background var(--primary)
diff --git a/src/client/app/desktop/ui.styl b/src/client/app/desktop/ui.styl
deleted file mode 100644
index b66c8f4025..0000000000
--- a/src/client/app/desktop/ui.styl
+++ /dev/null
@@ -1,181 +0,0 @@
-@import "../../const"
-
-button
- font-family sans-serif
-
- *
- pointer-events none
-
-button.ui
-.button.ui
- display inline-block
- cursor pointer
- padding 0 14px
- margin 0
- min-width 100px
- line-height 38px
- font-size 14px
- color #888
- text-decoration none
- background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
- border solid 1px #e2e2e2
- border-radius 4px
- outline none
-
- &.block
- display block
-
- &:focus
- &:after
- content ""
- pointer-events none
- position absolute
- top -5px
- right -5px
- bottom -5px
- left -5px
- border 2px solid rgba($theme-color, 0.3)
- border-radius 8px
-
- &:disabled
- opacity 0.7
- cursor default
-
- &:hover
- background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
- border-color #dcdcdc
-
- &:active
- background #ececec
- border-color #dcdcdc
-
- &.primary
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
-
- &:not(:disabled)
- font-weight bold
-
- &:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
-
- &:active:not(:disabled)
- background $theme-color
- border-color $theme-color
-
-input:not([type]).ui
-input[type='text'].ui
-input[type='password'].ui
-input[type='email'].ui
-input[type='date'].ui
-input[type='number'].ui
-textarea.ui
- display block
- padding 10px
- width 100%
- height 40px
- font-family sans-serif
- font-size 16px
- color #55595c
- border solid 1px #dadada
- border-radius 4px
-
- &:hover
- border-color #b0b0b0
-
- &:focus
- border-color $theme-color
-
-textarea.ui
- min-width 100%
- max-width 100%
- min-height 64px
-
-.ui.info
- display block
- margin 1em 0
- padding 0 1em
- font-size 90%
- color rgba(#000, 0.87)
- background #f8f8f9
- border solid 1px rgba(34, 36, 38, 0.22)
- border-radius 4px
-
- > p
- opacity 0.8
-
- > [data-fa]:first-child
- margin-right 0.25em
-
- &.warn
- color #573a08
- background #FFFAF3
- border-color #C9BA9B
-
-.ui.from.group
- display block
- margin 16px 0
-
- > p:first-child
- margin 0 0 6px 0
- font-size 90%
- font-weight bold
- color rgba(#373a3c, 0.9)
-
-html[data-darkmode]
- button.ui
- .button.ui
- color #fff
- background linear-gradient(to bottom, #313543 0%, #282c37 100%)
- border-color #1c2023
-
- &:hover
- background linear-gradient(to bottom, #2c2f3c 0%, #22262f 100%)
- border-color #151a1d
-
- &:active
- background #22262f
- border-color #151a1d
-
- &.primary
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
-
- &:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
-
- &:active:not(:disabled)
- background $theme-color
- border-color $theme-color
-
- input:not([type]).ui
- input[type='text'].ui
- input[type='password'].ui
- input[type='email'].ui
- input[type='date'].ui
- input[type='number'].ui
- textarea.ui
- display block
- padding 10px
- width 100%
- height 40px
- font-family sans-serif
- font-size 16px
- color #dee4e8
- background #191b22
- border solid 1px #495156
- border-radius 4px
-
- &:hover
- border-color #b0b0b0
-
- &:focus
- border-color $theme-color
-
- .ui.from.group
- > p:first-child
- color #c0c7cc
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
index de9650b21b..e2f1329b3b 100644
--- a/src/client/app/desktop/views/components/calendar.vue
+++ b/src/client/app/desktop/views/components/calendar.vue
@@ -128,13 +128,11 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- color isDark ? #c5ced6 : #777
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
+.mk-calendar
+ color var(--calendarDay)
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
overflow hidden
&[data-melt]
@@ -149,12 +147,10 @@ root(isDark)
line-height 42px
font-size 0.9em
font-weight bold
- color isDark ? #c5ced6 : #888
+ color var(--faceHeaderText)
+ background var(--faceHeader)
box-shadow 0 1px rgba(#000, 0.07)
- if isDark
- background #313543
-
> [data-fa]
margin-right 4px
@@ -166,13 +162,13 @@ root(isDark)
width 42px
font-size 0.9em
line-height 42px
- color isDark ? #9baec8 : #ccc
+ color var(--faceTextButton)
&:hover
- color isDark ? #b2c1d5 : #aaa
+ color var(--faceTextButtonHover)
&:active
- color isDark ? #b2c1d5 : #999
+ color var(--faceTextButtonActive)
&:first-of-type
left 0
@@ -195,65 +191,56 @@ root(isDark)
font-size 14px
&.weekday
- color isDark ? #43d5dc : #19a2a9
+ color var(--calendarWeek)
&[data-is-donichi]
- color isDark ? #ff6679 : #ef95a0
+ color var(--calendarSaturdayOrSunday)
&[data-today]
- box-shadow 0 0 0 1px isDark ? #43d5dc : #19a2a9 inset
+ box-shadow 0 0 0 1px var(--calendarWeek) inset
border-radius 6px
&[data-is-donichi]
- box-shadow 0 0 0 1px isDark ? #ff6679 : #ef95a0 inset
+ box-shadow 0 0 0 1px var(--calendarSaturdayOrSunday) inset
&.day
cursor pointer
- color isDark ? #c5ced6 : #777
+ color var(--calendarDay)
> div
border-radius 6px
&:hover > div
- background rgba(#000, isDark ? 0.1 : 0.025)
+ background var(--faceClearButtonHover)
&:active > div
- background rgba(#000, isDark ? 0.2 : 0.05)
+ background var(--faceClearButtonActive)
&[data-is-donichi]
- color isDark ? #ff6679 : #ef95a0
+ color var(--calendarSaturdayOrSunday)
&[data-is-out-of-range]
cursor default
- color rgba(isDark ? #c5ced6 : #777, 0.5)
-
- &[data-is-donichi]
- color rgba(isDark ? #ff6679 : #ef95a0, 0.5)
+ opacity 0.5
&[data-selected]
font-weight bold
> div
- background rgba(#000, isDark ? 0.1 : 0.025)
+ background var(--faceClearButtonHover)
&:active > div
- background rgba(#000, isDark ? 0.2 : 0.05)
+ background var(--faceClearButtonActive)
&[data-today]
> div
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
&:hover > div
- background lighten($theme-color, 10%)
+ background var(--primaryLighten10)
&:active > div
- background darken($theme-color, 10%)
-
-.mk-calendar[data-darkmode]
- root(true)
-
-.mk-calendar:not([data-darkmode])
- root(false)
+ background var(--primaryDarken10)
</style>
diff --git a/src/client/app/desktop/views/components/charts.vue b/src/client/app/desktop/views/components/charts.vue
index c4e92e429f..6d6f3a3596 100644
--- a/src/client/app/desktop/views/components/charts.vue
+++ b/src/client/app/desktop/views/components/charts.vue
@@ -19,6 +19,11 @@
<option value="drive">%i18n:@charts.drive%</option>
<option value="drive-total">%i18n:@charts.drive-total%</option>
</optgroup>
+ <optgroup label="%i18n:@network%">
+ <option value="network-requests">%i18n:@charts.network-requests%</option>
+ <option value="network-time">%i18n:@charts.network-time%</option>
+ <option value="network-usage">%i18n:@charts.network-usage%</option>
+ </optgroup>
</select>
<div>
<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
@@ -41,7 +46,10 @@ const colors = {
localPlus: 'rgb(52, 178, 118)',
remotePlus: 'rgb(158, 255, 209)',
localMinus: 'rgb(255, 97, 74)',
- remoteMinus: 'rgb(255, 149, 134)'
+ remoteMinus: 'rgb(255, 149, 134)',
+
+ incoming: 'rgb(52, 178, 118)',
+ outgoing: 'rgb(255, 97, 74)',
};
const rgba = (color: string): string => {
@@ -75,6 +83,9 @@ export default Vue.extend({
case 'drive-total': return this.driveTotalChart();
case 'drive-files': return this.driveFilesChart();
case 'drive-files-total': return this.driveFilesTotalChart();
+ case 'network-requests': return this.networkRequestsChart();
+ case 'network-time': return this.networkTimeChart();
+ case 'network-usage': return this.networkUsageChart();
}
},
@@ -89,7 +100,7 @@ export default Vue.extend({
created() {
(this as any).api('chart', {
- limit: 32
+ limit: 35
}).then(chart => {
this.chart = chart;
});
@@ -544,13 +555,101 @@ export default Vue.extend({
}
}
}];
- }
+ },
+
+ networkRequestsChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ requests: x.network.requests
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Requests',
+ fill: true,
+ backgroundColor: rgba(colors.localPlus),
+ borderColor: colors.localPlus,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.requests }))
+ }]
+ }];
+ },
+
+ networkTimeChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ time: x.network.requests != 0 ? (x.network.totalTime / x.network.requests) : 0,
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Avg time (ms)',
+ fill: true,
+ backgroundColor: rgba(colors.localPlus),
+ borderColor: colors.localPlus,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.time }))
+ }]
+ }];
+ },
+
+ networkUsageChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ incoming: x.network.incomingBytes,
+ outgoing: x.network.outgoingBytes
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Incoming',
+ fill: true,
+ backgroundColor: rgba(colors.incoming),
+ borderColor: colors.incoming,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.incoming }))
+ }, {
+ label: 'Outgoing',
+ fill: true,
+ backgroundColor: rgba(colors.outgoing),
+ borderColor: colors.outgoing,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.outgoing }))
+ }]
+ }, {
+ scales: {
+ yAxes: [{
+ ticks: {
+ callback: value => {
+ return Vue.filter('bytes')(value, 1);
+ }
+ }
+ }]
+ },
+ tooltips: {
+ callbacks: {
+ label: (tooltipItem, data) => {
+ const label = data.datasets[tooltipItem.datasetIndex].label || '';
+ return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
+ }
+ }
+ }
+ }];
+ },
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.gkgckalzgidaygcxnugepioremxvxvpt
padding 32px
@@ -576,12 +675,12 @@ export default Vue.extend({
*
&:not(.active)
- color $theme-color
+ color var(--primary)
cursor pointer
> div
> *
display block
- height 320px
+ height 350px
</style>
diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
index b894f0e109..806f7f5c3f 100644
--- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header">
<span v-html="title" :class="$style.title"></span>
<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
@@ -59,7 +59,7 @@ export default Vue.extend({
</script>
<style lang="stylus" module>
-@import '~const.styl'
+
.title
> [data-fa]
@@ -74,7 +74,7 @@ export default Vue.extend({
.footer
height 72px
- background lighten($theme-color, 95%)
+ background var(--primaryLighten95)
.upload
display inline-block
@@ -87,7 +87,7 @@ export default Vue.extend({
width 40px
height 40px
font-size 1em
- color rgba($theme-color, 0.5)
+ color var(--primaryAlpha05)
background transparent
outline none
border solid 1px transparent
@@ -95,13 +95,13 @@ export default Vue.extend({
&:hover
background transparent
- border-color rgba($theme-color, 0.3)
+ border-color var(--primaryAlpha03)
&:active
- color rgba($theme-color, 0.6)
+ color var(--primaryAlpha06)
background transparent
- border-color rgba($theme-color, 0.5)
- box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
+ border-color var(--primaryAlpha05)
+ //box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
&:focus
&:after
@@ -112,7 +112,7 @@ export default Vue.extend({
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
.ok
@@ -138,7 +138,7 @@ export default Vue.extend({
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
@@ -147,20 +147,20 @@ export default Vue.extend({
.ok
right 16px
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
+ color var(--primaryForeground)
+ background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
+ border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
+ background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
+ border-color var(--primary)
&:active:not(:disabled)
- background $theme-color
- border-color $theme-color
+ background var(--primary)
+ border-color var(--primary)
.cancel
right 148px
diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
index 0c4643fdcb..b970218e58 100644
--- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header">
<span v-html="title" :class="$style.title"></span>
</span>
@@ -37,7 +37,7 @@ export default Vue.extend({
</script>
<style lang="stylus" module>
-@import '~const.styl'
+
.title
> [data-fa]
@@ -48,7 +48,7 @@ export default Vue.extend({
.footer
height 72px
- background lighten($theme-color, 95%)
+ background var(--primaryLighten95)
.ok
.cancel
@@ -73,7 +73,7 @@ export default Vue.extend({
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
@@ -82,20 +82,20 @@ export default Vue.extend({
.ok
right 16px
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
+ color var(--primaryForeground)
+ background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
+ border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
+ background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
+ border-color var(--primary)
&:active:not(:disabled)
- background $theme-color
- border-color $theme-color
+ background var(--primary)
+ border-color var(--primary)
.cancel
right 148px
diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue
index e7deec675e..9e4541a752 100644
--- a/src/client/app/desktop/views/components/context-menu.menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.menu.vue
@@ -31,9 +31,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.menu
$width = 240px
$item-height = 38px
$padding = 10px
@@ -48,7 +46,7 @@ root(isDark)
&.divider
margin-top $padding
padding-top $padding
- border-top solid 1px isDark ? #1c2023 : #eee
+ border-top solid 1px var(--faceDivider)
&.nest
> p
@@ -69,7 +67,7 @@ root(isDark)
&:active
> p, a
- background $theme-color
+ background var(--primary)
> p, a
display block
@@ -77,7 +75,7 @@ root(isDark)
margin 0
padding 0 32px 0 38px
line-height $item-height
- color isDark ? #c8cece : #868C8C
+ color var(--text)
text-decoration none
cursor pointer
@@ -90,14 +88,14 @@ root(isDark)
&:hover
> p, a
text-decoration none
- background $theme-color
- color $theme-color-foreground
+ background var(--primary)
+ color var(--primaryForeground)
&:active
> p, a
text-decoration none
- background darken($theme-color, 10%)
- color $theme-color-foreground
+ background var(--primaryDarken10)
+ color var(--primaryForeground)
li > ul
visibility hidden
@@ -106,17 +104,11 @@ root(isDark)
left $width
margin-top -($padding)
width $width
- background isDark ? #282c37 :#fff
+ background var(--popupBg)
border-radius 0 4px 4px 4px
box-shadow 2px 2px 8px rgba(#000, 0.2)
transition visibility 0s linear 0.2s
-.menu[data-darkmode]
- root(true)
-
-.menu:not([data-darkmode])
- root(false)
-
</style>
<style lang="stylus" module>
diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
index afb6838eb6..b0a34866cd 100644
--- a/src/client/app/desktop/views/components/context-menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.vue
@@ -64,14 +64,14 @@ export default Vue.extend({
});
this.$emit('closed');
- this.$destroy();
+ this.destroyDom();
}
}
});
</script>
<style lang="stylus" scoped>
-root(isDark)
+.context-menu
$width = 240px
$item-height = 38px
$padding = 10px
@@ -82,15 +82,9 @@ root(isDark)
z-index 4096
width $width
font-size 0.8em
- background isDark ? #282c37 : #fff
+ background var(--popupBg)
border-radius 0 4px 4px 4px
box-shadow 2px 2px 8px rgba(#000, 0.2)
opacity 0
-.context-menu[data-darkmode]
- root(true)
-
-.context-menu:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue
index 4fa258549f..629c3b013a 100644
--- a/src/client/app/desktop/views/components/crop-window.vue
+++ b/src/client/app/desktop/views/components/crop-window.vue
@@ -61,7 +61,7 @@ export default Vue.extend({
</script>
<style lang="stylus" module>
-@import '~const.styl'
+
.header
> [data-fa]
@@ -73,7 +73,7 @@ export default Vue.extend({
.actions
height 72px
- background lighten($theme-color, 95%)
+ background var(--primaryLighten95)
.ok
.cancel
@@ -98,7 +98,7 @@ export default Vue.extend({
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
@@ -111,20 +111,20 @@ export default Vue.extend({
.ok
right 16px
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
+ color var(--primaryForeground)
+ background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
+ border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
+ background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
+ border-color var(--primary)
&:active:not(:disabled)
- background $theme-color
- border-color $theme-color
+ background var(--primary)
+ border-color var(--primary)
.cancel
.skip
@@ -155,11 +155,11 @@ export default Vue.extend({
}
.cropper-view-box {
- outline-color: $theme-color;
+ outline-color: var(--primary);
}
.cropper-line, .cropper-point {
- background-color: $theme-color;
+ background-color: var(--primary);
}
.cropper-bg {
diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue
index aff21c1754..baa6f911fe 100644
--- a/src/client/app/desktop/views/components/dialog.vue
+++ b/src/client/app/desktop/views/components/dialog.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
scale: 0.8,
duration: 300,
easing: [ 0.5, -0.5, 1, 0.5 ],
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
},
onBgClick() {
@@ -91,7 +91,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-dialog
> .bg
@@ -144,20 +144,20 @@ export default Vue.extend({
margin 0 0.375em
&:hover
- color $theme-color
+ color var(--primary)
&:active
- color darken($theme-color, 10%)
+ color var(--primaryDarken10)
transition color 0s ease
</style>
<style lang="stylus" module>
-@import '~const.styl'
+
.header
margin 1em 0
- color $theme-color
+ color var(--primary)
// color #43A4EC
font-weight bold
diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue
index 1f45b64324..191579538d 100644
--- a/src/client/app/desktop/views/components/drive-window.vue
+++ b/src/client/app/desktop/views/components/drive-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout">
+<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
<template slot="header">
<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
<span :class="$style.title">%fa:cloud%%i18n:@drive%</span>
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index 3ac8923c51..d7e24cfe71 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -200,9 +200,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.gvfdktuvdgwhmztnuekzkswkjygptfcv
padding 8px 0 0 0
height 180px
border-radius 4px
@@ -237,13 +235,13 @@ root(isDark)
background #ce2212
&[data-is-selected]
- background $theme-color
+ background var(--primary)
&:hover
- background lighten($theme-color, 10%)
+ background var(--primaryLighten10)
&:active
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
> .label
&:before
@@ -251,7 +249,7 @@ root(isDark)
display none
> .name
- color $theme-color-foreground
+ color var(--primaryForeground)
&[data-is-contextmenu-showing]
&:after
@@ -262,7 +260,7 @@ root(isDark)
right -4px
bottom -4px
left -4px
- border 2px dashed rgba($theme-color, 0.3)
+ border 2px dashed var(--primaryAlpha03)
border-radius 4px
> .label
@@ -337,16 +335,10 @@ root(isDark)
font-size 0.8em
text-align center
word-break break-all
- color isDark ? #fff : #444
+ color var(--text)
overflow hidden
> .ext
opacity 0.5
-.gvfdktuvdgwhmztnuekzkswkjygptfcv[data-darkmode]
- root(true)
-
-.gvfdktuvdgwhmztnuekzkswkjygptfcv:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index 83880fef5c..cfc2b64ff4 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -163,7 +163,7 @@ export default Vue.extend({
});
break;
default:
- alert('%i18n:@unhandled-error% ' + err);
+ alert(`%i18n:@unhandled-error% ${err}`);
}
});
}
@@ -214,12 +214,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.ynntpczxvnusfwdyxsfuhvcmuypqopdd
padding 8px
height 64px
- background isDark ? rgba($theme-color, 0.2) : lighten($theme-color, 95%)
+ background var(--desktopDriveFolderBg)
border-radius 4px
&, *
@@ -229,10 +227,10 @@ root(isDark)
pointer-events none
&:hover
- background isDark ? rgba(lighten($theme-color, 10%), 0.2) : lighten($theme-color, 90%)
+ background var(--desktopDriveFolderHoverBg)
&:active
- background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 85%)
+ background var(--desktopDriveFolderActiveBg)
&[data-is-contextmenu-showing]
&[data-draghover]
@@ -244,26 +242,20 @@ root(isDark)
right -4px
bottom -4px
left -4px
- border 2px dashed rgba($theme-color, 0.3)
+ border 2px dashed var(--primaryAlpha03)
border-radius 4px
&[data-draghover]
- background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 90%)
+ background var(--desktopDriveFolderActiveBg)
> .name
margin 0
font-size 0.9em
- color isDark ? #fff : darken($theme-color, 30%)
+ color var(--desktopDriveFolderFg)
> [data-fa]
margin-right 4px
margin-left 2px
text-align left
-.ynntpczxvnusfwdyxsfuhvcmuypqopdd[data-darkmode]
- root(true)
-
-.ynntpczxvnusfwdyxsfuhvcmuypqopdd:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index d919e4a5ea..1376a04d99 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -98,8 +98,7 @@ export default Vue.extend({
hierarchyFolders: [],
selectedFiles: [],
uploadings: [],
- connection: null,
- connectionId: null,
+ connection: null
/**
* ドロップされようとしているか
@@ -116,8 +115,7 @@ export default Vue.extend({
};
},
mounted() {
- this.connection = (this as any).os.streams.driveStream.getConnection();
- this.connectionId = (this as any).os.streams.driveStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('drive');
this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@@ -132,12 +130,7 @@ export default Vue.extend({
}
},
beforeDestroy() {
- this.connection.off('file_created', this.onStreamDriveFileCreated);
- this.connection.off('file_updated', this.onStreamDriveFileUpdated);
- this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
- this.connection.off('folder_created', this.onStreamDriveFolderCreated);
- this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
- (this as any).os.streams.driveStream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
onContextmenu(e) {
@@ -323,7 +316,7 @@ export default Vue.extend({
});
break;
default:
- alert('%i18n:@unhandled-error% ' + err);
+ alert(`%i18n:@unhandled-error% ${err}`);
}
});
}
@@ -404,7 +397,7 @@ export default Vue.extend({
folder: folder
});
} else {
- window.open(url + '/i/drive/folder/' + folder.id,
+ window.open(`${url}/i/drive/folder/${folder.id}`,
'drive_window',
'height=500, width=800');
}
@@ -585,18 +578,15 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
-
+.mk-drive
> nav
display block
z-index 2
width 100%
overflow auto
font-size 0.9em
- color isDark ? #d2d9dc : #555
- background isDark ? #282c37 : #fff
+ color var(--text)
+ background var(--face)
box-shadow 0 1px 0 rgba(#000, 0.05)
&, *
@@ -674,7 +664,7 @@ root(isDark)
padding 8px
height calc(100% - 38px)
overflow auto
- background isDark ? #191b22 : #fff
+ background var(--desktopDriveBg)
&, *
user-select none
@@ -697,8 +687,8 @@ root(isDark)
z-index 128
top 0
left 0
- border solid 1px $theme-color
- background rgba($theme-color, 0.5)
+ border solid 1px var(--primary)
+ background var(--primaryAlpha05)
pointer-events none
> .contents
@@ -769,7 +759,7 @@ root(isDark)
top 38px
width 100%
height calc(100% - 38px)
- border dashed 2px rgba($theme-color, 0.5)
+ border dashed 2px var(--primaryAlpha05)
pointer-events none
> .mk-uploader
@@ -780,10 +770,4 @@ root(isDark)
> input
display none
-.mk-drive[data-darkmode]
- root(true)
-
-.mk-drive:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index 62742a8f39..4d3d61dfe0 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -5,7 +5,8 @@
:disabled="wait"
>
<template v-if="!wait">
- <template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half%<template v-if="size == 'big'"> %i18n:@request-pending%</template></template>
+ <template v-if="u.hasPendingFollowRequestFromYou && u.isLocked">%fa:hourglass-half%<template v-if="size == 'big'"> %i18n:@request-pending%</template></template>
+ <template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked">%fa:hourglass-start%<template v-if="size == 'big'"> %i18n:@follow-processing%</template></template>
<template v-else-if="u.isFollowing">%fa:minus%<template v-if="size == 'big'"> %i18n:@following%</template></template>
<template v-else-if="!u.isFollowing && u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow-request%</template></template>
<template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow%</template></template>
@@ -33,35 +34,32 @@ export default Vue.extend({
return {
u: this.user,
wait: false,
- connection: null,
- connectionId: null
+ connection: null
};
},
mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
-
+ this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('follow', this.onFollow);
this.connection.on('unfollow', this.onUnfollow);
},
beforeDestroy() {
- this.connection.off('follow', this.onFollow);
- this.connection.off('unfollow', this.onUnfollow);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
onFollow(user) {
if (user.id == this.u.id) {
- this.user.isFollowing = user.isFollowing;
+ this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
onUnfollow(user) {
if (user.id == this.u.id) {
- this.user.isFollowing = user.isFollowing;
+ this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
@@ -74,7 +72,7 @@ export default Vue.extend({
userId: this.u.id
});
} else {
- if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ if (this.u.hasPendingFollowRequestFromYou) {
this.u = await (this as any).api('following/requests/cancel', {
userId: this.u.id
});
@@ -99,9 +97,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-follow-button
display block
cursor pointer
padding 0
@@ -124,37 +120,34 @@ root(isDark)
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
&:not(.active)
- color isDark ? #fff : #888
- background isDark ? linear-gradient(to bottom, #313543 0%, #282c37 100%) : linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
- border solid 1px isDark ? #1c2023 : #e2e2e2
+ color var(--primary)
+ border solid 1px var(--primary)
&:hover
- background isDark ? linear-gradient(to bottom, #2c2f3c 0%, #22262f 100%) : linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
- border-color isDark ? #151a1d : #dcdcdc
+ background var(--primaryAlpha03)
&:active
- background isDark ? #22262f : #ececec
- border-color isDark ? #151a1d : #dcdcdc
+ background var(--primaryAlpha05)
&.active
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
+ color var(--primaryForeground)
+ background var(--primary)
+ border solid 1px var(--primary)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
+ background var(--primaryLighten5)
+ border-color var(--primaryLighten5)
&:active:not(:disabled)
- background $theme-color
- border-color $theme-color
+ background var(--primaryDarken5)
+ border-color var(--primaryDarken5)
&.wait
cursor wait !important
@@ -165,10 +158,4 @@ root(isDark)
height 38px
line-height 38px
-.mk-follow-button[data-darkmode]
- root(true)
-
-.mk-follow-button:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index fdab7bc1ce..d5214adb2f 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window width="400px" height="550px" @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header">
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
</span>
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index 7cca833a82..aa9f2bde7b 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window width="400px" height="550px" @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header">
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
</span>
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 7dfd9e4359..4e8a212b00 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -14,7 +14,7 @@
<p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p>
<a class="refresh" @click="refresh">%i18n:@refresh%</a>
- <button class="close" @click="$destroy()" title="%i18n:@close%">%fa:times%</button>
+ <button class="close" @click="destroyDom()" title="%i18n:@close%">%fa:times%</button>
</div>
</template>
diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue
index 7c6cb9cd40..594eae58f8 100644
--- a/src/client/app/desktop/views/components/game-window.vue
+++ b/src/client/app/desktop/views/components/game-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
<mk-reversi :class="$style.content" @gamed="g => game = g"/>
</mk-window>
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index d45cc82e13..9008e26263 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -141,7 +141,6 @@ export default Vue.extend({
data() {
return {
connection: null,
- connectionId: null,
widgetAdderSelected: null,
trash: []
};
@@ -176,12 +175,11 @@ export default Vue.extend({
},
mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
},
beforeDestroy() {
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
@@ -237,15 +235,17 @@ export default Vue.extend({
warp(date) {
(this.$refs.tl as any).warp(date);
+ },
+
+ focus() {
+ (this.$refs.tl as any).focus();
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-home
display block
&[data-customize]
@@ -275,8 +275,8 @@ root(isDark)
left 0
width 100%
height 48px
- color isDark ? #fff : #000
- background isDark ? #313543 : #f7f7f7
+ color var(--text)
+ background var(--desktopHeaderBg)
box-shadow 0 1px 1px rgba(#000, 0.075)
> a
@@ -288,15 +288,15 @@ root(isDark)
padding 0 16px
line-height 48px
text-decoration none
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
transition background 0.1s ease
&:hover
- background lighten($theme-color, 10%)
+ background var(--primaryLighten10)
&:active
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
transition background 0s ease
> [data-fa]
@@ -316,7 +316,7 @@ root(isDark)
line-height 48px
&.trash
- border-left solid 1px isDark ? #1c2023 : #ddd
+ border-left solid 1px var(--faceDivider)
> div
width 100%
@@ -336,7 +336,7 @@ root(isDark)
display flex
justify-content center
margin 0 auto
- max-width 1220px
+ max-width 1240px
> *
.customize-container
@@ -351,13 +351,13 @@ root(isDark)
> .main
padding 16px
- width calc(100% - 275px * 2)
+ width calc(100% - 280px * 2)
order 2
> .form
margin-bottom 16px
- border solid 1px rgba(#000, 0.075)
- border-radius 4px
+ box-shadow var(--shadow)
+ border-radius var(--round)
@media (max-width 700px)
padding 0
@@ -367,7 +367,7 @@ root(isDark)
border-radius 0
> *:not(.main)
- width 275px
+ width 280px
padding 16px 0 16px 0
> *:not(:last-child)
@@ -391,10 +391,4 @@ root(isDark)
max-width 700px
margin 0 auto
-.mk-home[data-darkmode]
- root(true)
-
-.mk-home:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue
index e2cf4e48fd..976e897fe8 100644
--- a/src/client/app/desktop/views/components/input-dialog.vue
+++ b/src/client/app/desktop/views/components/input-dialog.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy">
+<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
<span slot="header" :class="$style.header">
%fa:i-cursor%{{ title }}
</span>
@@ -76,7 +76,7 @@ export default Vue.extend({
<style lang="stylus" module>
-@import '~const.styl'
+
.header
> [data-fa]
@@ -96,25 +96,25 @@ export default Vue.extend({
color #333
background #fff
outline none
- border solid 1px rgba($theme-color, 0.1)
+ border solid 1px var(--primaryAlpha01)
border-radius 4px
transition border-color .3s ease
&:hover
- border-color rgba($theme-color, 0.2)
+ border-color var(--primaryAlpha02)
transition border-color .1s ease
&:focus
- color $theme-color
- border-color rgba($theme-color, 0.5)
+ color var(--primary)
+ border-color var(--primaryAlpha05)
transition border-color 0s ease
&::-webkit-input-placeholder
- color rgba($theme-color, 0.3)
+ color var(--primaryAlpha03)
.actions
height 72px
- background lighten($theme-color, 95%)
+ background var(--primaryLighten95)
.ok
.cancel
@@ -139,7 +139,7 @@ export default Vue.extend({
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
@@ -148,20 +148,20 @@ export default Vue.extend({
.ok
right 16px
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
+ color var(--primaryForeground)
+ background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
+ border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
+ background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
+ border-color var(--primary)
&:active:not(:disabled)
- background $theme-color
- border-color $theme-color
+ background var(--primary)
+ border-color var(--primary)
.cancel
right 148px
diff --git a/src/client/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue
index 026522d907..89a340d3ae 100644
--- a/src/client/app/desktop/views/components/media-image-dialog.vue
+++ b/src/client/app/desktop/views/components/media-image-dialog.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
opacity: 0,
duration: 100,
easing: 'linear',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index 8b68f260fa..f9ab188ca5 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false">
+<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>
@@ -27,12 +27,13 @@ export default Vue.extend({
},
raw: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
style(): any {
return {
@@ -48,7 +49,7 @@ export default Vue.extend({
const mouseY = e.clientY - rect.top;
const xp = mouseX / this.$el.offsetWidth * 100;
const yp = mouseY / this.$el.offsetHeight * 100;
- this.$el.style.backgroundPosition = xp + '% ' + yp + '%';
+ this.$el.style.backgroundPosition = `${xp}% ${yp}%`;
this.$el.style.backgroundImage = `url("${this.image.url}")`;
},
@@ -89,7 +90,7 @@ export default Vue.extend({
text-align center
font-size 12px
- > b
+ > *
display block
</style>
diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue
index 959cefa42c..03c93c8939 100644
--- a/src/client/app/desktop/views/components/media-video-dialog.vue
+++ b/src/client/app/desktop/views/components/media-video-dialog.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
opacity: 0,
duration: 100,
easing: 'linear',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue
index 6c60f2da96..7859a59254 100644
--- a/src/client/app/desktop/views/components/media-video.vue
+++ b/src/client/app/desktop/views/components/media-video.vue
@@ -6,19 +6,12 @@
</div>
</div>
<div class="vwxdhznewyashiknzolsoihtlpicqepe" v-else>
- <video class="video"
- :src="video.url"
- :title="video.name"
- controls
- @dblclick.prevent="onClick"
- ref="video"
- v-if="inlinePlayable" />
<a class="thumbnail"
:href="video.url"
:style="imageStyle"
@click.prevent="onClick"
:title="video.name"
- v-else>
+ >
%fa:R play-circle%
</a>
</div>
@@ -36,16 +29,17 @@ export default Vue.extend({
},
inlinePlayable: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
imageStyle(): any {
return {
- 'background-image': `url(${this.video.url})`
+ 'background-image': null // TODO `url(${this.video.thumbnailUrl})`
};
}
},
@@ -79,7 +73,6 @@ export default Vue.extend({
justify-content center
align-items center
font-size 3.5em
-
cursor zoom-in
overflow hidden
background-position center
@@ -101,5 +94,4 @@ export default Vue.extend({
> b
display block
-
</style>
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 41b421b0e7..3706377607 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
<mk-messaging-room :user="user" :class="$style.content"/>
</mk-window>
diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue
index 9580c5061d..a8f0fc68b9 100644
--- a/src/client/app/desktop/views/components/messaging-window.vue
+++ b/src/client/app/desktop/views/components/messaging-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span>
<mk-messaging :class="$style.content" @navigate="navigate"/>
</mk-window>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 1ba4a9a447..b119f23d7a 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -37,20 +37,26 @@
</router-link>
</header>
<div class="body">
- <div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
- <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
- <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
- </div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media" :raw="true"/>
- </div>
- <mk-poll v-if="p.poll" :note="p"/>
- <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
- <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
- <div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
+ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
+ </div>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files" :raw="true"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p"/>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
+ <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
</div>
</div>
<footer>
@@ -86,12 +92,16 @@ import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue';
+import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
export default Vue.extend({
components: {
XSub
},
+ mixins: [noteSubscriber('note')],
+
props: {
note: {
type: Object,
@@ -104,6 +114,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
conversation: [],
conversationFetching: false,
replies: []
@@ -114,22 +125,24 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
+
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
+
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
+
title(): string {
return new Date(this.p.createdAt).toLocaleString();
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -184,22 +197,26 @@ export default Vue.extend({
this.conversation = conversation.reverse();
});
},
+
reply() {
(this as any).os.new(MkPostFormWindow, {
reply: this.p
});
},
+
renote() {
(this as any).os.new(MkRenoteFormWindow, {
note: this.p
});
},
+
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -211,14 +228,12 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-note-detail
overflow hidden
text-align left
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.1)
- border-radius 8px
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
> .read-more
display block
@@ -229,28 +244,28 @@ root(isDark)
text-align center
color #999
cursor pointer
- background isDark ? #21242d : #fafafa
+ background var(--subNoteBg)
outline none
border none
- border-bottom solid 1px isDark ? #1c2023 : #eef0f2
- border-radius 6px 6px 0 0
+ border-bottom solid 1px var(--faceDivider)
+ border-radius var(--round) var(--round) 0 0
&:hover
- background isDark ? #2e3440 : #f6f6f6
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
- background isDark ? #21242b : #f0f0f0
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
&:disabled
- color isDark ? #21242b : #ccc
+ cursor wait
> .conversation
> *
- border-bottom 1px solid isDark ? #1c2023 : #eef0f2
+ border-bottom 1px solid var(--faceDivider)
> .renote
- color #9dbb00
- background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ color var(--renoteText)
+ background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
> p
margin 0
@@ -273,7 +288,7 @@ root(isDark)
padding-top 8px
> .reply-to
- border-bottom 1px solid isDark ? #1c2023 : #eef0f2
+ border-bottom 1px solid var(--faceDivider)
> article
padding 28px 32px 18px 32px
@@ -285,7 +300,7 @@ root(isDark)
&:hover
> footer > button
- color isDark ? #707b97 : #888
+ color var(--noteActionsHighlighted)
> .avatar
width 60px
@@ -302,7 +317,7 @@ root(isDark)
display inline-block
margin 0
line-height 24px
- color isDark ? #fff : #627079
+ color var(--noteHeaderName)
font-size 18px
font-weight 700
text-align left
@@ -315,49 +330,61 @@ root(isDark)
display block
text-align left
margin 0
- color isDark ? #606984 : #ccc
+ color var(--noteHeaderAcct)
> .time
position absolute
top 0
right 32px
font-size 1em
- color isDark ? #606984 : #c0c0c0
+ color var(--noteHeaderInfo)
> .body
padding 8px 0
- > .text
+ > .cw
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
- font-size 1.5em
- color isDark ? #fff : #717171
+ color var(--noteText)
- > .renote
- margin 8px 0
+ > .text
+ margin-right 8px
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
+ > .content
+ > .text
+ cursor default
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 1.5em
+ color var(--noteText)
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ > .renote
+ margin 8px 0
- > .map
- width 100%
- height 300px
+ > *
+ padding 16px
+ border dashed 1px var(--quoteBorder)
+ border-radius 8px
- &:empty
- display none
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
- > .mk-url-preview
- margin-top 8px
+ > .map
+ width 100%
+ height 300px
+
+ &:empty
+ display none
+
+ > .mk-url-preview
+ margin-top 8px
> footer
font-size 1.2em
@@ -368,20 +395,20 @@ root(isDark)
background transparent
border none
font-size 1em
- color isDark ? #606984 : #ccc
+ color var(--noteActions)
cursor pointer
&:hover
- color isDark ? #a1a8bf : #444
+ color var(--noteActionsHover)
&.replyButton:hover
- color #0af
+ color var(--noteActionsReplyHover)
&.renoteButton:hover
- color #8d0
+ color var(--noteActionsRenoteHover)
&.reactionButton:hover
- color #fa0
+ color var(--noteActionsReactionHover)
> .count
display inline
@@ -389,16 +416,10 @@ root(isDark)
color #999
&.reacted, &.reacted:hover
- color #fa0
+ color var(--noteActionsReactionHover)
> .replies
> *
- border-top 1px solid isDark ? #1c2023 : #eef0f2
-
-.mk-note-detail[data-darkmode]
- root(true)
-
-.mk-note-detail:not([data-darkmode])
- root(false)
+ border-top 1px solid var(--faceDivider)
</style>
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index c723db98c0..4c1c7e7b2d 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,10 +1,16 @@
<template>
-<div class="mk-note-preview" :title="title">
+<div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title">
<mk-avatar class="avatar" :user="note.user" v-if="!mini"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -25,6 +31,13 @@ export default Vue.extend({
default: false
}
},
+
+ data() {
+ return {
+ showContent: false
+ };
+ },
+
computed: {
title(): string {
return new Date(this.note.createdAt).toLocaleString();
@@ -34,7 +47,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.qiziqtywpuaucsgarwajitwaakggnisj
display flex
font-size 0.9em
@@ -52,16 +65,22 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color var(--noteText)
-.mk-note-preview[data-darkmode]
- root(true)
+ > .text
+ margin-right 8px
-.mk-note-preview:not([data-darkmode])
- root(false)
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color var(--subNoteText)
</style>
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index fc851e83e9..ee52670f8f 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,10 +1,16 @@
<template>
-<div class="sub" :title="title">
+<div class="tkfdzaxtkdeianobciwadajxzbddorql" :title="title">
<mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<mk-note-header class="header" :note="note"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -14,7 +20,19 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note'],
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
+ },
+
computed: {
title(): string {
return new Date(this.note.createdAt).toLocaleString();
@@ -24,12 +42,12 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.tkfdzaxtkdeianobciwadajxzbddorql
display flex
margin 0
padding 16px 32px
font-size 0.9em
- background isDark ? #21242d : #fcfcfc
+ background var(--subNoteBg)
> .avatar
flex-shrink 0
@@ -48,20 +66,26 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color var(--noteText)
- pre
- max-height 120px
- font-size 80%
+ > .text
+ margin-right 8px
-.sub[data-darkmode]
- root(true)
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color var(--subNoteText)
-.sub:not([data-darkmode])
- root(false)
+ pre
+ max-height 120px
+ font-size 80%
</style>
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 7592ae3905..2db1479823 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -1,5 +1,5 @@
<template>
-<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
+<div class="note" tabindex="-1" v-hotkey="keymap" :title="title">
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
@@ -18,7 +18,7 @@
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
- <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@hide%' : '%i18n:@see-more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,32 +28,30 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
<a class="rp" v-if="p.renote">RP:</a>
</div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
- </div>
+ <div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div>
</div>
- <footer>
+ <footer v-if="p.deletedAt == null">
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
- <button class="replyButton" @click="reply" title="%i18n:@reply%">
+ <button class="replyButton" @click="reply()" title="%i18n:@reply%">
<template v-if="p.reply">%fa:reply-all%</template>
<template v-else>%fa:reply%</template>
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
</button>
- <button class="renoteButton" @click="renote" title="%i18n:@renote%">
+ <button class="renoteButton" @click="renote()" title="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
</button>
- <button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
+ <button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
</button>
- <button @click="menu" ref="menuButton">
+ <button @click="menu()" ref="menuButton">
%fa:ellipsis-h%
</button>
<!-- <button title="%i18n:@detail">
@@ -78,6 +76,8 @@ import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue';
+import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
function focus(el, fn) {
const target = fn(el);
@@ -95,22 +95,51 @@ export default Vue.extend({
XSub
},
- props: ['note'],
+ mixins: [noteSubscriber('note')],
+
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
data() {
return {
showContent: false,
- isDetailOpened: false,
- connection: null,
- connectionId: null
+ isDetailOpened: false
};
},
computed: {
+ keymap(): any {
+ return {
+ 'r|left': () => this.reply(true),
+ 'e|a|plus': () => this.react(true),
+ 'q|right': () => this.renote(true),
+ 'ctrl+q|ctrl+right': this.renoteDirectly,
+ 'up|k|shift+tab': this.focusBefore,
+ 'down|j|tab': this.focusAfter,
+ 'esc': this.blur,
+ 'm|o': () => this.menu(true),
+ 's': this.toggleShowContent,
+ '1': () => this.reactDirectly('like'),
+ '2': () => this.reactDirectly('love'),
+ '3': () => this.reactDirectly('laugh'),
+ '4': () => this.reactDirectly('hmm'),
+ '5': () => this.reactDirectly('surprise'),
+ '6': () => this.reactDirectly('congrats'),
+ '7': () => this.reactDirectly('angry'),
+ '8': () => this.reactDirectly('confused'),
+ '9': () => this.reactDirectly('rip'),
+ '0': () => this.reactDirectly('pudding'),
+ };
+ },
+
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -120,9 +149,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
@@ -142,156 +169,81 @@ export default Vue.extend({
}
},
- created() {
- if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
- }
- },
-
- mounted() {
- this.capture(true);
-
- if (this.$store.getters.isSignedIn) {
- this.connection.on('_connected_', this.onStreamConnected);
- }
-
- // Draw map
- if (this.p.geo) {
- const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
- if (shouldShowMap) {
- (this as any).os.getGoogleMaps().then(maps => {
- const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
- const map = new maps.Map(this.$refs.map, {
- center: uluru,
- zoom: 15
- });
- new maps.Marker({
- position: uluru,
- map: map
- });
- });
- }
- }
- },
-
- beforeDestroy() {
- this.decapture(true);
-
- if (this.$store.getters.isSignedIn) {
- this.connection.off('_connected_', this.onStreamConnected);
- (this as any).os.stream.dispose(this.connectionId);
- }
- },
-
methods: {
- capture(withHandler = false) {
- if (this.$store.getters.isSignedIn) {
- this.connection.send({
- type: 'capture',
- id: this.p.id
- });
- if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
- }
- },
-
- decapture(withHandler = false) {
- if (this.$store.getters.isSignedIn) {
- this.connection.send({
- type: 'decapture',
- id: this.p.id
- });
- if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
- }
- },
-
- onStreamConnected() {
- this.capture();
- },
-
- onStreamNoteUpdated(data) {
- const note = data.note;
- if (note.id == this.note.id) {
- this.$emit('update:note', note);
- } else if (note.id == this.note.renoteId) {
- this.note.renote = note;
- }
- },
-
- reply() {
+ reply(viaKeyboard = false) {
(this as any).os.new(MkPostFormWindow, {
- reply: this.p
- });
+ reply: this.p,
+ animation: !viaKeyboard
+ }).$once('closed', this.focus);
},
- renote() {
+ renote(viaKeyboard = false) {
(this as any).os.new(MkRenoteFormWindow, {
- note: this.p
+ note: this.p,
+ animation: !viaKeyboard
+ }).$once('closed', this.focus);
+ },
+
+ renoteDirectly() {
+ (this as any).api('notes/create', {
+ renoteId: this.p.id
});
},
- react() {
+ react(viaKeyboard = false) {
+ this.blur();
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
- note: this.p
+ note: this.p,
+ showFocus: viaKeyboard,
+ animation: !viaKeyboard
+ }).$once('closed', this.focus);
+ },
+
+ reactDirectly(reaction) {
+ (this as any).api('notes/reactions/create', {
+ noteId: this.p.id,
+ reaction: reaction
});
},
- menu() {
+ menu(viaKeyboard = false) {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
- note: this.p
- });
+ note: this.p,
+ animation: !viaKeyboard
+ }).$once('closed', this.focus);
},
- onKeydown(e) {
- let shouldBeCancel = true;
-
- switch (true) {
- case e.which == 38: // [↑]
- case e.which == 74: // [j]
- case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
- focus(this.$el, e => e.previousElementSibling);
- break;
-
- case e.which == 40: // [↓]
- case e.which == 75: // [k]
- case e.which == 9: // [Tab]
- focus(this.$el, e => e.nextElementSibling);
- break;
-
- case e.which == 81: // [q]
- case e.which == 69: // [e]
- this.renote();
- break;
+ toggleShowContent() {
+ this.showContent = !this.showContent;
+ },
- case e.which == 70: // [f]
- case e.which == 76: // [l]
- //this.like();
- break;
+ focus() {
+ this.$el.focus();
+ },
- case e.which == 82: // [r]
- this.reply();
- break;
+ blur() {
+ this.$el.blur();
+ },
- default:
- shouldBeCancel = false;
- }
+ focusBefore() {
+ focus(this.$el, e => e.previousElementSibling);
+ },
- if (shouldBeCancel) e.preventDefault();
+ focusAfter() {
+ focus(this.$el, e => e.nextElementSibling);
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.note
margin 0
padding 0
- background isDark ? #282C37 : #fff
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ background var(--face)
+ border-bottom solid 1px var(--faceDivider)
&[data-round]
&:first-child
@@ -316,7 +268,7 @@ root(isDark)
right 2px
bottom 2px
left 2px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 4px
> .renote
@@ -325,8 +277,8 @@ root(isDark)
padding 16px 32px 8px 32px
line-height 28px
white-space pre
- color #9dbb00
- background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ color var(--renoteText)
+ background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
.avatar
display inline-block
@@ -366,7 +318,7 @@ root(isDark)
&:hover
> .main > footer > button
- color isDark ? #707b97 : #888
+ color var(--noteActionsHighlighted)
> .avatar
flex-shrink 0
@@ -394,24 +346,11 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- color isDark ? #fff : #717171
+ color var(--noteText)
> .text
margin-right 8px
- > .toggle
- display inline-block
- padding 4px 8px
- font-size 0.7em
- color isDark ? #393f4f : #fff
- background isDark ? #687390 : #b1b9c1
- border-radius 2px
- cursor pointer
- user-select none
-
- &:hover
- background isDark ? #707b97 : #bbc4ce
-
> .content
> .text
@@ -420,7 +359,7 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- color isDark ? #fff : #717171
+ color var(--noteText)
>>> .title
display block
@@ -428,7 +367,7 @@ root(isDark)
padding 4px
font-size 90%
text-align center
- background isDark ? #2f3944 : #eef1f3
+ background var(--mfmTitleBg)
border-radius 4px
>>> .code
@@ -437,17 +376,17 @@ root(isDark)
>>> .quote
margin 8px
padding 6px 12px
- color isDark ? #6f808e : #aaa
- border-left solid 3px isDark ? #637182 : #eee
+ color var(--mfmQuote)
+ border-left solid 3px var(--mfmQuoteLine)
> .reply
margin-right 8px
- color isDark ? #99abbf : #717171
+ color var(--text)
> .rp
margin-left 4px
font-style oblique
- color #a0bf46
+ color var(--renoteText)
> .location
margin 4px 0
@@ -470,9 +409,9 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
- border dashed 1px isDark ? #4e945e : #c0dac6
+ border dashed 1px var(--quoteBorder)
border-radius 8px
> footer
@@ -481,22 +420,22 @@ root(isDark)
padding 0 8px
line-height 32px
font-size 1em
- color isDark ? #606984 : #ddd
+ color var(--noteActions)
background transparent
border none
cursor pointer
&:hover
- color isDark ? #a1a8bf : #444
+ color var(--noteActionsHover)
&.replyButton:hover
- color #0af
+ color var(--noteActionsReplyHover)
&.renoteButton:hover
- color #8d0
+ color var(--noteActionsRenoteHover)
&.reactionButton:hover
- color #fa0
+ color var(--noteActionsReactionHover)
> .count
display inline
@@ -504,18 +443,12 @@ root(isDark)
color #999
&.reacted, &.reacted:hover
- color #fa0
+ color var(--noteActionsReactionHover)
> .detail
padding-top 4px
background rgba(#000, 0.0125)
-.note[data-darkmode]
- root(true)
-
-.note:not([data-darkmode])
- root(false)
-
</style>
<style lang="stylus" module>
@@ -538,7 +471,7 @@ root(isDark)
padding 0 4px
margin-left 4px
font-size 80%
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
border-radius 4px
</style>
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index a1c1207a7b..84b13ed84e 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -10,17 +10,15 @@
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!--<transition-group name="mk-notes" class="transition">-->
- <div class="notes">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
<template v-for="(note, i) in _notes">
- <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+ <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!--</transition-group>-->
+ </component>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -91,7 +89,7 @@ export default Vue.extend({
},
focus() {
- (this.$el as any).children[0].focus();
+ (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
},
onNoteUpdated(i, note) {
@@ -122,7 +120,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
@@ -218,9 +216,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-notes
.transition
.mk-notes-enter
.mk-notes-leave-to
@@ -237,9 +233,9 @@ root(isDark)
line-height 32px
font-size 14px
text-align center
- color isDark ? #666b79 : #aaa
- background isDark ? #242731 : #fdfdfd
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ color var(--dateDividerFg)
+ background var(--dateDividerBg)
+ border-bottom solid 1px var(--faceDivider)
span
margin 0 16px
@@ -252,7 +248,7 @@ root(isDark)
position sticky
z-index 100
height 3px
- background $theme-color
+ background var(--primary)
> footer
> button
@@ -262,21 +258,15 @@ root(isDark)
width 100%
text-align center
color #ccc
- background isDark ? #282C37 : #fff
- border-top solid 1px isDark ? #1c2023 : #eaeaea
+ background var(--face)
+ border-top solid 1px var(--faceDivider)
border-bottom-left-radius 6px
border-bottom-right-radius 6px
&:hover
- background isDark ? #2e3440 : #f5f5f5
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
- background isDark ? #21242b : #eee
-
-.mk-notes[data-darkmode]
- root(true)
-
-.mk-notes:not([data-darkmode])
- root(false)
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
</style>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index bfe71903e4..95b8e1355a 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -2,8 +2,7 @@
<div class="mk-notifications">
<div class="notifications" v-if="notifications.length != 0">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notifications" class="transition"> -->
- <div>
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
<template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>
@@ -97,8 +96,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
</div>
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
@@ -120,10 +118,10 @@ export default Vue.extend({
notifications: [],
moreNotifications: false,
connection: null,
- connectionId: null,
getNoteSummary
};
},
+
computed: {
_notifications(): any[] {
return (this.notifications as any).map(notification => {
@@ -135,9 +133,9 @@ export default Vue.extend({
});
}
},
+
mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('notification', this.onNotification);
@@ -155,10 +153,11 @@ export default Vue.extend({
this.fetching = false;
});
},
+
beforeDestroy() {
- this.connection.off('notification', this.onNotification);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
+
methods: {
fetchMoreNotifications() {
this.fetchingMoreNotifications = true;
@@ -179,10 +178,11 @@ export default Vue.extend({
this.fetchingMoreNotifications = false;
});
},
+
onNotification(notification) {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
this.connection.send({
- type: 'read_notification',
+ type: 'readNotification',
id: notification.id
});
@@ -193,7 +193,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mk-notifications
.transition
.mk-notifications-enter
.mk-notifications-leave-to
@@ -210,7 +210,7 @@ root(isDark)
padding 16px
overflow-wrap break-word
font-size 13px
- border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
+ border-bottom solid 1px var(--faceDivider)
&:last-child
border-bottom none
@@ -221,7 +221,7 @@ root(isDark)
top 16px
right 12px
vertical-align top
- color isDark ? #606984 : rgba(#000, 0.6)
+ color var(--noteHeaderInfo)
font-size small
&:after
@@ -251,10 +251,10 @@ root(isDark)
margin-right 4px
.note-preview
- color isDark ? #c2cad4 : rgba(#000, 0.7)
+ color var(--noteText)
.note-ref
- color isDark ? #c2cad4 : rgba(#000, 0.7)
+ color var(--noteText)
[data-fa]
font-size 1em
@@ -285,9 +285,9 @@ root(isDark)
line-height 32px
text-align center
font-size 0.8em
- color isDark ? #666b79 : #aaa
- background isDark ? #242731 : #fdfdfd
- border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
+ color var(--dateDividerFg)
+ background var(--dateDividerBg)
+ border-bottom solid 1px var(--faceDivider)
span
margin 0 16px
@@ -329,10 +329,4 @@ root(isDark)
> [data-fa]
margin-right 4px
-.mk-notifications[data-darkmode]
- root(true)
-
-.mk-notifications:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
index 51a416e281..a5d191f2f3 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -1,10 +1,10 @@
<template>
-<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy">
+<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
<span slot="header" class="mk-post-form-window--header">
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
<span v-if="!reply">%i18n:@note%</span>
<span v-if="reply">%i18n:@reply%</span>
- <span class="count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span>
+ <span class="count" v-if="files.length != 0">{{ '%i18n:@attaches%'.replace('{}', files.length) }}</span>
<span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
</span>
@@ -14,7 +14,7 @@
:reply="reply"
@posted="onPosted"
@change-uploadings="onChangeUploadings"
- @change-attached-media="onChangeMedia"
+ @change-attached-files="onChangeFiles"
@geo-attached="onGeoAttached"
@geo-dettached="onGeoDettached"/>
</div>
@@ -25,25 +25,39 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['reply'],
+ props: {
+ reply: {
+ type: Object,
+ required: false
+ },
+
+ animation: {
+ type: Boolean,
+ required: false,
+ default: true
+ }
+ },
+
data() {
return {
uploadings: [],
- media: [],
+ files: [],
geo: null
};
},
+
mounted() {
this.$nextTick(() => {
(this.$refs.form as any).focus();
});
},
+
methods: {
onChangeUploadings(files) {
this.uploadings = files;
},
- onChangeMedia(media) {
- this.media = media;
+ onChangeFiles(files) {
+ this.files = files;
},
onGeoAttached(geo) {
this.geo = geo;
@@ -53,13 +67,17 @@ export default Vue.extend({
},
onPosted() {
(this.$refs.window as any).close();
+ },
+ onWindowClosed() {
+ this.$emit('closed');
+ this.destroyDom();
}
}
});
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mk-post-form-window
.mk-post-form-window--header
.icon
margin-right 8px
@@ -76,15 +94,6 @@ root(isDark)
.mk-post-form-window--body
.notePreview
- if isDark
- margin 16px 22px 0 22px
- else
margin 16px 22px
-.mk-post-form-window[data-darkmode]
- root(true)
-
-.mk-post-form-window:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index bacaea65ee..e25cc33579 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -20,7 +20,7 @@
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
v-autocomplete="'text'"
></textarea>
- <div class="medias" :class="{ with: poll }" v-show="files.length != 0">
+ <div class="files" :class="{ with: poll }" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id">
<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
@@ -35,7 +35,7 @@
<button class="upload" title="%i18n:@attach-media-from-local%" @click="chooseFile">%fa:upload%</button>
<button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button>
<button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button>
- <button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button>
+ <button class="poll" title="%i18n:@create-poll%" @click="poll = !poll">%fa:chart-pie%</button>
<button class="poll" title="%i18n:@hide-contents%" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" title="%i18n:@attach-location-information%" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<button class="visibility" title="%i18n:@visibility%" @click="setVisibility" ref="visibilityButton">
@@ -45,11 +45,11 @@
<span v-if="visibility === 'specified'">%fa:envelope%</span>
<span v-if="visibility === 'private'">%fa:lock%</span>
</button>
- <p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
+ <p class="text-count" :class="{ over: this.trimmedLength(text) > 1000 }">{{ 1000 - this.trimmedLength(text) }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
</button>
- <input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
+ <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
<div class="dropzone" v-if="draghover"></div>
</div>
</template>
@@ -62,6 +62,9 @@ import getFace from '../../../common/scripts/get-face';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
+import { erase, unique } from '../../../../../prelude/array';
+import { length } from 'stringz';
+import parseAcct from '../../../../../misc/acct/parse';
export default Vue.extend({
components: {
@@ -99,7 +102,7 @@ export default Vue.extend({
useCw: false,
cw: null,
geo: null,
- visibility: this.$store.state.device.visibility || 'public',
+ visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [],
autocomplete: null,
draghover: false,
@@ -110,9 +113,9 @@ export default Vue.extend({
computed: {
draftId(): string {
return this.renote
- ? 'renote:' + this.renote.id
+ ? `renote:${this.renote.id}`
: this.reply
- ? 'reply:' + this.reply.id
+ ? `reply:${this.reply.id}`
: 'note';
},
@@ -145,7 +148,7 @@ export default Vue.extend({
canPost(): boolean {
return !this.posting &&
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
- (this.text.trim().length <= 1000);
+ (length(this.text.trim()) <= 1000);
}
},
@@ -175,6 +178,18 @@ export default Vue.extend({
});
}
+ // 公開以外へのリプライ時は元の公開範囲を引き継ぐ
+ if (this.reply && ['home', 'followers', 'specified', 'private'].includes(this.reply.visibility)) {
+ this.visibility = this.reply.visibility;
+ }
+
+ // ダイレクトへのリプライはリプライ先ユーザーを初期設定
+ if (this.reply && this.reply.visibility === 'specified') {
+ (this as any).api('users/show', { userId: this.reply.userId }).then(user => {
+ this.visibleUsers.push(user);
+ });
+ }
+
this.$nextTick(() => {
// 書きかけの投稿を復元
if (!this.instant) {
@@ -188,7 +203,7 @@ export default Vue.extend({
(this.$refs.poll as any).set(draft.data.poll);
});
}
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
}
}
@@ -197,6 +212,10 @@ export default Vue.extend({
},
methods: {
+ trimmedLength(text: string) {
+ return length(text.trim());
+ },
+
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
@@ -225,12 +244,12 @@ export default Vue.extend({
attachMedia(driveFile) {
this.files.push(driveFile);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
detachMedia(id) {
this.files = this.files.filter(x => x.id != id);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onChangeFile() {
@@ -249,7 +268,7 @@ export default Vue.extend({
this.text = '';
this.files = [];
this.poll = false;
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onKeydown(e) {
@@ -297,7 +316,7 @@ export default Vue.extend({
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.files.push(file);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
e.preventDefault();
}
//#endregion
@@ -313,7 +332,7 @@ export default Vue.extend({
this.geo = pos.coords;
this.$emit('geo-attached', this.geo);
}, err => {
- alert('%i18n:@error%: ' + err.message);
+ alert(`%i18n:@error%: ${err.message}`);
}, {
enableHighAccuracy: true
});
@@ -336,17 +355,16 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
title: '%i18n:@enter-username%'
- }).then(username => {
- (this as any).api('users/show', {
- username
- }).then(user => {
+ }).then(acct => {
+ if (acct.startsWith('@')) acct = acct.substr(1);
+ (this as any).api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
});
},
removeVisibleUser(user) {
- this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ this.visibleUsers = erase(user, this.visibleUsers);
},
post() {
@@ -354,7 +372,7 @@ export default Vue.extend({
(this as any).api('notes/create', {
text: this.text == '' ? undefined : this.text,
- mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+ fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
@@ -391,7 +409,7 @@ export default Vue.extend({
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], [])));
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
@@ -428,12 +446,11 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-post-form
display block
padding 16px
- background isDark ? #282C37 : lighten($theme-color, 95%)
+ background var(--desktopPostFormBg)
+ overflow hidden
&:after
content ""
@@ -447,26 +464,26 @@ root(isDark)
width 100%
padding 12px
font-size 16px
- color isDark ? #fff : #333
- background isDark ? #191d23 : #fff
+ color var(--desktopPostFormTextareaFg)
+ background var(--desktopPostFormTextareaBg)
outline none
- border solid 1px rgba($theme-color, 0.1)
+ border solid 1px var(--primaryAlpha01)
border-radius 4px
transition border-color .2s ease
&:hover
- border-color rgba($theme-color, 0.2)
+ border-color var(--primaryAlpha02)
transition border-color .1s ease
&:focus
- border-color rgba($theme-color, 0.5)
+ border-color var(--primaryAlpha05)
transition border-color 0s ease
&:disabled
opacity 0.5
&::-webkit-input-placeholder
- color rgba($theme-color, 0.3)
+ color var(--primaryAlpha03)
> input
margin-bottom 8px
@@ -480,17 +497,17 @@ root(isDark)
&:hover
& + *
& + * + *
- border-color rgba($theme-color, 0.2)
+ border-color var(--primaryAlpha02)
transition border-color .1s ease
&:focus
& + *
& + * + *
- border-color rgba($theme-color, 0.5)
+ border-color var(--primaryAlpha05)
transition border-color 0s ease
&.with
- border-bottom solid 1px rgba($theme-color, 0.1) !important
+ border-bottom solid 1px var(--primaryAlpha01) !important
border-radius 4px 4px 0 0
> .visibleUsers
@@ -499,7 +516,7 @@ root(isDark)
> span
margin-right 16px
- color isDark ? #fff : #666
+ color var(--primary)
> .hashtags
margin 0 0 8px 0
@@ -508,23 +525,23 @@ root(isDark)
font-size 14px
> b
- color isDark ? #9baec8 : darken($theme-color, 20%)
+ color var(--primary)
> *
margin-right 8px
white-space nowrap
- > .medias
+ > .files
margin 0
padding 0
- background isDark ? #181b23 : lighten($theme-color, 98%)
- border solid 1px rgba($theme-color, 0.1)
+ background var(--desktopPostFormTextareaBg)
+ border solid 1px var(--primaryAlpha01)
border-top none
border-radius 0 0 4px 4px
transition border-color .3s ease
&.with
- border-bottom solid 1px rgba($theme-color, 0.1) !important
+ border-bottom solid 1px var(--primaryAlpha01) !important
border-radius 0
> .remain
@@ -534,7 +551,7 @@ root(isDark)
right 8px
margin 0
padding 0
- color rgba($theme-color, 0.4)
+ color var(--primaryAlpha04)
> div
padding 4px
@@ -568,8 +585,8 @@ root(isDark)
cursor pointer
> .mk-poll-editor
- background isDark ? #181b23 : lighten($theme-color, 98%)
- border solid 1px rgba($theme-color, 0.1)
+ background var(--desktopPostFormTextareaBg)
+ border solid 1px var(--primaryAlpha01)
border-top none
border-radius 0 0 4px 4px
transition border-color .3s ease
@@ -577,7 +594,7 @@ root(isDark)
> .mk-uploader
margin 8px 0 0 0
padding 8px
- border solid 1px rgba($theme-color, 0.2)
+ border solid 1px var(--primaryAlpha02)
border-radius 4px
input[type='file']
@@ -594,22 +611,20 @@ root(isDark)
width 110px
height 40px
font-size 1em
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+ color var(--primaryForeground)
+ background var(--primary)
outline none
- border solid 1px lighten($theme-color, 15%)
+ border none
border-radius 4px
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
+ background var(--primaryLighten5)
&:active:not(:disabled)
- background $theme-color
- border-color $theme-color
+ background var(--primaryDarken5)
&:focus
&:after
@@ -620,7 +635,7 @@ root(isDark)
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
@@ -630,13 +645,13 @@ root(isDark)
&.wait
background linear-gradient(
45deg,
- darken($theme-color, 10%) 25%,
- $theme-color 25%,
- $theme-color 50%,
- darken($theme-color, 10%) 50%,
- darken($theme-color, 10%) 75%,
- $theme-color 75%,
- $theme-color
+ var(--primaryDarken10) 25%,
+ var(--primary) 25%,
+ var(--primary) 50%,
+ var(--primaryDarken10) 50%,
+ var(--primaryDarken10) 75%,
+ var(--primary) 75%,
+ var(--primary)
)
background-size 32px 32px
animation stripe-bg 1.5s linear infinite
@@ -655,7 +670,7 @@ root(isDark)
right 138px
margin 0
line-height 40px
- color rgba($theme-color, 0.5)
+ color var(--primaryAlpha05)
&.over
color #ec3828
@@ -673,7 +688,7 @@ root(isDark)
width 40px
height 40px
font-size 1em
- color isDark ? $theme-color : rgba($theme-color, 0.5)
+ color var(--desktopPostFormTransparentButtonFg)
background transparent
outline none
border solid 1px transparent
@@ -681,12 +696,12 @@ root(isDark)
&:hover
background transparent
- border-color isDark ? rgba($theme-color, 0.5) : rgba($theme-color, 0.3)
+ border-color var(--primaryAlpha03)
&:active
- color rgba($theme-color, 0.6)
- background isDark ? transparent : linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%)
- border-color rgba($theme-color, 0.5)
+ color var(--primaryAlpha06)
+ background linear-gradient(to bottom, var(--desktopPostFormTransparentButtonActiveGradientStart) 0%, var(--desktopPostFormTransparentButtonActiveGradientEnd) 100%)
+ border-color var(--primaryAlpha05)
box-shadow 0 2px 4px rgba(#000, 0.15) inset
&:focus
@@ -698,7 +713,7 @@ root(isDark)
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
> .dropzone
@@ -707,13 +722,7 @@ root(isDark)
top 0
width 100%
height 100%
- border dashed 2px rgba($theme-color, 0.5)
+ border dashed 2px var(--primaryAlpha05)
pointer-events none
-.mk-post-form[data-darkmode]
- root(true)
-
-.mk-post-form:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue
index 2f59733d99..feda6050bc 100644
--- a/src/client/app/desktop/views/components/progress-dialog.vue
+++ b/src/client/app/desktop/views/components/progress-dialog.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
+<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
<span slot="header">{{ title }}<mk-ellipsis/></span>
<div :class="$style.body">
<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
@@ -37,7 +37,7 @@ export default Vue.extend({
</script>
<style lang="stylus" module>
-@import '~const.styl'
+
.body
padding 18px 24px 24px 24px
@@ -53,7 +53,7 @@ export default Vue.extend({
margin 0 0 4px 0
text-align center
line-height 16px
- color rgba($theme-color, 0.7)
+ color var(--primaryAlpha07)
&:after
content '%'
@@ -69,21 +69,21 @@ export default Vue.extend({
overflow hidden
&::-webkit-progress-value
- background $theme-color
+ background var(--primary)
&::-webkit-progress-bar
- background rgba($theme-color, 0.1)
+ background var(--primaryAlpha01)
.waiting
background linear-gradient(
45deg,
- lighten($theme-color, 30%) 25%,
- $theme-color 25%,
- $theme-color 50%,
- lighten($theme-color, 30%) 50%,
- lighten($theme-color, 30%) 75%,
- $theme-color 75%,
- $theme-color
+ var(--primaryLighten30) 25%,
+ var(--primary) 25%,
+ var(--primary) 50%,
+ var(--primaryLighten30) 50%,
+ var(--primaryLighten30) 75%,
+ var(--primary) 75%,
+ var(--primary)
)
background-size 32px 32px
animation progress-dialog-tag-progress-waiting 1.5s linear infinite
diff --git a/src/client/app/desktop/views/components/received-follow-requests-window.vue b/src/client/app/desktop/views/components/received-follow-requests-window.vue
index 26b7ec2590..3df1329c48 100644
--- a/src/client/app/desktop/views/components/received-follow-requests-window.vue
+++ b/src/client/app/desktop/views/components/received-follow-requests-window.vue
@@ -1,8 +1,8 @@
<template>
-<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<span slot="header">%fa:envelope R% %i18n:@title%</span>
- <div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">
+ <div class="slpqaxdoxhvglersgjukmvizkqbmbokc">
<div v-for="req in requests">
<router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
<span>
@@ -47,8 +47,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-
-root(isDark)
+.slpqaxdoxhvglersgjukmvizkqbmbokc
padding 16px
> button
@@ -57,16 +56,10 @@ root(isDark)
> div
display flex
padding 16px
- border solid 1px isDark ? #1c2023 : #eee
+ border solid 1px var(--faceDivider)
border-radius 4px
> span
margin 0 0 0 auto
-.slpqaxdoxhvglersgjukmvizkqbmbokc[data-darkmode]
- root(true)
-
-.slpqaxdoxhvglersgjukmvizkqbmbokc:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue
index df9d2f7fc7..b9760fcbe9 100644
--- a/src/client/app/desktop/views/components/renote-form-window.vue
+++ b/src/client/app/desktop/views/components/renote-form-window.vue
@@ -1,7 +1,7 @@
<template>
-<mk-window ref="window" is-modal @closed="$destroy">
+<mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation">
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
- <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
+ <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
</mk-window>
</template>
@@ -9,26 +9,48 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note'],
- mounted() {
- document.addEventListener('keydown', this.onDocumentKeydown);
+ props: {
+ note: {
+ type: Object,
+ required: true
+ },
+
+ animation: {
+ type: Boolean,
+ required: false,
+ default: true
+ }
},
- beforeDestroy() {
- document.removeEventListener('keydown', this.onDocumentKeydown);
+
+ computed: {
+ keymap(): any {
+ return {
+ 'esc': this.close,
+ 'enter': this.post,
+ 'q': this.quote,
+ };
+ }
},
+
methods: {
- onDocumentKeydown(e) {
- if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
- if (e.which == 27) { // Esc
- (this.$refs.window as any).close();
- }
- }
+ post() {
+ (this.$refs.form as any).ok();
+ },
+ quote() {
+ (this.$refs.form as any).onQuote();
+ },
+ close() {
+ (this.$refs.window as any).close();
},
onPosted() {
(this.$refs.window as any).close();
},
onCanceled() {
(this.$refs.window as any).close();
+ },
+ onWindowClosed() {
+ this.$emit('closed');
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index 38eab3362f..68d485bada 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -1,11 +1,11 @@
<template>
<div class="mk-renote-form">
- <mk-note-preview :note="note"/>
+ <mk-note-preview class="preview" :note="note"/>
<template v-if="!quote">
<footer>
<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a>
- <button class="ui cancel" @click="cancel">%i18n:@cancel%</button>
- <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:@reposting%' : '%i18n:@renote%' }}</button>
+ <ui-button class="button cancel" inline @click="cancel">%i18n:@cancel%</ui-button>
+ <ui-button class="button ok" inline primary @click="ok" :disabled="wait">{{ wait ? '%i18n:@reposting%' : '%i18n:@renote%' }}</ui-button>
</footer>
</template>
<template v-if="quote">
@@ -57,16 +57,13 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
-
- > .mk-note-preview
+.mk-renote-form
+ > .preview
margin 16px 22px
> footer
height 72px
- background isDark ? #313543 : lighten($theme-color, 95%)
+ background var(--desktopRenoteFormFooter)
> .quote
position absolute
@@ -74,7 +71,7 @@ root(isDark)
left 28px
line-height 40px
- button
+ > .button
display block
position absolute
bottom 16px
@@ -87,10 +84,4 @@ root(isDark)
&.ok
right 16px
-.mk-renote-form[data-darkmode]
- root(true)
-
-.mk-renote-form:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
index deb865b102..4247717748 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,13 +1,19 @@
<template>
-<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
+<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
- <mk-settings @done="close"/>
+ <mk-settings :initial-page="initialPage" @done="close"/>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
+ props: {
+ initialPage: {
+ type: String,
+ required: false
+ }
+ },
methods: {
close() {
(this as any).$refs.window.close();
diff --git a/src/client/app/desktop/views/components/settings.drive.vue b/src/client/app/desktop/views/components/settings.drive.vue
index e8a3cc9685..d254b27110 100644
--- a/src/client/app/desktop/views/components/settings.drive.vue
+++ b/src/client/app/desktop/views/components/settings.drive.vue
@@ -1,7 +1,6 @@
<template>
<div class="root">
<template v-if="!fetching">
- <el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/>
<p><b>{{ capacity | bytes }}</b>%i18n:max%<b>{{ usage | bytes }}</b>%i18n:in-use%</p>
</template>
</div>
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 262583b640..5f465a52bb 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -6,30 +6,28 @@
<button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button>
</label>
<label class="ui from group">
- <p>%i18n:@name%</p>
- <input v-model="name" type="text" class="ui"/>
+ <ui-input v-model="name" type="text">%i18n:@name%</ui-input>
</label>
<label class="ui from group">
- <p>%i18n:@location%</p>
- <input v-model="location" type="text" class="ui"/>
+ <ui-input v-model="location" type="text">%i18n:@location%</ui-input>
</label>
<label class="ui from group">
- <p>%i18n:@description%</p>
- <textarea v-model="description" class="ui"></textarea>
+ <ui-textarea v-model="description">%i18n:@description%</ui-textarea>
</label>
<label class="ui from group">
<p>%i18n:@birthday%</p>
- <el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/>
+ <input type="date" v-model="birthday"/>
</label>
- <button class="ui primary" @click="save">%i18n:@save%</button>
+ <ui-button primary @click="save">%i18n:@save%</ui-button>
<section>
<h2>%i18n:@locked-account%</h2>
- <mk-switch v-model="$store.state.i.isLocked" @change="onChangeIsLocked" text="%i18n:@is-locked%"/>
+ <ui-switch v-model="$store.state.i.isLocked" @change="onChangeIsLocked">%i18n:@is-locked%</ui-switch>
</section>
<section>
<h2>%i18n:@other%</h2>
- <mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
- <mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
+ <ui-switch v-model="$store.state.i.isBot" @change="onChangeIsBot">%i18n:@is-bot%</ui-switch>
+ <ui-switch v-model="$store.state.i.isCat" @change="onChangeIsCat">%i18n:@is-cat%</ui-switch>
+ <ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch>
</section>
</div>
</template>
@@ -46,6 +44,12 @@ export default Vue.extend({
birthday: null,
};
},
+ computed: {
+ alwaysMarkNsfw: {
+ get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+ set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); }
+ },
+ },
created() {
this.name = this.$store.state.i.name || '';
this.location = this.$store.state.i.profile.location;
diff --git a/src/client/app/desktop/views/components/settings.signins.vue b/src/client/app/desktop/views/components/settings.signins.vue
index a414c95c27..7d1bb4f4e7 100644
--- a/src/client/app/desktop/views/components/settings.signins.vue
+++ b/src/client/app/desktop/views/components/settings.signins.vue
@@ -23,25 +23,25 @@ export default Vue.extend({
return {
fetching: true,
signins: [],
- connection: null,
- connectionId: null
+ connection: null
};
},
+
mounted() {
(this as any).api('i/signin_history').then(signins => {
this.signins = signins;
this.fetching = false;
});
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('signin', this.onSignin);
},
+
beforeDestroy() {
- this.connection.off('signin', this.onSignin);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
+
methods: {
onSignin(signin) {
this.signins.unshift(signin);
diff --git a/src/client/app/desktop/views/components/settings.tags.vue b/src/client/app/desktop/views/components/settings.tags.vue
new file mode 100644
index 0000000000..dfc69a387e
--- /dev/null
+++ b/src/client/app/desktop/views/components/settings.tags.vue
@@ -0,0 +1,58 @@
+<template>
+<div class="vfcitkilproprqtbnpoertpsziierwzi">
+ <div v-for="timeline in timelines" class="timeline">
+ <ui-input v-model="timeline.title" @change="save">
+ <span>%i18n:@title%</span>
+ </ui-input>
+ <ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)">
+ <span>%i18n:@query%</span>
+ </ui-textarea>
+ <ui-button class="save" @click="save">%i18n:@save%</ui-button>
+ </div>
+ <ui-button class="add" @click="add">%i18n:@add%</ui-button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ data() {
+ return {
+ timelines: this.$store.state.settings.tagTimelines
+ };
+ },
+
+ methods: {
+ add() {
+ this.timelines.push({
+ id: uuid(),
+ title: '',
+ query: ''
+ });
+
+ this.save();
+ },
+
+ save() {
+ this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines });
+ },
+
+ onQueryChange(timeline, value) {
+ timeline.query = value.split('\n').map(tags => tags.split(' '));
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.vfcitkilproprqtbnpoertpsziierwzi
+ > .timeline
+ padding-bottom 16px
+ border-bottom solid 1px rgba(#000, 0.1)
+
+ > .add
+ margin-top 16px
+
+</style>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 7d6f1d55fb..1cb8d4d4c8 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -5,6 +5,7 @@
<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
+ <p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
@@ -19,18 +20,42 @@
</section>
<section class="web" v-show="page == 'web'">
+ <h1>%i18n:@theme%</h1>
+ <mk-theme/>
+ </section>
+
+ <section class="web" v-show="page == 'web'">
<h1>%i18n:@behaviour%</h1>
- <mk-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="%i18n:@fetch-on-scroll%">
- <span>%i18n:@fetch-on-scroll-desc%</span>
- </mk-switch>
- <mk-switch v-model="autoPopout" text="%i18n:@auto-popout%">
- <span>%i18n:@auto-popout-desc%</span>
- </mk-switch>
+ <ui-switch v-model="fetchOnScroll">
+ %i18n:@fetch-on-scroll%
+ <span slot="desc">%i18n:@fetch-on-scroll-desc%</span>
+ </ui-switch>
+ <ui-switch v-model="autoPopout">
+ %i18n:@auto-popout%
+ <span slot="desc">%i18n:@auto-popout-desc%</span>
+ </ui-switch>
+
+ <section>
+ <header>%i18n:@note-visibility%</header>
+ <ui-switch v-model="rememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch>
+ <section>
+ <header>%i18n:@default-note-visibility%</header>
+ <ui-select v-model="defaultNoteVisibility">
+ <option value="public">%i18n:common.note-visibility.public%</option>
+ <option value="home">%i18n:common.note-visibility.home%</option>
+ <option value="followers">%i18n:common.note-visibility.followers%</option>
+ <option value="specified">%i18n:common.note-visibility.specified%</option>
+ <option value="private">%i18n:common.note-visibility.private%</option>
+ </ui-select>
+ </section>
+ </section>
+
<details>
<summary>%i18n:@advanced%</summary>
- <mk-switch v-model="apiViaStream" text="%i18n:@api-via-stream%">
- <span>%i18n:@api-via-stream-desc%</span>
- </mk-switch>
+ <ui-switch v-model="apiViaStream">
+ %i18n:@api-via-stream%
+ <span slot="desc">%i18n:@api-via-stream-desc%</span>
+ </ui-switch>
</details>
</section>
@@ -42,58 +67,61 @@
<div class="div">
<button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button>
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
- <mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
- <mk-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons" text="%i18n:@circle-icons%"/>
- <mk-switch v-model="$store.state.settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="%i18n:@gradient-window-header%"/>
- <mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/>
+ <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
+ <ui-switch v-model="useShadow">%i18n:@use-shadow%</ui-switch>
+ <ui-switch v-model="roundedCorners">%i18n:@rounded-corners%</ui-switch>
+ <ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
+ <ui-switch v-model="reduceMotion">%i18n:common.reduce-motion%</ui-switch>
+ <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
+ <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
+ <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
</div>
- <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
- <mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
- <mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/>
- <mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
- <mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
- <mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
- <mk-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes" text="%i18n:@show-local-renotes%"/>
- <mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%">
- <span>%i18n:@show-maps-desc%</span>
- </mk-switch>
- <mk-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/>
- <mk-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels" text="%i18n:common.show-reversi-board-labels%"/>
- <mk-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones" text="%i18n:common.use-contrast-reversi-stones%"/>
+ <ui-switch v-model="showPostFormOnTopOfTl">%i18n:@post-form-on-timeline%</ui-switch>
+ <ui-switch v-model="suggestRecentHashtags">%i18n:@suggest-recent-hashtags%</ui-switch>
+ <ui-switch v-model="showClockOnHeader">%i18n:@show-clock-on-header%</ui-switch>
+ <ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw%</ui-switch>
+ <ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch>
+ <ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch>
+ <ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
+ <ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
+ <ui-switch v-model="showMaps">%i18n:@show-maps%</ui-switch>
+ <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
+ <ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
+ <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
</section>
<section class="web" v-show="page == 'web'">
<h1>%i18n:@sound%</h1>
- <mk-switch v-model="enableSounds" text="%i18n:@enable-sounds%">
- <span>%i18n:@enable-sounds-desc%</span>
- </mk-switch>
+ <ui-switch v-model="enableSounds">
+ %i18n:@enable-sounds%
+ <span slot="desc">%i18n:@enable-sounds-desc%</span>
+ </ui-switch>
<label>%i18n:@volume%</label>
- <el-slider
+ <input type="range"
v-model="soundVolume"
- :show-input="true"
- :format-tooltip="v => `${v * 100}%`"
:disabled="!enableSounds"
- :max="1"
- :step="0.1"
+ max="1"
+ step="0.1"
/>
<button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button>
</section>
<section class="web" v-show="page == 'web'">
<h1>%i18n:@mobile%</h1>
- <mk-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile" text="%i18n:@disable-via-mobile%"/>
+ <ui-switch v-model="disableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
</section>
<section class="web" v-show="page == 'web'">
<h1>%i18n:@language%</h1>
- <el-select v-model="lang" placeholder="%i18n:@pick-language%">
- <el-option-group label="%i18n:@recommended%">
- <el-option label="%i18n:@auto%" :value="null"/>
- </el-option-group>
- <el-option-group label="%i18n:@specify-language%">
- <el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/>
- </el-option-group>
- </el-select>
+ <select v-model="lang" placeholder="%i18n:@pick-language%">
+ <optgroup label="%i18n:@recommended%">
+ <option value="">%i18n:@auto%</option>
+ </optgroup>
+
+ <optgroup label="%i18n:@specify-language%">
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ </optgroup>
+ </select>
<div class="none ui info">
<p>%fa:info-circle%%i18n:@language-desc%</p>
</div>
@@ -109,9 +137,10 @@
<section class="notification" v-show="page == 'notification'">
<h1>%i18n:@notification%</h1>
- <mk-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch" text="%i18n:@auto-watch%">
- <span>%i18n:@auto-watch-desc%</span>
- </mk-switch>
+ <ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch">
+ %i18n:@auto-watch%
+ <span slot="desc">%i18n:@auto-watch-desc%</span>
+ </ui-switch>
</section>
<section class="drive" v-show="page == 'drive'">
@@ -119,6 +148,11 @@
<x-drive/>
</section>
+ <section class="hashtags" v-show="page == 'hashtags'">
+ <h1>%i18n:@tags%</h1>
+ <x-tags/>
+ </section>
+
<section class="mute" v-show="page == 'mute'">
<h1>%i18n:@mute%</h1>
<x-mute/>
@@ -174,24 +208,23 @@
</button>
<details>
<summary>%i18n:@update-settings%</summary>
- <mk-switch v-model="preventUpdate" text="%i18n:@prevent-update%">
- <span>%i18n:@prevent-update-desc%</span>
- </mk-switch>
+ <ui-switch v-model="preventUpdate">
+ %i18n:@prevent-update%
+ <span slot="desc">%i18n:@prevent-update-desc%</span>
+ </ui-switch>
</details>
</section>
<section class="other" v-show="page == 'other'">
<h1>%i18n:@advanced-settings%</h1>
- <mk-switch v-model="debug" text="%i18n:@debug-mode%">
- <span>%i18n:@debug-mode-desc%</span>
- </mk-switch>
- <mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%">
- <span>%i18n:@experimental-desc%</span>
- </mk-switch>
- <details v-if="debug">
- <summary>%i18n:@tools%</summary>
- <button class="ui button block" @click="taskmngr">%i18n:@task-manager%</button>
- </details>
+ <ui-switch v-model="debug">
+ %i18n:@debug-mode%
+ <span slot="desc">%i18n:@debug-mode-desc%</span>
+ </ui-switch>
+ <ui-switch v-model="enableExperimentalFeatures">
+ %i18n:@experimental%
+ <span slot="desc">%i18n:@experimental-desc%</span>
+ </ui-switch>
</section>
</div>
</div>
@@ -207,9 +240,9 @@ import XApi from './settings.api.vue';
import XApps from './settings.apps.vue';
import XSignins from './settings.signins.vue';
import XDrive from './settings.drive.vue';
+import XTags from './settings.tags.vue';
import { url, langs, version } from '../../../config';
import checkForUpdate from '../../../common/scripts/check-for-update';
-import MkTaskManager from './taskmanager.vue';
export default Vue.extend({
components: {
@@ -220,11 +253,18 @@ export default Vue.extend({
XApi,
XApps,
XSignins,
- XDrive
+ XDrive,
+ XTags
+ },
+ props: {
+ initialPage: {
+ type: String,
+ required: false
+ }
},
data() {
return {
- page: 'profile',
+ page: this.initialPage || 'profile',
meta: null,
version,
langs,
@@ -233,6 +273,11 @@ export default Vue.extend({
};
},
computed: {
+ reduceMotion: {
+ get() { return this.$store.state.device.reduceMotion; },
+ set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
+ },
+
apiViaStream: {
get() { return this.$store.state.device.apiViaStream; },
set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
@@ -276,6 +321,116 @@ export default Vue.extend({
enableExperimentalFeatures: {
get() { return this.$store.state.device.enableExperimentalFeatures; },
set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); }
+ },
+
+ alwaysShowNsfw: {
+ get() { return this.$store.state.device.alwaysShowNsfw; },
+ set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
+ },
+
+ useShadow: {
+ get() { return this.$store.state.settings.useShadow; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'useShadow', value }); }
+ },
+
+ roundedCorners: {
+ get() { return this.$store.state.settings.roundedCorners; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'roundedCorners', value }); }
+ },
+
+ fetchOnScroll: {
+ get() { return this.$store.state.settings.fetchOnScroll; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
+ },
+
+ rememberNoteVisibility: {
+ get() { return this.$store.state.settings.rememberNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
+ },
+
+ defaultNoteVisibility: {
+ get() { return this.$store.state.settings.defaultNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
+ },
+
+ showReplyTarget: {
+ get() { return this.$store.state.settings.showReplyTarget; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
+ },
+
+ showMyRenotes: {
+ get() { return this.$store.state.settings.showMyRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
+ },
+
+ showRenotedMyNotes: {
+ get() { return this.$store.state.settings.showRenotedMyNotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
+ },
+
+ showLocalRenotes: {
+ get() { return this.$store.state.settings.showLocalRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
+ },
+
+ showPostFormOnTopOfTl: {
+ get() { return this.$store.state.settings.showPostFormOnTopOfTl; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); }
+ },
+
+ suggestRecentHashtags: {
+ get() { return this.$store.state.settings.suggestRecentHashtags; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); }
+ },
+
+ showClockOnHeader: {
+ get() { return this.$store.state.settings.showClockOnHeader; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); }
+ },
+
+ showMaps: {
+ get() { return this.$store.state.settings.showMaps; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMaps', value }); }
+ },
+
+ circleIcons: {
+ get() { return this.$store.state.settings.circleIcons; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); }
+ },
+
+ contrastedAcct: {
+ get() { return this.$store.state.settings.contrastedAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); }
+ },
+
+ showFullAcct: {
+ get() { return this.$store.state.settings.showFullAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); }
+ },
+
+ iLikeSushi: {
+ get() { return this.$store.state.settings.iLikeSushi; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
+ },
+
+ games_reversi_showBoardLabels: {
+ get() { return this.$store.state.settings.games.reversi.showBoardLabels; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); }
+ },
+
+ games_reversi_useContrastStones: {
+ get() { return this.$store.state.settings.games.reversi.useContrastStones; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); }
+ },
+
+ disableAnimatedMfm: {
+ get() { return this.$store.state.settings.disableAnimatedMfm; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
+ },
+
+ disableViaMobile: {
+ get() { return this.$store.state.settings.disableViaMobile; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
}
},
created() {
@@ -284,9 +439,6 @@ export default Vue.extend({
});
},
methods: {
- taskmngr() {
- (this as any).os.new(MkTaskManager);
- },
customizeHome() {
this.$router.push('/i/customize-home');
this.$emit('done');
@@ -305,113 +457,11 @@ export default Vue.extend({
wallpaperId: null
});
},
- onChangeFetchOnScroll(v) {
- this.$store.dispatch('settings/set', {
- key: 'fetchOnScroll',
- value: v
- });
- },
onChangeAutoWatch(v) {
(this as any).api('i/update', {
autoWatch: v
});
},
- onChangeDark(v) {
- this.$store.dispatch('settings/set', {
- key: 'dark',
- value: v
- });
- },
- onChangeShowPostFormOnTopOfTl(v) {
- this.$store.dispatch('settings/set', {
- key: 'showPostFormOnTopOfTl',
- value: v
- });
- },
- onChangeSuggestRecentHashtags(v) {
- this.$store.dispatch('settings/set', {
- key: 'suggestRecentHashtags',
- value: v
- });
- },
- onChangeShowClockOnHeader(v) {
- this.$store.dispatch('settings/set', {
- key: 'showClockOnHeader',
- value: v
- });
- },
- onChangeShowReplyTarget(v) {
- this.$store.dispatch('settings/set', {
- key: 'showReplyTarget',
- value: v
- });
- },
- onChangeShowMyRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMyRenotes',
- value: v
- });
- },
- onChangeShowRenotedMyNotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showRenotedMyNotes',
- value: v
- });
- },
- onChangeShowLocalRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showLocalRenotes',
- value: v
- });
- },
- onChangeShowMaps(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMaps',
- value: v
- });
- },
- onChangeCircleIcons(v) {
- this.$store.dispatch('settings/set', {
- key: 'circleIcons',
- value: v
- });
- },
- onChangeILikeSushi(v) {
- this.$store.dispatch('settings/set', {
- key: 'iLikeSushi',
- value: v
- });
- },
- onChangeReversiBoardLabels(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.showBoardLabels',
- value: v
- });
- },
- onChangeUseContrastReversiStones(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.useContrastStones',
- value: v
- });
- },
- onChangeDisableAnimatedMfm(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableAnimatedMfm',
- value: v
- });
- },
- onChangeGradientWindowHeader(v) {
- this.$store.dispatch('settings/set', {
- key: 'gradientWindowHeader',
- value: v
- });
- },
- onChangeDisableViaMobile(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableViaMobile',
- value: v
- });
- },
checkForUpdate() {
this.checkingForUpdate = true;
checkForUpdate((this as any).os, true, true).then(newer => {
@@ -447,9 +497,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-settings
display flex
width 100%
height 100%
@@ -460,13 +508,13 @@ root(isDark)
height 100%
padding 16px 0 0 0
overflow auto
- border-right solid 1px isDark ? #1c2023 : #ddd
+ border-right solid 1px var(--faceDivider)
> p
display block
padding 10px 16px
margin 0
- color isDark ? #9aa2a7 : #666
+ color var(--desktopSettingsNavItem)
cursor pointer
user-select none
transition margin-left 0.2s ease
@@ -475,11 +523,11 @@ root(isDark)
margin-right 4px
&:hover
- color isDark ? #fff : #555
+ color var(--desktopSettingsNavItemHover)
&.active
margin-left 8px
- color $theme-color !important
+ color var(--primary) !important
> .pages
width 100%
@@ -489,14 +537,13 @@ root(isDark)
> section
margin 32px
- color isDark ? #c4ccd2 : #4a535a
+ color var(--text)
> h1
margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
- color isDark ? #e3e7ea : #555
- border-bottom solid 1px isDark ? #1c2023 : #eee
+ border-bottom solid 1px var(--faceDivider)
&, >>> *
.ui.button.block
@@ -509,18 +556,12 @@ root(isDark)
margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
- color isDark ? #e3e7ea : #555
- border-bottom solid 1px isDark ? #1c2023 : #eee
+ color var(--text)
+ border-bottom solid 1px var(--faceDivider)
> .web
> .div
- border-bottom solid 1px isDark ? #1c2023 : #eee
+ border-bottom solid 1px var(--faceDivider)
margin 16px 0
-.mk-settings[data-darkmode]
- root(true)
-
-.mk-settings:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index cb0374b910..fd8e658056 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -7,9 +7,9 @@
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a>
</div>
- <details v-if="note.media.length > 0">
- <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
- <mk-media-list :media-list="note.media"/>
+ <details v-if="note.files.length > 0">
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+ <mk-media-list :media-list="note.files"/>
</details>
<details v-if="note.poll">
<summary>%i18n:@poll%</summary>
@@ -38,7 +38,7 @@ export default Vue.extend({
> .rp
margin-left 4px
font-style oblique
- color #a0bf46
+ color var(--renoteText)
mk-poll
font-size 80%
diff --git a/src/client/app/desktop/views/components/taskmanager.vue b/src/client/app/desktop/views/components/taskmanager.vue
deleted file mode 100644
index 1f1385add8..0000000000
--- a/src/client/app/desktop/views/components/taskmanager.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-<template>
-<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager">
- <span slot="header" :class="$style.header">%fa:stethoscope%%i18n:@title%</span>
- <el-tabs :class="$style.content">
- <el-tab-pane label="Requests">
- <el-table
- :data="os.requests"
- style="width: 100%"
- :default-sort="{prop: 'date', order: 'descending'}"
- >
- <el-table-column type="expand">
- <template slot-scope="props">
- <pre>{{ props.row.data }}</pre>
- <pre>{{ props.row.res }}</pre>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Requested at"
- prop="date"
- sortable
- >
- <template slot-scope="scope">
- <b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b>
- <span>(<mk-time :time="scope.row.date"/>)</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Status"
- >
- <template slot-scope="scope">
- <span>{{ scope.row.status || '(pending)' }}</span>
- </template>
- </el-table-column>
- </el-table>
- </el-tab-pane>
-
- <el-tab-pane label="Streams">
- <el-table
- :data="os.connections"
- style="width: 100%"
- >
- <el-table-column
- label="Uptime"
- >
- <template slot-scope="scope">
- <mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/>
- <span v-else>-</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="User"
- >
- <template slot-scope="scope">
- <span>{{ scope.row.user || '(anonymous)' }}</span>
- </template>
- </el-table-column>
-
- <el-table-column
- prop="state"
- label="State"
- />
-
- <el-table-column
- prop="in"
- label="In"
- />
-
- <el-table-column
- prop="out"
- label="Out"
- />
- </el-table>
- </el-tab-pane>
-
- <el-tab-pane label="Streams (Inspect)">
- <el-tabs type="card" style="height:50%">
- <el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab">
- <div style="padding: 12px 0 0 12px">
- <el-button size="mini" @click="send(c)">Send</el-button>
- <el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button>
- <el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button>
- <el-button size="mini" type="danger" @click="c.close">Disconnect</el-button>
- </div>
-
- <el-table
- :data="c.inout"
- style="width: 100%"
- :default-sort="{prop: 'at', order: 'descending'}"
- >
- <el-table-column type="expand">
- <template slot-scope="props">
- <pre>{{ props.row.data }}</pre>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Date"
- prop="at"
- sortable
- >
- <template slot-scope="scope">
- <b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b>
- <span>(<mk-time :time="scope.row.at"/>)</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Type"
- >
- <template slot-scope="scope">
- <span>{{ getMessageType(scope.row.data) }}</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Incoming / Outgoing"
- prop="type"
- />
- </el-table>
- </el-tab-pane>
- </el-tabs>
- </el-tab-pane>
-
- <el-tab-pane label="Windows">
- <el-table
- :data="Array.from(os.windows.windows)"
- style="width: 100%"
- >
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name || '(unknown)' }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Operations"
- >
- <template slot-scope="scope">
- <el-button size="mini" type="danger" @click="scope.row.close">Close</el-button>
- </template>
- </el-table-column>
- </el-table>
- </el-tab-pane>
- </el-tabs>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- mounted() {
- (this as any).os.windows.on('added', this.onWindowsChanged);
- (this as any).os.windows.on('removed', this.onWindowsChanged);
- },
- beforeDestroy() {
- (this as any).os.windows.off('added', this.onWindowsChanged);
- (this as any).os.windows.off('removed', this.onWindowsChanged);
- },
- methods: {
- getMessageType(data): string {
- return data.type ? data.type : '-';
- },
- onWindowsChanged() {
- this.$forceUpdate();
- },
- send(c) {
- (this as any).apis.input({
- title: 'Send a JSON message',
- allowEmpty: false
- }).then(json => {
- c.send(JSON.parse(json));
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" module>
-.header
- > [data-fa]
- margin-right 4px
-
-.content
- height 100%
- overflow auto
-
-</style>
-
-<style>
-.el-tabs__header {
- margin-bottom: 0 !important;
-}
-
-.el-tabs__item {
- padding: 0 20px !important;
-}
-</style>
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 25fd5d36ac..2c17e936eb 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -23,6 +23,9 @@ export default Vue.extend({
src: {
type: String,
required: true
+ },
+ tagTl: {
+ required: false
}
},
@@ -32,8 +35,14 @@ export default Vue.extend({
moreFetching: false,
existMore: false,
connection: null,
- connectionId: null,
- date: null
+ date: null,
+ baseQuery: {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ },
+ query: {},
+ endpoint: null
};
},
@@ -42,53 +51,67 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0;
},
- stream(): any {
- switch (this.src) {
- case 'home': return (this as any).os.stream;
- case 'local': return (this as any).os.streams.localTimelineStream;
- case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
- case 'global': return (this as any).os.streams.globalTimelineStream;
- }
- },
-
- endpoint(): string {
- switch (this.src) {
- case 'home': return 'notes/timeline';
- case 'local': return 'notes/local-timeline';
- case 'hybrid': return 'notes/hybrid-timeline';
- case 'global': return 'notes/global-timeline';
- }
- },
-
canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore;
}
},
mounted() {
- this.connection = this.stream.getConnection();
- this.connectionId = this.stream.use();
+ const prepend = note => {
+ (this.$refs.timeline as any).prepend(note);
+ };
- this.connection.on('note', this.onNote);
- if (this.src == 'home') {
- this.connection.on('follow', this.onChangeFollowing);
- this.connection.on('unfollow', this.onChangeFollowing);
+ if (this.src == 'tag') {
+ this.endpoint = 'notes/search_by_tag';
+ this.query = {
+ query: this.tagTl.query
+ };
+ this.connection = (this as any).os.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 as any).os.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 as any).os.stream.useSharedConnection('localTimeline');
+ this.connection.on('note', prepend);
+ } else if (this.src == 'hybrid') {
+ this.endpoint = 'notes/hybrid-timeline';
+ this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline');
+ this.connection.on('note', prepend);
+ } else if (this.src == 'global') {
+ this.endpoint = 'notes/global-timeline';
+ this.connection = (this as any).os.stream.useSharedConnection('globalTimeline');
+ this.connection.on('note', prepend);
+ } else if (this.src == 'mentions') {
+ this.endpoint = 'notes/mentions';
+ this.connection = (this as any).os.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 as any).os.stream.useSharedConnection('main');
+ this.connection.on('mention', onNote);
}
- document.addEventListener('keydown', this.onKeydown);
-
this.fetch();
},
beforeDestroy() {
- this.connection.off('note', this.onNote);
- if (this.src == 'home') {
- this.connection.off('follow', this.onChangeFollowing);
- this.connection.off('unfollow', this.onChangeFollowing);
- }
- this.stream.dispose(this.connectionId);
-
- document.removeEventListener('keydown', this.onKeydown);
+ this.connection.dispose();
},
methods: {
@@ -96,13 +119,10 @@ export default Vue.extend({
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
- (this as any).api(this.endpoint, {
+ (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilDate: this.date ? this.date.getTime() : undefined,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- }).then(notes => {
+ untilDate: this.date ? this.date.getTime() : undefined
+ }, this.baseQuery, this.query)).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
@@ -119,13 +139,10 @@ export default Vue.extend({
this.moreFetching = true;
- const promise = (this as any).api(this.endpoint, {
+ const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- });
+ untilId: (this.$refs.timeline as any).tail().id
+ }, this.baseQuery, this.query));
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -140,15 +157,6 @@ export default Vue.extend({
return promise;
},
- onNote(note) {
- // Prepend a note
- (this.$refs.timeline as any).prepend(note);
- },
-
- onChangeFollowing() {
- this.fetch();
- },
-
focus() {
(this.$refs.timeline as any).focus();
},
@@ -156,21 +164,13 @@ export default Vue.extend({
warp(date) {
this.date = date;
this.fetch();
- },
-
- onKeydown(e) {
- if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
- if (e.which == 84) { // t
- this.focus();
- }
- }
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-timeline-core
> .mk-friends-maker
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 52a7753438..3e4c45d228 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -2,16 +2,25 @@
<div class="mk-timeline">
<header>
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
- <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
- <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+ <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+ <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
+ <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
- <button @click="chooseList" title="%i18n:@list%">%fa:list%</button>
+ <div class="buttons">
+ <button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></button>
+ <button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></button>
+ <button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
+ <button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
+ </div>
</header>
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
+ <x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
+ <x-core 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>
</template>
@@ -19,7 +28,8 @@
<script lang="ts">
import Vue from 'vue';
import XCore from './timeline.core.vue';
-import MkUserListsWindow from './user-lists-window.vue';
+import Menu from '../../../common/views/components/menu.vue';
+import MkSettingsWindow from './settings-window.vue';
export default Vue.extend({
components: {
@@ -29,7 +39,9 @@ export default Vue.extend({
data() {
return {
src: 'home',
- list: null
+ list: null,
+ tagTl: null,
+ enableLocalTimeline: false
};
},
@@ -38,16 +50,28 @@ export default Vue.extend({
this.saveSrc();
},
- list() {
+ list(x) {
this.saveSrc();
+ if (x != null) this.tagTl = null;
+ },
+
+ tagTl(x) {
+ this.saveSrc();
+ if (x != null) this.list = null;
}
},
created() {
+ (this as any).os.getMeta().then(meta => {
+ this.enableLocalTimeline = !meta.disableLocalTimeline;
+ });
+
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;
}
} else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid';
@@ -64,20 +88,86 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
- arg: this.list
+ arg: this.src == 'list' ? this.list : this.tagTl
});
},
+ focus() {
+ (this.$refs.tl as any).focus();
+ },
+
warp(date) {
(this.$refs.tl as any).warp(date);
},
- chooseList() {
- const w = (this as any).os.new(MkUserListsWindow);
- w.$once('choosen', list => {
- this.list = list;
- this.src = 'list';
- w.close();
+ async chooseList() {
+ const lists = await (this as any).api('users/lists/list');
+
+ let menu = [{
+ icon: '%fa:plus%',
+ text: '%i18n:@add-list%',
+ action: () => {
+ (this as any).apis.input({
+ title: '%i18n:@list-name%',
+ }).then(async title => {
+ const list = await (this as any).api('users/lists/create', {
+ title
+ });
+
+ this.list = list;
+ this.src = 'list';
+ });
+ }
+ }];
+
+ if (lists.length > 0) {
+ menu.push(null);
+ }
+
+ menu = menu.concat(lists.map(list => ({
+ icon: '%fa:list%',
+ text: list.title,
+ action: () => {
+ this.list = list;
+ this.src = 'list';
+ }
+ })));
+
+ this.os.new(Menu, {
+ source: this.$refs.listButton,
+ compact: false,
+ items: menu
+ });
+ },
+
+ chooseTag() {
+ let menu = [{
+ icon: '%fa:plus%',
+ text: '%i18n:@add-tag-timeline%',
+ action: () => {
+ (this as any).os.new(MkSettingsWindow, {
+ initialPage: 'hashtags'
+ });
+ }
+ }];
+
+ if (this.$store.state.settings.tagTimelines.length > 0) {
+ menu.push(null);
+ }
+
+ menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({
+ icon: '%fa:hashtag%',
+ text: t.title,
+ action: () => {
+ this.tagTl = t;
+ this.src = 'tag';
+ }
+ })));
+
+ this.os.new(Menu, {
+ source: this.$refs.tagButton,
+ compact: false,
+ items: menu
});
}
}
@@ -85,36 +175,54 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
+.mk-timeline
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
+ overflow hidden
> header
padding 0 8px
z-index 10
- background isDark ? #313543 : #fff
- border-radius 6px 6px 0 0
- box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
+ background var(--faceHeader)
+ box-shadow 0 1px var(--desktopTimelineHeaderShadow)
- > button
+ > .buttons
position absolute
z-index 2
top 0
right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color isDark ? #9baec8 : #ccc
+ padding-right 8px
- &:hover
- color isDark ? #b2c1d5 : #aaa
+ > button
+ padding 0 8px
+ font-size 0.9em
+ line-height 42px
+ color var(--faceTextButton)
- &:active
- color isDark ? #b2c1d5 : #999
+ > .badge
+ position absolute
+ top -4px
+ right 4px
+ font-size 10px
+ color var(--primary)
+
+ &:hover
+ color var(--faceTextButtonHover)
+
+ &[data-active]
+ color var(--primary)
+ cursor default
+
+ &:before
+ content ""
+ display block
+ position absolute
+ bottom 0
+ left 0
+ width 100%
+ height 2px
+ background var(--primary)
> span
display inline-block
@@ -124,7 +232,7 @@ root(isDark)
user-select none
&[data-active]
- color $theme-color
+ color var(--primary)
cursor default
font-weight bold
@@ -136,19 +244,13 @@ root(isDark)
left -8px
width calc(100% + 16px)
height 2px
- background $theme-color
+ background var(--primary)
&:not([data-active])
- color isDark ? #9aa2a7 : #6f7477
+ color var(--desktopTimelineSrc)
cursor pointer
&:hover
- color isDark ? #d9dcde : #525a5f
-
-.mk-timeline[data-darkmode]
- root(true)
-
-.mk-timeline:not([data-darkmode])
- root(false)
+ color var(--desktopTimelineSrcHover)
</style>
diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue
index 68413914c0..dafede4c36 100644
--- a/src/client/app/desktop/views/components/ui-notification.vue
+++ b/src/client/app/desktop/views/components/ui-notification.vue
@@ -27,7 +27,7 @@ export default Vue.extend({
translateY: -64,
duration: 500,
easing: 'easeInElastic',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}, 6000);
});
@@ -36,7 +36,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mk-ui-notification
display block
position fixed
z-index 10000
@@ -46,10 +46,10 @@ root(isDark)
margin 0 auto
padding 128px 0 0 0
width 500px
- color rgba(isDark ? #fff : #000, 0.6)
- background rgba(isDark ? #282C37 : #fff, 0.9)
+ color var(--desktopNotificationFg)
+ background var(--desktopNotificationBg)
border-radius 0 0 8px 8px
- box-shadow 0 2px 4px rgba(#000, isDark ? 0.4 : 0.2)
+ box-shadow 0 2px 4px var(--desktopNotificationShadow)
transform translateY(-64px)
opacity 0
@@ -58,10 +58,4 @@ root(isDark)
line-height 64px
text-align center
-.mk-ui-notification[data-darkmode]
- root(true)
-
-.mk-ui-notification:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index 5e26389d89..a541dea121 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -1,5 +1,5 @@
<template>
-<div class="account">
+<div class="account" v-hotkey.global="keymap">
<button class="header" :data-active="isOpen" @click="toggle">
<span class="username">{{ $store.state.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
<mk-avatar class="avatar" :user="$store.state.i"/>
@@ -63,6 +63,13 @@ export default Vue.extend({
isOpen: false
};
},
+ computed: {
+ keymap(): any {
+ return {
+ 'a|m': this.toggle
+ };
+ }
+ },
beforeDestroy() {
this.close();
},
@@ -120,14 +127,12 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.account
> .header
display block
margin 0
padding 0
- color #9eaba8
+ color var(--desktopHeaderFg)
border none
background transparent
cursor pointer
@@ -137,14 +142,11 @@ root(isDark)
&:hover
&[data-active='true']
- color isDark ? #fff : darken(#9eaba8, 20%)
+ color var(--desktopHeaderHoverFg)
> .avatar
filter saturate(150%)
- &:active
- color isDark ? #fff : darken(#9eaba8, 30%)
-
> .username
display block
float left
@@ -170,7 +172,7 @@ root(isDark)
transition filter 100ms ease
> .menu
- $bgcolor = isDark ? #282c37 : #fff
+ $bgcolor = var(--face)
display block
position absolute
top 56px
@@ -213,7 +215,7 @@ root(isDark)
& + ul
padding-top 10px
- border-top solid 1px isDark ? #1c2023 : #eee
+ border-top solid 1px var(--faceDivider)
> li
display block
@@ -227,7 +229,7 @@ root(isDark)
padding 0 28px
margin 0
line-height 40px
- color isDark ? #c8cece : #868C8C
+ color var(--text)
cursor pointer
*
@@ -242,8 +244,8 @@ root(isDark)
padding 2px 8px
font-size 90%
font-style normal
- background $theme-color
- color $theme-color-foreground
+ background var(--primary)
+ color var(--primaryForeground)
border-radius 8px
> [data-fa]:first-child
@@ -262,11 +264,11 @@ root(isDark)
&:hover, &:active
text-decoration none
- background $theme-color
- color $theme-color-foreground
+ background var(--primary)
+ color var(--primaryForeground)
&:active
- background darken($theme-color, 10%)
+ background var(--primaryDarken10)
&.signout
$color = #e64137
@@ -283,10 +285,4 @@ root(isDark)
transform-origin: center -16px;
}
-.account[data-darkmode]
- root(true)
-
-.account:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.clock.vue b/src/client/app/desktop/views/components/ui.header.clock.vue
index 1c3f12f2f2..b8b638bc41 100644
--- a/src/client/app/desktop/views/components/ui.header.clock.vue
+++ b/src/client/app/desktop/views/components/ui.header.clock.vue
@@ -89,7 +89,7 @@ export default Vue.extend({
display table-cell
vertical-align middle
height 48px
- color #9eaba8
+ color var(--desktopHeaderFg)
> .yyyymmdd
opacity 0.7
diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue
index 6292b764c6..122570a696 100644
--- a/src/client/app/desktop/views/components/ui.header.nav.vue
+++ b/src/client/app/desktop/views/components/ui.header.nav.vue
@@ -42,8 +42,7 @@ export default Vue.extend({
data() {
return {
hasGameInvitations: false,
- connection: null,
- connectionId: null
+ connection: null
};
},
computed: {
@@ -53,18 +52,15 @@ export default Vue.extend({
},
mounted() {
if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
- this.connection.on('reversi_invited', this.onReversiInvited);
+ this.connection.on('reversiInvited', this.onReversiInvited);
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
- this.connection.off('reversi_invited', this.onReversiInvited);
- this.connection.off('reversi_no_invites', this.onReversiNoInvites);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
}
},
methods: {
@@ -95,9 +91,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.nav
display inline-block
margin 0
padding 0
@@ -120,7 +114,7 @@ root(isDark)
&.active
> a
- border-bottom solid 3px $theme-color
+ border-bottom solid 3px var(--primary)
> a
display inline-block
@@ -129,7 +123,7 @@ root(isDark)
padding 0 24px
font-size 13px
font-variant small-caps
- color isDark ? #b8c5ca : #9eaba8
+ color var(--desktopHeaderFg)
text-decoration none
transition none
cursor pointer
@@ -138,7 +132,7 @@ root(isDark)
pointer-events none
&:hover
- color isDark ? #fff : darken(#9eaba8, 20%)
+ color var(--desktopHeaderHoverFg)
text-decoration none
> [data-fa]:first-child
@@ -147,7 +141,7 @@ root(isDark)
> [data-fa]:last-child
margin-left 5px
font-size 10px
- color $theme-color
+ color var(--primary)
@media (max-width 1100px)
margin-left -5px
@@ -162,10 +156,4 @@ root(isDark)
@media (max-width 700px)
padding 0 12px
-.nav[data-darkmode]
- root(true)
-
-.nav:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue
index 59a16df9ec..c59a49556d 100644
--- a/src/client/app/desktop/views/components/ui.header.notifications.vue
+++ b/src/client/app/desktop/views/components/ui.header.notifications.vue
@@ -1,5 +1,5 @@
<template>
-<div class="notifications">
+<div class="notifications" v-hotkey.global="keymap">
<button :data-active="isOpen" @click="toggle" title="%i18n:@title%">
%fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template>
</button>
@@ -19,11 +19,19 @@ export default Vue.extend({
isOpen: false
};
},
+
computed: {
hasUnreadNotification(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
+ },
+
+ keymap(): any {
+ return {
+ 'shift+n': this.toggle
+ };
}
},
+
methods: {
toggle() {
this.isOpen ? this.close() : this.open();
@@ -53,16 +61,13 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
-
+.notifications
> button
display block
margin 0
padding 0
width 32px
- color #9eaba8
+ color var(--desktopHeaderFg)
border none
background transparent
cursor pointer
@@ -72,10 +77,7 @@ root(isDark)
&:hover
&[data-active='true']
- color isDark ? #fff : darken(#9eaba8, 20%)
-
- &:active
- color isDark ? #fff : darken(#9eaba8, 30%)
+ color var(--desktopHeaderHoverFg)
> [data-fa].bell
font-size 1.2em
@@ -85,10 +87,10 @@ root(isDark)
margin-left -5px
vertical-align super
font-size 10px
- color $theme-color
+ color var(--primary)
> .pop
- $bgcolor = isDark ? #282c37 : #fff
+ $bgcolor = var(--face)
display block
position absolute
top 56px
@@ -127,10 +129,4 @@ root(isDark)
font-size 1rem
overflow auto
-.notifications[data-darkmode]
- root(true)
-
-.notifications:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue
index 3665488542..9527792a34 100644
--- a/src/client/app/desktop/views/components/ui.header.post.vue
+++ b/src/client/app/desktop/views/components/ui.header.post.vue
@@ -17,7 +17,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.note
display inline-block
@@ -33,8 +33,8 @@ export default Vue.extend({
font-size 1.2em
font-weight normal
text-decoration none
- color $theme-color-foreground
- background $theme-color !important
+ color var(--primaryForeground)
+ background var(--primary) !important
outline none
border none
border-radius 4px
@@ -45,10 +45,10 @@ export default Vue.extend({
pointer-events none
&:hover
- background lighten($theme-color, 10%) !important
+ background var(--primaryLighten10) !important
&:active
- background darken($theme-color, 10%) !important
+ background var(--primaryDarken10) !important
transition background 0s ease
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue
index 9a36e52fcc..d22efbf84f 100644
--- a/src/client/app/desktop/views/components/ui.header.search.vue
+++ b/src/client/app/desktop/views/components/ui.header.search.vue
@@ -28,8 +28,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-root(isDark)
+.search
> [data-fa]
display block
position absolute
@@ -38,7 +37,7 @@ root(isDark)
width 48px
text-align center
line-height 48px
- color #9eaba8
+ color var(--desktopHeaderFg)
pointer-events none
> *
@@ -52,26 +51,20 @@ root(isDark)
width 14em
height 32px
font-size 1em
- background rgba(#000, 0.05)
+ background var(--desktopHeaderSearchBg)
outline none
- //border solid 1px #ddd
border none
border-radius 16px
transition color 0.5s ease, border 0.5s ease
- color isDark ? #fff : #000
+ color var(--desktopHeaderSearchFg)
&::placeholder
- color #9eaba8
+ color var(--desktopHeaderFg)
&:hover
- background isDark ? rgba(#fff, 0.04) : rgba(#000, 0.08)
+ background var(--desktopHeaderSearchHoverBg)
&:focus
- box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important
+ box-shadow 0 0 0 2px var(--primaryAlpha05) !important
-.search[data-darkmode]
- root(true)
-
-.search:not([data-darkmode])
- root(false)
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 6de4eaf744..4cfcda0f1a 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -1,16 +1,18 @@
<template>
-<div class="header">
+<div class="header" :style="style">
+ <p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p>
<mk-special-message/>
<div class="main" ref="main">
<div class="backdrop"></div>
<div class="main">
- <p ref="welcomeback" v-if="$store.getters.isSignedIn">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>%i18n:@adjective%</p>
<div class="container" ref="mainContainer">
<div class="left">
<x-nav/>
</div>
<div class="center">
- <div class="icon" @click="goToTop"></div>
+ <div class="icon" @click="goToTop">
+ <img svg-inline src="../../assets/header-icon.svg"/>
+ </div>
</div>
<div class="right">
<x-search/>
@@ -28,6 +30,7 @@
<script lang="ts">
import Vue from 'vue';
import * as anime from 'animejs';
+import { env } from '../../../config';
import XNav from './ui.header.nav.vue';
import XSearch from './ui.header.search.vue';
@@ -43,62 +46,27 @@ export default Vue.extend({
XAccount,
XNotifications,
XPost,
- XClock,
+ XClock
},
- mounted() {
- this.$store.commit('setUiHeaderHeight', 48);
-
- if (this.$store.getters.isSignedIn) {
- const ago = (new Date().getTime() - new Date(this.$store.state.i.lastUsedAt).getTime()) / 1000;
- const isHisasiburi = ago >= 3600;
- this.$store.state.i.lastUsedAt = new Date();
-
- if (isHisasiburi) {
- (this.$refs.welcomeback as any).style.display = 'block';
- (this.$refs.main as any).style.overflow = 'hidden';
-
- anime({
- targets: this.$refs.welcomeback,
- top: '0',
- opacity: 1,
- delay: 1000,
- duration: 500,
- easing: 'easeOutQuad'
- });
-
- anime({
- targets: this.$refs.mainContainer,
- opacity: 0,
- delay: 1000,
- duration: 500,
- easing: 'easeOutQuad'
- });
-
- setTimeout(() => {
- anime({
- targets: this.$refs.welcomeback,
- top: '-48px',
- opacity: 0,
- duration: 500,
- complete: () => {
- (this.$refs.welcomeback as any).style.display = 'none';
- (this.$refs.main as any).style.overflow = 'initial';
- },
- easing: 'easeInQuad'
- });
+ data() {
+ return {
+ env: env
+ };
+ },
- anime({
- targets: this.$refs.mainContainer,
- opacity: 1,
- duration: 500,
- easing: 'easeInQuad'
- });
- }, 2500);
- }
+ computed: {
+ style(): any {
+ return {
+ 'box-shadow': this.$store.state.settings.useShadow ? '0 0px 8px rgba(0, 0, 0, 0.2)' : 'none'
+ };
}
},
+ mounted() {
+ this.$store.commit('setUiHeaderHeight', this.$el.offsetHeight);
+ },
+
methods: {
goToTop() {
window.scrollTo({
@@ -111,13 +79,20 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
- position -webkit-sticky
- position sticky
+.header
+ position fixed
top 0
z-index 1000
width 100%
- box-shadow 0 1px 1px rgba(#000, 0.075)
+
+ > .warn
+ display block
+ margin 0
+ padding 4px
+ text-align center
+ font-size 12px
+ background #f00
+ color #fff
> .main
height 48px
@@ -128,7 +103,7 @@ root(isDark)
z-index 1000
width 100%
height 48px
- background isDark ? #313543 : #f7f7f7
+ background var(--desktopHeaderBg)
> .main
z-index 1001
@@ -138,17 +113,6 @@ root(isDark)
font-size 0.9rem
user-select none
- > p
- display none
- position absolute
- top 48px
- width 100%
- line-height 48px
- margin 0
- text-align center
- color isDark ? #fff : #888
- opacity 0
-
> .container
display flex
width 100%
@@ -166,13 +130,15 @@ root(isDark)
margin auto
display block
width 48px
- height 48px
- background-image isDark ? url('/assets/desktop/header-icon.dark.svg') : url('/assets/desktop/header-icon.light.svg')
- background-size 24px
- background-position center
- background-repeat no-repeat
- opacity 0.3
+ text-align center
cursor pointer
+ opacity 0.5
+
+ > svg
+ width 24px
+ height 48px
+ vertical-align top
+ fill var(--desktopHeaderFg)
> .left,
> .center
@@ -189,10 +155,4 @@ root(isDark)
> .mk-ui-header-search
display none
-.header[data-darkmode]
- root(true)
-
-.header:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
index d410c3d980..2d1e98447b 100644
--- a/src/client/app/desktop/views/components/ui.vue
+++ b/src/client/app/desktop/views/components/ui.vue
@@ -1,6 +1,7 @@
<template>
-<div class="mk-ui" :style="style">
- <x-header class="header" v-show="!zenMode"/>
+<div class="mk-ui" v-hotkey.global="keymap">
+ <div class="bg" v-if="$store.getters.isSignedIn && $store.state.i.wallpaperUrl" :style="style"></div>
+ <x-header class="header" v-show="!zenMode" ref="header"/>
<div class="content">
<slot></slot>
</div>
@@ -16,11 +17,13 @@ export default Vue.extend({
components: {
XHeader
},
+
data() {
return {
zenMode: false
};
},
+
computed: {
style(): any {
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
@@ -28,27 +31,37 @@ export default Vue.extend({
backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
};
+ },
+
+ keymap(): any {
+ return {
+ 'p': this.post,
+ 'n': this.post,
+ 'z': this.toggleZenMode
+ };
}
},
- mounted() {
- document.addEventListener('keydown', this.onKeydown);
+
+ watch: {
+ '$store.state.uiHeaderHeight'() {
+ this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
+ }
},
- beforeDestroy() {
- document.removeEventListener('keydown', this.onKeydown);
+
+ mounted() {
+ this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
},
- methods: {
- onKeydown(e) {
- if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
- if (e.which == 80 || e.which == 78) { // p or n
- e.preventDefault();
- (this as any).apis.post();
- }
+ methods: {
+ post() {
+ (this as any).apis.post();
+ },
- if (e.which == 90) { // z
- e.preventDefault();
- this.zenMode = !this.zenMode;
- }
+ toggleZenMode() {
+ this.zenMode = !this.zenMode;
+ this.$nextTick(() => {
+ this.$store.commit('setUiHeaderHeight', this.$refs.header.$el.offsetHeight);
+ });
}
}
});
@@ -56,20 +69,22 @@ export default Vue.extend({
<style lang="stylus" scoped>
.mk-ui
- display flex
- flex-direction column
- flex 1
- background-size cover
- background-position center
- background-attachment fixed
+ min-height 100vh
+ padding-top 48px
+
+ > .bg
+ position fixed
+ top 0
+ left 0
+ width 100%
+ height 100vh
+ background-size cover
+ background-position center
+ background-attachment fixed
+ opacity 0.3
> .header
@media (max-width 1000px)
display none
- > .content
- display flex
- flex-direction column
- flex 1
- overflow hidden
</style>
diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
index 0a6f758763..3407851fc5 100644
--- a/src/client/app/desktop/views/components/user-list-timeline.vue
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -6,7 +6,6 @@
<script lang="ts">
import Vue from 'vue';
-import { UserListStream } from '../../../common/scripts/streaming/user-list';
const fetchLimit = 10;
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 72ae9cf4e4..9c384314cf 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -1,8 +1,8 @@
<template>
-<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<span slot="header">%fa:list% %i18n:@title%</span>
- <div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">
+ <div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
<button class="ui" @click="add">%i18n:@create-list%</button>
<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
</div>
@@ -47,8 +47,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-
-root(isDark)
+.xkxvokkjlptzyewouewmceqcxhpgzprp
padding 16px
> button
@@ -57,13 +56,7 @@ root(isDark)
> a
display block
padding 16px
- border solid 1px isDark ? #1c2023 : #eee
+ border solid 1px var(--faceDivider)
border-radius 4px
-.xkxvokkjlptzyewouewmceqcxhpgzprp[data-darkmode]
- root(true)
-
-.xkxvokkjlptzyewouewmceqcxhpgzprp:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 1e1755ec3c..7f5e79eae1 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -75,7 +75,7 @@ export default Vue.extend({
'margin-top': '-8px',
duration: 200,
easing: 'easeOutQuad',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
@@ -83,14 +83,12 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-user-preview
position absolute
z-index 2048
margin-top -8px
width 250px
- background isDark ? #282c37 : #fff
+ background var(--face)
background-clip content-box
border solid 1px rgba(#000, 0.1)
border-radius 4px
@@ -99,7 +97,7 @@ root(isDark)
> .banner
height 84px
- background-color isDark ? #1c1e26 : #f5f5f5
+ background-color rgba(0, 0, 0, 0.1)
background-size cover
background-position center
@@ -111,7 +109,7 @@ root(isDark)
z-index 2
width 58px
height 58px
- border solid 3px isDark ? #282c37 : #fff
+ border solid 3px var(--face)
border-radius 8px
> .title
@@ -123,19 +121,20 @@ root(isDark)
margin 0
font-weight bold
line-height 16px
- color isDark ? #fff : #656565
+ color var(--text)
> .username
display block
margin 0
line-height 16px
font-size 0.8em
- color isDark ? #606984 : #999
+ color var(--text)
+ opacity 0.7
> .description
padding 0 16px
font-size 0.7em
- color isDark ? #9ea4ad : #555
+ color var(--text)
> .status
padding 8px 16px
@@ -147,21 +146,15 @@ root(isDark)
> p
margin 0
font-size 0.7em
- color #aaa
+ color var(--text)
> span
font-size 1em
- color $theme-color
+ color var(--primary)
> .mk-follow-button
position absolute
top 92px
right 8px
-.mk-user-preview[data-darkmode]
- root(true)
-
-.mk-user-preview:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index 262fd38cd1..f42d577fce 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,17 +1,16 @@
<template>
-<div class="root item">
- <mk-avatar class="avatar" :user="user"/>
- <div class="main">
- <header>
- <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
- <span class="username">@{{ user | acct }}</span>
- </header>
- <div class="body">
- <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
- <div class="description">{{ user.description }}</div>
+<div class="zvdbznxvfixtmujpsigoccczftvpiwqh">
+ <div class="banner" :style="bannerStyle"></div>
+ <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+ <div class="body">
+ <router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
+ <span class="username">@{{ user | acct }}</span>
+ <div class="description">
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
</div>
+ <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
+ <mk-follow-button :user="user" :size="'big'"/>
</div>
- <mk-follow-button :user="user"/>
</div>
</template>
@@ -19,76 +18,69 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['user']
+ props: ['user'],
+
+ computed: {
+ bannerStyle(): any {
+ if (this.user.bannerUrl == null) return {};
+ return {
+ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundImage: `url(${ this.user.bannerUrl })`
+ };
+ }
+ },
});
</script>
<style lang="stylus" scoped>
-.root.item
- padding 16px
+.zvdbznxvfixtmujpsigoccczftvpiwqh
+ $bg = #fff
+
+ margin 16px auto
+ max-width calc(100% - 32px)
font-size 16px
+ text-align center
+ background $bg
+ box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
- &:after
- content ""
- display block
- clear both
+ > .banner
+ height 100px
+ background-color #f9f4f4
+ background-position center
+ background-size cover
> .avatar
display block
- float left
- margin 0 16px 0 0
- width 58px
- height 58px
- border-radius 8px
-
- > .main
- float left
- width calc(100% - 74px)
-
- > header
- margin-bottom 2px
+ margin -40px auto 0 auto
+ width 80px
+ height 80px
+ border-radius 100%
+ border solid 4px $bg
- > .name
- display inline
- margin 0
- padding 0
- color #777
- font-size 1em
- font-weight 700
- text-align left
- text-decoration none
+ > .body
+ padding 4px 32px 32px 32px
- &:hover
- text-decoration underline
+ @media (max-width 400px)
+ padding 4px 16px 16px 16px
- > .username
- text-align left
- margin 0 0 0 8px
- color #ccc
+ > .name
+ font-size 20px
+ font-weight bold
- > .body
- > .followed
- display inline-block
- margin 0 0 4px 0
- padding 2px 8px
- vertical-align top
- font-size 10px
- color #71afc7
- background #eefaff
- border-radius 4px
+ > .username
+ display block
+ opacity 0.7
- > .description
- cursor default
- display block
- margin 0
- padding 0
- overflow-wrap break-word
- font-size 1.1em
- color #717171
+ > .description
+ margin 16px 0
- > .mk-follow-button
- position absolute
- top 16px
- right 16px
+ > .followed
+ margin 0 0 16px 0
+ padding 0
+ line-height 24px
+ font-size 0.8em
+ color #71afc7
+ background #eefaff
+ border-radius 4px
</style>
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index 0423db8ed7..1316f277b7 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
props: ['fetch', 'count', 'youKnowCount'],
data() {
return {
- limit: 30,
+ limit: 20,
mode: 'all',
fetching: true,
moreFetching: false,
@@ -69,14 +69,18 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-users-list
height 100%
- background #fff
+ overflow auto
+ background #eee
> nav
- z-index 1
+ z-index 10
+ position sticky
+ top 0
+ background #fff
box-shadow 0 1px 0 rgba(#000, 0.1)
> div
@@ -100,8 +104,8 @@ export default Vue.extend({
&[data-active]
font-weight bold
- color $theme-color
- border-color $theme-color
+ color var(--primary)
+ border-color var(--primary)
cursor default
> span
@@ -114,16 +118,14 @@ export default Vue.extend({
background #eee
border-radius 20px
- > .users
- height calc(100% - 54px)
- overflow auto
-
- > *
- border-bottom solid 1px rgba(#000, 0.05)
+ > button
+ display block
+ width calc(100% - 32px)
+ margin 16px
+ padding 16px
- > *
- max-width 600px
- margin 0 auto
+ &:hover
+ background rgba(#000, 0.1)
> .no
margin 0
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 7cfcd68eba..a506357039 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -1,6 +1,6 @@
<template>
<div class="mk-widget-container" :class="{ naked }">
- <header :class="{ withGradient }" v-if="showHeader">
+ <header v-if="showHeader">
<div class="title"><slot name="header"></slot></div>
<slot name="func"></slot>
</header>
@@ -20,32 +20,23 @@ export default Vue.extend({
type: Boolean,
default: false
}
- },
- computed: {
- withGradient(): boolean {
- return this.$store.getters.isSignedIn
- ? this.$store.state.settings.gradientWindowHeader != null
- ? this.$store.state.settings.gradientWindowHeader
- : false
- : false;
- }
}
});
</script>
<style lang="stylus" scoped>
-root(isDark)
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, isDark ? 0.2 : 0.075)
- border-radius 6px
+.mk-widget-container
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
overflow hidden
&.naked
background transparent !important
- border none !important
+ box-shadow none !important
> header
- background isDark ? #313543 : #fff
+ background var(--faceHeader)
> .title
z-index 1
@@ -54,7 +45,7 @@ root(isDark)
line-height 42px
font-size 0.9em
font-weight bold
- color isDark ? #e3e5e8 : #888
+ color var(--faceHeaderText)
box-shadow 0 1px rgba(#000, 0.07)
> [data-fa]
@@ -72,23 +63,12 @@ root(isDark)
width 42px
font-size 0.9em
line-height 42px
- color isDark ? #9baec8 : #ccc
+ color var(--faceTextButton)
&:hover
- color isDark ? #b2c1d5 : #aaa
+ color var(--faceTextButtonHover)
&:active
- color isDark ? #b2c1d5 : #999
-
- &.withGradient
- > .title
- background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec)
- box-shadow 0 1px rgba(#000, 0.11)
-
-.mk-widget-container[data-darkmode]
- root(true)
-
-.mk-widget-container:not([data-darkmode])
- root(false)
+ color var(--faceTextButtonActive)
</style>
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index ec044ad27e..a1893ffd6b 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -4,7 +4,6 @@
<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }">
<div class="body">
<header ref="header"
- :class="{ withGradient: $store.state.settings.gradientWindowHeader }"
@contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown"
>
<h1><slot name="header"></slot></h1>
@@ -76,6 +75,11 @@ export default Vue.extend({
name: {
type: String,
default: null
+ },
+ animation: {
+ type: Boolean,
+ required: false,
+ default: true
}
},
@@ -106,7 +110,7 @@ export default Vue.extend({
mounted() {
if (this.preventMount) {
- this.$destroy();
+ this.destroyDom();
return;
}
@@ -142,7 +146,7 @@ export default Vue.extend({
anime({
targets: bg,
opacity: 1,
- duration: 100,
+ duration: this.animation ? 100 : 0,
easing: 'linear'
});
}
@@ -152,7 +156,7 @@ export default Vue.extend({
targets: main,
opacity: 1,
scale: [1.1, 1],
- duration: 200,
+ duration: this.animation ? 200 : 0,
easing: 'easeOutQuad'
});
@@ -160,7 +164,7 @@ export default Vue.extend({
setTimeout(() => {
this.$emit('opened');
- }, 300);
+ }, this.animation ? 300 : 0);
},
close() {
@@ -174,7 +178,7 @@ export default Vue.extend({
anime({
targets: bg,
opacity: 0,
- duration: 300,
+ duration: this.animation ? 300 : 0,
easing: 'linear'
});
}
@@ -185,14 +189,14 @@ export default Vue.extend({
targets: main,
opacity: 0,
scale: 0.8,
- duration: 300,
+ duration: this.animation ? 300 : 0,
easing: [0.5, -0.5, 1, 0.5]
});
setTimeout(() => {
- this.$destroy();
this.$emit('closed');
- }, 300);
+ this.destroyDom();
+ }, this.animation ? 300 : 0);
},
popout() {
@@ -458,9 +462,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-window
display block
> .bg
@@ -488,10 +490,7 @@ root(isDark)
&:focus
&:not([data-is-modal])
> .body
- if isDark
- box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 12px 0 rgba(#000, 0.5)
- else
- box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(#000, 0.2)
+ box-shadow 0 0 0px 1px var(--primaryAlpha05), 0 2px 12px 0 var(--desktopWindowShadow)
> .handle
$size = 8px
@@ -557,13 +556,9 @@ root(isDark)
> .body
height 100%
overflow hidden
- background isDark ? #282C37 : #fff
+ background var(--face)
border-radius 6px
-
- if isDark
- box-shadow 0 2px 12px 0 rgba(#000, 0.5)
- else
- box-shadow 0 2px 6px 0 rgba(#000, 0.2)
+ box-shadow 0 2px 12px 0 rgba(#000, 0.5)
> header
$header-height = 40px
@@ -573,14 +568,10 @@ root(isDark)
overflow hidden
white-space nowrap
cursor move
- background isDark ? #313543 : #fff
+ background var(--faceHeader)
border-radius 6px 6px 0 0
box-shadow 0 1px 0 rgba(#000, 0.1)
- &.withGradient
- background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec)
- box-shadow 0 1px 0 rgba(#000, 0.15)
-
&, *
user-select none
@@ -595,7 +586,7 @@ root(isDark)
font-size 1em
line-height $header-height
font-weight normal
- color isDark ? #e3e5e8 : #666
+ color var(--desktopWindowTitle)
> div:last-child
position absolute
@@ -610,16 +601,16 @@ root(isDark)
padding 0
cursor pointer
font-size 1em
- color isDark ? #9baec8 : rgba(#000, 0.4)
+ color var(--faceTextButton)
border none
outline none
background transparent
&:hover
- color isDark ? #b2c1d5 : rgba(#000, 0.6)
+ color var(--faceTextButtonHover)
&:active
- color isDark ? #b2c1d5 : darken(#000, 30%)
+ color var(--faceTextButtonActive)
> [data-fa]
padding 0
@@ -634,10 +625,4 @@ root(isDark)
> .main > .body > .content
height calc(100% - 40px)
-.mk-window[data-darkmode]
- root(true)
-
-.mk-window:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.announcements.vue b/src/client/app/desktop/views/pages/admin/admin.announcements.vue
new file mode 100644
index 0000000000..5c1ed74b29
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.announcements.vue
@@ -0,0 +1,52 @@
+<template>
+<div class="qldxjjsrseehkusjuoooapmsprvfrxyl mk-admin-card">
+ <header>%i18n:@announcements%</header>
+ <textarea v-model="broadcasts" placeholder='[ { "title": "Title1", "text": "Text1" }, { "title": "Title2", "text": "Text2" } ]'></textarea>
+ <button class="ui" @click="save">%i18n:@save%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ broadcasts: '',
+ };
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.broadcasts = JSON.stringify(meta.broadcasts, null, ' ');
+ });
+ },
+ methods: {
+ save() {
+ let json;
+
+ try {
+ json = JSON.parse(this.broadcasts);
+ } catch (e) {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
+ return;
+ }
+
+ (this as any).api('admin/update-meta', {
+ broadcasts: json
+ }).then(() => {
+ (this as any).os.apis.dialog({ text: `Saved` });
+ }.catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.qldxjjsrseehkusjuoooapmsprvfrxyl
+ textarea
+ width 100%
+ min-height 300px
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue b/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue
index d14ce12553..63b24cea47 100644
--- a/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue
@@ -111,7 +111,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.zyknedwtlthezamcjlolyusmipqmjgxz
> svg
display block
width 50%
@@ -125,7 +125,7 @@ root(isDark)
> text
font-size 10px
- fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
+ fill var(--chartCaption)
> tspan
opacity 0.5
@@ -135,10 +135,4 @@ root(isDark)
display block
clear both
-.zyknedwtlthezamcjlolyusmipqmjgxz[data-darkmode]
- root(true)
-
-.zyknedwtlthezamcjlolyusmipqmjgxz:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
index ebb54d782e..c0075220bc 100644
--- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
@@ -1,22 +1,42 @@
<template>
<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
<header>%i18n:@dashboard%</header>
+
<div v-if="stats" class="stats">
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
</div>
+
<div class="cpu-memory">
<x-cpu-memory :connection="connection"/>
</div>
- <div>
- <label>
- <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
- <span>disableRegistration</span>
- </label>
- <button class="ui" @click="invite">%i18n:@invite%</button>
- <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+
+ <div v-if="this.$store.state.i && this.$store.state.i.isAdmin" class="form">
+ <div>
+ <label>
+ <p>%i18n:@banner-url%</p>
+ <input v-model="bannerUrl">
+ </label>
+ <button class="ui" @click="updateMeta">%i18n:@save%</button>
+ </div>
+
+ <div>
+ <label>
+ <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
+ <span>%i18n:@disableRegistration%</span>
+ </label>
+ <button class="ui" @click="invite">%i18n:@invite%</button>
+ <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+ </div>
+
+ <div>
+ <label>
+ <input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta">
+ <span>%i18n:@disableLocalTimeline%</span>
+ </label>
+ </div>
</div>
</div>
</template>
@@ -33,17 +53,19 @@ export default Vue.extend({
return {
stats: null,
disableRegistration: false,
+ disableLocalTimeline: false,
+ bannerUrl: null,
inviteCode: null,
- connection: null,
- connectionId: null
+ connection: null
};
},
created() {
- this.connection = (this as any).os.streams.serverStatsStream.getConnection();
- this.connectionId = (this as any).os.streams.serverStatsStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('serverStats');
(this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration;
+ this.disableLocalTimeline = meta.disableLocalTimeline;
+ this.bannerUrl = meta.bannerUrl;
});
(this as any).api('stats').then(stats => {
@@ -51,17 +73,25 @@ export default Vue.extend({
});
},
beforeDestroy() {
- (this as any).os.streams.serverStatsStream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
invite() {
(this as any).api('admin/invite').then(x => {
this.inviteCode = x.code;
+ }).catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
});
},
updateMeta() {
(this as any).api('admin/update-meta', {
- disableRegistration: this.disableRegistration
+ disableRegistration: this.disableRegistration,
+ disableLocalTimeline: this.disableLocalTimeline,
+ bannerUrl: this.bannerUrl
+ }).then(() => {
+ (this as any).os.apis.dialog({ text: `Saved` });
+ }).catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
});
}
}
@@ -69,7 +99,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.obdskegsannmntldydackcpzezagxqfy
> .stats
@@ -86,7 +116,7 @@ export default Vue.extend({
> *:first-child
display block
- color $theme-color
+ color var(--primary)
> *:last-child
font-size 70%
@@ -97,4 +127,9 @@ export default Vue.extend({
border solid 1px #eee
border-radius: 8px
+ > .form
+ > div
+ padding 16px
+ border-bottom solid 1px #eee
+
</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.hashtags.vue b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue
new file mode 100644
index 0000000000..10bab1cbd7
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue
@@ -0,0 +1,45 @@
+<template>
+<div class="jdnqwkzlnxcfftthoybjxrebyolvoucw mk-admin-card">
+ <header>%i18n:@hided-tags%</header>
+ <textarea v-model="hidedTags"></textarea>
+ <button class="ui" @click="save">%i18n:@save%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ hidedTags: '',
+ };
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.hidedTags = meta.hidedTags.join('\n');
+ });
+ },
+ methods: {
+ save() {
+ (this as any).api('admin/update-meta', {
+ hidedTags: this.hidedTags.split('\n')
+ }).then(() => {
+ (this as any).os.apis.dialog({ text: `Saved` });
+ }).catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+
+
+.jdnqwkzlnxcfftthoybjxrebyolvoucw
+ textarea
+ width 100%
+ min-height 300px
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue
index 8d8e37e181..a8ff937bbe 100644
--- a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue
@@ -21,25 +21,31 @@ export default Vue.extend({
async suspendUser() {
this.suspending = true;
- const user = await (this as any).os.api(
- "users/show",
- parseAcct(this.username)
- );
+ const process = async () => {
+ const user = await (this as any).os.api(
+ "users/show",
+ parseAcct(this.username)
+ );
- await (this as any).os.api("admin/suspend-user", {
- userId: user.id
+ await (this as any).os.api("admin/suspend-user", {
+ userId: user.id
+ });
+
+ (this as any).os.apis.dialog({ text: "%i18n:@suspended%" });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
});
this.suspending = false;
-
- (this as any).os.apis.dialog({ text: "%i18n:@suspended%" });
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
header
margin 10px 0
diff --git a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue
index ec423969be..146f5a41d4 100644
--- a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue
@@ -21,25 +21,32 @@ export default Vue.extend({
async unsuspendUser() {
this.unsuspending = true;
- const user = await (this as any).os.api(
- "users/show",
- parseAcct(this.username)
- );
+ const process = async () => {
+ const user = await (this as any).os.api(
+ "users/show",
+ parseAcct(this.username)
+ );
- await (this as any).os.api("admin/unsuspend-user", {
- userId: user.id
+ await (this as any).os.api("admin/unsuspend-user", {
+ userId: user.id
+ });
+
+ (this as any).os.apis.dialog({ text: "%i18n:@unsuspended%" });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
});
this.unsuspending = false;
- (this as any).os.apis.dialog({ text: "%i18n:@unsuspended%" });
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
header
margin 10px 0
diff --git a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue
index e8204e69f4..5e0fdae5c1 100644
--- a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue
@@ -21,25 +21,31 @@ export default Vue.extend({
async unverifyUser() {
this.unverifying = true;
- const user = await (this as any).os.api(
- "users/show",
- parseAcct(this.username)
- );
+ const process = async () => {
+ const user = await (this as any).os.api(
+ "users/show",
+ parseAcct(this.username)
+ );
- await (this as any).os.api("admin/unverify-user", {
- userId: user.id
+ await (this as any).os.api("admin/unverify-user", {
+ userId: user.id
+ });
+
+ (this as any).os.apis.dialog({ text: "%i18n:@unverified%" });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
});
this.unverifying = false;
-
- (this as any).os.apis.dialog({ text: "%i18n:@unverified%" });
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
header
margin 10px 0
diff --git a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue
index 91fb04af80..d237a5f9c1 100644
--- a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue
@@ -21,25 +21,31 @@ export default Vue.extend({
async verifyUser() {
this.verifying = true;
- const user = await (this as any).os.api(
- "users/show",
- parseAcct(this.username)
- );
+ const process = async () => {
+ const user = await (this as any).os.api(
+ "users/show",
+ parseAcct(this.username)
+ );
- await (this as any).os.api("admin/verify-user", {
- userId: user.id
+ await (this as any).os.api("admin/verify-user", {
+ userId: user.id
+ });
+
+ (this as any).os.apis.dialog({ text: "%i18n:@verified%" });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
});
this.verifying = false;
-
- (this as any).os.apis.dialog({ text: "%i18n:@verified%" });
}
}
});
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
header
margin 10px 0
diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue
index 3438462cd6..ad417e5121 100644
--- a/src/client/app/desktop/views/pages/admin/admin.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.vue
@@ -3,7 +3,16 @@
<nav>
<ul>
<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li>
- <li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
+
+ <li v-if="this.$store.state.i && this.$store.state.i.isAdmin"
+ @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
+
+ <li v-if="this.$store.state.i && this.$store.state.i.isAdmin"
+ @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li>
+
+ <li v-if="this.$store.state.i && this.$store.state.i.isAdmin"
+ @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li>
+
<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> -->
<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->
</ul>
@@ -13,6 +22,12 @@
<x-dashboard/>
<x-charts/>
</div>
+ <div v-show="page == 'announcements'">
+ <x-announcements/>
+ </div>
+ <div v-show="page == 'hashtags'">
+ <x-hashtags/>
+ </div>
<div v-if="page == 'users'">
<x-suspend-user/>
<x-unsuspend-user/>
@@ -28,6 +43,8 @@
<script lang="ts">
import Vue from "vue";
import XDashboard from "./admin.dashboard.vue";
+import XAnnouncements from "./admin.announcements.vue";
+import XHashtags from "./admin.hashtags.vue";
import XSuspendUser from "./admin.suspend-user.vue";
import XUnsuspendUser from "./admin.unsuspend-user.vue";
import XVerifyUser from "./admin.verify-user.vue";
@@ -37,6 +54,8 @@ import XCharts from "../../components/charts.vue";
export default Vue.extend({
components: {
XDashboard,
+ XAnnouncements,
+ XHashtags,
XSuspendUser,
XUnsuspendUser,
XVerifyUser,
@@ -57,7 +76,7 @@ export default Vue.extend({
</script>
<style lang="stylus">
-@import '~const.styl'
+
.mk-admin
display flex
@@ -93,7 +112,7 @@ export default Vue.extend({
&.active
margin-left 8px
- color $theme-color !important
+ color var(--primary) !important
> main
width 100%
diff --git a/src/client/app/desktop/views/pages/deck/deck.column-core.vue b/src/client/app/desktop/views/pages/deck/deck.column-core.vue
index 7f219c0be1..e1490cb0e4 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column-core.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column-core.vue
@@ -6,6 +6,9 @@
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
+<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/>
+<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
+<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/>
</template>
<script lang="ts">
@@ -13,12 +16,16 @@ import Vue from 'vue';
import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-column.vue';
import XWidgetsColumn from './deck.widgets-column.vue';
+import XMentionsColumn from './deck.mentions-column.vue';
+import XDirectColumn from './deck.direct-column.vue';
export default Vue.extend({
components: {
XTlColumn,
XNotificationsColumn,
- XWidgetsColumn
+ XWidgetsColumn,
+ XMentionsColumn,
+ XDirectColumn
},
props: {
diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue
index d59d430da6..c372ef490e 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column.vue
@@ -3,18 +3,20 @@
@dragover.prevent.stop="onDragover"
@dragenter.prevent="onDragenter"
@dragleave="onDragleave"
- @drop.prevent.stop="onDrop"
->
+ @drop.prevent.stop="onDrop">
<header :class="{ indicate: count > 0 }"
draggable="true"
- @click="toggleActive"
+ @click="goTop"
@dragstart="onDragstart"
@dragend="onDragend"
- @contextmenu.prevent.stop="onContextmenu"
- >
+ @contextmenu.prevent.stop="onContextmenu">
+ <button class="toggleActive" @click="toggleActive" v-if="isStacked">
+ <template v-if="active">%fa:angle-up%</template>
+ <template v-else>%fa:angle-down%</template>
+ </button>
<slot name="header"></slot>
<span class="count" v-if="count > 0">({{ count }})</span>
- <button ref="menu" @click.stop="showMenu">%fa:caret-down%</button>
+ <button class="menu" ref="menu" @click.stop="showMenu">%fa:caret-down%</button>
</header>
<div ref="body" v-show="active">
<slot></slot>
@@ -26,6 +28,7 @@
import Vue from 'vue';
import Menu from '../../../../common/views/components/menu.vue';
import contextmenu from '../../../api/contextmenu';
+import { countIf } from '../../../../../../prelude/array';
export default Vue.extend({
props: {
@@ -115,7 +118,7 @@ export default Vue.extend({
toggleActive() {
if (!this.isStacked) return;
const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id));
- if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return;
+ if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return;
this.active = !this.active;
},
@@ -211,6 +214,13 @@ export default Vue.extend({
});
},
+ goTop() {
+ this.$refs.body.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ },
+
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk-deck-column', this.column.id);
@@ -259,24 +269,22 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs
$header-height = 42px
width 330px
min-width 330px
height 100%
- background isDark ? #282C37 : #fff
+ background var(--face)
border-radius 6px
- box-shadow 0 2px 16px rgba(#000, 0.1)
+ //box-shadow 0 2px 16px rgba(#000, 0.1)
overflow hidden
&.draghover
- box-shadow 0 0 0 2px rgba($theme-color, 0.8)
+ box-shadow 0 0 0 2px var(--primaryAlpha08)
&.dragging
- box-shadow 0 0 0 2px rgba($theme-color, 0.4)
+ box-shadow 0 0 0 2px var(--primaryAlpha04)
&.dropready
*
@@ -291,23 +299,23 @@ root(isDark)
min-width 285px
&.naked
- background rgba(#000, isDark ? 0.25 : 0.1)
+ background var(--deckAcrylicColumnBg)
> header
background transparent
box-shadow none
- if !isDark
- > button
- color #bbb
+ > button
+ color var(--text)
> header
+ display flex
z-index 1
line-height $header-height
padding 0 16px
font-size 14px
- color isDark ? #e3e5e8 : #888
- background isDark ? #313543 : #fff
+ color var(--faceHeaderText)
+ background var(--faceHeader)
box-shadow 0 1px rgba(#000, 0.15)
cursor pointer
@@ -318,7 +326,7 @@ root(isDark)
pointer-events none
&.indicate
- box-shadow 0 3px 0 0 $theme-color
+ box-shadow 0 3px 0 0 var(--primary)
> span
[data-fa]
@@ -328,30 +336,29 @@ root(isDark)
margin-left 4px
opacity 0.5
- > button
- position absolute
- top 0
- right 0
+ > .toggleActive
+ > .menu
width $header-height
line-height $header-height
font-size 16px
- color isDark ? #9baec8 : #ccc
+ color var(--faceTextButton)
&:hover
- color isDark ? #b2c1d5 : #aaa
+ color var(--faceTextButtonHover)
&:active
- color isDark ? #b2c1d5 : #999
+ color var(--faceTextButtonActive)
+
+ > .toggleActive
+ margin-left -16px
+
+ > .menu
+ margin-left auto
+ margin-right -16px
> div
height "calc(100% - %s)" % $header-height
overflow auto
overflow-x hidden
-.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs[data-darkmode]
- root(true)
-
-.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/deck/deck.direct-column.vue b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue
new file mode 100644
index 0000000000..d5093761f4
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue
@@ -0,0 +1,38 @@
+<template>
+<x-column :name="name" :column="column" :is-stacked="isStacked">
+ <span slot="header">%fa:envelope R%{{ name }}</span>
+
+ <x-direct/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import XDirect from './deck.direct.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XDirect
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ computed: {
+ name(): string {
+ if (this.column.name) return this.column.name;
+ return '%i18n:common.deck.direct%';
+ }
+ },
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.direct.vue b/src/client/app/desktop/views/pages/deck/deck.direct.vue
new file mode 100644
index 0000000000..c771e58a6e
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.direct.vue
@@ -0,0 +1,93 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+
+ mounted() {
+ this.connection = (this as any).os.stream.useSharedConnection('main');
+ this.connection.on('mention', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.dispose();
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ visibility: 'specified'
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ const promise = (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ visibility: 'specified'
+ });
+
+ promise.then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ // Prepend a note
+ if (note.visibility == 'specified') {
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
new file mode 100644
index 0000000000..02d99d3883
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
@@ -0,0 +1,116 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ tagTl: {
+ type: Object,
+ required: true
+ },
+ mediaOnly: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ mediaView: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+
+ watch: {
+ mediaOnly() {
+ this.fetch();
+ }
+ },
+
+ mounted() {
+ if (this.connection) this.connection.close();
+ this.connection = (this as any).os.stream.connectToChannel('hashtag', this.tagTl.query);
+ this.connection.on('note', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.close();
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/search_by_tag', {
+ limit: fetchLimit + 1,
+ withFiles: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl.query
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ const promise = (this as any).api('notes/search_by_tag', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ withFiles: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl.query
+ });
+
+ promise.then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ if (this.mediaOnly && note.files.length == 0) return;
+
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
index 70048f99e3..e543130310 100644
--- a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
@@ -5,7 +5,6 @@
<script lang="ts">
import Vue from 'vue';
import XNotes from './deck.notes.vue';
-import { UserListStream } from '../../../../common/scripts/streaming/user-list';
const fetchLimit = 10;
@@ -68,7 +67,7 @@ export default Vue.extend({
(this as any).api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -90,7 +89,7 @@ export default Vue.extend({
listId: this.list.id,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -109,7 +108,7 @@ export default Vue.extend({
return promise;
},
onNote(note) {
- if (this.mediaOnly && note.media.length == 0) return;
+ if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
new file mode 100644
index 0000000000..8ec10164f2
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
@@ -0,0 +1,38 @@
+<template>
+<x-column :name="name" :column="column" :is-stacked="isStacked">
+ <span slot="header">%fa:at%{{ name }}</span>
+
+ <x-mentions/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import XMentions from './deck.mentions.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XMentions
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ computed: {
+ name(): string {
+ if (this.column.name) return this.column.name;
+ return '%i18n:common.deck.mentions%';
+ }
+ },
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions.vue b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
new file mode 100644
index 0000000000..17b572f146
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
@@ -0,0 +1,89 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+
+ mounted() {
+ this.connection = (this as any).os.stream.useSharedConnection('main');
+ this.connection.on('mention', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.dispose();
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ const promise = (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ });
+
+ promise.then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.note.sub.vue b/src/client/app/desktop/views/pages/deck/deck.note.sub.vue
index 3ba9ae914e..445bf7e365 100644
--- a/src/client/app/desktop/views/pages/deck/deck.note.sub.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.note.sub.vue
@@ -29,11 +29,11 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.fnlfosztlhtptnongximhlbykxblytcq
display flex
padding 16px
font-size 10px
- background isDark ? #21242d : #fcfcfc
+ background var(--subNoteBg)
&.smart
> .main
@@ -62,16 +62,10 @@ root(isDark)
> .text
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ color var(--subNoteText)
pre
max-height 120px
font-size 80%
-.fnlfosztlhtptnongximhlbykxblytcq[data-darkmode]
- root(true)
-
-.fnlfosztlhtptnongximhlbykxblytcq:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue
index e6d062eac9..e843ac54fe 100644
--- a/src/client/app/desktop/views/pages/deck/deck.note.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.note.vue
@@ -18,7 +18,7 @@
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
- <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,14 +28,15 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
<a class="rp" v-if="p.renote != null">RP:</a>
</div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote" :mini="true"/>
</div>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="false" :mini="true"/>
</div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
</div>
@@ -53,11 +54,11 @@
</article>
</div>
<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi">
- <div v-if="note.media.length > 0">
- <mk-media-list :media-list="note.media"/>
+ <div v-if="note.files.length > 0">
+ <mk-media-list :media-list="note.files"/>
</div>
- <div v-if="note.renote && note.renote.media.length > 0">
- <mk-media-list :media-list="note.renote.media"/>
+ <div v-if="note.renote && note.renote.files.length > 0">
+ <mk-media-list :media-list="note.renote.files"/>
</div>
</div>
</template>
@@ -69,12 +70,15 @@ import parse from '../../../../../../mfm/parse';
import MkNoteMenu from '../../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue';
import XSub from './deck.note.sub.vue';
+import noteSubscriber from '../../../../common/scripts/note-subscriber';
export default Vue.extend({
components: {
XSub
},
+ mixins: [noteSubscriber('note')],
+
props: {
note: {
type: Object,
@@ -89,9 +93,7 @@ export default Vue.extend({
data() {
return {
- showContent: false,
- connection: null,
- connectionId: null
+ showContent: false
};
},
@@ -99,7 +101,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -119,64 +121,7 @@ export default Vue.extend({
}
},
- created() {
- if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
- }
- },
-
- mounted() {
- this.capture(true);
-
- if (this.$store.getters.isSignedIn) {
- this.connection.on('_connected_', this.onStreamConnected);
- }
- },
-
- beforeDestroy() {
- this.decapture(true);
-
- if (this.$store.getters.isSignedIn) {
- this.connection.off('_connected_', this.onStreamConnected);
- (this as any).os.stream.dispose(this.connectionId);
- }
- },
-
methods: {
- capture(withHandler = false) {
- if (this.$store.getters.isSignedIn) {
- this.connection.send({
- type: 'capture',
- id: this.p.id
- });
- if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
- }
- },
-
- decapture(withHandler = false) {
- if (this.$store.getters.isSignedIn) {
- this.connection.send({
- type: 'decapture',
- id: this.p.id
- });
- if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
- }
- },
-
- onStreamConnected() {
- this.capture();
- },
-
- onStreamNoteUpdated(data) {
- const note = data.note;
- if (note.id == this.note.id) {
- this.$emit('update:note', note);
- } else if (note.id == this.note.renoteId) {
- this.note.renote = note;
- }
- },
-
reply() {
(this as any).apis.post({
reply: this.p
@@ -209,9 +154,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-mediaRoot(isDark)
+.srwrkujossgfuhrbnvqkybtzxpblgchi
font-size 13px
margin 4px 12px
@@ -221,9 +164,9 @@ mediaRoot(isDark)
&:last-child
margin-bottom 12px
-root(isDark)
+.zyjjkidcqjnlegkqebitfviomuqmseqk
font-size 13px
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ border-bottom solid 1px var(--faceDivider)
&:last-of-type
border-bottom none
@@ -241,8 +184,8 @@ root(isDark)
padding 8px 16px 0 16px
line-height 28px
white-space pre
- color #9dbb00
- background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ color var(--renoteText)
+ background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
.avatar
flex-shrink 0
@@ -304,24 +247,11 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- color isDark ? #fff : #717171
+ color var(--noteText)
> .text
margin-right 8px
- > .toggle
- display inline-block
- padding 4px 8px
- font-size 0.7em
- color isDark ? #393f4f : #fff
- background isDark ? #687390 : #b1b9c1
- border-radius 2px
- cursor pointer
- user-select none
-
- &:hover
- background isDark ? #707b97 : #bbc4ce
-
> .content
> .text
@@ -329,7 +259,7 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- color isDark ? #fff : #717171
+ color var(--noteText)
>>> .title
display block
@@ -337,7 +267,7 @@ root(isDark)
padding 4px
font-size 90%
text-align center
- background isDark ? #2f3944 : #eef1f3
+ background var(--mfmTitleBg)
border-radius 4px
>>> .code
@@ -346,31 +276,31 @@ root(isDark)
>>> .quote
margin 8px
padding 6px 12px
- color isDark ? #6f808e : #aaa
- border-left solid 3px isDark ? #637182 : #eee
+ color var(--mfmQuote)
+ border-left solid 3px var(--mfmQuoteLine)
> .reply
margin-right 8px
- color isDark ? #99abbf : #717171
+ color var(--noteText)
> .rp
margin-left 4px
font-style oblique
- color #a0bf46
+ color var(--renoteText)
[data-is-me]:after
content "you"
padding 0 4px
margin-left 4px
font-size 80%
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
border-radius 4px
.mk-url-preview
margin-top 8px
- > .media
+ > .files
> img
display block
max-width 100%
@@ -393,9 +323,9 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
- border dashed 1px isDark ? #4e945e : #c0dac6
+ border dashed 1px var(--quoteBorder)
border-radius 8px
> .app
@@ -410,14 +340,14 @@ root(isDark)
border none
box-shadow none
font-size 1em
- color isDark ? #606984 : #ddd
+ color var(--noteActions)
cursor pointer
&:not(:last-child)
margin-right 28px
&:hover
- color isDark ? #9198af : #666
+ color var(--noteActionsHover)
> .count
display inline
@@ -425,18 +355,6 @@ root(isDark)
color #999
&.reacted
- color $theme-color
-
-.zyjjkidcqjnlegkqebitfviomuqmseqk[data-darkmode]
- root(true)
-
-.zyjjkidcqjnlegkqebitfviomuqmseqk:not([data-darkmode])
- root(false)
-
-.srwrkujossgfuhrbnvqkybtzxpblgchi[data-darkmode]
- mediaRoot(true)
-
-.srwrkujossgfuhrbnvqkybtzxpblgchi:not([data-darkmode])
- mediaRoot(false)
+ color var(--primary)
</style>
diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue
index f7fca5de92..884be3a841 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notes.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue
@@ -127,7 +127,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
@@ -195,9 +195,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.eamppglmnmimdhrlzhplwpvyeaqmmhxu
.transition
.mk-notes-enter
.mk-notes-leave-to
@@ -214,9 +212,9 @@ root(isDark)
line-height 32px
font-size 14px
text-align center
- color isDark ? #666b79 : #aaa
- background isDark ? #242731 : #fdfdfd
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ color var(--dateDividerFg)
+ background var(--dateDividerBg)
+ border-bottom solid 1px var(--faceDivider)
span
margin 0 16px
@@ -232,21 +230,15 @@ root(isDark)
width 100%
text-align center
color #ccc
- background isDark ? #282C37 : #fff
- border-top solid 1px isDark ? #1c2023 : #eaeaea
+ background var(--face)
+ border-top solid 1px var(--faceDivider)
border-bottom-left-radius 6px
border-bottom-right-radius 6px
&:hover
- background isDark ? #2e3440 : #f5f5f5
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
&:active
- background isDark ? #21242b : #eee
-
-.eamppglmnmimdhrlzhplwpvyeaqmmhxu[data-darkmode]
- root(true)
-
-.eamppglmnmimdhrlzhplwpvyeaqmmhxu:not([data-darkmode])
- root(false)
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
</style>
diff --git a/src/client/app/desktop/views/pages/deck/deck.notification.vue b/src/client/app/desktop/views/pages/deck/deck.notification.vue
index d0093ff282..149bd10293 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notification.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notification.vue
@@ -109,7 +109,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.dsfykdcjpuwfvpefwufddclpjhzktmpw
> .notification
padding 16px
font-size 13px
@@ -142,14 +142,14 @@ root(isDark)
> .mk-time
margin-left auto
- color isDark ? #606984 : #c0c0c0
+ color var(--noteHeaderInfo)
font-size 0.9em
> .note-preview
- color isDark ? #fff : #717171
+ color var(--noteText)
> .note-ref
- color isDark ? #fff : #717171
+ color var(--noteText)
[data-fa]
font-size 1em
@@ -170,10 +170,4 @@ root(isDark)
> div > header i
color #888
-.dsfykdcjpuwfvpefwufddclpjhzktmpw[data-darkmode]
- root(true)
-
-.dsfykdcjpuwfvpefwufddclpjhzktmpw:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
index fcb74b9140..29de691fe2 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
@@ -1,8 +1,7 @@
<template>
<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!--<transition-group name="mk-notifications" class="transition notifications">-->
- <div class="notifications">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications">
<x-notification class="notification" :notification="notification" :key="notification.id"/>
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
@@ -10,8 +9,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!--</transition-group>-->
+ </component>
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
@@ -40,8 +38,7 @@ export default Vue.extend({
notifications: [],
queue: [],
moreNotifications: false,
- connection: null,
- connectionId: null
+ connection: null
};
},
@@ -64,8 +61,7 @@ export default Vue.extend({
},
mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('notification', this.onNotification);
@@ -88,8 +84,7 @@ export default Vue.extend({
},
beforeDestroy() {
- this.connection.off('notification', this.onNotification);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
this.column.$off('top', this.onTop);
this.column.$off('bottom', this.onBottom);
@@ -119,7 +114,7 @@ export default Vue.extend({
onNotification(notification) {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
this.connection.send({
- type: 'read_notification',
+ type: 'readNotification',
id: notification.id
});
@@ -157,8 +152,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
-
+.oxynyeqmfvracxnglgulyqfgqxnxmehl
.transition
.mk-notifications-enter
.mk-notifications-leave-to
@@ -171,7 +165,7 @@ root(isDark)
> .notifications
> .notification:not(:last-child)
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ border-bottom solid 1px var(--faceDivider)
> .date
display block
@@ -179,9 +173,9 @@ root(isDark)
line-height 32px
text-align center
font-size 0.8em
- color isDark ? #666b79 : #aaa
- background isDark ? #242731 : #fdfdfd
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ color var(--dateDividerFg)
+ background var(--dateDividerBg)
+ border-bottom solid 1px var(--faceDivider)
span
margin 0 16px
@@ -223,10 +217,4 @@ root(isDark)
> [data-fa]
margin-right 4px
-.oxynyeqmfvracxnglgulyqfgqxnxmehl[data-darkmode]
- root(true)
-
-.oxynyeqmfvracxnglgulyqfgqxnxmehl:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
index 231b505f5d..d245e3ecf5 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
@@ -6,14 +6,16 @@
<template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
<template v-if="column.type == 'global'">%fa:globe%</template>
<template v-if="column.type == 'list'">%fa:list%</template>
+ <template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
<span>{{ name }}</span>
</span>
<div class="editor" style="padding:0 12px" v-if="edit">
- <mk-switch v-model="column.isMediaOnly" @change="onChangeSettings" text="%i18n:@is-media-only%"/>
- <mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
+ <ui-switch v-model="column.isMediaOnly" @change="onChangeSettings">%i18n:@is-media-only%</ui-switch>
+ <ui-switch v-model="column.isMediaView" @change="onChangeSettings">%i18n:@is-media-view%</ui-switch>
</div>
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
+ <x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
</x-column>
</template>
@@ -23,12 +25,14 @@ import Vue from 'vue';
import XColumn from './deck.column.vue';
import XTl from './deck.tl.vue';
import XListTl from './deck.list-tl.vue';
+import XHashtagTl from './deck.hashtag-tl.vue';
export default Vue.extend({
components: {
XColumn,
XTl,
- XListTl
+ XListTl,
+ XHashtagTl
},
props: {
@@ -65,6 +69,7 @@ export default Vue.extend({
case 'hybrid': return '%i18n:common.deck.hybrid%';
case 'global': return '%i18n:common.deck.global%';
case 'list': return this.column.list.title;
+ case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
}
}
},
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue
index a9e4d489c3..8aed80fa1b 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue
@@ -36,18 +36,17 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
- connection: null,
- connectionId: null
+ connection: null
};
},
computed: {
stream(): any {
switch (this.src) {
- case 'home': return (this as any).os.stream;
- case 'local': return (this as any).os.streams.localTimelineStream;
- case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
- case 'global': return (this as any).os.streams.globalTimelineStream;
+ case 'home': return (this as any).os.stream.useSharedConnection('homeTimeline');
+ case 'local': return (this as any).os.stream.useSharedConnection('localTimeline');
+ case 'hybrid': return (this as any).os.stream.useSharedConnection('hybridTimeline');
+ case 'global': return (this as any).os.stream.useSharedConnection('globalTimeline');
}
},
@@ -68,8 +67,7 @@ export default Vue.extend({
},
mounted() {
- this.connection = this.stream.getConnection();
- this.connectionId = this.stream.use();
+ this.connection = this.stream;
this.connection.on('note', this.onNote);
if (this.src == 'home') {
@@ -81,12 +79,7 @@ export default Vue.extend({
},
beforeDestroy() {
- this.connection.off('note', this.onNote);
- if (this.src == 'home') {
- this.connection.off('follow', this.onChangeFollowing);
- this.connection.off('unfollow', this.onChangeFollowing);
- }
- this.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
@@ -96,7 +89,7 @@ export default Vue.extend({
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api(this.endpoint, {
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -117,7 +110,7 @@ export default Vue.extend({
const promise = (this as any).api(this.endpoint, {
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
@@ -138,7 +131,7 @@ export default Vue.extend({
},
onNote(note) {
- if (this.mediaOnly && note.media.length == 0) return;
+ if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue
index 26b989656e..22b4c50bb4 100644
--- a/src/client/app/desktop/views/pages/deck/deck.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui :class="$style.root">
- <div class="qlvquzbjribqcaozciifydkngcwtyzje" :data-darkmode="$store.state.device.darkmode">
+ <div class="qlvquzbjribqcaozciifydkngcwtyzje" :style="style">
<template v-for="ids in layout">
<div v-if="ids.length > 1" class="folder">
<template v-for="id, i in ids">
@@ -35,6 +35,11 @@ export default Vue.extend({
if (this.$store.state.settings.deck == null) return [];
if (this.$store.state.settings.deck.layout == null) return this.$store.state.settings.deck.columns.map(c => [c.id]);
return this.$store.state.settings.deck.layout;
+ },
+ style(): any {
+ return {
+ height: `calc(100vh - ${this.$store.state.uiHeaderHeight}px)`
+ };
}
},
@@ -85,6 +90,7 @@ export default Vue.extend({
},
mounted() {
+ document.title = (this as any).os.instanceName;
document.documentElement.style.overflow = 'hidden';
},
@@ -138,6 +144,24 @@ export default Vue.extend({
});
}
}, {
+ icon: '%fa:at%',
+ text: '%i18n:common.deck.mentions%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'mentions'
+ });
+ }
+ }, {
+ icon: '%fa:envelope R%',
+ text: '%i18n:common.deck.direct%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'direct'
+ });
+ }
+ }, {
icon: '%fa:list%',
text: '%i18n:common.deck.list%',
action: () => {
@@ -152,6 +176,20 @@ export default Vue.extend({
});
}
}, {
+ icon: '%fa:hashtag%',
+ text: '%i18n:common.deck.hashtag%',
+ action: () => {
+ (this as any).apis.input({
+ title: '%i18n:@enter-hashtag-tl-title%'
+ }).then(title => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'hashtag',
+ tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
+ });
+ });
+ }
+ }, {
icon: '%fa:bell R%',
text: '%i18n:common.deck.notifications%',
action: () => {
@@ -183,9 +221,7 @@ export default Vue.extend({
</style>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.qlvquzbjribqcaozciifydkngcwtyzje
display flex
flex 1
padding 16px 0 16px 16px
@@ -213,18 +249,12 @@ root(isDark)
> button
padding 0 16px
- color isDark ? #93a0a5 : #888
+ color var(--faceTextButton)
&:hover
- color isDark ? #b8c5ca : #777
+ color var(--faceTextButtonHover)
&:active
- color isDark ? #fff : #555
-
-.qlvquzbjribqcaozciifydkngcwtyzje[data-darkmode]
- root(true)
-
-.qlvquzbjribqcaozciifydkngcwtyzje:not([data-darkmode])
- root(false)
+ color var(--faceTextButtonActive)
</style>
diff --git a/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue b/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
index 15397232e0..e1fecc98bc 100644
--- a/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
@@ -135,9 +135,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.wtdtxvecapixsepjtcupubtsmometobz
.gqpwvtwtprsbmnssnbicggtwqhmylhnq
> header
padding 16px
@@ -169,14 +167,5 @@ root(isDark)
background rgba(#000, 0.7)
border-radius 4px
- > header
- color isDark ? #fff : #000
-
-.wtdtxvecapixsepjtcupubtsmometobz[data-darkmode]
- root(true)
-
-.wtdtxvecapixsepjtcupubtsmometobz:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue
index 217dcb7751..dec6c4551a 100644
--- a/src/client/app/desktop/views/pages/drive.vue
+++ b/src/client/app/desktop/views/pages/drive.vue
@@ -31,7 +31,7 @@ export default Vue.extend({
const title = folder.name + ' | %i18n:@title%';
// Rewrite URL
- history.pushState(null, title, '/i/drive/folder/' + folder.id);
+ history.pushState(null, title, `/i/drive/folder/${folder.id}`);
document.title = title;
}
diff --git a/src/client/app/desktop/views/pages/games/reversi.vue b/src/client/app/desktop/views/pages/games/reversi.vue
index ce9b42c65f..1b0e790a22 100644
--- a/src/client/app/desktop/views/pages/games/reversi.vue
+++ b/src/client/app/desktop/views/pages/games/reversi.vue
@@ -16,10 +16,10 @@ export default Vue.extend({
methods: {
nav(game, actualNav) {
if (actualNav) {
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
} else {
// TODO: https://github.com/vuejs/vue-router/issues/703
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
}
}
}
diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
index c7ff0904e0..e595ef4c36 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <mk-home :mode="mode" @loaded="loaded"/>
+ <mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/>
</mk-ui>
</template>
@@ -15,6 +15,13 @@ export default Vue.extend({
default: 'timeline'
}
},
+ computed: {
+ keymap(): any {
+ return {
+ 't': this.focus
+ };
+ }
+ },
mounted() {
document.title = (this as any).os.instanceName;
@@ -23,6 +30,9 @@ export default Vue.extend({
methods: {
loaded() {
Progress.done();
+ },
+ focus() {
+ this.$refs.home.focus();
}
}
});
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 1ebd53cef4..4be33dda04 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = 'メッセージ: ' + getUserName(this.user);
+ document.title = `メッセージ: ${getUserName(this.user)}`;
Progress.done();
});
diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue
index c846f2418f..b82ed0a208 100644
--- a/src/client/app/desktop/views/pages/selectdrive.vue
+++ b/src/client/app/desktop/views/pages/selectdrive.vue
@@ -54,7 +54,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mkp-selectdrive
display block
@@ -72,7 +72,7 @@ export default Vue.extend({
left 0
width 100%
height 72px
- background lighten($theme-color, 95%)
+ background var(--primaryLighten95)
.upload
display inline-block
@@ -85,7 +85,7 @@ export default Vue.extend({
width 40px
height 40px
font-size 1em
- color rgba($theme-color, 0.5)
+ color var(--primaryAlpha05)
background transparent
outline none
border solid 1px transparent
@@ -93,13 +93,13 @@ export default Vue.extend({
&:hover
background transparent
- border-color rgba($theme-color, 0.3)
+ border-color var(--primaryAlpha03)
&:active
- color rgba($theme-color, 0.6)
+ color var(--primaryAlpha06)
background transparent
- border-color rgba($theme-color, 0.5)
- box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
+ border-color var(--primaryAlpha05)
+ //box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
&:focus
&:after
@@ -110,7 +110,7 @@ export default Vue.extend({
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
.ok
@@ -136,7 +136,7 @@ export default Vue.extend({
right -5px
bottom -5px
left -5px
- border 2px solid rgba($theme-color, 0.3)
+ border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
@@ -145,20 +145,20 @@ export default Vue.extend({
.ok
right 16px
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
+ color var(--primaryForeground)
+ background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
+ border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
+ background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
+ border-color var(--primary)
&:active:not(:disabled)
- background $theme-color
- border-color $theme-color
+ background var(--primary)
+ border-color var(--primary)
.cancel
right 148px
diff --git a/src/client/app/desktop/views/pages/stats/stats.vue b/src/client/app/desktop/views/pages/stats/stats.vue
index 41005b6398..219885fb9e 100644
--- a/src/client/app/desktop/views/pages/stats/stats.vue
+++ b/src/client/app/desktop/views/pages/stats/stats.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
</script>
<style lang="stylus">
-@import '~const.styl'
+
.tcrwdhwpuxrwmcttxjcsehgpagpstqey
width 100%
@@ -43,7 +43,7 @@ export default Vue.extend({
> .stats
display flex
justify-content center
- margin-bottom 16px
+ margin 0 auto 16px auto
padding 32px
background #fff
box-shadow 0 2px 8px rgba(#000, 0.1)
@@ -54,11 +54,12 @@ export default Vue.extend({
> *:first-child
display block
- color $theme-color
+ color var(--primary)
> *:last-child
font-size 70%
> div
- max-width 850px
+ max-width 950px
+ margin 0 auto
</style>
diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue
deleted file mode 100644
index 7d9a4606a1..0000000000
--- a/src/client/app/desktop/views/pages/user-list.users.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<template>
-<div>
- <mk-widget-container>
- <template slot="header">%fa:users% %i18n:@users%</template>
- <button slot="func" title="%i18n:@add-user%" @click="add">%fa:plus%</button>
-
- <div data-id="d0b63759-a822-4556-a5ce-373ab966e08a">
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p>
- <template v-else-if="users.length != 0">
- <div class="user" v-for="_user in users">
- <mk-avatar class="avatar" :user="_user"/>
- <div class="body">
- <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
- <p class="username">@{{ _user | acct }}</p>
- </div>
- </div>
- </template>
- <p class="empty" v-else>%i18n:@no-one%</p>
- </div>
- </mk-widget-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- props: {
- list: {
- type: Object,
- required: true
- }
- },
- data() {
- return {
- fetching: true,
- users: []
- };
- },
- mounted() {
- (this as any).api('users/show', {
- userIds: this.list.userIds
- }).then(users => {
- this.users = users;
- this.fetching = false;
- });
- },
- methods: {
- add() {
- (this as any).apis.input({
- title: '%i18n:@username%',
- }).then(async (username: string) => {
- if (username.startsWith('@')) username = username.slice(1);
- const user = await (this as any).api('users/show', {
- username
- });
-
- (this as any).api('users/lists/push', {
- listId: this.list.id,
- userId: user.id
- });
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-root(isDark)
- > .user
- padding 16px
- border-bottom solid 1px isDark ? #1c2023 : #eee
-
- &:last-child
- border-bottom none
-
- &:after
- content ""
- display block
- clear both
-
- > .avatar
- display block
- float left
- margin 0 12px 0 0
- width 42px
- height 42px
- border-radius 8px
-
- > .body
- float left
- width calc(100% - 54px)
-
- > .name
- margin 0
- font-size 16px
- line-height 24px
- color isDark ? #fff : #555
-
- > .username
- display block
- margin 0
- font-size 15px
- line-height 16px
- color isDark ? #606984 : #ccc
-
- > .empty
- margin 0
- padding 16px
- text-align center
- color #aaa
-
- > .fetching
- margin 0
- padding 16px
- text-align center
- color #aaa
-
-[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"][data-darkmode]
- root(true)
-
-[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"]:not([data-darkmode])
- root(false)
-
-</style>
diff --git a/src/client/app/desktop/views/pages/user-list.vue b/src/client/app/desktop/views/pages/user-list.vue
deleted file mode 100644
index 2241b84e5e..0000000000
--- a/src/client/app/desktop/views/pages/user-list.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<template>
-<mk-ui>
- <div v-if="!fetching" data-id="02010e15-cc48-4245-8636-16078a9b623c">
- <div>
- <div><h1>{{ list.title }}</h1></div>
- <x-users :list="list"/>
- </div>
- <main>
- <mk-user-list-timeline :list="list"/>
- </main>
- </div>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XUsers from './user-list.users.vue';
-
-export default Vue.extend({
- components: {
- XUsers
- },
- data() {
- return {
- fetching: true,
- list: null
- };
- },
- watch: {
- $route: 'fetch'
- },
- mounted() {
- this.fetch();
- },
- methods: {
- fetch() {
- this.fetching = true;
-
- (this as any).api('users/lists/show', {
- listId: this.$route.params.list
- }).then(list => {
- this.list = list;
- this.fetching = false;
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-[data-id="02010e15-cc48-4245-8636-16078a9b623c"]
- display flex
- justify-content center
- margin 0 auto
- max-width 1200px
-
- > main
- > div > div
- > *:not(:last-child)
- margin-bottom 16px
-
- > main
- padding 16px
- width calc(100% - 275px * 2)
-
- > div
- width 275px
- margin 0
- padding 16px 0 16px 16px
-
-</style>
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index e4a771910a..cf05006c00 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -1,5 +1,5 @@
<template>
-<div class="followers-you-know">
+<div class="vahgrswmbzfdlmomxnqftuueyvwaafth">
<p class="title">%fa:users%%i18n:@title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<div v-if="!fetching && users.length > 0">
@@ -36,10 +36,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.followers-you-know
- background #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
+.vahgrswmbzfdlmomxnqftuueyvwaafth
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
> .title
z-index 1
@@ -48,7 +48,7 @@ export default Vue.extend({
line-height 42px
font-size 0.9em
font-weight bold
- color #888
+ color var(--faceHeaderText)
box-shadow 0 1px rgba(#000, 0.07)
> i
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 516eea0288..36ae360248 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -1,5 +1,5 @@
<template>
-<div class="friends">
+<div class="hozptpaliadatkehcmcayizwzwwctpbc">
<p class="title">%fa:users%%i18n:@title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<template v-if="!fetching && users.length != 0">
@@ -40,11 +40,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
-.friends
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
+.hozptpaliadatkehcmcayizwzwwctpbc
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
overflow hidden
> .title
@@ -54,8 +53,8 @@ root(isDark)
line-height 42px
font-size 0.9em
font-weight bold
- background isDark ? #313543 : inherit
- color isDark ? #e3e5e8 : #888
+ background var(--faceHeader)
+ color var(--faceHeaderText)
box-shadow 0 1px rgba(#000, 0.07)
> i
@@ -73,7 +72,7 @@ root(isDark)
> .user
padding 16px
- border-bottom solid 1px isDark ? #21242f : #eee
+ border-bottom solid 1px var(--faceDivider)
&:last-child
border-bottom none
@@ -99,24 +98,19 @@ root(isDark)
margin 0
font-size 16px
line-height 24px
- color isDark ? #ccc : #555
+ color var(--text)
> .username
display block
margin 0
font-size 15px
line-height 16px
- color isDark ? #555 : #ccc
+ color var(--text)
+ opacity 0.7
> .mk-follow-button
position absolute
top 16px
right 16px
-.friends[data-darkmode]
- root(true)
-
-.friends:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index d8f4656ed0..76eb8f9e1c 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -6,7 +6,7 @@
<div class="title">
<p class="name">{{ user | userName }}</p>
<div>
- <span class="username"><mk-acct :user="user"/></span>
+ <span class="username"><mk-acct :user="user" :detail="true" /></span>
<span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span>
<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span>
<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span>
@@ -100,12 +100,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- background isDark ? #282C37 : #fff
- border 1px solid rgba(#000, 0.075)
- border-radius 6px
+.header
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
overflow hidden
&[data-is-dark-background]
@@ -182,12 +180,12 @@ root(isDark)
> .body
padding 16px 16px 16px 154px
- color isDark ? #c5ced6 : #555
+ color var(--text)
> .status
margin-top 16px
padding-top 16px
- border-top solid 1px rgba(#000, isDark ? 0.2 : 0.1)
+ border-top solid 1px var(--faceDivider)
font-size 80%
> *
@@ -196,24 +194,18 @@ root(isDark)
margin-right 16px
&:not(:last-child)
- border-right solid 1px rgba(#000, isDark ? 0.2 : 0.1)
+ border-right solid 1px var(--faceDivider)
&.clickable
cursor pointer
&:hover
- color isDark ? #fff : #000
+ color var(--faceTextButtonHover)
> b
margin-right 4px
font-size 1rem
font-weight bold
- color $theme-color
-
-.header[data-darkmode]
- root(true)
-
-.header:not([data-darkmode])
- root(false)
+ color var(--primary)
</style>
diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue
index 8397e56484..628d5b6d95 100644
--- a/src/client/app/desktop/views/pages/user/user.photos.vue
+++ b/src/client/app/desktop/views/pages/user/user.photos.vue
@@ -1,10 +1,10 @@
<template>
-<div class="photos">
+<div class="dzsuvbsrrrwobdxifudxuefculdfiaxd">
<p class="title">%fa:camera%%i18n:@title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<div class="stream" v-if="!fetching && images.length > 0">
<div v-for="image in images" class="img"
- :style="`background-image: url(${image.url})`"
+ :style="`background-image: url(${image.thumbnailUrl})`"
></div>
</div>
<p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
@@ -24,12 +24,12 @@ export default Vue.extend({
mounted() {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: true,
+ withFiles: true,
limit: 9
}).then(notes => {
notes.forEach(note => {
- note.media.forEach(media => {
- if (this.images.length < 9) this.images.push(media);
+ note.files.forEach(file => {
+ if (this.images.length < 9) this.images.push(file);
});
});
this.fetching = false;
@@ -39,11 +39,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
-.photos
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
+.dzsuvbsrrrwobdxifudxuefculdfiaxd
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
overflow hidden
> .title
@@ -53,8 +52,8 @@ root(isDark)
line-height 42px
font-size 0.9em
font-weight bold
- background: isDark ? #313543 : inherit
- color isDark ? #e3e5e8 : #888
+ background var(--faceHeader)
+ color var(--faceHeaderText)
box-shadow 0 1px rgba(#000, 0.07)
> i
@@ -88,10 +87,4 @@ root(isDark)
> i
margin-right 4px
-.photos[data-darkmode]
- root(true)
-
-.photos:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index efd5be4672..fe10b54378 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -85,10 +85,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
+.profile
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
> *:first-child
border-top none !important
@@ -96,7 +96,7 @@ root(isDark)
> .friend-form
padding 16px
text-align center
- border-bottom solid 1px isDark ? #21242f : #eee
+ border-bottom solid 1px var(--faceDivider)
> .followed
margin 12px 0 0 0
@@ -114,7 +114,7 @@ root(isDark)
> .action-form
padding 16px
text-align center
- border-bottom solid 1px isDark ? #21242f : #eee
+ border-bottom solid 1px var(--faceDivider)
> *
width 100%
@@ -122,10 +122,4 @@ root(isDark)
&:not(:last-child)
margin-bottom 12px
-.profile[data-darkmode]
- root(true)
-
-.profile:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
index 67987fcb94..608c12b7e2 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -66,7 +66,7 @@ export default Vue.extend({
limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined,
includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media'
+ withFiles: this.mode == 'with-media'
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -86,7 +86,7 @@ export default Vue.extend({
userId: this.user.id,
limit: fetchLimit + 1,
includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media',
+ withFiles: this.mode == 'with-media',
untilId: (this.$refs.timeline as any).tail().id
});
@@ -112,17 +112,16 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- background isDark ? #282C37 : #fff
+.oh5y2r7l5lx8j6jj791ykeiwgihheguk
+ background var(--face)
+ border-radius var(--round)
+ overflow hidden
> header
padding 0 8px
z-index 10
- background isDark ? #313543 : #fff
- border-radius 6px 6px 0 0
- box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
+ background var(--faceHeader)
+ box-shadow 0 1px var(--desktopTimelineHeaderShadow)
> span
display inline-block
@@ -132,7 +131,7 @@ root(isDark)
user-select none
&[data-active]
- color $theme-color
+ color var(--primary)
cursor default
font-weight bold
@@ -144,14 +143,14 @@ root(isDark)
left -8px
width calc(100% + 16px)
height 2px
- background $theme-color
+ background var(--primary)
&:not([data-active])
- color isDark ? #9aa2a7 : #6f7477
+ color var(--desktopTimelineSrc)
cursor pointer
&:hover
- color isDark ? #d9dcde : #525a5f
+ color var(--desktopTimelineSrcHover)
> .loading
padding 64px 0
@@ -170,10 +169,4 @@ root(isDark)
font-size 3em
color #ccc
-.oh5y2r7l5lx8j6jj791ykeiwgihheguk[data-darkmode]
- root(true)
-
-.oh5y2r7l5lx8j6jj791ykeiwgihheguk:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index afb5e674d9..a8da890936 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -1,15 +1,16 @@
<template>
<mk-ui>
- <div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
+ <div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
<div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div>
<div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></div>
<main>
<div class="main">
<x-header :user="user"/>
- <mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
+ <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<x-timeline class="timeline" ref="tl" :user="user"/>
</div>
<div class="side">
+ <div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
<x-profile :user="user"/>
<x-twitter :user="user" v-if="user.host === null && user.twitter"/>
<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
@@ -28,7 +29,6 @@
<script lang="ts">
import Vue from 'vue';
import parseAcct from '../../../../../../misc/acct/parse';
-import getUserName from '../../../../../../misc/get-user-name';
import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue';
import XTimeline from './user.timeline.vue';
@@ -79,7 +79,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.xygkxeaeontfaokvqmiblezmhvhostak
width 980px
padding 16px
margin 0 auto
@@ -89,17 +89,16 @@ root(isDark)
margin-bottom 16px
padding 14px 16px
font-size 14px
- border-radius 6px
+ box-shadow var(--shadow)
+ border-radius var(--round)
&.is-suspended
- color isDark ? #ffb4b4 : #570808
- background isDark ? #611d1d : #ffdbdb
- border solid 1px isDark ? #d64a4a : #e09696
+ color var(--suspendedInfoFg)
+ background var(--suspendedInfoBg)
&.is-remote
- color isDark ? #ffbd3e : #573c08
- background isDark ? #42321c : #fff0db
- border solid 1px isDark ? #90733c : #dcbb7b
+ color var(--remoteInfoFg)
+ background var(--remoteInfoBg)
> a
font-weight bold
@@ -119,8 +118,7 @@ root(isDark)
margin-right 16px
> .timeline
- border 1px solid rgba(#000, 0.075)
- border-radius 6px
+ box-shadow var(--shadow)
> .side
width 275px
@@ -134,24 +132,22 @@ root(isDark)
font-size 0.8em
color #aaa
+ > .instance
+ box-shadow var(--shadow)
+ border-radius var(--round)
+
> .nav
padding 16px
font-size 12px
- color #aaa
- background isDark ? #21242f : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
+ color var(--text)
+ background var(--face)
+ box-shadow var(--shadow)
+ border-radius var(--round)
a
- color #999
+ color var(--text)99
i
- color #ccc
-
-.xygkxeaeontfaokvqmiblezmhvhostak[data-darkmode]
- root(true)
-
-.xygkxeaeontfaokvqmiblezmhvhostak:not([data-darkmode])
- root(false)
+ color var(--text)
</style>
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index ac2f921a21..65651f7ffc 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -1,45 +1,147 @@
<template>
<div class="mk-welcome">
- <img ref="pointer" class="pointer" src="/assets/pointer.png" alt="">
+ <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
+
<button @click="dark">
<template v-if="$store.state.device.darkmode">%fa:moon%</template>
<template v-else>%fa:R moon%</template>
</button>
- <div class="body">
- <div class="container">
- <div class="info">
- <span><b>{{ host }}</b></span>
- <span class="stats" v-if="stats">
- <span>%fa:user% {{ stats.originalUsersCount | number }}</span>
- <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
- </span>
- </div>
- <main>
- <div class="about">
+
+ <mk-forkit class="forkit"/>
+
+ <main>
+ <div class="body">
+ <div class="main block">
+ <div>
<h1 v-if="name != 'Misskey'">{{ name }}</h1>
- <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1>
- <p class="powerd-by" v-if="name != 'Misskey'" v-html="'%i18n:@powered-by-misskey%'"></p>
- <p class="desc" v-html="description || '%i18n:common.about%'"></p>
- <a ref="signup" @click="signup">📦 %i18n:@signup%</a>
+ <h1 v-else><img svg-inline src="../../../../assets/title.svg" :alt="name"></h1>
+
+ <div class="info">
+ <span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span>
+ <span class="stats" v-if="stats">
+ <span>%fa:user% {{ stats.originalUsersCount | number }}</span>
+ <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
+ </span>
+ </div>
+
+ <div class="desc">
+ <span class="desc" v-html="description || '%i18n:common.about%'"></span>
+ <a class="about" @click="about">%i18n:@about%</a>
+ </div>
+
+ <p class="sign">
+ <span class="signup" @click="signup">%i18n:@signup%</span>
+ <span class="divider">|</span>
+ <span class="signin" @click="signin">%i18n:@signin%</span>
+ </p>
+
+ <img src="/assets/ai.png" alt="" title="藍" class="char">
</div>
- <div class="login">
- <mk-signin/>
+ </div>
+
+ <div class="announcements block">
+ <header>%fa:broadcast-tower% %i18n:@announcements%</header>
+ <div v-if="announcements && announcements.length > 0">
+ <div v-for="announcement in announcements">
+ <h1 v-html="announcement.title"></h1>
+ <div v-html="announcement.text"></div>
+ </div>
+ </div>
+ </div>
+
+ <div class="photos block">
+ <header>%fa:images% %i18n:@photos%</header>
+ <div>
+ <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
+ </div>
+ </div>
+
+ <div class="tag-cloud block">
+ <div>
+ <mk-tag-cloud/>
+ </div>
+ </div>
+
+ <div class="nav block">
+ <div>
+ <mk-nav class="nav"/>
+ </div>
+ </div>
+
+ <div class="side">
+ <div class="trends block">
+ <div>
+ <mk-trends/>
+ </div>
+ </div>
+
+ <div class="tl block">
+ <header>%fa:comment-alt R% %i18n:@timeline%</header>
+ <div>
+ <mk-welcome-timeline class="tl" :max="20"/>
+ </div>
+ </div>
+
+ <div class="info block">
+ <header>%fa:info-circle% %i18n:@info%</header>
+ <div>
+ <div v-if="meta" class="body">
+ <p>Version: <b>{{ meta.version }}</b></p>
+ <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ </div>
+ </div>
</div>
- </main>
- <div class="hashtags">
- <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
</div>
- <mk-nav class="nav"/>
</div>
- <mk-forkit class="forkit"/>
- <img src="assets/title.dark.svg" :alt="name">
- </div>
- <div class="tl">
- <mk-welcome-timeline :max="20"/>
- </div>
- <modal name="signup" width="500px" height="auto" scrollable>
- <header :class="$style.signupFormHeader">%i18n:@signup%</header>
- <mk-signup :class="$style.signupForm"/>
+ </main>
+
+ <modal name="about" class="about modal" width="800px" height="auto" scrollable>
+ <article class="fpdezooorhntlzyeszemrsqdlgbysvxq">
+ <h1>%i18n:common.intro.title%</h1>
+ <p v-html="'%i18n:common.intro.about%'"></p>
+ <section>
+ <h2>%i18n:common.intro.features%</h2>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.rich-contents%</h3>
+ <p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/post.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.reaction%</h3>
+ <p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/reaction.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.ui%</h3>
+ <p v-html="'%i18n:common.intro.ui-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/ui.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.drive%</h3>
+ <p v-html="'%i18n:common.intro.drive-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/drive.png" alt=""></div>
+ </section>
+ </section>
+ <p v-html="'%i18n:common.intro.outro%'"></p>
+ </article>
+ </modal>
+
+ <modal name="signup" class="modal" width="450px" height="auto" scrollable>
+ <header class="formHeader">%i18n:@signup%</header>
+ <mk-signup class="form"/>
+ </modal>
+
+ <modal name="signin" class="modal" width="450px" height="auto" scrollable>
+ <header class="formHeader">%i18n:@signin%</header>
+ <mk-signin class="form"/>
</modal>
</div>
</template>
@@ -47,52 +149,65 @@
<script lang="ts">
import Vue from 'vue';
import { host, copyright } from '../../../config';
+import { concat } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
+ meta: null,
stats: null,
+ banner: null,
copyright,
host,
name: 'Misskey',
description: '',
- pointerInterval: null,
- tags: []
+ announcements: [],
+ photos: []
};
},
+
created() {
(this as any).os.getMeta().then(meta => {
+ this.meta = meta;
this.name = meta.name;
this.description = meta.description;
+ this.announcements = meta.broadcasts;
+ this.banner = meta.bannerUrl;
});
(this as any).api('stats').then(stats => {
this.stats = stats;
});
- (this as any).api('hashtags/trend').then(stats => {
- this.tags = stats.map(x => x.tag);
+ const image = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif'
+ ];
+
+ (this as any).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);
});
},
- mounted() {
- this.point();
- this.pointerInterval = setInterval(this.point, 100);
- },
- beforeDestroy() {
- clearInterval(this.pointerInterval);
- },
+
methods: {
- point() {
- const x = this.$refs.signup.getBoundingClientRect();
- this.$refs.pointer.style.top = x.top + x.height + 'px';
- this.$refs.pointer.style.left = x.left + 'px';
+ about() {
+ this.$modal.show('about');
},
+
signup() {
this.$modal.show('signup');
},
+
signin() {
this.$modal.show('signin');
},
+
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
@@ -103,189 +218,289 @@ export default Vue.extend({
});
</script>
-<style>
-#wait {
- right: auto;
- left: 15px;
-}
+<style lang="stylus">
+#wait
+ right auto
+ left 15px
+
+.v--modal-overlay
+ background rgba(0, 0, 0, 0.6)
+
+.modal
+ .form
+ padding 24px 48px 48px 48px
+
+ .formHeader
+ text-align center
+ padding 48px 0 12px 0
+ margin 0 48px
+ font-size 1.5em
+
+ .v--modal-box
+ background var(--face)
+ color var(--text)
+
+ .formHeader
+ border-bottom solid 1px rgba(#000, 0.2)
+
+.v--modal-overlay.about
+ .v--modal-box.v--modal
+ margin 32px 0
+
+.fpdezooorhntlzyeszemrsqdlgbysvxq
+ padding 64px
+
+ > p:last-child
+ margin-bottom 0
+
+ > h1
+ margin-top 0
+
+ > section
+ > h2
+ border-bottom 1px solid var(--faceDivider)
+
+ > section
+ display grid
+ grid-template-rows 1fr
+ grid-template-columns 180px 1fr
+ gap 32px
+ margin-bottom 32px
+ padding-bottom 32px
+ border-bottom 1px solid var(--faceDivider)
+
+ &:nth-child(odd)
+ grid-template-columns 1fr 180px
+
+ > .body
+ grid-column 1
+
+ > .image
+ grid-column 2
+
+ > .body
+ grid-row 1
+ grid-column 2
+
+ > .image
+ grid-row 1
+ grid-column 1
+
+ > img
+ display block
+ width 100%
+ height 100%
+ object-fit cover
</style>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-welcome
display flex
min-height 100vh
- > .pointer
- display block
+ > .banner
+ position absolute
+ top 0
+ left 0
+ width 100%
+ height 400px
+ 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))
+
+ > .forkit
position absolute
- z-index 1
top 0
right 0
- width 180px
- margin 0 0 0 -180px
- transform rotateY(180deg) translateX(-10px) translateY(-48px)
- pointer-events none
> button
position fixed
z-index 1
- top 0
- left 0
+ bottom 16px
+ left 16px
padding 16px
font-size 18px
- color #fff
+ color var(--text)
- display none // TODO
+ > main
+ margin 0 auto
+ padding 64px
+ width 100%
+ max-width 1200px
- > .body
- flex 1
- padding 64px 0 0 0
- text-align center
- background #578394
- background-position center
- background-size cover
+ .block
+ color var(--text)
+ background var(--face)
+ box-shadow var(--shadow)
+ //border-radius 8px
+ overflow auto
- &:before
- content ''
- display block
- position absolute
- top 0
- left 0
- right 0
- bottom 0
- background rgba(#000, 0.5)
+ > header
+ z-index 1
+ padding 0 16px
+ line-height 48px
+ background var(--faceHeader)
+ box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
- > .forkit
- position absolute
- top 0
- right 0
+ & + div
+ max-height calc(100% - 48px)
- > img
- position absolute
- bottom 16px
- right 16px
- width 150px
+ > div
+ overflow auto
- > .container
- $aboutWidth = 380px
- $loginWidth = 340px
- $width = $aboutWidth + $loginWidth
+ > .body
+ display grid
+ grid-template-rows 390px 1fr 256px 64px
+ grid-template-columns 1fr 1fr 350px
+ gap 16px
+ height 1150px
- > .info
- margin 0 auto 16px auto
- width $width
- font-size 14px
- color #fff
+ > .main
+ grid-row 1
+ grid-column 1 / 3
+ border-top solid 5px var(--primary)
- > .stats
- margin-left 16px
- padding-left 16px
- border-left solid 1px #fff
+ > div
+ padding 32px
+ min-height 100%
- > *
- margin-right 16px
+ > h1
+ margin 0
- > main
- display flex
- margin auto
- width $width
- border-radius 8px
- overflow hidden
- box-shadow 0 2px 8px rgba(#000, 0.3)
+ > svg
+ margin -8px 0 0 -16px
+ width 280px
+ height 100px
+ fill currentColor
- > .about
- width $aboutWidth
- color #444
- background #fff
+ > .info
+ margin 0 auto 16px auto
+ width $width
+ font-size 14px
- > h1
- margin 0 0 16px 0
- padding 32px 32px 0 32px
- color #444
+ > .stats
+ margin-left 16px
+ padding-left 16px
+ border-left solid 1px var(--faceDivider)
+
+ > *
+ margin-right 16px
+
+ > .desc
+ max-width calc(100% - 150px)
+
+ > .sign
+ font-size 120%
+ margin-bottom 0
+
+ > .divider
+ margin 0 16px
+
+ > .signin
+ > .signup
+ cursor pointer
- > img
- width 170px
- vertical-align bottom
+ &:hover
+ color var(--primary)
- > .powerd-by
- margin 16px
+ > .char
+ display block
+ position absolute
+ right 16px
+ bottom 0
+ height 320px
opacity 0.7
- > .desc
- margin 0
- padding 0 32px 16px 32px
+ > *:not(.char)
+ z-index 1
- > a
- display inline-block
- margin 0 0 32px 0
- font-weight bold
+ > .announcements
+ grid-row 2
+ grid-column 1
- > .login
- width $loginWidth
- padding 16px 32px 32px 32px
- background isDark ? #2e3440 : #f5f5f5
+ > div
+ padding 32px
- > .hashtags
- margin 16px auto
- width $width
- font-size 14px
- color #fff
- background rgba(#000, 0.3)
- border-radius 8px
+ > div
+ padding 0 0 16px 0
+ margin 0 0 16px 0
+ border-bottom 1px solid var(--faceDivider)
- > *
- display inline-block
- margin 14px
+ > h1
+ margin 0
+ font-size 1.25em
- > .nav
- display block
- margin 16px 0
- font-size 14px
- color #fff
+ > .photos
+ grid-row 2
+ grid-column 2
- > .tl
- margin 0
- width 410px
- height 100vh
- text-align left
- background isDark ? #313543 : #fff
+ > div
+ display grid
+ grid-template-rows 1fr 1fr 1fr
+ grid-template-columns 1fr 1fr
+ gap 8px
+ height 100%
+ padding 16px
- > *
- max-height 100%
- overflow auto
+ > div
+ //border-radius 4px
+ background-position center center
+ background-size cover
-.mk-welcome[data-darkmode]
- root(true)
+ > .tag-cloud
+ grid-row 3
+ grid-column 1 / 3
-.mk-welcome:not([data-darkmode])
- root(false)
+ > div
+ height 256px
+ padding 32px
-</style>
+ > .nav
+ display flex
+ justify-content center
+ align-items center
+ grid-row 4
+ grid-column 1 / 3
+ font-size 14px
+
+ > .side
+ display grid
+ grid-row 1 / 5
+ grid-column 3
+ grid-template-rows 1fr 350px
+ grid-template-columns 1fr
+ gap 16px
+
+ > .tl
+ grid-row 1
+ grid-column 1
+ overflow auto
-<style lang="stylus" module>
-.signupForm
- padding 24px 48px 48px 48px
+ > .trends
+ grid-row 2
+ grid-column 1
+ padding 8px
-.signupFormHeader
- padding 48px 0 12px 0
- margin: 0 48px
- font-size 1.5em
- color #777
- border-bottom solid 1px #eee
+ > .info
+ grid-row 3
+ grid-column 1
-.signinForm
- padding 24px 48px 48px 48px
+ > div
+ padding 16px
-.signinFormHeader
- padding 48px 0 12px 0
- margin: 0 48px
- font-size 1.5em
- color #777
- border-bottom solid 1px #eee
+ > .body
+ > p
+ display block
+ margin 0
-.nav
- a
- color #666
</style>
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 8ff0bb5d0d..c10ac1ca17 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -4,7 +4,7 @@
<template slot="header">%fa:chart-pie%%i18n:@title%</template>
<button slot="func" title="%i18n:@refresh%" @click="fetch">%fa:sync%</button>
- <div class="mkw-polls--body" :data-darkmode="$store.state.device.darkmode">
+ <div class="mkw-polls--body">
<div class="poll" v-if="!fetching && poll != null">
<p v-if="poll.text"><router-link :to="poll | notePage">{{ poll.text }}</router-link></p>
<p v-if="!poll.text"><router-link :to="poll | notePage">%fa:link%</router-link></p>
@@ -64,11 +64,11 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mkw-polls--body
> .poll
padding 16px
font-size 12px
- color isDark ? #9ea4ad : #555
+ color var(--text)
> p
margin 0 0 8px 0
@@ -91,10 +91,4 @@ root(isDark)
> [data-fa]
margin-right 4px
-.mkw-polls--body[data-darkmode]
- root(true)
-
-.mkw-polls--body:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index 19a2790d95..a763f4d17c 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -68,7 +68,7 @@ export default define({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mkw-post-form
background #fff
@@ -107,8 +107,8 @@ export default define({
margin 0
padding 0 10px
height 28px
- color $theme-color-foreground
- background $theme-color !important
+ color var(--primaryForeground)
+ background var(--primary) !important
outline none
border none
border-radius 4px
@@ -116,10 +116,10 @@ export default define({
cursor pointer
&:hover
- background lighten($theme-color, 10%) !important
+ background var(--primaryLighten10) !important
&:active
- background darken($theme-color, 10%) !important
+ background var(--primaryDarken10) !important
transition background 0s ease
</style>
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index a22607b612..30b7b95d35 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -1,20 +1,24 @@
<template>
-<div class="mkw-profile"
- :data-compact="props.design == 1 || props.design == 2"
- :data-melt="props.design == 2"
->
- <div class="banner"
- :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
- title="%i18n:@update-banner%"
- @click="() => os.apis.updateBanner()"
- ></div>
- <mk-avatar class="avatar" :user="$store.state.i"
- :disable-link="true"
- @click="() => os.apis.updateAvatar()"
- title="%i18n:@update-avatar%"
- />
- <router-link class="name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
- <p class="username">@{{ $store.state.i | acct }}</p>
+<div class="egwyvoaaryotefqhqtmiyawwefemjfsd">
+ <mk-widget-container :show-header="false" :naked="props.design == 2">
+ <div class="egwyvoaaryotefqhqtmiyawwefemjfsd-body"
+ :data-compact="props.design == 1 || props.design == 2"
+ :data-melt="props.design == 2"
+ >
+ <div class="banner"
+ :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
+ title="%i18n:@update-banner%"
+ @click="() => os.apis.updateBanner()"
+ ></div>
+ <mk-avatar class="avatar" :user="$store.state.i"
+ :disable-link="true"
+ @click="() => os.apis.updateAvatar()"
+ title="%i18n:@update-avatar%"
+ />
+ <router-link class="name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
+ <p class="username">@{{ $store.state.i | acct }}</p>
+ </div>
+ </mk-widget-container>
</div>
</template>
@@ -41,12 +45,7 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
- overflow hidden
- background isDark ? #282c37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
-
+.egwyvoaaryotefqhqtmiyawwefemjfsd-body
&[data-compact]
> .banner:before
content ""
@@ -75,9 +74,6 @@ root(isDark)
display none
&[data-melt]
- background transparent !important
- border none !important
-
> .banner
visibility hidden
@@ -90,7 +86,7 @@ root(isDark)
> .banner
height 100px
- background-color isDark ? #303e4a : #f5f5f5
+ background-color var(--primaryAlpha01)
background-size cover
background-position center
cursor pointer
@@ -102,7 +98,7 @@ root(isDark)
left 16px
width 58px
height 58px
- border solid 3px isDark ? #282c37 : #fff
+ border solid 3px var(--face)
border-radius 8px
cursor pointer
@@ -111,19 +107,14 @@ root(isDark)
margin 10px 0 0 84px
line-height 16px
font-weight bold
- color isDark ? #fff : #555
+ color var(--text)
> .username
display block
margin 4px 0 8px 84px
line-height 16px
font-size 0.9em
- color isDark ? #606984 : #999
-
-.mkw-profile[data-darkmode]
- root(true)
-
-.mkw-profile:not([data-darkmode])
- root(false)
+ color var(--text)
+ opacity 0.7
</style>
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index c33bf2f2f2..a886796132 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -49,7 +49,7 @@ export default define({
offset: this.offset,
renote: false,
reply: false,
- media: false,
+ file: false,
poll: false
}).then(notes => {
const note = notes ? notes[0] : null;
@@ -67,7 +67,7 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mkw-trends
.mkw-trends--body
> .note
padding 16px
@@ -98,10 +98,4 @@ root(isDark)
> [data-fa]
margin-right 4px
-.mkw-trends[data-darkmode]
- root(true)
-
-.mkw-trends:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 328fa56697..28c6372b6f 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -73,11 +73,11 @@ export default define({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mkw-users
.mkw-users--body
> .user
padding 16px
- border-bottom solid 1px isDark ? #1c2023 : #eee
+ border-bottom solid 1px var(--faceDivider)
&:last-child
border-bottom none
@@ -103,14 +103,15 @@ root(isDark)
margin 0
font-size 16px
line-height 24px
- color isDark ? #fff : #555
+ color var(--text)
> .username
display block
margin 0
font-size 15px
line-height 16px
- color isDark ? #606984 : #ccc
+ color var(--text)
+ opacity 0.7
> .mk-follow-button
position absolute
@@ -132,10 +133,4 @@ root(isDark)
> [data-fa]
margin-right 4px
-.mkw-users[data-darkmode]
- root(true)
-
-.mkw-users:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/init.css b/src/client/app/init.css
index 6ee25d64e2..92bb1d8cf4 100644
--- a/src/client/app/init.css
+++ b/src/client/app/init.css
@@ -32,7 +32,7 @@ body > noscript {
left: 0;
width: 100%;
height: 100%;
- background: #fff;
+ background: var(--bg);
cursor: wait;
}
#ini > svg {
@@ -47,10 +47,6 @@ body > noscript {
animation: ini 0.6s infinite linear;
}
-html[data-darkmode] #ini {
- background: #191b22;
-}
-
@keyframes ini {
from {
transform: rotate(0deg);
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index cf97957400..c2381067da 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -5,31 +5,27 @@
import Vue from 'vue';
import Vuex from 'vuex';
import VueRouter from 'vue-router';
-import VModal from 'vue-js-modal';
import * as TreeView from 'vue-json-tree-view';
import VAnimateCss from 'v-animate-css';
-import Element from 'element-ui';
-import ElementLocaleEn from 'element-ui/lib/locale/lang/en';
-import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
+import VModal from 'vue-js-modal';
+import VueHotkey from './common/hotkey';
import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update';
import MiOS, { API } from './mios';
import { version, codename, lang } from './config';
+import { builtinThemes, lightTheme, applyTheme } from './theme';
-let elementLocale;
-switch (lang) {
- case 'ja-JP': elementLocale = ElementLocaleJa; break;
- case 'en-US': elementLocale = ElementLocaleEn; break;
- default: elementLocale = ElementLocaleEn; break;
+if (localStorage.getItem('theme') == null) {
+ applyTheme(lightTheme);
}
Vue.use(Vuex);
Vue.use(VueRouter);
-Vue.use(VModal);
Vue.use(TreeView);
Vue.use(VAnimateCss);
-Vue.use(Element, { locale: elementLocale });
+Vue.use(VModal);
+Vue.use(VueHotkey);
// Register global directives
require('./common/views/directives');
@@ -42,9 +38,13 @@ require('./common/views/widgets');
require('./common/views/filters');
Vue.mixin({
- destroyed(this: any) {
- if (this.$el.parentNode) {
- this.$el.parentNode.removeChild(this.$el);
+ methods: {
+ destroyDom() {
+ this.$destroy();
+
+ if (this.$el.parentNode) {
+ this.$el.parentNode.removeChild(this.$el);
+ }
}
}
});
@@ -91,45 +91,55 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
const launch = (router: VueRouter, api?: (os: MiOS) => API) => {
os.apis = api ? api(os) : null;
- //#region Dark/Light
- Vue.mixin({
- data() {
- return {
- _unwatchDarkmode_: null
- };
- },
- mounted() {
- const apply = v => {
- if (this.$el.setAttribute == null) return;
- if (v) {
- this.$el.setAttribute('data-darkmode', 'true');
- } else {
- this.$el.removeAttribute('data-darkmode');
- }
- };
-
- apply(os.store.state.device.darkmode);
-
- this._unwatchDarkmode_ = os.store.watch(s => {
- return s.device.darkmode;
- }, apply);
- },
- beforeDestroy() {
- this._unwatchDarkmode_();
+ //#region theme
+ os.store.watch(s => {
+ return s.device.darkmode;
+ }, v => {
+ const themes = os.store.state.device.themes.concat(builtinThemes);
+ const dark = themes.find(t => t.id == os.store.state.device.darkTheme);
+ const light = themes.find(t => t.id == os.store.state.device.lightTheme);
+ applyTheme(v ? dark : light);
+ });
+ os.store.watch(s => {
+ return s.device.lightTheme;
+ }, v => {
+ const themes = os.store.state.device.themes.concat(builtinThemes);
+ const theme = themes.find(t => t.id == v);
+ if (!os.store.state.device.darkmode) {
+ applyTheme(theme);
}
});
-
os.store.watch(s => {
- return s.device.darkmode;
+ return s.device.darkTheme;
}, v => {
- if (v) {
- document.documentElement.setAttribute('data-darkmode', 'true');
- } else {
- document.documentElement.removeAttribute('data-darkmode');
+ const themes = os.store.state.device.themes.concat(builtinThemes);
+ const theme = themes.find(t => t.id == v);
+ if (os.store.state.device.darkmode) {
+ applyTheme(theme);
}
});
//#endregion
+ //#region shadow
+ const shadow = '0 3px 8px rgba(0, 0, 0, 0.2)';
+ if (os.store.state.settings.useShadow) document.documentElement.style.setProperty('--shadow', shadow);
+ os.store.watch(s => {
+ return s.settings.useShadow;
+ }, v => {
+ document.documentElement.style.setProperty('--shadow', v ? shadow : 'none');
+ });
+ //#endregion
+
+ //#region rounded corners
+ const round = '6px';
+ if (os.store.state.settings.roundedCorners) document.documentElement.style.setProperty('--round', round);
+ os.store.watch(s => {
+ return s.settings.roundedCorners;
+ }, v => {
+ document.documentElement.style.setProperty('--round', v ? round : '0');
+ });
+ //#endregion
+
Vue.mixin({
data() {
return {
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 664848b5e7..42171e71fa 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -1,22 +1,14 @@
+import autobind from 'autobind-decorator';
import Vue from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid';
import initStore from './store';
-import { apiUrl, swPublickey, version, lang, googleMapsApiKey } from './config';
+import { apiUrl, version, lang } from './config';
import Progress from './common/scripts/loading';
-import Connection from './common/scripts/streaming/stream';
-import { HomeStreamManager } from './common/scripts/streaming/home';
-import { DriveStreamManager } from './common/scripts/streaming/drive';
-import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats';
-import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats';
-import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index';
-import { ReversiStreamManager } from './common/scripts/streaming/games/reversi/reversi';
import Err from './common/views/components/connect-failed.vue';
-import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline';
-import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline';
-import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline';
+import Stream from './common/scripts/stream';
//#region api requests
let spinner = null;
@@ -101,30 +93,7 @@ export default class MiOS extends EventEmitter {
/**
* A connection manager of home stream
*/
- public stream: HomeStreamManager;
-
- /**
- * Connection managers
- */
- public streams: {
- localTimelineStream: LocalTimelineStreamManager;
- hybridTimelineStream: HybridTimelineStreamManager;
- globalTimelineStream: GlobalTimelineStreamManager;
- driveStream: DriveStreamManager;
- serverStatsStream: ServerStatsStreamManager;
- notesStatsStream: NotesStatsStreamManager;
- messagingIndexStream: MessagingIndexStreamManager;
- reversiStream: ReversiStreamManager;
- } = {
- localTimelineStream: null,
- hybridTimelineStream: null,
- globalTimelineStream: null,
- driveStream: null,
- serverStatsStream: null,
- notesStatsStream: null,
- messagingIndexStream: null,
- reversiStream: null
- };
+ public stream: Stream;
/**
* A registration of service worker
@@ -150,71 +119,36 @@ export default class MiOS extends EventEmitter {
this.shouldRegisterSw = shouldRegisterSw;
- //#region BIND
- this.log = this.log.bind(this);
- this.logInfo = this.logInfo.bind(this);
- this.logWarn = this.logWarn.bind(this);
- this.logError = this.logError.bind(this);
- this.init = this.init.bind(this);
- this.api = this.api.bind(this);
- this.getMeta = this.getMeta.bind(this);
- this.registerSw = this.registerSw.bind(this);
- //#endregion
-
if (this.debug) {
(window as any).os = this;
}
}
- private googleMapsIniting = false;
-
- public getGoogleMaps() {
- return new Promise((res, rej) => {
- if ((window as any).google && (window as any).google.maps) {
- res((window as any).google.maps);
- } else {
- this.once('init-google-maps', () => {
- res((window as any).google.maps);
- });
-
- //#region load google maps api
- if (!this.googleMapsIniting) {
- this.googleMapsIniting = true;
- (window as any).initGoogleMaps = () => {
- this.emit('init-google-maps');
- };
- const head = document.getElementsByTagName('head')[0];
- const script = document.createElement('script');
- script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`);
- script.setAttribute('async', 'true');
- script.setAttribute('defer', 'true');
- head.appendChild(script);
- }
- //#endregion
- }
- });
- }
-
+ @autobind
public log(...args) {
if (!this.debug) return;
console.log.apply(null, args);
}
+ @autobind
public logInfo(...args) {
if (!this.debug) return;
console.info.apply(null, args);
}
+ @autobind
public logWarn(...args) {
if (!this.debug) return;
console.warn.apply(null, args);
}
+ @autobind
public logError(...args) {
if (!this.debug) return;
console.error.apply(null, args);
}
+ @autobind
public signout() {
this.store.dispatch('logout');
location.href = '/';
@@ -224,27 +158,10 @@ export default class MiOS extends EventEmitter {
* Initialize MiOS (boot)
* @param callback A function that call when initialized
*/
+ @autobind
public async init(callback) {
this.store = initStore(this);
- //#region Init stream managers
- this.streams.serverStatsStream = new ServerStatsStreamManager(this);
- this.streams.notesStatsStream = new NotesStatsStreamManager(this);
-
- this.once('signedin', () => {
- // Init home stream manager
- this.stream = new HomeStreamManager(this, this.store.state.i);
-
- // Init other stream manager
- this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i);
- this.streams.hybridTimelineStream = new HybridTimelineStreamManager(this, this.store.state.i);
- this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i);
- this.streams.driveStream = new DriveStreamManager(this, this.store.state.i);
- this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.store.state.i);
- this.streams.reversiStream = new ReversiStreamManager(this, this.store.state.i);
- });
- //#endregion
-
// ユーザーをフェッチしてコールバックする
const fetchme = (token, cb) => {
let me = null;
@@ -264,7 +181,7 @@ export default class MiOS extends EventEmitter {
// When success
.then(res => {
// When failed to authenticate user
- if (res.status !== 200) {
+ if (res.status !== 200 && res.status < 500) {
return this.signout();
}
@@ -295,6 +212,8 @@ export default class MiOS extends EventEmitter {
const fetched = () => {
this.emit('signedin');
+ this.stream = new Stream(this);
+
// Finish init
callback();
@@ -327,6 +246,8 @@ export default class MiOS extends EventEmitter {
} else {
// Finish init
callback();
+
+ this.stream = new Stream(this);
}
});
}
@@ -335,6 +256,7 @@ export default class MiOS extends EventEmitter {
/**
* Register service worker
*/
+ @autobind
private registerSw() {
// Check whether service worker and push manager supported
const isSwSupported =
@@ -361,7 +283,7 @@ export default class MiOS extends EventEmitter {
// A public key your push server will use to send
// messages to client apps via a push server.
- applicationServerKey: urlBase64ToUint8Array(swPublickey)
+ applicationServerKey: urlBase64ToUint8Array(this.meta.data.swPublickey)
};
// Subscribe push notification
@@ -417,7 +339,8 @@ export default class MiOS extends EventEmitter {
* @param endpoint エンドポイント名
* @param data パラメータ
*/
- public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> {
+ @autobind
+ public api(endpoint: string, data: { [x: string]: any } = {}, forceFetch = false): Promise<{ [x: string]: any }> {
if (++pending === 1) {
spinner = document.createElement('div');
spinner.setAttribute('id', 'wait');
@@ -429,13 +352,12 @@ export default class MiOS extends EventEmitter {
};
const promise = new Promise((resolve, reject) => {
- const viaStream = this.stream && this.stream.hasConnection && this.store.state.device.apiViaStream;
+ const viaStream = this.stream && this.store.state.device.apiViaStream && !forceFetch;
if (viaStream) {
- const stream = this.stream.borrow();
const id = Math.random().toString();
- stream.once(`api-res:${id}`, res => {
+ this.stream.once(`api:${id}`, res => {
if (res == null || Object.keys(res).length == 0) {
resolve(null);
} else if (res.res) {
@@ -445,11 +367,10 @@ export default class MiOS extends EventEmitter {
}
});
- stream.send({
- type: 'api',
- id,
- endpoint,
- data
+ this.stream.send('api', {
+ id: id,
+ ep: endpoint,
+ data: data
});
} else {
// Append a credential
@@ -502,6 +423,7 @@ export default class MiOS extends EventEmitter {
* Misskeyのメタ情報を取得します
* @param force キャッシュを無視するか否か
*/
+ @autobind
public getMeta(force = false) {
return new Promise<{ [x: string]: any }>(async (res, rej) => {
if (this.isMetaFetching) {
@@ -529,16 +451,6 @@ export default class MiOS extends EventEmitter {
}
});
}
-
- public connections: Connection[] = [];
-
- public registerStreamConnection(connection: Connection) {
- this.connections.push(connection);
- }
-
- public unregisterStreamConnection(connection: Connection) {
- this.connections = this.connections.filter(c => c != connection);
- }
}
class WindowSystem extends EventEmitter {
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 15b2f6b691..5c0f0af852 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,13 +1,12 @@
-import PostForm from '../views/components/post-form.vue';
+import PostForm from '../views/components/post-form-dialog.vue';
export default (os) => (opts) => {
const o = opts || {};
- const app = document.getElementById('app');
- app.style.display = 'none';
+ document.documentElement.style.overflow = 'hidden';
function recover() {
- app.style.display = 'block';
+ document.documentElement.style.overflow = 'auto';
}
const vm = new PostForm({
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 5b9d45462a..9412c85980 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -6,7 +6,6 @@ import VueRouter from 'vue-router';
// Style
import './style.styl';
-import '../../element.scss';
import init from '../init';
diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl
index df8f4a8fae..095e5266fd 100644
--- a/src/client/app/mobile/style.styl
+++ b/src/client/app/mobile/style.styl
@@ -8,12 +8,4 @@
html
height 100%
- background #ececed !important
-
- &[data-darkmode]
- background #191B22 !important
-
-body
- display flex
- flex-direction column
- min-height 100%
+ background var(--bg)
diff --git a/src/client/app/mobile/views/components/dialog.vue b/src/client/app/mobile/views/components/dialog.vue
index 9ee01cb782..fff44a28c3 100644
--- a/src/client/app/mobile/views/components/dialog.vue
+++ b/src/client/app/mobile/views/components/dialog.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
scale: 0.8,
duration: 300,
easing: [ 0.5, -0.5, 1, 0.5 ],
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
},
onBgClick() {
@@ -91,7 +91,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-dialog
> .bg
@@ -145,20 +145,20 @@ export default Vue.extend({
margin 0 0.375em
&:hover
- color $theme-color
+ color var(--primary)
&:active
- color darken($theme-color, 10%)
+ color var(--primaryDarken10)
transition color 0s ease
</style>
<style lang="stylus" module>
-@import '~const.styl'
+
.header
margin 0 0 1em 0
- color $theme-color
+ color var(--primary)
// color #43A4EC
font-weight bold
diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue
index d95d5fa223..5fca19939e 100644
--- a/src/client/app/mobile/views/components/drive-file-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-file-chooser.vue
@@ -1,12 +1,12 @@
<template>
-<div class="mk-drive-file-chooser">
+<div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb">
<div class="body">
<header>
<h1>%i18n:@select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
<button class="close" @click="cancel">%fa:times%</button>
<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
</header>
- <mk-drive ref="browser"
+ <mk-drive class="drive" ref="browser"
:select-file="true"
:multiple="multiple"
@change-selection="onChangeSelection"
@@ -31,24 +31,24 @@ export default Vue.extend({
},
onSelected(file) {
this.$emit('selected', file);
- this.$destroy();
+ this.destroyDom();
},
cancel() {
this.$emit('canceled');
- this.$destroy();
+ this.destroyDom();
},
ok() {
this.$emit('selected', this.files);
- this.$destroy();
+ this.destroyDom();
}
}
});
</script>
<style lang="stylus" scoped>
-.mk-drive-file-chooser
+.cdxzvcfawjxdyxsekbxbfgtplebnoneb
position fixed
- z-index 2048
+ z-index 20000
top 0
left 0
width 100%
@@ -59,10 +59,11 @@ export default Vue.extend({
> .body
width 100%
height 100%
- background #fff
+ background var(--faceHeader)
> header
- border-bottom solid 1px #eee
+ border-bottom solid 1px var(--faceDivider)
+ color var(--text)
> h1
margin 0
@@ -90,7 +91,7 @@ export default Vue.extend({
line-height 42px
width 42px
- > .mk-drive
+ > .drive
height calc(100% - 42px)
overflow scroll
-webkit-overflow-scrolling touch
diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue
index 7934fb7816..6d3fba1efd 100644
--- a/src/client/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue
@@ -19,11 +19,11 @@ export default Vue.extend({
methods: {
cancel() {
this.$emit('canceled');
- this.$destroy();
+ this.destroyDom();
},
ok() {
this.$emit('selected', (this.$refs.browser as any).folder);
- this.$destroy();
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index deb9941be8..7425afe1e2 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -1,5 +1,5 @@
<template>
-<div class="file-detail">
+<div class="pyvicwrksnfyhpfgkjwqknuururpaztw">
<div class="preview">
<img v-if="kind == 'image'" ref="img"
:src="file.url"
@@ -25,7 +25,7 @@
</div>
<div class="info">
<div>
- <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
+ <span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span>
<span class="separator"></span>
<span class="data-size">{{ file.datasize | bytes }}</span>
<span class="separator"></span>
@@ -38,10 +38,10 @@
</div>
<div class="menu">
<div>
- <a :href="`${file.url}?download`" :download="file.name">%fa:download%%i18n:@download%</a>
- <button @click="rename">%fa:pencil-alt%%i18n:@rename%</button>
- <button @click="move">%fa:R folder-open%%i18n:@move%</button>
- <button @click="del">%fa:trash-alt R%%i18n:@delete%</button>
+ <ui-button link :href="`${file.url}?download`" :download="file.name">%fa:download% %i18n:@download%</ui-button>
+ <ui-button @click="rename">%fa:pencil-alt% %i18n:@rename%</ui-button>
+ <ui-button @click="move">%fa:R folder-open% %i18n:@move%</ui-button>
+ <ui-button @click="del">%fa:trash-alt R% %i18n:@delete%</ui-button>
</div>
</div>
<div class="exif" v-show="exif">
@@ -67,7 +67,7 @@
import Vue from 'vue';
import * as EXIF from 'exif-js';
import * as hljs from 'highlight.js';
-import gcd from '../../../common/scripts/gcd';
+import { gcd } from '../../../../../prelude/math';
export default Vue.extend({
props: ['file'],
@@ -134,11 +134,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.file-detail
-
+.pyvicwrksnfyhpfgkjwqknuururpaztw
> .preview
padding 8px
- background #f0f0f0
+ background var(--bg)
> img
display block
@@ -149,9 +148,10 @@ export default Vue.extend({
> footer
padding 8px 8px 0 8px
- font-size 0.8em
- color #888
text-align center
+ font-size 0.8em
+ color var(--text)
+ opacity 0.7
> .separator
display inline
@@ -179,25 +179,17 @@ export default Vue.extend({
> .info
padding 14px
font-size 0.8em
- border-top solid 1px #dfdfdf
+ border-top solid 1px var(--faceDivider)
> div
max-width 500px
margin 0 auto
+ color var(--text)
> .separator
padding 0 4px
- color #cdcdcd
-
- > .type
- > .data-size
- color #9d9d9d
-
- > mk-file-type-icon
- margin-right 4px
> .created-at
- color #bdbdbd
> [data-fa]
margin-right 2px
@@ -207,42 +199,15 @@ export default Vue.extend({
> .menu
padding 14px
- border-top solid 1px #dfdfdf
+ border-top solid 1px var(--faceDivider)
> div
max-width 500px
margin 0 auto
- > *
- display block
- width 100%
- padding 10px 16px
- margin 0 0 12px 0
- color #333
- font-size 0.9em
- text-align center
- text-decoration none
- text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
- background-image linear-gradient(#fafafa, #eaeaea)
- border 1px solid #ddd
- border-bottom-color #cecece
- border-radius 3px
-
- &:last-child
- margin-bottom 0
-
- &:active
- background-color #767676
- background-image none
- border-color #444
- box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
-
- > [data-fa]
- margin-right 4px
-
> .hash
padding 14px
- border-top solid 1px #dfdfdf
+ border-top solid 1px var(--faceDivider)
> div
max-width 500px
@@ -252,7 +217,7 @@ export default Vue.extend({
display block
margin 0
padding 0
- color #555
+ color var(--text)
font-size 0.9em
> [data-fa]
@@ -273,7 +238,7 @@ export default Vue.extend({
> .exif
padding 14px
- border-top solid 1px #dfdfdf
+ border-top solid 1px var(--faceDivider)
> div
max-width 500px
@@ -283,7 +248,7 @@ export default Vue.extend({
display block
margin 0
padding 0
- color #555
+ color var(--text)
font-size 0.9em
> [data-fa]
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index 6dec4b9f4f..68978bb944 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -1,5 +1,5 @@
<template>
-<a class="file" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
+<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
<div class="container">
<div class="thumbnail" :style="thumbnail"></div>
<div class="body">
@@ -7,20 +7,12 @@
<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>
- <!--
- if file.tags.length > 0
- ul.tags
- each tag in file.tags
- li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
- -->
<footer>
<span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
<span class="separator"></span>
<span class="data-size">{{ file.datasize | bytes }}</span>
<span class="separator"></span>
- <span class="created-at">
- %fa:R clock%<mk-time :time="file.createdAt"/>
- </span>
+ <span class="created-at">%fa:R clock%<mk-time :time="file.createdAt"/></span>
<template v-if="file.isSensitive">
<span class="separator"></span>
<span class="nsfw">%fa:eye-slash% %i18n:@nsfw%</span>
@@ -71,9 +63,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-.file
+.vupkuhvjnjyqaqhsiogfbywvjxynrgsm
display block
text-decoration none !important
@@ -111,7 +101,7 @@ export default Vue.extend({
padding 0
font-size 0.9em
font-weight bold
- color #555
+ color var(--text)
text-overflow ellipsis
overflow-wrap break-word
@@ -135,22 +125,22 @@ export default Vue.extend({
display block
margin 4px 0 0 0
font-size 0.7em
+ color var(--text)
> .separator
padding 0 4px
- color #CDCDCD
> .type
- color #9D9D9D
+ opacity 0.7
> .mk-file-type-icon
margin-right 4px
> .data-size
- color #9D9D9D
+ opacity 0.7
> .created-at
- color #BDBDBD
+ opacity 0.7
> [data-fa]
margin-right 2px
@@ -159,7 +149,7 @@ export default Vue.extend({
color #bf4633
&[data-is-selected]
- background $theme-color
+ background var(--primary)
&, *
color #fff !important
diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue
index 22ff38fecb..05dcbd083e 100644
--- a/src/client/app/mobile/views/components/drive.folder.vue
+++ b/src/client/app/mobile/views/components/drive.folder.vue
@@ -1,5 +1,5 @@
<template>
-<a class="root folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
+<a class="jvwxssxsytqlqvrpiymarjlzlsxskqsr" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
<div class="container">
<p class="name">%fa:folder%{{ folder.name }}</p>%fa:angle-right%
</div>
@@ -24,9 +24,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.root.folder
+.jvwxssxsytqlqvrpiymarjlzlsxskqsr
display block
- color #777
+ color var(--text)
text-decoration none !important
*
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index c313d225e4..469f6da240 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mk-drive">
+<div class="kmmwchoexgckptowjmjgfsygeltxfeqs">
<nav ref="nav">
<a @click.prevent="goRoot()" href="/i/drive">%fa:cloud%%i18n:@drive%</a>
<template v-for="folder in hierarchyFolders">
@@ -26,11 +26,11 @@
</p>
</div>
<div class="folders" v-if="folders.length > 0">
- <x-folder v-for="folder in folders" :key="folder.id" :folder="folder"/>
+ <x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/>
<p v-if="moreFolders">%i18n:@load-more%</p>
</div>
<div class="files" v-if="files.length > 0">
- <x-file v-for="file in files" :key="file.id" :file="file"/>
+ <x-file class="file" v-for="file in files" :key="file.id" :file="file"/>
<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
{{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:@load-more%' }}
</button>
@@ -81,8 +81,7 @@ export default Vue.extend({
hierarchyFolders: [],
selectedFiles: [],
info: null,
- connection: null,
- connectionId: null,
+ connection: null
fetching: true,
fetchingMoreFiles: false,
@@ -94,9 +93,15 @@ export default Vue.extend({
return this.selectFile;
}
},
+ watch: {
+ top() {
+ if (this.isNaked) {
+ (this.$refs.nav as any).style.top = `${this.top}px`;
+ }
+ }
+ },
mounted() {
- this.connection = (this as any).os.streams.driveStream.getConnection();
- this.connectionId = (this as any).os.streams.driveStream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('drive');
this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@@ -117,12 +122,7 @@ export default Vue.extend({
}
},
beforeDestroy() {
- this.connection.off('file_created', this.onStreamDriveFileCreated);
- this.connection.off('file_updated', this.onStreamDriveFileUpdated);
- this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
- this.connection.off('folder_created', this.onStreamDriveFolderCreated);
- this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
- (this as any).os.streams.driveStream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
onStreamDriveFileCreated(file) {
@@ -466,8 +466,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-drive
- background #fff
+.kmmwchoexgckptowjmjgfsygeltxfeqs
+ background var(--face)
> nav
display block
@@ -480,10 +480,10 @@ export default Vue.extend({
overflow auto
white-space nowrap
font-size 0.9em
- color rgba(#000, 0.67)
+ color var(--text)
-webkit-backdrop-filter blur(12px)
backdrop-filter blur(12px)
- background-color rgba(#fff, 0.75)
+ background-color var(--mobileDriveNavBg)
border-bottom solid 1px rgba(#000, 0.13)
> p
@@ -509,7 +509,7 @@ export default Vue.extend({
opacity 0.5
> .info
- border-bottom solid 1px #eee
+ border-bottom solid 1px var(--faceDivider)
&:empty
display none
@@ -520,15 +520,15 @@ export default Vue.extend({
margin 0 auto
padding 4px 16px
font-size 10px
- color #777
+ color var(--text)
> .folders
> .folder
- border-bottom solid 1px #eee
+ border-bottom solid 1px var(--faceDivider)
> .files
> .file
- border-bottom solid 1px #eee
+ border-bottom solid 1px var(--faceDivider)
> .more
display block
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index 360ee91d4b..3c8b2f98e6 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -5,7 +5,8 @@
:disabled="wait"
>
<template v-if="!wait">
- <template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template>
+ <template v-if="u.hasPendingFollowRequestFromYou && u.isLocked">%fa:hourglass-half% %i18n:@request-pending%</template>
+ <template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked">%fa:hourglass-start% %i18n:@follow-processing%</template>
<template v-else-if="u.isFollowing">%fa:minus% %i18n:@following%</template>
<template v-else-if="!u.isFollowing && u.isLocked">%fa:plus% %i18n:@follow-request%</template>
<template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus% %i18n:@follow%</template>
@@ -27,33 +28,31 @@ export default Vue.extend({
return {
u: this.user,
wait: false,
- connection: null,
- connectionId: null
+ connection: null
};
},
mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('follow', this.onFollow);
this.connection.on('unfollow', this.onUnfollow);
},
beforeDestroy() {
- this.connection.off('follow', this.onFollow);
- this.connection.off('unfollow', this.onUnfollow);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
onFollow(user) {
if (user.id == this.u.id) {
this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
onUnfollow(user) {
if (user.id == this.u.id) {
this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
@@ -66,7 +65,7 @@ export default Vue.extend({
userId: this.u.id
});
} else {
- if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ if (this.u.hasPendingFollowRequestFromYou) {
this.u = await (this as any).api('following/requests/cancel', {
userId: this.u.id
});
@@ -91,7 +90,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-follow-button
display block
@@ -103,29 +102,29 @@ export default Vue.extend({
line-height 36px
font-size 14px
font-weight bold
- color $theme-color
+ color var(--primary)
background transparent
outline none
- border solid 1px $theme-color
+ border solid 1px var(--primary)
border-radius 36px
&:hover
- background rgba($theme-color, 0.1)
+ background var(--primaryAlpha01)
&:active
- background rgba($theme-color, 0.2)
+ background var(--primaryAlpha02)
&.active
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
&:hover
- background lighten($theme-color, 10%)
- border-color lighten($theme-color, 10%)
+ background var(--primaryLighten10)
+ border-color var(--primaryLighten10)
&:active
- background darken($theme-color, 10%)
- border-color darken($theme-color, 10%)
+ background var(--primaryDarken10)
+ border-color var(--primaryDarken10)
&.wait
cursor wait !important
diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
index e0461d2bc2..dbb82f4b18 100644
--- a/src/client/app/mobile/views/components/friends-maker.vue
+++ b/src/client/app/mobile/views/components/friends-maker.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
this.fetch();
},
close() {
- this.$destroy();
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index e40069bbe3..652a2ad3a4 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -1,5 +1,5 @@
<template>
-<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false">
+<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>
@@ -19,12 +19,13 @@ export default Vue.extend({
},
raw: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ }
computed: {
style(): any {
let url = `url(${this.image.thumbnailUrl})`;
@@ -65,7 +66,7 @@ export default Vue.extend({
text-align center
font-size 12px
- > b
+ > *
display block
</style>
diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue
index aea7f41460..59ba695b93 100644
--- a/src/client/app/mobile/views/components/media-video.vue
+++ b/src/client/app/mobile/views/components/media-video.vue
@@ -9,31 +9,35 @@
:href="video.url"
target="_blank"
:style="imageStyle"
- :title="video.name">
+ :title="video.name"
+>
%fa:R play-circle%
</a>
</template>
<script lang="ts">
-import Vue from 'vue'
+import Vue from 'vue';
+
export default Vue.extend({
props: {
video: {
type: Object,
required: true
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
imageStyle(): any {
return {
- 'background-image': `url(${this.video.url})`
+ 'background-image': null // TODO `url(${this.video.thumbnailUrl})`
};
}
- },})
+ }
+});
</script>
<style lang="stylus" scoped>
diff --git a/src/client/app/mobile/views/components/mute-button.vue b/src/client/app/mobile/views/components/mute-button.vue
index 3cb568615d..316fbda8f1 100644
--- a/src/client/app/mobile/views/components/mute-button.vue
+++ b/src/client/app/mobile/views/components/mute-button.vue
@@ -41,11 +41,11 @@ export default Vue.extend({
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-mute-button
display block
- user-select none
+ user-select none
cursor pointer
padding 0 16px
margin 0
@@ -53,27 +53,27 @@ export default Vue.extend({
line-height 36px
font-size 14px
font-weight bold
- color $theme-color
+ color var(--primary)
background transparent
outline none
- border solid 1px $theme-color
+ border solid 1px var(--primary)
border-radius 36px
&:hover
- background rgba($theme-color, 0.1)
+ background var(--primaryAlpha01)
&:active
- background rgba($theme-color, 0.2)
+ background var(--primaryAlpha02)
&.active
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
&:hover
- background lighten($theme-color, 10%)
- border-color lighten($theme-color, 10%)
+ background var(--primaryLighten10)
+ border-color var(--primaryLighten10)
&:active
- background darken($theme-color, 10%)
- border-color darken($theme-color, 10%)
+ background var(--primaryDarken10)
+ border-color var(--primaryDarken10)
</style>
diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue
index e8427798cd..de9c9c1450 100644
--- a/src/client/app/mobile/views/components/note-card.vue
+++ b/src/client/app/mobile/views/components/note-card.vue
@@ -27,17 +27,18 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mk-note-card
display inline-block
width 150px
//height 120px
font-size 12px
- background isDark ? #282c37 : #fff
+ background var(--face)
border-radius 4px
+ box-shadow 0 2px 8px rgba(0, 0, 0, 0.2)
> a
display block
- color isDark ? #fff : #2c3940
+ color var(--noteText)
&:hover
text-decoration none
@@ -75,17 +76,11 @@ root(isDark)
left 0
width 100%
height 20px
- background isDark ? linear-gradient(to bottom, rgba(#282c37, 0) 0%, #282c37 100%) : linear-gradient(to bottom, rgba(#fff, 0) 0%, #fff 100%)
+ background linear-gradient(to bottom, transparent 0%, var(--face) 100%)
> .mk-time
display inline-block
padding 8px
color #aaa
-.mk-note-card[data-darkmode]
- root(true)
-
-.mk-note-card:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index f9996f9da6..082f72f1a9 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -35,20 +35,26 @@
</div>
</header>
<div class="body">
- <div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
- <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
- <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
- </div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media" :raw="true"/>
- </div>
- <mk-poll v-if="p.poll" :note="p"/>
- <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
- <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
- <div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
+ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
+ </div>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files" :raw="true"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p"/>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
+ <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
</div>
</div>
<router-link class="time" :to="p | notePage">
@@ -85,12 +91,16 @@ import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue';
+import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
export default Vue.extend({
components: {
XSub
},
+ mixins: [noteSubscriber('note')],
+
props: {
note: {
type: Object,
@@ -103,6 +113,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
conversation: [],
conversationFetching: false,
replies: []
@@ -113,19 +124,20 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
+
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
+
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -180,16 +192,19 @@ export default Vue.extend({
this.conversation = conversation.reverse();
});
},
+
reply() {
(this as any).apis.post({
reply: this.p
});
},
+
renote() {
(this as any).apis.post({
renote: this.p
});
},
+
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
@@ -198,6 +213,7 @@ export default Vue.extend({
big: true
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -210,13 +226,11 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-note-detail
overflow hidden
width 100%
text-align left
- background isDark ? #282C37 : #fff
+ background var(--face)
border-radius 8px
box-shadow 0 0 2px rgba(#000, 0.1)
@@ -235,26 +249,26 @@ root(isDark)
text-align center
color #999
cursor pointer
- background isDark ? #21242d : #fafafa
+ background var(--subNoteBg)
outline none
border none
- border-bottom solid 1px isDark ? #1c2023 : #eef0f2
+ border-bottom solid 1px var(--faceDivider)
border-radius 6px 6px 0 0
box-shadow none
&:hover
- background isDark ? #16181d : #f6f6f6
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
- &:disabled
- color #ccc
+ &:active
+ box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
> .conversation
> *
- border-bottom 1px solid isDark ? #1c2023 : #eef0f2
+ border-bottom 1px solid var(--faceDivider)
> .renote
- color #9dbb00
- background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ color var(--renoteText)
+ background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
> p
margin 0
@@ -277,7 +291,7 @@ root(isDark)
padding-top 8px
> .reply-to
- border-bottom 1px solid isDark ? #1c2023 : #eef0f2
+ border-bottom 1px solid var(--faceDivider)
> article
padding 14px 16px 9px 16px
@@ -310,7 +324,7 @@ root(isDark)
> .name
display inline-block
margin .4em 0
- color isDark ? #fff : #627079
+ color var(--noteHeaderName)
font-size 16px
font-weight bold
text-align left
@@ -323,53 +337,66 @@ root(isDark)
display block
text-align left
margin 0
- color isDark ? #606984 : #ccc
+ color var(--noteHeaderAcct)
> .body
padding 8px 0
- > .text
+ > .cw
+ cursor default
display block
margin 0
padding 0
overflow-wrap break-word
- font-size 16px
- color isDark ? #fff : #717171
+ color var(--noteText)
- @media (min-width 500px)
- font-size 24px
+ > .text
+ margin-right 8px
- > .renote
- margin 8px 0
+ > .content
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
+ > .text
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 16px
+ color var(--noteText)
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ @media (min-width 500px)
+ font-size 24px
- > .map
- width 100%
- height 200px
+ > .renote
+ margin 8px 0
- &:empty
- display none
+ > *
+ padding 16px
+ border dashed 1px var(--quoteBorder)
+ border-radius 8px
- > .mk-url-preview
- margin-top 8px
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
- > .media
- > img
- display block
- max-width 100%
+ > .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 isDark ? #606984 : #c0c0c0
+ color var(--noteHeaderInfo)
> footer
font-size 1.2em
@@ -381,14 +408,14 @@ root(isDark)
border none
box-shadow none
font-size 1em
- color isDark ? #606984 : #ddd
+ color var(--noteActions)
cursor pointer
&:not(:last-child)
margin-right 28px
&:hover
- color isDark ? #9198af : #666
+ color var(--noteActionsHover)
> .count
display inline
@@ -396,16 +423,10 @@ root(isDark)
color #999
&.reacted
- color $theme-color
+ color var(--primary)
> .replies
> *
- border-top 1px solid isDark ? #1c2023 : #eef0f2
-
-.mk-note-detail[data-darkmode]
- root(true)
-
-.mk-note-detail:not([data-darkmode])
- root(false)
+ 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
index 5d56d2d326..525f54998e 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,10 +1,16 @@
<template>
-<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<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">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -14,12 +20,23 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note']
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
+ }
});
</script>
<style lang="stylus" scoped>
-root(isDark)
+.yohlumlkhizgfkvvscwfcrcggkotpvry
display flex
margin 0
padding 0
@@ -65,16 +82,22 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color var(--noteText)
-.mk-note-preview[data-darkmode]
- root(true)
+ > .text
+ margin-right 8px
-.mk-note-preview:not([data-darkmode])
- root(false)
+ > .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
index a68aec40a1..24f5be160c 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,10 +1,16 @@
<template>
-<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<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">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -24,16 +30,22 @@ export default Vue.extend({
type: Boolean,
default: true
}
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
}
});
</script>
<style lang="stylus" scoped>
-root(isDark)
+.zlrxdaqttccpwhpaagdmkawtzklsccam
display flex
padding 16px
font-size 10px
- background isDark ? #21242d : #fcfcfc
+ background var(--subNoteBg)
@media (min-width 350px)
font-size 12px
@@ -77,20 +89,25 @@ root(isDark)
margin-bottom 2px
> .body
-
- > .text
+ > .cw
+ cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color var(--noteText)
- pre
- max-height 120px
- font-size 80%
+ > .text
+ margin-right 8px
-.sub[data-darkmode]
- root(true)
+ > .content
+ > .text
+ margin 0
+ padding 0
+ color var(--subNoteText)
-.sub:not([data-darkmode])
- root(false)
+ 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
index d0cea135f9..f370fbf874 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -18,7 +18,7 @@
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
- <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,20 +28,18 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
<a class="rp" v-if="p.renote != null">RP:</a>
</div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
- </div>
+ <div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
</div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
</div>
- <footer>
+ <footer v-if="p.deletedAt == null">
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
<button @click="reply">
<template v-if="p.reply">%fa:reply-all%</template>
@@ -70,19 +68,21 @@ import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue';
+import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
export default Vue.extend({
components: {
XSub
},
+ mixins: [noteSubscriber('note')],
+
props: ['note'],
data() {
return {
- showContent: false,
- connection: null,
- connectionId: null
+ showContent: false
};
},
@@ -90,7 +90,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -100,9 +100,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
@@ -118,82 +116,7 @@ export default Vue.extend({
}
},
- created() {
- if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
- }
- },
-
- mounted() {
- this.capture(true);
-
- if (this.$store.getters.isSignedIn) {
- this.connection.on('_connected_', this.onStreamConnected);
- }
-
- // Draw map
- if (this.p.geo) {
- const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
- if (shouldShowMap) {
- (this as any).os.getGoogleMaps().then(maps => {
- const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
- const map = new maps.Map(this.$refs.map, {
- center: uluru,
- zoom: 15
- });
- new maps.Marker({
- position: uluru,
- map: map
- });
- });
- }
- }
- },
-
- beforeDestroy() {
- this.decapture(true);
-
- if (this.$store.getters.isSignedIn) {
- this.connection.off('_connected_', this.onStreamConnected);
- (this as any).os.stream.dispose(this.connectionId);
- }
- },
-
methods: {
- capture(withHandler = false) {
- if (this.$store.getters.isSignedIn) {
- this.connection.send({
- type: 'capture',
- id: this.p.id
- });
- if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
- }
- },
-
- decapture(withHandler = false) {
- if (this.$store.getters.isSignedIn) {
- this.connection.send({
- type: 'decapture',
- id: this.p.id
- });
- if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
- }
- },
-
- onStreamConnected() {
- this.capture();
- },
-
- onStreamNoteUpdated(data) {
- const note = data.note;
- if (note.id == this.note.id) {
- this.$emit('update:note', note);
- } else if (note.id == this.note.renoteId) {
- this.note.renote = note;
- }
- },
-
reply() {
(this as any).apis.post({
reply: this.p
@@ -227,11 +150,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.note
font-size 12px
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ border-bottom solid 1px var(--faceDivider)
&:last-of-type
border-bottom none
@@ -255,8 +176,8 @@ root(isDark)
padding 8px 16px
line-height 28px
white-space pre
- color #9dbb00
- background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ color var(--renoteText)
+ background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
@media (min-width 500px)
padding 16px
@@ -348,24 +269,11 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- color isDark ? #fff : #717171
+ color var(--noteText)
> .text
margin-right 8px
- > .toggle
- display inline-block
- padding 4px 8px
- font-size 0.7em
- color isDark ? #393f4f : #fff
- background isDark ? #687390 : #b1b9c1
- border-radius 2px
- cursor pointer
- user-select none
-
- &:hover
- background isDark ? #707b97 : #bbc4ce
-
> .content
> .text
@@ -373,7 +281,7 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- color isDark ? #fff : #717171
+ color var(--noteText)
>>> .title
display block
@@ -381,7 +289,7 @@ root(isDark)
padding 4px
font-size 90%
text-align center
- background isDark ? #2f3944 : #eef1f3
+ background var(--mfmTitleBg)
border-radius 4px
>>> .code
@@ -390,31 +298,31 @@ root(isDark)
>>> .quote
margin 8px
padding 6px 12px
- color isDark ? #6f808e : #aaa
- border-left solid 3px isDark ? #637182 : #eee
+ color var(--mfmQuote)
+ border-left solid 3px var(--mfmQuoteLine)
> .reply
margin-right 8px
- color isDark ? #99abbf : #717171
+ color var(--noteText)
> .rp
margin-left 4px
font-style oblique
- color #a0bf46
+ color var(--renoteText)
[data-is-me]:after
content "you"
padding 0 4px
margin-left 4px
font-size 80%
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
border-radius 4px
.mk-url-preview
margin-top 8px
- > .media
+ > .files
> img
display block
max-width 100%
@@ -437,9 +345,9 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
- border dashed 1px isDark ? #4e945e : #c0dac6
+ border dashed 1px var(--quoteBorder)
border-radius 8px
> .app
@@ -454,14 +362,14 @@ root(isDark)
border none
box-shadow none
font-size 1em
- color isDark ? #606984 : #ddd
+ color var(--noteActions)
cursor pointer
&:not(:last-child)
margin-right 28px
&:hover
- color isDark ? #9198af : #666
+ color var(--noteActionsHover)
> .count
display inline
@@ -469,17 +377,7 @@ root(isDark)
color #999
&.reacted
- color $theme-color
-
- &.menu
- @media (max-width 350px)
- display none
-
-.note[data-darkmode]
- root(true)
-
-.note:not([data-darkmode])
- root(false)
+ color var(--primary)
</style>
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 714e521c0f..8f0a1ef196 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -14,8 +14,7 @@
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notes" class="transition"> -->
- <div class="transition">
+ <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" @update:note="onNoteUpdated(i, $event)"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
@@ -23,8 +22,7 @@
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -125,7 +123,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
@@ -219,11 +217,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-notes
overflow hidden
- background isDark ? #282C37 : #fff
+ background var(--face)
border-radius 8px
box-shadow 0 0 2px rgba(#000, 0.1)
@@ -245,9 +241,9 @@ root(isDark)
line-height 32px
text-align center
font-size 0.9em
- color isDark ? #666b79 : #aaa
- background isDark ? #242731 : #fdfdfd
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ color var(--dateDividerFg)
+ background var(--dateDividerBg)
+ border-bottom solid 1px var(--faceDivider)
span
margin 0 16px
@@ -278,7 +274,7 @@ root(isDark)
> footer
text-align center
- border-top solid 1px isDark ? #1c2023 : #eaeaea
+ border-top solid 1px var(--faceDivider)
&:empty
display none
@@ -295,10 +291,4 @@ root(isDark)
&:disabled
opacity 0.7
-.mk-notes[data-darkmode]
- root(true)
-
-.mk-notes:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index ee90c6b46b..4a09104341 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -105,7 +105,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mk-notification
> .notification
padding 16px
font-size 12px
@@ -154,14 +154,14 @@ root(isDark)
> .mk-time
margin-left auto
- color isDark ? #606984 : #c0c0c0
+ color var(--noteHeaderInfo)
font-size 0.9em
> .note-preview
- color isDark ? #fff : #717171
+ color var(--noteText)
> .note-ref
- color isDark ? #fff : #717171
+ color var(--noteText)
[data-fa]
font-size 1em
@@ -182,10 +182,4 @@ root(isDark)
> div > header i
color #888
-.mk-notification[data-darkmode]
- root(true)
-
-.mk-notification:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 9f20c3fb22..e1a2967071 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -1,8 +1,7 @@
<template>
<div class="mk-notifications">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notifications" class="transition notifications"> -->
- <div class="transition notifications">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications">
<mk-notification :notification="notification" :key="notification.id"/>
<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
@@ -10,8 +9,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
@@ -25,6 +23,7 @@
<script lang="ts">
import Vue from 'vue';
+
export default Vue.extend({
data() {
return {
@@ -32,10 +31,10 @@ export default Vue.extend({
fetchingMoreNotifications: false,
notifications: [],
moreNotifications: false,
- connection: null,
- connectionId: null
+ connection: null
};
},
+
computed: {
_notifications(): any[] {
return (this.notifications as any).map(notification => {
@@ -47,9 +46,9 @@ export default Vue.extend({
});
}
},
+
mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('notification', this.onNotification);
@@ -68,10 +67,11 @@ export default Vue.extend({
this.$emit('fetched');
});
},
+
beforeDestroy() {
- this.connection.off('notification', this.onNotification);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
+
methods: {
fetchMoreNotifications() {
this.fetchingMoreNotifications = true;
@@ -92,10 +92,11 @@ export default Vue.extend({
this.fetchingMoreNotifications = false;
});
},
+
onNotification(notification) {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
this.connection.send({
- type: 'read_notification',
+ type: 'readNotification',
id: notification.id
});
@@ -106,9 +107,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.mk-notifications
margin 0 auto
- background isDark ? #282C37 :#fff
+ background var(--face)
border-radius 8px
box-shadow 0 0 2px rgba(#000, 0.1)
overflow hidden
@@ -128,7 +129,7 @@ root(isDark)
> .notifications
> .mk-notification:not(:last-child)
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ border-bottom solid 1px var(--faceDivider)
> .date
display block
@@ -136,9 +137,9 @@ root(isDark)
line-height 32px
text-align center
font-size 0.8em
- color isDark ? #666b79 : #aaa
- background isDark ? #242731 : #fdfdfd
- border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+ color var(--dateDividerFg)
+ background var(--dateDividerBg)
+ border-bottom solid 1px var(--faceDivider)
span
margin 0 16px
@@ -171,10 +172,4 @@ root(isDark)
> [data-fa]
margin-right 4px
-.mk-notifications[data-darkmode]
- root(true)
-
-.mk-notifications:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue
index 6d4a481dbe..5f94b91ddd 100644
--- a/src/client/app/mobile/views/components/notify.vue
+++ b/src/client/app/mobile/views/components/notify.vue
@@ -1,6 +1,8 @@
<template>
-<div class="mk-notify">
- <mk-notification-preview :notification="notification"/>
+<div class="mk-notify" :class="pos">
+ <div>
+ <mk-notification-preview :notification="notification"/>
+ </div>
</div>
</template>
@@ -10,11 +12,16 @@ import * as anime from 'animejs';
export default Vue.extend({
props: ['notification'],
+ computed: {
+ pos() {
+ return this.$store.state.device.mobileNotificationPosition;
+ }
+ },
mounted() {
this.$nextTick(() => {
anime({
targets: this.$el,
- bottom: '0px',
+ [this.pos]: '0px',
duration: 500,
easing: 'easeOutQuad'
});
@@ -22,10 +29,10 @@ export default Vue.extend({
setTimeout(() => {
anime({
targets: this.$el,
- bottom: '-64px',
+ [this.pos]: `-${this.$el.offsetHeight}px`,
duration: 500,
easing: 'easeOutQuad',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}, 6000);
});
@@ -35,15 +42,32 @@ export default Vue.extend({
<style lang="stylus" scoped>
.mk-notify
+ $height = 78px
+
position fixed
- z-index 1024
- bottom -64px
+ z-index 10000
left 0
+ right 0
width 100%
- height 64px
+ max-width 500px
+ height $height
+ margin 0 auto
+ padding 8px
pointer-events none
- -webkit-backdrop-filter blur(2px)
- backdrop-filter blur(2px)
- background-color rgba(#000, 0.5)
+ 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
new file mode 100644
index 0000000000..15b36db945
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-form-dialog.vue
@@ -0,0 +1,126 @@
+<template>
+<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
+ <div class="bg" ref="bg"></div>
+ <div class="main" ref="main">
+ <mk-post-form ref="form"
+ :reply="reply"
+ :renote="renote"
+ :initial-text="initialText"
+ :instant="instant"
+ @posted="onPosted"
+ @cancel="onCanceled"/>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+ props: {
+ reply: {
+ type: Object,
+ required: false
+ },
+ renote: {
+ type: Object,
+ required: false
+ },
+ initialText: {
+ type: String,
+ required: false
+ },
+ instant: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ mounted() {
+ this.$nextTick(() => {
+ (this.$refs.bg as any).style.pointerEvents = 'auto';
+ anime({
+ targets: this.$refs.bg,
+ opacity: 1,
+ duration: 100,
+ easing: 'linear'
+ });
+
+ anime({
+ targets: this.$refs.main,
+ opacity: 1,
+ translateY: [-16, 0],
+ duration: 300,
+ easing: 'easeOutQuad'
+ });
+ });
+ },
+
+ methods: {
+ focus() {
+ this.$refs.form.focus();
+ },
+
+ close() {
+ (this.$refs.bg as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.bg,
+ opacity: 0,
+ duration: 300,
+ easing: 'linear'
+ });
+
+ (this.$refs.main as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.main,
+ opacity: 0,
+ translateY: 16,
+ duration: 300,
+ easing: 'easeOutQuad',
+ complete: () => this.destroyDom()
+ });
+ },
+
+ onPosted() {
+ this.$emit('posted');
+ this.close();
+ },
+
+ onCanceled() {
+ this.$emit('cancel');
+ this.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.ulveipglmagnxfgvitaxyszerjwiqmwl
+ > .bg
+ display block
+ position fixed
+ z-index 10000
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background rgba(#000, 0.7)
+ opacity 0
+ pointer-events none
+
+ > .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
index a74df67c0a..3de920cf22 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -4,14 +4,14 @@
<header>
<button class="cancel" @click="cancel">%fa:times%</button>
<div>
- <span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
+ <span class="text-count" :class="{ over: trimmedLength(text) > 1000 }">{{ 1000 - trimmedLength(text) }}</span>
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button>
</div>
</header>
<div class="form">
- <mk-note-preview v-if="reply" :note="reply"/>
- <mk-note-preview v-if="renote" :note="renote"/>
+ <mk-note-preview class="preview" v-if="reply" :note="reply"/>
+ <mk-note-preview class="preview" v-if="renote" :note="renote"/>
<div v-if="visibility == 'specified'" class="visibleUsers">
<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
<a @click="addVisibleUser">+%i18n:@add-visible-user%</a>
@@ -42,7 +42,7 @@
<span v-if="visibility === 'private'">%fa:lock%</span>
</button>
</footer>
- <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
+ <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">
@@ -59,6 +59,9 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho
import getFace from '../../../common/scripts/get-face';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
+import { erase, unique } from '../../../../../prelude/array';
+import { length } from 'stringz';
+import parseAcct from '../../../../../misc/acct/parse';
export default Vue.extend({
components: {
@@ -94,7 +97,7 @@ export default Vue.extend({
files: [],
poll: false,
geo: null,
- visibility: this.$store.state.device.visibility || 'public',
+ visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [],
useCw: false,
cw: null,
@@ -105,9 +108,9 @@ export default Vue.extend({
computed: {
draftId(): string {
return this.renote
- ? 'renote:' + this.renote.id
+ ? `renote:${this.renote.id}`
: this.reply
- ? 'reply:' + this.reply.id
+ ? `reply:${this.reply.id}`
: 'note';
},
@@ -170,12 +173,30 @@ export default Vue.extend({
});
}
+ // 公開以外へのリプライ時は元の公開範囲を引き継ぐ
+ if (this.reply && ['home', 'followers', 'specified', 'private'].includes(this.reply.visibility)) {
+ this.visibility = this.reply.visibility;
+ }
+
+ // ダイレクトへのリプライはリプライ先ユーザーを初期設定
+ if (this.reply && this.reply.visibility === 'specified') {
+ (this as any).api('users/show', { userId: this.reply.userId }).then(user => {
+ this.visibleUsers.push(user);
+ });
+ }
+
+ this.focus();
+
this.$nextTick(() => {
this.focus();
});
},
methods: {
+ trimmedLength(text: string) {
+ return length(text.trim());
+ },
+
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
@@ -198,12 +219,12 @@ export default Vue.extend({
attachMedia(driveFile) {
this.files.push(driveFile);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
detachMedia(file) {
this.files = this.files.filter(x => x.id != file.id);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onChangeFile() {
@@ -227,7 +248,7 @@ export default Vue.extend({
navigator.geolocation.getCurrentPosition(pos => {
this.geo = pos.coords;
}, err => {
- alert('%i18n:@error%: ' + err.message);
+ alert(`%i18n:@error%: ${err.message}`);
}, {
enableHighAccuracy: true
});
@@ -250,24 +271,23 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
title: '%i18n:@username-prompt%'
- }).then(username => {
- (this as any).api('users/show', {
- username
- }).then(user => {
+ }).then(acct => {
+ if (acct.startsWith('@')) acct = acct.substr(1);
+ (this as any).api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
});
},
removeVisibleUser(user) {
- this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ this.visibleUsers = erase(user, this.visibleUsers);
},
clear() {
this.text = '';
this.files = [];
this.poll = false;
- this.$emit('change-attached-media');
+ this.$emit('change-attached-files');
},
post() {
@@ -275,7 +295,7 @@ export default Vue.extend({
const viaMobile = this.$store.state.settings.disableViaMobile !== true;
(this as any).api('notes/create', {
text: this.text == '' ? undefined : this.text,
- mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+ fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
@@ -293,9 +313,6 @@ export default Vue.extend({
viaMobile: viaMobile
}).then(data => {
this.$emit('posted');
- this.$nextTick(() => {
- this.$destroy();
- });
}).catch(err => {
this.posting = false;
});
@@ -303,13 +320,12 @@ export default Vue.extend({
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], [])));
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
cancel() {
this.$emit('cancel');
- this.$destroy();
},
kao() {
@@ -320,9 +336,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.mk-post-form
max-width 500px
width calc(100% - 16px)
margin 8px auto
@@ -338,27 +352,27 @@ root(isDark)
margin 32px auto
> .form
- background isDark ? #282C37 : #fff
+ 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 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1)
+ box-shadow 0 1px 0 0 var(--mobilePostFormDivider)
> .cancel
padding 0
width 50px
line-height 50px
font-size 24px
- color isDark ? #9baec8 : #555
+ color var(--text)
> div
position absolute
top 0
right 0
- color #657786
+ color var(--text)
> .text-count
line-height 50px
@@ -372,8 +386,8 @@ root(isDark)
padding 0 16px
line-height 34px
vertical-align bottom
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
border-radius 4px
&:disabled
@@ -383,7 +397,7 @@ root(isDark)
max-width 500px
margin 0 auto
- > .mk-note-preview
+ > .preview
padding 16px
> .visibleUsers
@@ -392,7 +406,7 @@ root(isDark)
> span
margin-right 16px
- color isDark ? #fff : #666
+ color var(--text)
> input
z-index 1
@@ -404,11 +418,11 @@ root(isDark)
margin 0
width 100%
font-size 16px
- color isDark ? #fff : #333
- background isDark ? #191d23 : #fff
+ color var(--inputText)
+ background var(--mobilePostFormTextareaBg)
border none
border-radius 0
- box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1)
+ box-shadow 0 1px 0 0 var(--mobilePostFormDivider)
&:disabled
opacity 0.5
@@ -464,7 +478,7 @@ root(isDark)
width 48px
height 48px
font-size 20px
- color #657786
+ color var(--mobilePostFormButton)
background transparent
outline none
border none
@@ -477,10 +491,4 @@ root(isDark)
> *
margin-right 8px
-.mk-post-form[data-darkmode]
- root(true)
-
-.mk-post-form:not([data-darkmode])
- root(false)
-
</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
index a4ce49786e..2238edf278 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -7,9 +7,9 @@
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId">RP: ...</a>
</div>
- <details v-if="note.media.length > 0">
- <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
- <mk-media-list :media-list="note.media"/>
+ <details v-if="note.files.length > 0">
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+ <mk-media-list :media-list="note.files"/>
</details>
<details v-if="note.poll">
<summary>%i18n:@poll%</summary>
@@ -37,7 +37,7 @@ export default Vue.extend({
> .rp
margin-left 4px
font-style oblique
- color #a0bf46
+ color var(--renoteText)
mk-poll
font-size 80%
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index a616586c56..9793d03a8c 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -1,9 +1,9 @@
<template>
-<div class="header">
+<div class="header" ref="root">
+ <p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p>
<mk-special-message/>
<div class="main" ref="main">
<div class="backdrop"></div>
- <p ref="welcomeback" v-if="$store.getters.isSignedIn">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>%i18n:@adjective%</p>
<div class="content" ref="mainContainer">
<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
<template v-if="hasUnreadNotification || hasUnreadMessagingMessage || hasGameInvitation">%fa:circle%</template>
@@ -20,93 +20,51 @@
<script lang="ts">
import Vue from 'vue';
import * as anime from 'animejs';
+import { env } from '../../../config';
export default Vue.extend({
props: ['func'],
+
data() {
return {
hasGameInvitation: false,
connection: null,
- connectionId: null
+ env: env
};
},
+
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;
}
},
+
mounted() {
- this.$store.commit('setUiHeaderHeight', 48);
+ this.$store.commit('setUiHeaderHeight', this.$refs.root.offsetHeight);
if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
- this.connection.on('reversi_invited', this.onReversiInvited);
+ this.connection.on('reversiInvited', this.onReversiInvited);
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
-
- const ago = (new Date().getTime() - new Date(this.$store.state.i.lastUsedAt).getTime()) / 1000;
- const isHisasiburi = ago >= 3600;
- this.$store.state.i.lastUsedAt = new Date();
-
- if (isHisasiburi) {
- (this.$refs.welcomeback as any).style.display = 'block';
- (this.$refs.main as any).style.overflow = 'hidden';
-
- anime({
- targets: this.$refs.welcomeback,
- top: '0',
- opacity: 1,
- delay: 1000,
- duration: 500,
- easing: 'easeOutQuad'
- });
-
- anime({
- targets: this.$refs.mainContainer,
- opacity: 0,
- delay: 1000,
- duration: 500,
- easing: 'easeOutQuad'
- });
-
- setTimeout(() => {
- anime({
- targets: this.$refs.welcomeback,
- top: '-48px',
- opacity: 0,
- duration: 500,
- complete: () => {
- (this.$refs.welcomeback as any).style.display = 'none';
- (this.$refs.main as any).style.overflow = 'initial';
- },
- easing: 'easeInQuad'
- });
-
- anime({
- targets: this.$refs.mainContainer,
- opacity: 1,
- duration: 500,
- easing: 'easeInQuad'
- });
- }, 2500);
- }
}
},
+
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
- this.connection.off('reversi_invited', this.onReversiInvited);
- this.connection.off('reversi_no_invites', this.onReversiNoInvites);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
}
},
+
methods: {
onReversiInvited() {
this.hasGameInvitation = true;
},
+
onReversiNoInvites() {
this.hasGameInvitation = false;
}
@@ -115,9 +73,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+.header
$height = 48px
position fixed
@@ -131,10 +87,19 @@ root(isDark)
> .indicator
height 3px
- background $theme-color
+ background var(--primary)
+
+ > .warn
+ display block
+ margin 0
+ padding 4px
+ text-align center
+ font-size 12px
+ background #f00
+ color #fff
> .main
- color rgba(#fff, 0.9)
+ color var(--mobileHeaderFg)
> .backdrop
position absolute
@@ -144,20 +109,7 @@ root(isDark)
height $height
-webkit-backdrop-filter blur(12px)
backdrop-filter blur(12px)
- //background-color rgba(#1b2023, 0.75)
- background-color isDark ? #313543 : #595f6f
-
- > p
- display none
- position absolute
- z-index 1002
- top $height
- width 100%
- line-height $height
- margin 0
- text-align center
- color #fff
- opacity 0
+ background-color var(--mobileHeaderBg)
> .content
z-index 1001
@@ -176,9 +128,6 @@ root(isDark)
overflow hidden
text-overflow ellipsis
- [data-fa], [data-icon]
- margin-right 4px
-
> img
display inline-block
vertical-align bottom
@@ -207,7 +156,7 @@ root(isDark)
left 8px
pointer-events none
font-size 10px
- color $theme-color
+ color var(--primary)
> button:last-child
display block
@@ -222,10 +171,4 @@ root(isDark)
line-height $height
border-left solid 1px rgba(#000, 0.1)
-.header[data-darkmode]
- root(true)
-
-.header:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 39ea513b76..c9c0c082b2 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -34,6 +34,12 @@
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@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 v-html="announcement.text"></div>
+ </article>
+ </div>
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
</div>
</transition>
@@ -46,50 +52,60 @@ import { lang } from '../../../config';
export default Vue.extend({
props: ['isOpen'],
+
data() {
return {
hasGameInvitation: false,
connection: null,
- connectionId: null,
- aboutUrl: `/docs/${lang}/about`
+ aboutUrl: `/docs/${lang}/about`,
+ announcements: []
};
},
+
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;
}
},
+
mounted() {
+ (this as any).os.getMeta().then(meta => {
+ this.announcements = meta.broadcasts;
+ });
+
if (this.$store.getters.isSignedIn) {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
- this.connection.on('reversi_invited', this.onReversiInvited);
+ this.connection.on('reversiInvited', this.onReversiInvited);
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
+
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
- this.connection.off('reversi_invited', this.onReversiInvited);
- this.connection.off('reversi_no_invites', this.onReversiNoInvites);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
}
},
+
methods: {
search() {
const query = window.prompt('%i18n:@search%');
if (query == null || query == '') return;
- this.$router.push('/search?q=' + encodeURIComponent(query));
+ this.$router.push(`/search?q=${encodeURIComponent(query)}`);
},
+
onReversiInvited() {
this.hasGameInvitation = true;
},
+
onReversiNoInvites() {
this.hasGameInvitation = false;
},
+
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
@@ -101,10 +117,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- $color = isDark ? #c9d2e0 : #777
+.nav
+ $color = var(--text)
.backdrop
position fixed
@@ -113,7 +127,7 @@ root(isDark)
z-index 1025
width 100%
height 100%
- background isDark ? rgba(#000, 0.7) : rgba(#000, 0.2)
+ background var(--mobileNavBackdrop)
.body
position fixed
@@ -124,7 +138,7 @@ root(isDark)
height 100%
overflow auto
-webkit-overflow-scrolling touch
- background isDark ? #16191f : #fff
+ background var(--secondary)
.me
display block
@@ -178,11 +192,11 @@ root(isDark)
text-decoration none
&[data-active]
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
> [data-fa]:last-child
- color $theme-color-foreground
+ color var(--primaryForeground)
> [data-fa]:first-child
margin-right 0.5em
@@ -192,7 +206,7 @@ root(isDark)
> [data-fa].circle
margin-left 6px
font-size 10px
- color $theme-color
+ color var(--primary)
> [data-fa]:last-child
position absolute
@@ -204,6 +218,17 @@ root(isDark)
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
@@ -234,10 +259,4 @@ root(isDark)
opacity: 0;
}
-.nav[data-darkmode]
- root(true)
-
-.nav:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue
index 7e2d39f259..b16c246b10 100644
--- a/src/client/app/mobile/views/components/ui.vue
+++ b/src/client/app/mobile/views/components/ui.vue
@@ -23,33 +23,43 @@ export default Vue.extend({
XHeader,
XNav
},
+
props: ['title'],
+
data() {
return {
isDrawerOpening: false,
- connection: null,
- connectionId: null
+ connection: null
};
},
+
+ 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 as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
+ this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('notification', this.onNotification);
}
},
+
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
- this.connection.off('notification', this.onNotification);
- (this as any).os.stream.dispose(this.connectionId);
+ this.connection.dispose();
}
},
+
methods: {
onNotification(notification) {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
this.connection.send({
- type: 'read_notification',
+ type: 'readNotification',
id: notification.id
});
diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue
index 9b3f11f5c2..97200eb5b3 100644
--- a/src/client/app/mobile/views/components/user-list-timeline.vue
+++ b/src/client/app/mobile/views/components/user-list-timeline.vue
@@ -6,7 +6,6 @@
<script lang="ts">
import Vue from 'vue';
-import { UserListStream } from '../../../common/scripts/streaming/user-list';
const fetchLimit = 10;
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 6be675c0a7..7cd23d6655 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: this.withMedia,
+ withFiles: this.withMedia,
limit: fetchLimit + 1
}).then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -62,7 +62,7 @@ export default Vue.extend({
const promise = (this as any).api('users/notes', {
userId: this.user.id,
- withMedia: this.withMedia,
+ withFiles: this.withMedia,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id
});
diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
index a57b821293..f06f5245b8 100644
--- a/src/client/app/mobile/views/components/users-list.vue
+++ b/src/client/app/mobile/views/components/users-list.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
.mk-users-list
@@ -87,8 +87,8 @@ export default Vue.extend({
&[data-active]
font-weight bold
- color $theme-color
- border-color $theme-color
+ color var(--primary)
+ border-color var(--primary)
> span
display inline-block
diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
index a713a10621..2a4025002b 100644
--- a/src/client/app/mobile/views/components/widget-container.vue
+++ b/src/client/app/mobile/views/components/widget-container.vue
@@ -25,8 +25,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
- background isDark ? #21242f : #eee
+.mk-widget-container
+ background var(--face)
border-radius 8px
box-shadow 0 4px 16px rgba(#000, 0.1)
overflow hidden
@@ -35,17 +35,14 @@ root(isDark)
background transparent !important
box-shadow none !important
- &.hideHeader
- background isDark ? #21242f : #fff
-
> header
> .title
margin 0
padding 8px 10px
font-size 15px
font-weight normal
- color isDark ? #b8c5cc : #465258
- background isDark ? #282c37 : #fff
+ color var(--faceHeaderText)
+ background var(--faceHeader)
border-radius 8px 8px 0 0
> [data-fa]
@@ -65,10 +62,4 @@ root(isDark)
font-size 15px
color #465258
-.mk-widget-container[data-darkmode]
- root(true)
-
-.mk-widget-container:not([data-darkmode])
- root(false)
-
</style>
diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue
index c7cbe0f72e..bf02adca9d 100644
--- a/src/client/app/mobile/views/pages/drive.vue
+++ b/src/client/app/mobile/views/pages/drive.vue
@@ -1,9 +1,9 @@
<template>
<mk-ui>
<span slot="header">
- <template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
- <template v-if="file"><mk-file-type-icon data-icon :type="file.type"/>{{ file.name }}</template>
- <template v-if="!folder && !file">%fa:cloud%%i18n:@drive%</template>
+ <template v-if="folder"><span style="margin-right:4px;">%fa:R 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:cloud%</span>%i18n:@drive%</template>
</span>
<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
<mk-drive
@@ -11,7 +11,7 @@
:init-folder="initFolder"
:init-file="initFile"
:is-naked="true"
- :top="48"
+ :top="$store.state.uiHeaderHeight"
@begin-fetch="Progress.start()"
@fetched-mid="Progress.set(0.5)"
@fetched="Progress.done()"
@@ -44,7 +44,6 @@ export default Vue.extend({
},
mounted() {
document.title = `${(this as any).os.instanceName} Drive`;
- document.documentElement.style.background = '#fff';
},
beforeDestroy() {
window.removeEventListener('popstate', this.onPopState);
@@ -80,7 +79,7 @@ export default Vue.extend({
if (!silent) {
// Rewrite URL
- history.pushState(null, title, '/i/drive/folder/' + folder.id);
+ history.pushState(null, title, `/i/drive/folder/${folder.id}`);
}
document.title = title;
@@ -93,7 +92,7 @@ export default Vue.extend({
if (!silent) {
// Rewrite URL
- history.pushState(null, title, '/i/drive/file/' + file.id);
+ history.pushState(null, title, `/i/drive/file/${file.id}`);
}
document.title = title;
diff --git a/src/client/app/mobile/views/pages/favorites.vue b/src/client/app/mobile/views/pages/favorites.vue
index 6b9aec6a0c..a25f70147b 100644
--- a/src/client/app/mobile/views/pages/favorites.vue
+++ b/src/client/app/mobile/views/pages/favorites.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <span slot="header">%fa:star%%i18n:@title%</span>
+ <span slot="header"><span style="margin-right:4px;">%fa:star%</span>%i18n:@title%</span>
<main>
<template v-for="favorite in favorites">
@@ -71,7 +71,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
main
width 100%
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 421c150856..601f6670c1 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -49,7 +49,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + (this as any).os.instanceName;
+ document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`;
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index ff201ff2bd..0efac6110e 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -48,7 +48,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + (this as any).os.instanceName;
+ document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`;
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue
index d6849a1c11..7f8f919005 100644
--- a/src/client/app/mobile/views/pages/games/reversi.vue
+++ b/src/client/app/mobile/views/pages/games/reversi.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <span slot="header">%fa:gamepad%%i18n:@reversi%</span>
+ <span slot="header"><span style="margin-right:4px;">%fa:gamepad%</span>%i18n:@reversi%</span>
<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
</mk-ui>
</template>
@@ -11,15 +11,14 @@ import Vue from 'vue';
export default Vue.extend({
mounted() {
document.title = `${(this as any).os.instanceName} %i18n:@reversi%`;
- document.documentElement.style.background = '#fff';
},
methods: {
nav(game, actualNav) {
if (actualNav) {
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
} else {
// TODO: https://github.com/vuejs/vue-router/issues/703
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
}
}
}
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 416b006cd8..1979747bf7 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -21,6 +21,9 @@ export default Vue.extend({
src: {
type: String,
required: true
+ },
+ tagTl: {
+ required: false
}
},
@@ -29,10 +32,17 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
+ streamManager: null,
connection: null,
- connectionId: null,
unreadCount: 0,
- date: null
+ date: null,
+ baseQuery: {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ },
+ query: {},
+ endpoint: null
};
},
@@ -41,49 +51,67 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0;
},
- stream(): any {
- switch (this.src) {
- case 'home': return (this as any).os.stream;
- case 'local': return (this as any).os.streams.localTimelineStream;
- case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
- case 'global': return (this as any).os.streams.globalTimelineStream;
- }
- },
-
- endpoint(): string {
- switch (this.src) {
- case 'home': return 'notes/timeline';
- case 'local': return 'notes/local-timeline';
- case 'hybrid': return 'notes/hybrid-timeline';
- case 'global': return 'notes/global-timeline';
- }
- },
-
canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore;
}
},
mounted() {
- this.connection = this.stream.getConnection();
- this.connectionId = this.stream.use();
+ const prepend = note => {
+ (this.$refs.timeline as any).prepend(note);
+ };
- this.connection.on('note', this.onNote);
- if (this.src == 'home') {
- this.connection.on('follow', this.onChangeFollowing);
- this.connection.on('unfollow', this.onChangeFollowing);
+ if (this.src == 'tag') {
+ this.endpoint = 'notes/search_by_tag';
+ this.query = {
+ query: this.tagTl.query
+ };
+ this.connection = (this as any).os.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 as any).os.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 as any).os.stream.useSharedConnection('localTimeline');
+ this.connection.on('note', prepend);
+ } else if (this.src == 'hybrid') {
+ this.endpoint = 'notes/hybrid-timeline';
+ this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline');
+ this.connection.on('note', prepend);
+ } else if (this.src == 'global') {
+ this.endpoint = 'notes/global-timeline';
+ this.connection = (this as any).os.stream.useSharedConnection('globalTimeline');
+ this.connection.on('note', prepend);
+ } else if (this.src == 'mentions') {
+ this.endpoint = 'notes/mentions';
+ this.connection = (this as any).os.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 as any).os.stream.useSharedConnection('main');
+ this.connection.on('mention', onNote);
}
this.fetch();
},
beforeDestroy() {
- this.connection.off('note', this.onNote);
- if (this.src == 'home') {
- this.connection.off('follow', this.onChangeFollowing);
- this.connection.off('unfollow', this.onChangeFollowing);
- }
- this.stream.dispose(this.connectionId);
+ this.connection.dispose();
},
methods: {
@@ -91,13 +119,10 @@ export default Vue.extend({
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
- (this as any).api(this.endpoint, {
+ (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilDate: this.date ? this.date.getTime() : undefined,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- }).then(notes => {
+ untilDate: this.date ? this.date.getTime() : undefined
+ }, this.baseQuery, this.query)).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
@@ -114,13 +139,10 @@ export default Vue.extend({
this.moreFetching = true;
- const promise = (this as any).api(this.endpoint, {
+ const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- });
+ untilId: (this.$refs.timeline as any).tail().id
+ }, this.baseQuery, this.query));
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -135,15 +157,6 @@ export default Vue.extend({
return promise;
},
- onNote(note) {
- // Prepend a note
- (this.$refs.timeline as any).prepend(note);
- },
-
- onChangeFollowing() {
- this.fetch();
- },
-
focus() {
(this.$refs.timeline as any).focus();
},
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 706c9cd28b..edba8585bd 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -1,35 +1,46 @@
<template>
<mk-ui>
<span slot="header" @click="showNav = true">
- <span>
+ <span :class="$style.title">
<span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
+ <span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
+ <span v-if="src == 'messages'">%fa:envelope R%%i18n:@messages%</span>
<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
+ <span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
</span>
<span style="margin-left:8px">
<template v-if="!showNav">%fa:angle-down%</template>
<template v-else>%fa:angle-up%</template>
</span>
+ <i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i>
</span>
<template slot="func">
<button @click="fn">%fa:pencil-alt%</button>
</template>
- <main :data-darkmode="$store.state.device.darkmode">
+ <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:home% %i18n:@home%</span>
- <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
- <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+ <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+ <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
+ <div class="hr"></div>
+ <span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></span>
+ <span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa: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:list% {{ l.title }}</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:hashtag% {{ tl.title }}</span>
</div>
</div>
</div>
@@ -39,6 +50,9 @@
<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>
@@ -60,7 +74,9 @@ export default Vue.extend({
src: 'home',
list: null,
lists: null,
- showNav: false
+ tagTl: null,
+ showNav: false,
+ enableLocalTimeline: false
};
},
@@ -70,9 +86,16 @@ export default Vue.extend({
this.saveSrc();
},
- list() {
+ 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) {
@@ -85,10 +108,16 @@ export default Vue.extend({
},
created() {
+ (this as any).os.getMeta().then(meta => {
+ this.enableLocalTimeline = !meta.disableLocalTimeline;
+ });
+
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;
}
} else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid';
@@ -113,7 +142,7 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
- arg: this.list
+ arg: this.src == 'list' ? this.list : this.tagTl
});
},
@@ -125,10 +154,28 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
+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
@@ -145,38 +192,37 @@ root(isDark)
left 0
right 0
width 300px
+ max-height calc(100% - 70px)
margin 0 auto
- background isDark ? #272f3a : #fff
+ overflow auto
+ -webkit-overflow-scrolling touch
+ background var(--popupBg)
border-radius 8px
box-shadow 0 0 16px rgba(#000, 0.1)
- $balloon-size = 16px
-
- &:after
- content ""
- display block
- position absolute
- top -($balloon-size * 2) + 1.5px
- left s('calc(50% - %s)', $balloon-size)
- border-top solid $balloon-size transparent
- border-left solid $balloon-size transparent
- border-right solid $balloon-size transparent
- border-bottom solid $balloon-size isDark ? #272f3a : #fff
-
> div
padding 8px 0
- > *
+ > .hr
+ margin 8px 0
+ border-top solid 1px var(--faceDivider)
+
+ > *:not(.hr)
display block
padding 8px 16px
- color isDark ? #cdd0d8 : #666
+ color var(--text)
&[data-active]
- color $theme-color-foreground
- background $theme-color
+ color var(--primaryForeground)
+ background var(--primary)
&:not([data-active]):hover
- background isDark ? #353e4a : #eee
+ background var(--mobileHomeTlItemHover)
+
+ > .badge
+ margin-left 6px
+ font-size 10px
+ color var(--primary)
> .tl
max-width 680px
@@ -189,10 +235,17 @@ root(isDark)
@media (min-width 600px)
padding 32px
-main[data-darkmode]
- root(true)
+</style>
+
+<style lang="stylus" module>
+.title
+ i
+ margin-right 4px
-main:not([data-darkmode])
- root(false)
+.badge
+ margin-left 6px
+ font-size 10px
+ color var(--primary)
+ vertical-align middle
</style>
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index 401397d856..750ba26294 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -1,7 +1,7 @@
<template>
<mk-ui>
<span slot="header">
- <template v-if="user">%fa:R comments%{{ user | userName }}</template>
+ <template v-if="user"><span style="margin-right:4px;">%fa:R comments%</span>{{ user | userName }}</template>
<template v-else><mk-ellipsis/></template>
</span>
<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index 3883505281..98ae79fe6c 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <span slot="header">%fa:R comments%%i18n:@messaging%</span>
+ <span slot="header"><span style="margin-right:4px;">%fa:R comments%</span>%i18n:@messaging%</span>
<mk-messaging @navigate="navigate" :header-top="48"/>
</mk-ui>
</template>
diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue
index fee60b350e..d7307c79a8 100644
--- a/src/client/app/mobile/views/pages/note.vue
+++ b/src/client/app/mobile/views/pages/note.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <span slot="header">%fa:R sticky-note%%i18n:@title%</span>
+ <span slot="header"><span style="margin-right:4px;">%fa:R sticky-note%</span>%i18n:@title%</span>
<main v-if="!fetching">
<div>
<mk-note-detail :note="note"/>
diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
index 4d3c8ee534..ce33332faf 100644
--- a/src/client/app/mobile/views/pages/notifications.vue
+++ b/src/client/app/mobile/views/pages/notifications.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <span slot="header">%fa:R bell%%i18n:@notifications%</span>
+ <span slot="header"><span style="margin-right:4px;">%fa:R bell%</span>%i18n:@notifications%</span>
<template slot="func"><button @click="fn">%fa:check%</button></template>
<main>
@@ -34,7 +34,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
main
width 100%
diff --git a/src/client/app/mobile/views/pages/received-follow-requests.vue b/src/client/app/mobile/views/pages/received-follow-requests.vue
index 77938c3d60..beaf6bba57 100644
--- a/src/client/app/mobile/views/pages/received-follow-requests.vue
+++ b/src/client/app/mobile/views/pages/received-follow-requests.vue
@@ -52,8 +52,6 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
main
width 100%
max-width 680px
@@ -69,7 +67,7 @@ main
> div
display flex
padding 16px
- border solid 1px isDark ? #1c2023 : #eee
+ border solid 1px var(--faceDivider)
border-radius 4px
> span
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index 1a162b346c..c098b8c65e 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -5,7 +5,7 @@
<button class="upload" @click="upload">%fa:upload%</button>
<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
</header>
- <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="42"/>
+ <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="$store.state.uiHeaderHeight"/>
</div>
</template>
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 7437eb8b47..94fa38cec9 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -1,8 +1,8 @@
<template>
<mk-ui>
- <span slot="header">%fa:cog%%i18n:@settings%</span>
- <main :data-darkmode="$store.state.device.darkmode">
- <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div>
+ <span slot="header"><span style="margin-right:4px;">%fa:cog%</span>%i18n:@settings%</span>
+ <main>
+ <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
<div>
<x-profile/>
@@ -10,80 +10,127 @@
<ui-card>
<div slot="title">%fa:palette% %i18n:@design%</div>
- <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
- <ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
- <ui-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi">%i18n:common.i-like-sushi%</ui-switch>
- <ui-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
- <ui-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
- <ui-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+ <section>
+ <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
+ <ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
+ <ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
+ <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
+ <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
+ <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
+ <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
+ <ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
+ <ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
+ <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+ </section>
- <div>
- <div>%i18n:@timeline%</div>
- <ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
- <ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
- <ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
- <ui-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
- </div>
+ <section>
+ <header>%i18n:@theme%</header>
+ <div>
+ <mk-theme/>
+ </div>
+ </section>
- <div>
- <div>%i18n:@post-style%</div>
+ <section>
+ <header>%i18n:@timeline%</header>
+ <div>
+ <ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch>
+ <ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch>
+ <ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
+ <ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
+ </div>
+ </section>
+
+ <section>
+ <header>%i18n:@post-style%</header>
<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio>
<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio>
- </div>
+ </section>
+
+ <section>
+ <header>%i18n:@notification-position%</header>
+ <ui-radio v-model="mobileNotificationPosition" value="bottom">%i18n:@notification-position-bottom%</ui-radio>
+ <ui-radio v-model="mobileNotificationPosition" value="top">%i18n:@notification-position-top%</ui-radio>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:cog% %i18n:@behavior%</div>
- <ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
- <ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
- <ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
- <ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
- <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
+
+ <section>
+ <ui-switch v-model="fetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
+ <ui-switch v-model="disableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
+ <ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
+ <ui-switch v-model="loadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
+ <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
+ </section>
+
+ <section>
+ <header>%i18n:@note-visibility%</header>
+ <ui-switch v-model="rememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch>
+ <section>
+ <header>%i18n:@default-note-visibility%</header>
+ <ui-select v-model="defaultNoteVisibility">
+ <option value="public">%i18n:common.note-visibility.public%</option>
+ <option value="home">%i18n:common.note-visibility.home%</option>
+ <option value="followers">%i18n:common.note-visibility.followers%</option>
+ <option value="specified">%i18n:common.note-visibility.specified%</option>
+ <option value="private">%i18n:common.note-visibility.private%</option>
+ </ui-select>
+ </section>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:volume-up% %i18n:@sound%</div>
- <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
+ <section>
+ <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:language% %i18n:@lang%</div>
- <ui-select v-model="lang" placeholder="%i18n:@auto%">
- <optgroup label="%i18n:@recommended%">
- <option value="">%i18n:@auto%</option>
- </optgroup>
+ <section class="fit-top">
+ <ui-select v-model="lang" placeholder="%i18n:@auto%">
+ <optgroup label="%i18n:@recommended%">
+ <option value="">%i18n:@auto%</option>
+ </optgroup>
- <optgroup label="%i18n:@specify-language%">
- <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
- </optgroup>
- </ui-select>
- <span>%fa:info-circle% %i18n:@lang-tip%</span>
+ <optgroup label="%i18n:@specify-language%">
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ </optgroup>
+ </ui-select>
+ <span>%fa:info-circle% %i18n:@lang-tip%</span>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:B twitter% %i18n:@twitter%</div>
- <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
- <p>
- <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
- <span v-if="$store.state.i.twitter"> or </span>
- <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
- </p>
+ <section>
+ <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
+ <p>
+ <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
+ <span v-if="$store.state.i.twitter"> or </span>
+ <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
+ </p>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:sync-alt% %i18n:@update%</div>
- <div>%i18n:@version% <i>{{ version }}</i></div>
- <template v-if="latestVersion !== undefined">
- <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
- </template>
- <ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
- <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
- <template v-else>%i18n:@check-for-updates%</template>
- </ui-button>
+ <section>
+ <div>%i18n:@version% <i>{{ version }}</i></div>
+ <template v-if="latestVersion !== undefined">
+ <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
+ </template>
+ <ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
+ <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
+ <template v-else>%i18n:@check-for-updates%</template>
+ </ui-button>
+ </section>
</ui-card>
</div>
@@ -129,11 +176,26 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
},
+ reduceMotion: {
+ get() { return this.$store.state.device.reduceMotion; },
+ set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
+ },
+
+ alwaysShowNsfw: {
+ get() { return this.$store.state.device.alwaysShowNsfw; },
+ set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
+ },
+
postStyle: {
get() { return this.$store.state.device.postStyle; },
set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
},
+ mobileNotificationPosition: {
+ get() { return this.$store.state.device.mobileNotificationPosition; },
+ set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); }
+ },
+
lightmode: {
get() { return this.$store.state.device.lightmode; },
set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
@@ -153,99 +215,95 @@ export default Vue.extend({
get() { return this.$store.state.device.enableSounds; },
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
},
- },
- mounted() {
- document.title = '%i18n:@settings%';
- },
+ fetchOnScroll: {
+ get() { return this.$store.state.settings.fetchOnScroll; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
+ },
- methods: {
- signout() {
- (this as any).os.signout();
+ rememberNoteVisibility: {
+ get() { return this.$store.state.settings.rememberNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
},
- onChangeFetchOnScroll(v) {
- this.$store.dispatch('settings/set', {
- key: 'fetchOnScroll',
- value: v
- });
+ disableViaMobile: {
+ get() { return this.$store.state.settings.disableViaMobile; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
},
- onChangeDisableViaMobile(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableViaMobile',
- value: v
- });
+ loadRemoteMedia: {
+ get() { return this.$store.state.settings.loadRemoteMedia; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'loadRemoteMedia', value }); }
},
- onChangeLoadRemoteMedia(v) {
- this.$store.dispatch('settings/set', {
- key: 'loadRemoteMedia',
- value: v
- });
+ circleIcons: {
+ get() { return this.$store.state.settings.circleIcons; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); }
},
- onChangeCircleIcons(v) {
- this.$store.dispatch('settings/set', {
- key: 'circleIcons',
- value: v
- });
+ contrastedAcct: {
+ get() { return this.$store.state.settings.contrastedAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); }
},
- onChangeILikeSushi(v) {
- this.$store.dispatch('settings/set', {
- key: 'iLikeSushi',
- value: v
- });
+ showFullAcct: {
+ get() { return this.$store.state.settings.showFullAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); }
},
- onChangeReversiBoardLabels(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.showBoardLabels',
- value: v
- });
+ iLikeSushi: {
+ get() { return this.$store.state.settings.iLikeSushi; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
},
- onChangeUseContrastReversiStones(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.useContrastStones',
- value: v
- });
+ games_reversi_showBoardLabels: {
+ get() { return this.$store.state.settings.games.reversi.showBoardLabels; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); }
},
- onChangeDisableAnimatedMfm(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableAnimatedMfm',
- value: v
- });
+ games_reversi_useContrastStones: {
+ get() { return this.$store.state.settings.games.reversi.useContrastStones; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); }
},
- onChangeShowReplyTarget(v) {
- this.$store.dispatch('settings/set', {
- key: 'showReplyTarget',
- value: v
- });
+ disableAnimatedMfm: {
+ get() { return this.$store.state.settings.disableAnimatedMfm; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
},
- onChangeShowMyRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMyRenotes',
- value: v
- });
+ showReplyTarget: {
+ get() { return this.$store.state.settings.showReplyTarget; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
},
- onChangeShowRenotedMyNotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showRenotedMyNotes',
- value: v
- });
+ showMyRenotes: {
+ get() { return this.$store.state.settings.showMyRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
},
- onChangeShowLocalRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showLocalRenotes',
- value: v
- });
+ showRenotedMyNotes: {
+ get() { return this.$store.state.settings.showRenotedMyNotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
+ },
+
+ showLocalRenotes: {
+ get() { return this.$store.state.settings.showLocalRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
+ },
+
+ defaultNoteVisibility: {
+ get() { return this.$store.state.settings.defaultNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
+ },
+ },
+
+ mounted() {
+ document.title = '%i18n:@settings%';
+ },
+
+ methods: {
+ signout() {
+ (this as any).os.signout();
},
checkForUpdate() {
@@ -271,36 +329,31 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+main
margin 0 auto
- max-width 500px
+ max-width 600px
width 100%
> .signin-as
margin 16px
padding 16px
text-align center
- color isDark ? #49ab63 : #2c662d
- background isDark ? #273c34 : #fcfff5
+ color var(--mobileSignedInAsFg)
+ background var(--mobileSignedInAsBg)
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 isDark ? #ff5f56 : #cc2727
- background isDark ? #652222 : #fff6f5
+ color var(--mobileSignedInAsFg)
+ background var(--mobileSignedInAsBg)
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 isDark ? #c9d2e0 : #888
-
-main[data-darkmode]
- root(true)
-
-main:not([data-darkmode])
- root(false)
+ color var(--text)
+ opacity 0.7
</style>
diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue
index 3b797cdde1..127f531902 100644
--- a/src/client/app/mobile/views/pages/settings/settings.profile.vue
+++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue
@@ -2,47 +2,64 @@
<ui-card>
<div slot="title">%fa:user% %i18n:@title%</div>
- <ui-form :disabled="saving">
- <ui-input v-model="name" :max="30">
- <span>%i18n:@name%</span>
- </ui-input>
+ <section class="fit-top">
+ <ui-form :disabled="saving">
+ <ui-input v-model="name" :max="30">
+ <span>%i18n:@name%</span>
+ </ui-input>
- <ui-input v-model="username" readonly>
- <span>%i18n:@account%</span>
- <span slot="prefix">@</span>
- <span slot="suffix">@{{ host }}</span>
- </ui-input>
+ <ui-input v-model="username" readonly>
+ <span>%i18n:@account%</span>
+ <span slot="prefix">@</span>
+ <span slot="suffix">@{{ host }}</span>
+ </ui-input>
- <ui-input v-model="location">
- <span>%i18n:@location%</span>
- <span slot="prefix">%fa:map-marker-alt%</span>
- </ui-input>
+ <ui-input v-model="location">
+ <span>%i18n:@location%</span>
+ <span slot="prefix">%fa:map-marker-alt%</span>
+ </ui-input>
- <ui-input v-model="birthday" type="date">
- <span>%i18n:@birthday%</span>
- <span slot="prefix">%fa:birthday-cake%</span>
- </ui-input>
+ <ui-input v-model="birthday" type="date">
+ <span>%i18n:@birthday%</span>
+ <span slot="prefix">%fa:birthday-cake%</span>
+ </ui-input>
- <ui-textarea v-model="description" :max="500">
- <span>%i18n:@description%</span>
- </ui-textarea>
+ <ui-textarea v-model="description" :max="500">
+ <span>%i18n:@description%</span>
+ </ui-textarea>
- <ui-input type="file" @change="onAvatarChange">
- <span>%i18n:@avatar%</span>
- <span slot="icon">%fa:image%</span>
- <span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
- </ui-input>
+ <ui-input type="file" @change="onAvatarChange">
+ <span>%i18n:@avatar%</span>
+ <span slot="icon">%fa:image%</span>
+ <span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
+ </ui-input>
- <ui-input type="file" @change="onBannerChange">
- <span>%i18n:@banner%</span>
- <span slot="icon">%fa:image%</span>
- <span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
- </ui-input>
+ <ui-input type="file" @change="onBannerChange">
+ <span>%i18n:@banner%</span>
+ <span slot="icon">%fa:image%</span>
+ <span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
+ </ui-input>
- <ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch>
+ <ui-button @click="save(true)">%i18n:@save%</ui-button>
+ </ui-form>
+ </section>
- <ui-button @click="save">%i18n:@save%</ui-button>
- </ui-form>
+ <section>
+ <header>%i18n:@advanced%</header>
+
+ <div>
+ <ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch>
+ <ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch>
+ </div>
+ </section>
+
+ <section>
+ <header>%i18n:@privacy%</header>
+
+ <div>
+ <ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch>
+ </div>
+ </section>
</ui-card>
</template>
@@ -62,12 +79,20 @@ export default Vue.extend({
avatarId: null,
bannerId: null,
isCat: false,
+ isLocked: false,
saving: false,
avatarUploading: false,
bannerUploading: false
};
},
+ computed: {
+ alwaysMarkNsfw: {
+ get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+ set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); }
+ },
+ },
+
created() {
this.name = this.$store.state.i.name || '';
this.username = this.$store.state.i.username;
@@ -77,6 +102,7 @@ export default Vue.extend({
this.avatarId = this.$store.state.i.avatarId;
this.bannerId = this.$store.state.i.bannerId;
this.isCat = this.$store.state.i.isCat;
+ this.isLocked = this.$store.state.i.isLocked;
},
methods: {
@@ -124,7 +150,7 @@ export default Vue.extend({
});
},
- save() {
+ save(notify) {
this.saving = true;
(this as any).api('i/update', {
@@ -134,7 +160,8 @@ export default Vue.extend({
birthday: this.birthday || null,
avatarId: this.avatarId,
bannerId: this.bannerId,
- isCat: this.isCat
+ isCat: this.isCat,
+ isLocked: this.isLocked
}).then(i => {
this.saving = false;
this.$store.state.i.avatarId = i.avatarId;
@@ -142,7 +169,9 @@ export default Vue.extend({
this.$store.state.i.bannerId = i.bannerId;
this.$store.state.i.bannerUrl = i.bannerUrl;
- alert('%i18n:@saved%');
+ if (notify) {
+ alert('%i18n:@saved%');
+ }
});
}
}
diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue
index a545e2b839..3f963501e0 100644
--- a/src/client/app/mobile/views/pages/tag.vue
+++ b/src/client/app/mobile/views/pages/tag.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
+ <span slot="header"><span style="margin-right:4px;">%fa:hashtag%</span>{{ $route.params.tag }}</span>
<main>
<p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue
index 1c6a829cd5..f8c8aafa61 100644
--- a/src/client/app/mobile/views/pages/user-list.vue
+++ b/src/client/app/mobile/views/pages/user-list.vue
@@ -53,7 +53,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
main
width 100%
diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue
index abd04c1496..fc80f5d1c6 100644
--- a/src/client/app/mobile/views/pages/user-lists.vue
+++ b/src/client/app/mobile/views/pages/user-lists.vue
@@ -43,7 +43,7 @@ export default Vue.extend({
title
});
- this.$router.push('/i/lists/' + list.id);
+ this.$router.push(`/i/lists/${list.id}`);
});
}
}
@@ -51,7 +51,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
+
main
width 100%
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 8918847a8f..a2a6bd7a83 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,7 +1,7 @@
<template>
<mk-ui>
<template slot="header" v-if="!fetching"><img :src="user.avatarUrl" alt="">{{ user | userName }}</template>
- <main v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
+ <main v-if="!fetching">
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
<header>
@@ -16,7 +16,7 @@
</div>
<div class="title">
<h1>{{ user | userName }}</h1>
- <span class="username"><mk-acct :user="user"/></span>
+ <span class="username"><mk-acct :user="user" :detail="true" /></span>
<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span>
</div>
<div class="description">
@@ -107,7 +107,7 @@ export default Vue.extend({
this.fetching = false;
Progress.done();
- document.title = Vue.filter('userName')(this.user) + ' | ' + (this as any).os.instanceName;
+ document.title = `${Vue.filter('userName')(this.user)} | ${(this as any).os.instanceName}`;
});
}
}
@@ -115,10 +115,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-@import '~const.styl'
-
-root(isDark)
- $bg = isDark ? #22252f : #f7f7f7
+main
+ $bg = var(--face)
> .is-suspended
> .is-remote
@@ -148,7 +146,7 @@ root(isDark)
> .banner
padding-bottom 33.3%
- background-color isDark ? #5f7273 : #cacaca
+ background-color rgba(0, 0, 0, 0.1)
background-size cover
background-position center
@@ -198,26 +196,26 @@ root(isDark)
margin 0
line-height 22px
font-size 20px
- color isDark ? #fff : #757c82
+ color var(--mobileUserPageName)
> .username
display inline-block
line-height 20px
font-size 16px
font-weight bold
- color isDark ? #657786 : #969ea5
+ color var(--mobileUserPageAcct)
> .followed
margin-left 8px
padding 2px 4px
font-size 12px
- color isDark ? #657786 : #fff
- background isDark ? #f8f8f8 : #a7bec7
+ color var(--mobileUserPageFollowedFg)
+ background var(--mobileUserPageFollowedBg)
border-radius 4px
> .description
margin 8px 0
- color isDark ? #fff : #757c82
+ color var(--mobileUserPageDescription)
> .info
margin 8px 0
@@ -225,14 +223,14 @@ root(isDark)
> p
display inline
margin 0 16px 0 0
- color isDark ? #a9b9c1 : #90989c
+ color var(--text)
> i
margin-right 4px
> .status
> a
- color isDark ? #657786 : #818a92
+ color var(--text)
&:not(:last-child)
margin-right 16px
@@ -240,7 +238,7 @@ root(isDark)
> b
margin-right 4px
font-size 16px
- color isDark ? #fff : #787e86
+ color var(--mobileUserPageStatusHighlight)
> i
font-size 14px
@@ -249,7 +247,7 @@ root(isDark)
position -webkit-sticky
position sticky
top 47px
- box-shadow 0 4px 4px isDark ? rgba(#000, 0.3) : rgba(#000, 0.07)
+ box-shadow 0 4px 4px var(--mobileUserPageHeaderShadow)
background-color $bg
z-index 2
@@ -266,7 +264,7 @@ root(isDark)
line-height 48px
font-size 12px
text-decoration none
- color isDark ? #657786 : #9ca1a5
+ color var(--text)
border-bottom solid 2px transparent
@media (min-width 400px)
@@ -275,8 +273,8 @@ root(isDark)
&[data-active]
font-weight bold
- color $theme-color
- border-color $theme-color
+ color var(--primary)
+ border-color var(--primary)
> .body
max-width 680px
@@ -289,10 +287,4 @@ root(isDark)
@media (min-width 600px)
padding 32px
-main[data-darkmode]
- root(true)
-
-main:not([data-darkmode])
- root(false)
-
</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
index 73ff1d5173..261a3f796c 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -4,7 +4,7 @@
<div class="stream" v-if="!fetching && images.length > 0">
<a v-for="image in images"
class="img"
- :style="`background-image: url(${image.media.url})`"
+ :style="`background-image: url(${image.media.thumbnailUrl})`"
:href="image.note | notePage"
></a>
</div>
@@ -26,7 +26,7 @@ export default Vue.extend({
mounted() {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: true,
+ withFiles: true,
limit: 6
}).then(notes => {
notes.forEach(note => {
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
index 8b57276b17..2c7134ed43 100644
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ b/src/client/app/mobile/views/pages/user/home.vue
@@ -1,6 +1,6 @@
<template>
<div class="root home">
- <mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
+ <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<section class="recent-notes">
<h2>%fa:R comments%%i18n:@recent-notes%</h2>
<div>
@@ -54,7 +54,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-root(isDark)
+.root.home
max-width 600px
margin 0 auto
@@ -65,7 +65,7 @@ root(isDark)
margin 0 0 16px 0
> section
- background isDark ? #21242f : #eee
+ background var(--face)
border-radius 8px
box-shadow 0 4px 16px rgba(#000, 0.1)
@@ -80,8 +80,8 @@ root(isDark)
padding 8px 10px
font-size 15px
font-weight normal
- color isDark ? #b8c5cc : #465258
- background isDark ? #282c37 : #fff
+ color var(--text)
+ background var(--faceHeader)
border-radius 8px 8px 0 0
@media (min-width 500px)
@@ -98,12 +98,6 @@ root(isDark)
display block
margin 16px
text-align center
- color isDark ? #cad2da : #929aa0
-
-.root.home[data-darkmode]
- root(true)
-
-.root.home:not([data-darkmode])
- root(false)
+ color var(--text)
</style>
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 49227790ff..32f74bfe3a 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -1,7 +1,9 @@
<template>
-<div class="welcome">
+<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes">
+ <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
+
<div>
- <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name">
+ <img svg-inline src="../../../../assets/title.svg" :alt="name">
<p class="host">{{ host }}</p>
<div class="about">
<h2>{{ name }}</h2>
@@ -15,12 +17,53 @@
<mk-welcome-timeline/>
</div>
<div class="hashtags">
- <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
+ <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:user% {{ stats.originalUsersCount | number }}</span>
<span>%fa: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>
+ <div v-html="announcement.text"></div>
+ </article>
+ </div>
+ <article class="about-misskey">
+ <h1>%i18n:common.intro.title%</h1>
+ <p v-html="'%i18n:common.intro.about%'"></p>
+ <section>
+ <h2>%i18n:common.intro.features%</h2>
+ <section>
+ <h3>%i18n:common.intro.rich-contents%</h3>
+ <div class="image"><img src="/assets/about/post.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.reaction%</h3>
+ <div class="image"><img src="/assets/about/reaction.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.ui%</h3>
+ <div class="image"><img src="/assets/about/ui.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.ui-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.drive%</h3>
+ <div class="image"><img src="/assets/about/drive.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.drive-desc%'"></p>
+ </section>
+ </section>
+ <p v-html="'%i18n:common.intro.outro%'"></p>
+ </article>
+ <div class="info" v-if="meta">
+ <p>Version: <b>{{ meta.version }}</b></p>
+ <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ </div>
<footer>
<small>{{ copyright }}</small>
</footer>
@@ -30,50 +73,87 @@
<script lang="ts">
import Vue from 'vue';
-import { apiUrl, copyright, host } from '../../../config';
+import { copyright, host } from '../../../config';
+import { concat } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
- apiUrl,
+ meta: null,
copyright,
stats: null,
+ banner: null,
host,
name: 'Misskey',
description: '',
- tags: []
+ photos: [],
+ announcements: []
};
},
created() {
(this as any).os.getMeta().then(meta => {
+ this.meta = meta;
this.name = meta.name;
this.description = meta.description;
+ this.announcements = meta.broadcasts;
+ this.banner = meta.bannerUrl;
});
(this as any).api('stats').then(stats => {
this.stats = stats;
});
- (this as any).api('hashtags/trend').then(stats => {
- this.tags = stats.map(x => x.tag);
+ const image = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif'
+ ];
+
+ (this as any).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);
});
}
});
</script>
<style lang="stylus" scoped>
-.welcome
+.wgwfgvvimdjvhjfwxropcwksnzftjqes
text-align center
- //background #fff
- > div
+ > .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
- > img
+ > svg
display block
- max-width 200px
+ width 200px
+ height 50px
margin 0 auto
> .host
@@ -89,8 +169,8 @@ export default Vue.extend({
> .about
margin-top 16px
padding 16px
- color #555
- background #fff
+ color var(--text)
+ background var(--face)
border-radius 6px
> h2
@@ -138,27 +218,98 @@ export default Vue.extend({
-webkit-overflow-scrolling touch
> .hashtags
- padding 16px 0
- border solid 2px #ddd
- border-radius 8px
+ padding 0 8px
+ height 200px
- > *
- margin 0 16px
+ > .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 #444
+ 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
+
+ > *
+ margin 0 16px
+
> footer
text-align center
- color #444
+ color var(--text)
> small
display block
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index a83103632e..c649529c0e 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <span slot="header">%fa:home%%i18n:@dashboard%</span>
+ <span slot="header"><span style="margin-right:4px;">%fa:home%</span>%i18n:@dashboard%</span>
<template slot="func">
<button @click="customizing = !customizing">%fa:cog%</button>
</template>
diff --git a/src/client/app/safe.js b/src/client/app/safe.js
index 3d73fa1a9c..026fc66c6e 100644
--- a/src/client/app/safe.js
+++ b/src/client/app/safe.js
@@ -12,16 +12,6 @@ if (!('fetch' in window)) {
'To run Misskey, please update your browser to latest version or try other browsers.');
}
-// Detect Edge
-if (navigator.userAgent.toLowerCase().indexOf('edge') != -1) {
- alert(
- '現在、お使いのブラウザ(Microsoft Edge)ではMisskeyは正しく動作しません。' +
- 'サポートしているブラウザ: Google Chrome, Mozilla Firefox, Apple Safari など' +
- '\n\n' +
- 'Currently, Misskey cannot run correctly on your browser (Microsoft Edge). ' +
- 'Supported browsers: Google Chrome, Mozilla Firefox, Apple Safari, etc');
-}
-
// Check whether cookie enabled
if (!navigator.cookieEnabled) {
alert(
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 469563495f..545261225a 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -4,18 +4,23 @@ import * as nestedProperty from 'nested-property';
import MiOS from './mios';
import { hostname } from './config';
+import { erase } from '../../prelude/array';
const defaultSettings = {
home: null,
mobileHome: [],
deck: null,
+ tagTimelines: [],
fetchOnScroll: true,
showMaps: true,
showPostFormOnTopOfTl: false,
suggestRecentHashtags: true,
showClockOnHeader: true,
+ useShadow: true,
+ roundedCorners: false,
circleIcons: true,
- gradientWindowHeader: false,
+ contrastedAcct: true,
+ showFullAcct: false,
showReplyTarget: true,
showMyRenotes: true,
showRenotedMyNotes: true,
@@ -24,6 +29,8 @@ const defaultSettings = {
disableViaMobile: false,
memo: null,
iLikeSushi: false,
+ rememberNoteVisibility: false,
+ defaultNoteVisibility: 'public',
games: {
reversi: {
showBoardLabels: false,
@@ -33,9 +40,13 @@ const defaultSettings = {
};
const defaultDeviceSettings = {
+ reduceMotion: false,
apiViaStream: true,
autoPopout: false,
darkmode: false,
+ darkTheme: 'dark',
+ lightTheme: 'light',
+ themes: [],
enableSounds: true,
soundVolume: 0.5,
lang: null,
@@ -43,7 +54,9 @@ const defaultDeviceSettings = {
debug: false,
lightmode: false,
loadRawImages: false,
- postStyle: 'standard'
+ alwaysShowNsfw: false,
+ postStyle: 'standard',
+ mobileNotificationPosition: 'bottom'
};
export default (os: MiOS) => new Vuex.Store({
@@ -194,7 +207,7 @@ export default (os: MiOS) => new Vuex.Store({
removeDeckColumn(state, id) {
state.deck.columns = state.deck.columns.filter(c => c.id != id);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
@@ -265,7 +278,7 @@ export default (os: MiOS) => new Vuex.Store({
stackLeftDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
const left = state.deck.layout[i - 1];
if (left) state.deck.layout[i - 1].push(id);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
@@ -273,7 +286,7 @@ export default (os: MiOS) => new Vuex.Store({
popRightDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
state.deck.layout.splice(i + 1, 0, [id]);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
diff --git a/src/client/app/sw.js b/src/client/app/sw.js
index ac7ea20acf..d381bfb7a5 100644
--- a/src/client/app/sw.js
+++ b/src/client/app/sw.js
@@ -3,6 +3,7 @@
*/
import composeNotification from './common/scripts/compose-notification';
+import { erase } from '../../prelude/array';
// キャッシュするリソース
const cachee = [
@@ -24,8 +25,7 @@ self.addEventListener('activate', ev => {
// Clean up old caches
ev.waitUntil(
caches.keys().then(keys => Promise.all(
- keys
- .filter(key => key != _VERSION_)
+ erase(_VERSION_, keys)
.map(key => caches.delete(key))
))
);
diff --git a/src/client/app/theme.ts b/src/client/app/theme.ts
new file mode 100644
index 0000000000..9c5be74fa1
--- /dev/null
+++ b/src/client/app/theme.ts
@@ -0,0 +1,104 @@
+import * as tinycolor from 'tinycolor2';
+
+export type Theme = {
+ id: string;
+ name: string;
+ author: string;
+ desc?: string;
+ base?: 'dark' | 'light';
+ vars: { [key: string]: string };
+ props: { [key: string]: string };
+};
+
+export const lightTheme: Theme = require('../theme/light.json5');
+export const darkTheme: Theme = require('../theme/dark.json5');
+export const pinkTheme: Theme = require('../theme/pink.json5');
+export const blackTheme: Theme = require('../theme/black.json5');
+export const halloweenTheme: Theme = require('../theme/halloween.json5');
+
+export const builtinThemes = [
+ lightTheme,
+ darkTheme,
+ pinkTheme,
+ blackTheme,
+ halloweenTheme
+];
+
+export function applyTheme(theme: Theme, persisted = true) {
+ // Deep copy
+ const _theme = JSON.parse(JSON.stringify(theme));
+
+ if (_theme.base) {
+ const base = [lightTheme, darkTheme].find(x => x.id == _theme.base);
+ _theme.vars = Object.assign({}, base.vars, _theme.vars);
+ _theme.props = Object.assign({}, base.props, _theme.props);
+ }
+
+ const props = compile(_theme);
+
+ Object.entries(props).forEach(([k, v]) => {
+ document.documentElement.style.setProperty(`--${k}`, v.toString());
+ });
+
+ if (persisted) {
+ localStorage.setItem('theme', JSON.stringify(props));
+ }
+}
+
+function compile(theme: Theme): { [key: string]: string } {
+ function getColor(code: string): tinycolor.Instance {
+ // ref
+ if (code[0] == '@') {
+ return getColor(theme.props[code.substr(1)]);
+ }
+ if (code[0] == '$') {
+ return getColor(theme.vars[code.substr(1)]);
+ }
+
+ // func
+ if (code[0] == ':') {
+ const parts = code.split('<');
+ const func = parts.shift().substr(1);
+ const arg = parseFloat(parts.shift());
+ const color = getColor(parts.join('<'));
+
+ switch (func) {
+ case 'darken': return color.darken(arg);
+ case 'lighten': return color.lighten(arg);
+ case 'alpha': return color.setAlpha(arg);
+ }
+ }
+
+ return tinycolor(code);
+ }
+
+ const props = {};
+
+ Object.entries(theme.props).forEach(([k, v]) => {
+ const c = getColor(v);
+ props[k] = genValue(c);
+ });
+
+ const primary = getColor(props['primary']);
+
+ for (let i = 1; i < 10; i++) {
+ const color = primary.clone().setAlpha(i / 10);
+ props['primaryAlpha0' + i] = genValue(color);
+ }
+
+ for (let i = 1; i < 100; i++) {
+ const color = primary.clone().lighten(i);
+ props['primaryLighten' + i] = genValue(color);
+ }
+
+ for (let i = 1; i < 100; i++) {
+ const color = primary.clone().darken(i);
+ props['primaryDarken' + i] = genValue(color);
+ }
+
+ return props;
+}
+
+function genValue(c: tinycolor.Instance): string {
+ return c.toRgbString();
+}
diff --git a/src/client/app/tsconfig.json b/src/client/app/tsconfig.json
index e31b52dab1..4a05469673 100644
--- a/src/client/app/tsconfig.json
+++ b/src/client/app/tsconfig.json
@@ -14,7 +14,8 @@
"removeComments": false,
"noLib": false,
"strict": true,
- "strictNullChecks": false
+ "strictNullChecks": false,
+ "experimentalDecorators": true
},
"compileOnSave": false,
"include": [
diff --git a/src/client/assets/code-highlight.css b/src/client/assets/code-highlight.css
deleted file mode 100644
index f0807dc9c3..0000000000
--- a/src/client/assets/code-highlight.css
+++ /dev/null
@@ -1,93 +0,0 @@
-.hljs {
- font-family: Consolas, 'Courier New', Courier, Monaco, monospace;
-}
-
-.hljs,
-.hljs-subst {
- color: #444;
-}
-
-.hljs-comment {
- color: #888888;
-}
-
-.hljs-keyword {
- color: #2973b7;
-}
-
-.hljs-number {
- color: #ae81ff;
-}
-
-.hljs-string {
- color: #e96900;
-}
-
-.hljs-regexp {
- color: #e9003f;
-}
-
-.hljs-attribute,
-.hljs-selector-tag,
-.hljs-meta-keyword,
-.hljs-doctag,
-.hljs-name {
- font-weight: bold;
-}
-
-.hljs-type,
-.hljs-selector-id,
-.hljs-selector-class,
-.hljs-quote,
-.hljs-template-tag,
-.hljs-deletion {
- color: #880000;
-}
-
-.hljs-title,
-.hljs-section {
- color: #880000;
- font-weight: bold;
-}
-
-.hljs-symbol,
-.hljs-variable,
-.hljs-template-variable,
-.hljs-link,
-.hljs-selector-attr,
-.hljs-selector-pseudo {
- color: #BC6060;
-}
-
-/* Language color: hue: 90; */
-
-.hljs-literal {
- color: #78A960;
-}
-
-.hljs-built_in,
-.hljs-bullet,
-.hljs-code,
-.hljs-addition {
- color: #397300;
-}
-
-/* Meta color: hue: 200 */
-
-.hljs-meta {
- color: #1f7199;
-}
-
-.hljs-meta-string {
- color: #4d99bf;
-}
-
-/* Misc effects */
-
-.hljs-emphasis {
- font-style: italic;
-}
-
-.hljs-strong {
- font-weight: bold;
-}
diff --git a/src/client/assets/pointer.png b/src/client/assets/pointer.png
index 0d03f75d2b..c9aaada5a3 100644
--- a/src/client/assets/pointer.png
+++ b/src/client/assets/pointer.png
Binary files differ
diff --git a/src/client/assets/reactions/angry.png b/src/client/assets/reactions/angry.png
deleted file mode 100644
index 7e32dd6809..0000000000
--- a/src/client/assets/reactions/angry.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/confused.png b/src/client/assets/reactions/confused.png
deleted file mode 100644
index c791854183..0000000000
--- a/src/client/assets/reactions/confused.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/congrats.png b/src/client/assets/reactions/congrats.png
deleted file mode 100644
index fdea27fcb9..0000000000
--- a/src/client/assets/reactions/congrats.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/hmm.png b/src/client/assets/reactions/hmm.png
deleted file mode 100644
index 725fe3898d..0000000000
--- a/src/client/assets/reactions/hmm.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/laugh.png b/src/client/assets/reactions/laugh.png
deleted file mode 100644
index 3b3c10a27a..0000000000
--- a/src/client/assets/reactions/laugh.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/like.png b/src/client/assets/reactions/like.png
deleted file mode 100644
index 526b391f96..0000000000
--- a/src/client/assets/reactions/like.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/love.png b/src/client/assets/reactions/love.png
deleted file mode 100644
index 9fe82cd070..0000000000
--- a/src/client/assets/reactions/love.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/pudding.png b/src/client/assets/reactions/pudding.png
deleted file mode 100644
index e4d10a229d..0000000000
--- a/src/client/assets/reactions/pudding.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/rip.png b/src/client/assets/reactions/rip.png
deleted file mode 100644
index 4800fdb91b..0000000000
--- a/src/client/assets/reactions/rip.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/surprise.png b/src/client/assets/reactions/surprise.png
deleted file mode 100644
index aa55592ded..0000000000
--- a/src/client/assets/reactions/surprise.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/sushi.png b/src/client/assets/reactions/sushi.png
deleted file mode 100644
index c30d44eb15..0000000000
--- a/src/client/assets/reactions/sushi.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/title.light.svg b/src/client/assets/title.light.svg
deleted file mode 100644
index 95ad11c399..0000000000
--- a/src/client/assets/title.light.svg
+++ /dev/null
@@ -1,140 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="614.71039"
- height="205.08009"
- viewBox="0 0 162.64213 54.260776"
- version="1.1"
- id="svg8"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="misskey.svg"
- inkscape:export-filename="C:\Users\Takumiya_Cho\Desktop\misskey.png"
- inkscape:export-xdpi="96"
- inkscape:export-ydpi="96">
- <defs
- id="defs2">
- <inkscape:path-effect
- effect="simplify"
- id="path-effect5115"
- is_visible="true"
- steps="1"
- threshold="0.000408163"
- smooth_angles="360"
- helper_size="0"
- simplify_individual_paths="false"
- simplify_just_coalesce="false"
- simplifyindividualpaths="false"
- simplifyJustCoalesce="false" />
- <inkscape:path-effect
- effect="simplify"
- id="path-effect5104"
- is_visible="true"
- steps="1"
- threshold="0.000408163"
- smooth_angles="360"
- helper_size="0"
- simplify_individual_paths="false"
- simplify_just_coalesce="false"
- simplifyindividualpaths="false"
- simplifyJustCoalesce="false" />
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0.0"
- inkscape:pageshadow="2"
- inkscape:zoom="0.9899495"
- inkscape:cx="370.82839"
- inkscape:cy="79.043895"
- inkscape:document-units="mm"
- inkscape:current-layer="layer1"
- showgrid="false"
- units="px"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="false"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-center="true"
- inkscape:snap-page="true"
- inkscape:window-width="1920"
- inkscape:window-height="1017"
- inkscape:window-x="-8"
- inkscape:window-y="1072"
- inkscape:window-maximized="1"
- inkscape:object-paths="true"
- inkscape:bbox-paths="true"
- fit-margin-top="50"
- fit-margin-left="50"
- fit-margin-bottom="20"
- fit-margin-right="50" />
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="レイヤー 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(-11.097531,-173.29664)">
- <g
- transform="matrix(0.28612302,0,0,0.28612302,17.176981,141.74334)"
- id="text4489-6"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.92471898px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- aria-label="Mi">
- <path
- sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz"
- inkscape:connector-curvature="0"
- id="path5210"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#212d3a;fill-opacity:1;stroke-width:0.92471898px"
- d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" />
- <path
- inkscape:connector-curvature="0"
- id="path5212"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#212d3a;fill-opacity:1;stroke-width:0.92471898px"
- d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" />
- </g>
- <path
- inkscape:connector-curvature="0"
- id="path5199"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 72.022691,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791824,1.29083 2.581666,1.69422 2.581666,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685756,0.0807 1.169817,0.24203 4.477578,0.60508 0.443724,0 0.968125,-0.0403 0.201693,0 0.201693,-0.24203 0.04034,-0.20169 -0.242032,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895911,-0.48406 -1.12948,-0.32271 -1.895912,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685756,0.84711 0.685756,1.93625 0,1.25049 -0.927787,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" />
- <path
- inkscape:connector-curvature="0"
- id="path5201"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 89.577027,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791823,1.29083 2.581667,1.69422 2.581667,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685755,0.0807 1.169818,0.24203 4.477579,0.60508 0.443724,0 0.968125,-0.0403 0.201692,0 0.201692,-0.24203 0.04034,-0.20169 -0.242031,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895912,-0.48406 -1.129479,-0.32271 -1.895911,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685755,0.84711 0.685755,1.93625 0,1.25049 -0.927786,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" />
- <path
- inkscape:connector-curvature="0"
- id="path5203"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 115.65209,203.87137 q 0.12101,0.0807 2.86404,2.78336 1.25049,1.21016 1.25049,2.94471 0,1.61354 -1.16982,2.86404 -1.16982,1.21016 -2.90437,1.21016 -1.65388,0 -2.86404,-1.16982 l -4.03385,-3.91284 q -0.16136,-0.12102 -0.32271,-0.12102 -0.32271,0 -0.32271,1.21016 0,1.69422 -1.21016,2.90438 -1.21015,1.16981 -2.90437,1.16981 -1.69422,0 -2.90438,-1.16981 -1.169807,-1.21016 -1.169807,-2.90438 v -18.79776 q 0,-1.69422 1.169807,-2.86404 1.21016,-1.21015 2.90438,-1.21015 1.69422,0 2.90437,1.21015 1.21016,1.16982 1.21016,2.86404 v 6.29281 q 0,0.40339 0.28237,0.5244 0.24203,0.12102 0.5244,-0.0807 0.16135,-0.0807 4.84063,-3.18675 1.0488,-0.64542 2.25895,-0.64542 2.21862,0 3.42878,1.81524 0.64542,1.0488 0.64542,2.25896 0,2.21862 -1.81524,3.42877 l -2.54133,1.61354 v 0.0403 l -0.0807,0.0403 q -0.56474,0.36305 -0.0403,0.88745 z" />
- <path
- inkscape:connector-curvature="0"
- id="path5205"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 131.25181,213.92955 q -4.19521,0 -7.18026,-2.94472 -2.94472,-2.98505 -2.94472,-7.18026 0,-4.15487 2.94472,-7.09958 2.98505,-2.98505 7.18026,-2.98505 4.15487,0 6.97857,2.78335 0.92778,0.92779 0.92778,2.25896 0,1.33118 -0.92778,2.25896 l -4.67928,4.63893 q -1.00846,1.00847 -2.01692,1.00847 -1.45219,0 -2.25896,-0.80677 -0.80677,-0.80677 -0.80677,-2.13795 0,-1.29083 0.92778,-2.21862 l 0.80678,-0.84711 q 0.16135,-0.12101 0.0807,-0.24203 -0.12101,-0.0807 -0.32271,-0.0403 -0.80677,0.20169 -1.37151,0.80677 -1.12948,1.08914 -1.12948,2.622 0,1.5732 1.08915,2.70268 1.12947,1.08914 2.70268,1.08914 1.53286,0 2.622,-1.12947 0.92779,-0.92779 2.25896,-0.92779 1.33117,0 2.25896,0.92779 0.92779,0.92778 0.92779,2.25895 0,1.33118 -0.92779,2.25896 -2.98505,2.94472 -7.13992,2.94472 z" />
- <path
- inkscape:connector-curvature="0"
- id="path5207"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#212d3a;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 160.51049,198.1433 v 5.60705 q 0,0.56474 -0.0807,1.21016 v 7.38195 q 0,4.51792 -2.74302,7.2206 -2.70268,2.70269 -7.30128,2.70269 -2.66234,0 -4.80028,-1.00847 -2.13795,-0.96812 -2.13795,-3.3481 0,-0.80677 0.36305,-1.53286 0.96812,-2.17828 3.3481,-2.17828 0.56474,0 1.5732,0.32271 1.00847,0.3227 1.65388,0.3227 1.69422,0 2.21862,-0.72609 0.20169,-0.28237 0.0807,-0.44372 -0.16136,-0.24204 -0.56474,-0.16136 -0.68576,0.12102 -1.49253,0.12102 -4.07419,0 -6.97856,-2.90438 -2.90438,-2.90437 -2.90438,-6.97857 v -5.60705 q 0,-1.69422 1.16982,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.90438,1.21016 1.21015,1.16982 1.21015,2.86404 v 5.60705 q 0,0.68576 0.48407,1.21016 0.5244,0.48406 1.21015,0.48406 0.7261,0 1.21016,-0.48406 0.48406,-0.5244 0.48406,-1.21016 v -5.60705 q 0,-1.69422 1.21016,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.86404,1.21016 1.21016,1.16982 1.21016,2.86404 z" />
- </g>
-</svg>
diff --git a/src/client/assets/title.dark.svg b/src/client/assets/title.svg
index 10139024ad..0e4e0b8b3b 100644
--- a/src/client/assets/title.dark.svg
+++ b/src/client/assets/title.svg
@@ -97,44 +97,44 @@
<g
transform="matrix(0.28612302,0,0,0.28612302,17.176981,141.74334)"
id="text4489-6"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.92471898px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.92471898px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
aria-label="Mi">
<path
sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz"
inkscape:connector-curvature="0"
id="path5210"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#fff;fill-opacity:1;stroke-width:0.92471898px"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.92471898px"
d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" />
<path
inkscape:connector-curvature="0"
id="path5212"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#fff;fill-opacity:1;stroke-width:0.92471898px"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill-opacity:1;stroke-width:0.92471898px"
d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" />
</g>
<path
inkscape:connector-curvature="0"
id="path5199"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 72.022691,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791824,1.29083 2.581666,1.69422 2.581666,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685756,0.0807 1.169817,0.24203 4.477578,0.60508 0.443724,0 0.968125,-0.0403 0.201693,0 0.201693,-0.24203 0.04034,-0.20169 -0.242032,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895911,-0.48406 -1.12948,-0.32271 -1.895912,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685756,0.84711 0.685756,1.93625 0,1.25049 -0.927787,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" />
<path
inkscape:connector-curvature="0"
id="path5201"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 89.577027,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791823,1.29083 2.581667,1.69422 2.581667,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685755,0.0807 1.169818,0.24203 4.477579,0.60508 0.443724,0 0.968125,-0.0403 0.201692,0 0.201692,-0.24203 0.04034,-0.20169 -0.242031,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895912,-0.48406 -1.129479,-0.32271 -1.895911,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685755,0.84711 0.685755,1.93625 0,1.25049 -0.927786,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" />
<path
inkscape:connector-curvature="0"
id="path5203"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 115.65209,203.87137 q 0.12101,0.0807 2.86404,2.78336 1.25049,1.21016 1.25049,2.94471 0,1.61354 -1.16982,2.86404 -1.16982,1.21016 -2.90437,1.21016 -1.65388,0 -2.86404,-1.16982 l -4.03385,-3.91284 q -0.16136,-0.12102 -0.32271,-0.12102 -0.32271,0 -0.32271,1.21016 0,1.69422 -1.21016,2.90438 -1.21015,1.16981 -2.90437,1.16981 -1.69422,0 -2.90438,-1.16981 -1.169807,-1.21016 -1.169807,-2.90438 v -18.79776 q 0,-1.69422 1.169807,-2.86404 1.21016,-1.21015 2.90438,-1.21015 1.69422,0 2.90437,1.21015 1.21016,1.16982 1.21016,2.86404 v 6.29281 q 0,0.40339 0.28237,0.5244 0.24203,0.12102 0.5244,-0.0807 0.16135,-0.0807 4.84063,-3.18675 1.0488,-0.64542 2.25895,-0.64542 2.21862,0 3.42878,1.81524 0.64542,1.0488 0.64542,2.25896 0,2.21862 -1.81524,3.42877 l -2.54133,1.61354 v 0.0403 l -0.0807,0.0403 q -0.56474,0.36305 -0.0403,0.88745 z" />
<path
inkscape:connector-curvature="0"
id="path5205"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 131.25181,213.92955 q -4.19521,0 -7.18026,-2.94472 -2.94472,-2.98505 -2.94472,-7.18026 0,-4.15487 2.94472,-7.09958 2.98505,-2.98505 7.18026,-2.98505 4.15487,0 6.97857,2.78335 0.92778,0.92779 0.92778,2.25896 0,1.33118 -0.92778,2.25896 l -4.67928,4.63893 q -1.00846,1.00847 -2.01692,1.00847 -1.45219,0 -2.25896,-0.80677 -0.80677,-0.80677 -0.80677,-2.13795 0,-1.29083 0.92778,-2.21862 l 0.80678,-0.84711 q 0.16135,-0.12101 0.0807,-0.24203 -0.12101,-0.0807 -0.32271,-0.0403 -0.80677,0.20169 -1.37151,0.80677 -1.12948,1.08914 -1.12948,2.622 0,1.5732 1.08915,2.70268 1.12947,1.08914 2.70268,1.08914 1.53286,0 2.622,-1.12947 0.92779,-0.92779 2.25896,-0.92779 1.33117,0 2.25896,0.92779 0.92779,0.92778 0.92779,2.25895 0,1.33118 -0.92779,2.25896 -2.98505,2.94472 -7.13992,2.94472 z" />
<path
inkscape:connector-curvature="0"
id="path5207"
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 160.51049,198.1433 v 5.60705 q 0,0.56474 -0.0807,1.21016 v 7.38195 q 0,4.51792 -2.74302,7.2206 -2.70268,2.70269 -7.30128,2.70269 -2.66234,0 -4.80028,-1.00847 -2.13795,-0.96812 -2.13795,-3.3481 0,-0.80677 0.36305,-1.53286 0.96812,-2.17828 3.3481,-2.17828 0.56474,0 1.5732,0.32271 1.00847,0.3227 1.65388,0.3227 1.69422,0 2.21862,-0.72609 0.20169,-0.28237 0.0807,-0.44372 -0.16136,-0.24204 -0.56474,-0.16136 -0.68576,0.12102 -1.49253,0.12102 -4.07419,0 -6.97856,-2.90438 -2.90438,-2.90437 -2.90438,-6.97857 v -5.60705 q 0,-1.69422 1.16982,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.90438,1.21016 1.21015,1.16982 1.21015,2.86404 v 5.60705 q 0,0.68576 0.48407,1.21016 0.5244,0.48406 1.21015,0.48406 0.7261,0 1.21016,-0.48406 0.48406,-0.5244 0.48406,-1.21016 v -5.60705 q 0,-1.69422 1.21016,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.86404,1.21016 1.21016,1.16982 1.21016,2.86404 z" />
</g>
</svg>
diff --git a/src/client/const.styl b/src/client/const.styl
deleted file mode 100644
index b6560701d9..0000000000
--- a/src/client/const.styl
+++ /dev/null
@@ -1,4 +0,0 @@
-json('../const.json')
-
-$theme-color = themeColor
-$theme-color-foreground = themeColorForeground
diff --git a/src/client/element.scss b/src/client/element.scss
deleted file mode 100644
index 917198e024..0000000000
--- a/src/client/element.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-/* Element variable definitons */
-/* SEE: http://element.eleme.io/#/en-US/component/custom-theme */
-
-@import '../const.json';
-
-/* theme color */
-$--color-primary: $themeColor;
-
-/* icon font path, required */
-$--font-path: '~element-ui/lib/theme-chalk/fonts';
-
-@import "~element-ui/packages/theme-chalk/src/index";
diff --git a/src/client/style.styl b/src/client/style.styl
index 6d1e53e5a6..8ebba2f15e 100644
--- a/src/client/style.styl
+++ b/src/client/style.styl
@@ -1,10 +1,8 @@
@charset 'utf-8'
-@import "./const"
-
/*
::selection
- background $theme-color
+ background var(--primary)
color #fff
*/
@@ -24,10 +22,8 @@ html, body
a
text-decoration none
- color $theme-color
+ color var(--primary)
cursor pointer
- tap-highlight-color rgba($theme-color, 0.7) !important
- -webkit-tap-highlight-color rgba($theme-color, 0.7) !important
&:hover
text-decoration underline
@@ -35,3 +31,9 @@ a
*
cursor pointer
+@css {
+ a {
+ tap-highlight-color: var(--primaryAlpha07) !important;
+ -webkit-tap-highlight-color: var(--primaryAlpha07) !important;
+ }
+}
diff --git a/src/client/theme/black.json5 b/src/client/theme/black.json5
new file mode 100644
index 0000000000..91a812f88a
--- /dev/null
+++ b/src/client/theme/black.json5
@@ -0,0 +1,20 @@
+{
+ id: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2',
+
+ name: 'Future',
+ author: 'syuilo',
+
+ base: 'dark',
+
+ vars: {
+ primary: 'rgb(94, 158, 185)',
+ secondary: 'rgb(22, 24, 30)',
+ text: 'rgb(214, 218, 224)',
+ },
+
+ props: {
+ renoteGradient: '#0a2d3c',
+ renoteText: '$primary',
+ quoteBorder: '$primary',
+ },
+}
diff --git a/src/client/theme/dark.json5 b/src/client/theme/dark.json5
new file mode 100644
index 0000000000..4fa38a3ae0
--- /dev/null
+++ b/src/client/theme/dark.json5
@@ -0,0 +1,209 @@
+{
+ id: 'dark',
+
+ name: 'Dark',
+ author: 'syuilo',
+ desc: 'Default dark theme',
+ kind: 'dark',
+
+ vars: {
+ primary: '#fb4e4e',
+ secondary: '#282C37',
+ text: '#d6dae0',
+ },
+
+ props: {
+ primary: '$primary',
+ primaryForeground: '#fff',
+ secondary: '$secondary',
+ bg: ':darken<8<$secondary',
+ text: '$text',
+
+ scrollbarTrack: ':darken<5<$secondary',
+ scrollbarHandle: ':lighten<5<$secondary',
+ scrollbarHandleHover: ':lighten<10<$secondary',
+
+ face: '$secondary',
+ faceText: '#fff',
+ faceHeader: ':lighten<5<$secondary',
+ faceHeaderText: '#e3e5e8',
+ faceDivider: 'rgba(0, 0, 0, 0.3)',
+ faceTextButton: '$text',
+ faceTextButtonHover: ':lighten<10<$text',
+ faceTextButtonActive: ':darken<10<$text',
+ faceClearButtonHover: 'rgba(0, 0, 0, 0.1)',
+ faceClearButtonActive: 'rgba(0, 0, 0, 0.2)',
+ popupBg: ':lighten<5<$secondary',
+ popupFg: '#d6dce2',
+
+ subNoteBg: 'rgba(0, 0, 0, 0.18)',
+ subNoteText: ':alpha<0.7<$text',
+ renoteGradient: '#314027',
+ renoteText: '#9dbb00',
+ quoteBorder: '#4e945e',
+ noteText: '#fff',
+ noteHeaderName: '#fff',
+ noteHeaderBadgeFg: '#758188',
+ noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.25)',
+ noteHeaderAdminFg: '#f15f71',
+ noteHeaderAdminBg: '#5d282e',
+ noteHeaderAcct: ':alpha<0.65<$text',
+ noteHeaderInfo: ':alpha<0.5<$text',
+
+ noteActions: ':alpha<0.45<$text',
+ noteActionsHover: ':alpha<0.6<$text',
+ noteActionsReplyHover: '#0af',
+ noteActionsRenoteHover: '#8d0',
+ noteActionsReactionHover: '#fa0',
+ noteActionsHighlighted: ':alpha<0.7<$text',
+
+ noteAttachedFile: 'rgba(255, 255, 255, 0.1)',
+
+ modalBackdrop: 'rgba(0, 0, 0, 0.5)',
+
+ dateDividerBg: ':darken<2<$secondary',
+ dateDividerFg: ':alpha<0.7<$text',
+
+ switchTrack: 'rgba(255, 255, 255, 0.15)',
+ radioBorder: 'rgba(255, 255, 255, 0.6)',
+ inputBorder: 'rgba(255, 255, 255, 0.7)',
+ inputLabel: 'rgba(255, 255, 255, 0.7)',
+ inputText: '#fff',
+
+ buttonBg: 'rgba(255, 255, 255, 0.05)',
+ buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+ buttonActiveBg: 'rgba(255, 255, 255, 0.15)',
+
+ autocompleteItemHoverBg: 'rgba(255, 255, 255, 0.1)',
+ autocompleteItemText: 'rgba(255, 255, 255, 0.8)',
+ autocompleteItemTextSub: 'rgba(255, 255, 255, 0.3)',
+
+ cwButtonBg: '#687390',
+ cwButtonFg: '#393f4f',
+ cwButtonHoverBg: '#707b97',
+
+ reactionPickerButtonHoverBg: 'rgba(255, 255, 255, 0.18)',
+
+ reactionViewerBorder: 'rgba(255, 255, 255, 0.1)',
+
+ pollEditorInputBg: 'rgba(0, 0, 0, 0.25)',
+
+ pollChoiceText: '#fff',
+ pollChoiceBorder: 'rgba(255, 255, 255, 0.1)',
+
+ urlPreviewBorder: 'rgba(0, 0, 0, 0.4)',
+ urlPreviewBorderHover: 'rgba(255, 255, 255, 0.2)',
+ urlPreviewTitle: '$text',
+ urlPreviewText: ':alpha<0.7<$text',
+ urlPreviewInfo: ':alpha<0.8<$text',
+
+ calendarWeek: '#43d5dc',
+ calendarSaturdayOrSunday: '#ff6679',
+ calendarDay: '$text',
+
+ materBg: 'rgba(0, 0, 0, 0.3)',
+
+ chartCaption: ':alpha<0.6<$text',
+
+ announcementsBg: '#253a50',
+ announcementsTitle: '#539eff',
+ announcementsText: '#fff',
+
+ donationBg: '#5d5242',
+ donationFg: '#e4dbce',
+
+ googleSearchBg: 'rgba(0, 0, 0, 0.2)',
+ googleSearchFg: '#dee4e8',
+ googleSearchBorder: 'rgba(255, 255, 255, 0.2)',
+ googleSearchHoverBorder: 'rgba(255, 255, 255, 0.3)',
+ googleSearchHoverButton: 'rgba(255, 255, 255, 0.1)',
+
+ mfmTitleBg: 'rgba(0, 0, 0, 0.2)',
+ mfmQuote: ':alpha<0.7<$text',
+ mfmQuoteLine: ':alpha<0.6<$text',
+
+ suspendedInfoBg: '#611d1d',
+ suspendedInfoFg: '#ffb4b4',
+ remoteInfoBg: '#42321c',
+ remoteInfoFg: '#ffbd3e',
+
+ messagingRoomBg: '@bg',
+ messagingRoomInfo: '#fff',
+ messagingRoomDateDividerLine: 'rgba(255, 255, 255, 0.1)',
+ messagingRoomDateDividerText: 'rgba(255, 255, 255, 0.3)',
+ messagingRoomMessageInfo: 'rgba(255, 255, 255, 0.4)',
+ messagingRoomMessageBg: '$secondary',
+ messagingRoomMessageFg: '#fff',
+
+ formButtonBorder: 'rgba(255, 255, 255, 0.1)',
+ formButtonHoverBg: ':alpha<0.2<$primary',
+ formButtonHoverBorder: ':alpha<0.5<$primary',
+ formButtonActiveBg: ':alpha<0.12<$primary',
+
+ desktopHeaderBg: ':lighten<5<$secondary',
+ desktopHeaderFg: '$text',
+ desktopHeaderHoverFg: '#fff',
+ desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.1)',
+ desktopHeaderSearchHoverBg: 'rgba(255, 255, 255, 0.04)',
+ desktopHeaderSearchFg: '#fff',
+ desktopNotificationBg: ':alpha<0.9<$secondary',
+ desktopNotificationFg: ':alpha<0.7<$text',
+ desktopNotificationShadow: 'rgba(0, 0, 0, 0.4)',
+ desktopPostFormBg: '@face',
+ desktopPostFormTextareaBg: 'rgba(0, 0, 0, 0.25)',
+ desktopPostFormTextareaFg: '#fff',
+ desktopPostFormTransparentButtonFg: '$primary',
+ desktopPostFormTransparentButtonActiveGradientStart: ':darken<8<$secondary',
+ desktopPostFormTransparentButtonActiveGradientEnd: ':darken<3<$secondary',
+ desktopRenoteFormFooter: ':lighten<5<$secondary',
+ desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.15)',
+ desktopTimelineSrc: '@faceTextButton',
+ desktopTimelineSrcHover: '@faceTextButtonHover',
+ desktopWindowTitle: '@faceHeaderText',
+ desktopWindowShadow: 'rgba(0, 0, 0, 0.5)',
+ desktopDriveBg: '@bg',
+ desktopDriveFolderBg: ':alpha<0.2<$primary',
+ desktopDriveFolderHoverBg: ':alpha<0.3<$primary',
+ desktopDriveFolderActiveBg: ':alpha<0.3<:darken<10<$primary',
+ desktopDriveFolderFg: '#fff',
+ desktopSettingsNavItem: ':alpha<0.8<$text',
+ desktopSettingsNavItemHover: ':lighten<10<$text',
+
+ deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.25)',
+
+ mobileHeaderBg: ':lighten<5<$secondary',
+ mobileHeaderFg: '$text',
+ mobileNavBackdrop: 'rgba(0, 0, 0, 0.7)',
+ mobilePostFormDivider: 'rgba(0, 0, 0, 0.2)',
+ mobilePostFormTextareaBg: 'rgba(0, 0, 0, 0.3)',
+ mobilePostFormButton: '$text',
+ mobileDriveNavBg: ':alpha<0.75<$secondary',
+ mobileHomeTlItemHover: 'rgba(255, 255, 255, 0.1)',
+ mobileUserPageName: '#fff',
+ mobileUserPageAcct: '$text',
+ mobileUserPageDescription: '$text',
+ mobileUserPageFollowedBg: 'rgba(0, 0, 0, 0.3)',
+ mobileUserPageFollowedFg: '$text',
+ mobileUserPageStatusHighlight: '#fff',
+ mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.3)',
+ mobileAnnouncement: 'rgba(30, 129, 216, 0.2)',
+ mobileAnnouncementFg: '#fff',
+ mobileSignedInAsBg: '#273c34',
+ mobileSignedInAsFg: '#49ab63',
+ mobileSignoutBg: '#652222',
+ mobileSignoutFg: '#ff5f56',
+
+ reversiBannerGradientStart: '#45730e',
+ reversiBannerGradientEnd: '#464300',
+ reversiDescBg: 'rgba(255, 255, 255, 0.1)',
+ reversiListItemShadow: 'rgba(0, 0, 0, 0.7)',
+ reversiMapSelectBorder: 'rgba(255, 255, 255, 0.1)',
+ reversiMapSelectHoverBorder: 'rgba(255, 255, 255, 0.2)',
+ reversiRoomFormShadow: 'rgba(0, 0, 0, 0.7)',
+ reversiRoomFooterBg: ':alpha<0.9<$secondary',
+ reversiGameHeaderLine: ':alpha<0.5<$secondary',
+ reversiGameEmptyCell: ':lighten<2<$secondary',
+ reversiGameEmptyCellMyTurn: ':lighten<5<$secondary',
+ reversiGameEmptyCellCanPut: ':lighten<4<$secondary',
+ },
+}
diff --git a/src/client/theme/halloween.json5 b/src/client/theme/halloween.json5
new file mode 100644
index 0000000000..608105903a
--- /dev/null
+++ b/src/client/theme/halloween.json5
@@ -0,0 +1,21 @@
+{
+ id: '42e4f09b-67d5-498c-af7d-29faa54745b0',
+
+ name: 'Halloween',
+ author: 'syuilo',
+ desc: 'Hello, Happy Halloween!',
+
+ base: 'dark',
+
+ vars: {
+ primary: '#d67036',
+ secondary: '#1f1d30',
+ text: '#b1bee3',
+ },
+
+ props: {
+ renoteGradient: '#5d2d1a',
+ renoteText: '#ff6c00',
+ quoteBorder: '#c3631c',
+ },
+}
diff --git a/src/client/theme/light.json5 b/src/client/theme/light.json5
new file mode 100644
index 0000000000..9f17a63dda
--- /dev/null
+++ b/src/client/theme/light.json5
@@ -0,0 +1,209 @@
+{
+ id: 'light',
+
+ name: 'Light',
+ author: 'syuilo',
+ desc: 'Default light theme',
+ kind: 'light',
+
+ vars: {
+ primary: '#fb4e4e',
+ secondary: '#fff',
+ text: '#666',
+ },
+
+ props: {
+ primary: '$primary',
+ primaryForeground: '#fff',
+ secondary: '$secondary',
+ bg: ':darken<8<$secondary',
+ text: '$text',
+
+ scrollbarTrack: '#fff',
+ scrollbarHandle: '#00000033',
+ scrollbarHandleHover: '#00000066',
+
+ face: '$secondary',
+ faceText: '$text',
+ faceHeader: ':lighten<5<$secondary',
+ faceHeaderText: '$text',
+ faceDivider: 'rgba(0, 0, 0, 0.082)',
+ faceTextButton: ':alpha<0.7<$text',
+ faceTextButtonHover: ':alpha<0.7<:darken<7<$text',
+ faceTextButtonActive: ':alpha<0.7<:darken<10<$text',
+ faceClearButtonHover: 'rgba(0, 0, 0, 0.025)',
+ faceClearButtonActive: 'rgba(0, 0, 0, 0.05)',
+ popupBg: ':lighten<5<$secondary',
+ popupFg: '#586069',
+
+ subNoteBg: 'rgba(0, 0, 0, 0.01)',
+ subNoteText: ':alpha<0.7<$text',
+ renoteGradient: '#edfde2',
+ renoteText: '#9dbb00',
+ quoteBorder: '#c0dac6',
+ noteText: '$text',
+ noteHeaderName: ':darken<2<$text',
+ noteHeaderBadgeFg: '#aaa',
+ noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.05)',
+ noteHeaderAdminFg: '#f15f71',
+ noteHeaderAdminBg: '#ffdfdf',
+ noteHeaderAcct: ':alpha<0.7<@noteHeaderName',
+ noteHeaderInfo: ':alpha<0.7<@noteHeaderName',
+
+ noteActions: ':alpha<0.3<$text',
+ noteActionsHover: ':alpha<0.9<$text',
+ noteActionsReplyHover: '#0af',
+ noteActionsRenoteHover: '#8d0',
+ noteActionsReactionHover: '#fa0',
+ noteActionsHighlighted: '#888',
+
+ noteAttachedFile: 'rgba(0, 0, 0, 0.05)',
+
+ modalBackdrop: 'rgba(0, 0, 0, 0.1)',
+
+ dateDividerBg: ':darken<2<$secondary',
+ dateDividerFg: ':alpha<0.7<$text',
+
+ switchTrack: 'rgba(0, 0, 0, 0.25)',
+ radioBorder: 'rgba(0, 0, 0, 0.4)',
+ inputBorder: 'rgba(0, 0, 0, 0.42)',
+ inputLabel: 'rgba(0, 0, 0, 0.54)',
+ inputText: '#000',
+
+ buttonBg: 'rgba(0, 0, 0, 0.05)',
+ buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
+ buttonActiveBg: 'rgba(0, 0, 0, 0.15)',
+
+ autocompleteItemHoverBg: 'rgba(0, 0, 0, 0.1)',
+ autocompleteItemText: 'rgba(0, 0, 0, 0.8)',
+ autocompleteItemTextSub: 'rgba(0, 0, 0, 0.3)',
+
+ cwButtonBg: '#b1b9c1',
+ cwButtonFg: '#fff',
+ cwButtonHoverBg: '#bbc4ce',
+
+ reactionPickerButtonHoverBg: '#eee',
+
+ reactionViewerBorder: 'rgba(0, 0, 0, 0.1)',
+
+ pollEditorInputBg: '#fff',
+
+ pollChoiceText: '#000',
+ pollChoiceBorder: 'rgba(0, 0, 0, 0.1)',
+
+ urlPreviewBorder: 'rgba(0, 0, 0, 0.1)',
+ urlPreviewBorderHover: 'rgba(0, 0, 0, 0.2)',
+ urlPreviewTitle: '$text',
+ urlPreviewText: ':alpha<0.7<$text',
+ urlPreviewInfo: ':alpha<0.8<$text',
+
+ calendarWeek: '#19a2a9',
+ calendarSaturdayOrSunday: '#ef95a0',
+ calendarDay: '$text',
+
+ materBg: 'rgba(0, 0, 0, 0.1)',
+
+ chartCaption: ':alpha<0.6<$text',
+
+ announcementsBg: '#f3f9ff',
+ announcementsTitle: '#4078c0',
+ announcementsText: '#57616f',
+
+ donationBg: '#fbead4',
+ donationFg: '#777d71',
+
+ googleSearchBg: '#fff',
+ googleSearchFg: '#55595c',
+ googleSearchBorder: 'rgba(0, 0, 0, 0.2)',
+ googleSearchHoverBorder: 'rgba(0, 0, 0, 0.3)',
+ googleSearchHoverButton: 'rgba(0, 0, 0, 0.05)',
+
+ mfmTitleBg: 'rgba(0, 0, 0, 0.07)',
+ mfmQuote: ':alpha<0.6<$text',
+ mfmQuoteLine: ':alpha<0.5<$text',
+
+ suspendedInfoBg: '#ffdbdb',
+ suspendedInfoFg: '#570808',
+ remoteInfoBg: '#fff0db',
+ remoteInfoFg: '#573c08',
+
+ messagingRoomBg: '#fff',
+ messagingRoomInfo: '#000',
+ messagingRoomDateDividerLine: 'rgba(0, 0, 0, 0.1)',
+ messagingRoomDateDividerText: 'rgba(0, 0, 0, 0.3)',
+ messagingRoomMessageInfo: 'rgba(0, 0, 0, 0.4)',
+ messagingRoomMessageBg: '#eee',
+ messagingRoomMessageFg: '#333',
+
+ formButtonBorder: 'rgba(0, 0, 0, 0.1)',
+ formButtonHoverBg: ':alpha<0.12<$primary',
+ formButtonHoverBorder: ':alpha<0.3<$primary',
+ formButtonActiveBg: ':alpha<0.12<$primary',
+
+ desktopHeaderBg: ':lighten<5<$secondary',
+ desktopHeaderFg: '$text',
+ desktopHeaderHoverFg: ':darken<7<$text',
+ desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.05)',
+ desktopHeaderSearchHoverBg: 'rgba(0, 0, 0, 0.08)',
+ desktopHeaderSearchFg: '#000',
+ desktopNotificationBg: ':alpha<0.9<$secondary',
+ desktopNotificationFg: ':alpha<0.7<$text',
+ desktopNotificationShadow: 'rgba(0, 0, 0, 0.2)',
+ desktopPostFormBg: ':lighten<33<$primary',
+ desktopPostFormTextareaBg: '#fff',
+ desktopPostFormTextareaFg: '#333',
+ desktopPostFormTransparentButtonFg: ':alpha<0.5<$primary',
+ desktopPostFormTransparentButtonActiveGradientStart: ':lighten<30<$primary',
+ desktopPostFormTransparentButtonActiveGradientEnd: ':lighten<33<$primary',
+ desktopRenoteFormFooter: ':lighten<33<$primary',
+ desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.08)',
+ desktopTimelineSrc: '$text',
+ desktopTimelineSrcHover: ':darken<7<$text',
+ desktopWindowTitle: '$text',
+ desktopWindowShadow: 'rgba(0, 0, 0, 0.2)',
+ desktopDriveBg: '#fff',
+ desktopDriveFolderBg: ':lighten<31<$primary',
+ desktopDriveFolderHoverBg: ':lighten<27<$primary',
+ desktopDriveFolderActiveBg: ':lighten<25<$primary',
+ desktopDriveFolderFg: ':darken<10<$primary',
+ desktopSettingsNavItem: ':alpha<0.8<$text',
+ desktopSettingsNavItemHover: ':darken<10<$text',
+
+ deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.1)',
+
+ mobileHeaderBg: ':lighten<5<$secondary',
+ mobileHeaderFg: '$text',
+ mobileNavBackdrop: 'rgba(0, 0, 0, 0.2)',
+ mobilePostFormDivider: 'rgba(0, 0, 0, 0.1)',
+ mobilePostFormTextareaBg: '#fff',
+ mobilePostFormButton: '$text',
+ mobileDriveNavBg: ':alpha<0.75<$secondary',
+ mobileHomeTlItemHover: 'rgba(0, 0, 0, 0.05)',
+ mobileUserPageName: '#757c82',
+ mobileUserPageAcct: '#969ea5',
+ mobileUserPageDescription: '#757c82',
+ mobileUserPageFollowedBg: '#a7bec7',
+ mobileUserPageFollowedFg: '#fff',
+ mobileUserPageStatusHighlight: '#787e86',
+ mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.07)',
+ mobileAnnouncement: 'rgba(155, 196, 232, 0.2)',
+ mobileAnnouncementFg: '#3f4967',
+ mobileSignedInAsBg: '#fcfff5',
+ mobileSignedInAsFg: '#2c662d',
+ mobileSignoutBg: '#fff6f5',
+ mobileSignoutFg: '#cc2727',
+
+ reversiBannerGradientStart: '#8bca3e',
+ reversiBannerGradientEnd: '#d6cf31',
+ reversiDescBg: 'rgba(0, 0, 0, 0.1)',
+ reversiListItemShadow: 'rgba(0, 0, 0, 0.15)',
+ reversiMapSelectBorder: 'rgba(0, 0, 0, 0.1)',
+ reversiMapSelectHoverBorder: 'rgba(0, 0, 0, 0.2)',
+ reversiRoomFormShadow: 'rgba(0, 0, 0, 0.1)',
+ reversiRoomFooterBg: ':alpha<0.9<$secondary',
+ reversiGameHeaderLine: '#c4cdd4',
+ reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)',
+ reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)',
+ reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.9)',
+ },
+}
diff --git a/src/client/theme/pink.json5 b/src/client/theme/pink.json5
new file mode 100644
index 0000000000..71e963dc91
--- /dev/null
+++ b/src/client/theme/pink.json5
@@ -0,0 +1,20 @@
+{
+ id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36',
+
+ name: 'Lavender',
+ author: 'sokuyuku & syuilo',
+
+ base: 'light',
+
+ vars: {
+ primary: 'rgb(206, 147, 191)',
+ secondary: 'rgb(253, 242, 243)',
+ text: 'rgb(161, 139, 146)',
+ },
+
+ props: {
+ renoteGradient: '#f7e4ec',
+ renoteText: '$primary',
+ quoteBorder: '$primary',
+ },
+}