summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/app/auth/assets/icon.svg1
-rw-r--r--src/client/app/auth/assets/logo.svg7
-rw-r--r--src/client/app/auth/script.ts1
-rw-r--r--src/client/app/auth/views/index.vue20
-rw-r--r--src/client/app/base.pug10
-rw-r--r--src/client/app/boot.js42
-rw-r--r--src/client/app/ch/script.ts15
-rw-r--r--src/client/app/ch/style.styl10
-rw-r--r--src/client/app/ch/tags/channel.tag409
-rw-r--r--src/client/app/ch/tags/header.tag20
-rw-r--r--src/client/app/ch/tags/index.tag37
-rw-r--r--src/client/app/ch/tags/index.ts3
-rw-r--r--src/client/app/common/define-widget.ts21
-rw-r--r--src/client/app/common/scripts/can-hide-text.ts16
-rw-r--r--src/client/app/common/scripts/check-for-update.ts2
-rw-r--r--src/client/app/common/scripts/compose-notification.ts2
-rw-r--r--src/client/app/common/scripts/streaming/channel.ts13
-rw-r--r--src/client/app/common/scripts/streaming/home.ts54
-rw-r--r--src/client/app/common/scripts/streaming/notes-stats.ts (renamed from src/client/app/common/scripts/streaming/server.ts)10
-rw-r--r--src/client/app/common/scripts/streaming/reversi-game.ts (renamed from src/client/app/common/scripts/streaming/othello-game.ts)4
-rw-r--r--src/client/app/common/scripts/streaming/reversi.ts (renamed from src/client/app/common/scripts/streaming/othello.ts)8
-rw-r--r--src/client/app/common/scripts/streaming/server-stats.ts30
-rw-r--r--src/client/app/common/views/components/acct.vue19
-rw-r--r--src/client/app/common/views/components/analog-clock.vue127
-rw-r--r--src/client/app/common/views/components/avatar.vue13
-rw-r--r--src/client/app/common/views/components/connect-failed.troubleshooter.vue6
-rw-r--r--src/client/app/common/views/components/connect-failed.vue8
-rw-r--r--src/client/app/common/views/components/forkit.vue3
-rw-r--r--src/client/app/common/views/components/index.ts28
-rw-r--r--src/client/app/common/views/components/media-list.vue87
-rw-r--r--src/client/app/common/views/components/menu.vue196
-rw-r--r--src/client/app/common/views/components/messaging-room.form.vue12
-rw-r--r--src/client/app/common/views/components/messaging-room.message.vue32
-rw-r--r--src/client/app/common/views/components/messaging-room.vue94
-rw-r--r--src/client/app/common/views/components/messaging.vue2
-rw-r--r--src/client/app/common/views/components/note-header.vue117
-rw-r--r--src/client/app/common/views/components/note-html.ts13
-rw-r--r--src/client/app/common/views/components/note-menu.vue176
-rw-r--r--src/client/app/common/views/components/poll-editor.vue2
-rw-r--r--src/client/app/common/views/components/poll.vue8
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue2
-rw-r--r--src/client/app/common/views/components/reversi.game.vue (renamed from src/client/app/common/views/components/othello.game.vue)32
-rw-r--r--src/client/app/common/views/components/reversi.gameroom.vue (renamed from src/client/app/common/views/components/othello.gameroom.vue)8
-rw-r--r--src/client/app/common/views/components/reversi.room.vue (renamed from src/client/app/common/views/components/othello.room.vue)18
-rw-r--r--src/client/app/common/views/components/reversi.vue (renamed from src/client/app/common/views/components/othello.vue)34
-rw-r--r--src/client/app/common/views/components/signin.vue121
-rw-r--r--src/client/app/common/views/components/signup.vue194
-rw-r--r--src/client/app/common/views/components/time.vue29
-rw-r--r--src/client/app/common/views/components/twitter-setting.vue14
-rw-r--r--src/client/app/common/views/components/ui/button.vue82
-rw-r--r--src/client/app/common/views/components/ui/card.vue46
-rw-r--r--src/client/app/common/views/components/ui/form.vue30
-rw-r--r--src/client/app/common/views/components/ui/input.vue350
-rw-r--r--src/client/app/common/views/components/ui/radio.vue120
-rw-r--r--src/client/app/common/views/components/ui/select.vue215
-rw-r--r--src/client/app/common/views/components/ui/switch.vue135
-rw-r--r--src/client/app/common/views/components/ui/textarea.vue174
-rw-r--r--src/client/app/common/views/components/uploader.vue2
-rw-r--r--src/client/app/common/views/components/url-preview.vue40
-rw-r--r--src/client/app/common/views/components/visibility-chooser.vue17
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue26
-rw-r--r--src/client/app/common/views/widgets/access-log.vue91
-rw-r--r--src/client/app/common/views/widgets/analog-clock.vue41
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue4
-rw-r--r--src/client/app/common/views/widgets/calendar.vue187
-rw-r--r--src/client/app/common/views/widgets/donation.vue6
-rw-r--r--src/client/app/common/views/widgets/hashtags.chart.vue89
-rw-r--r--src/client/app/common/views/widgets/hashtags.vue118
-rw-r--r--src/client/app/common/views/widgets/index.ts10
-rw-r--r--src/client/app/common/views/widgets/memo.vue111
-rw-r--r--src/client/app/common/views/widgets/posts-monitor.vue211
-rw-r--r--src/client/app/common/views/widgets/rss.vue23
-rw-r--r--src/client/app/common/views/widgets/server.cpu-memory.vue46
-rw-r--r--src/client/app/common/views/widgets/server.vue6
-rw-r--r--src/client/app/common/views/widgets/slideshow.vue2
-rw-r--r--src/client/app/config.ts8
-rw-r--r--src/client/app/desktop/api/choose-drive-file.ts19
-rw-r--r--src/client/app/desktop/api/choose-drive-folder.ts15
-rw-r--r--src/client/app/desktop/api/contextmenu.ts17
-rw-r--r--src/client/app/desktop/api/dialog.ts19
-rw-r--r--src/client/app/desktop/api/input.ts21
-rw-r--r--src/client/app/desktop/api/notify.ts13
-rw-r--r--src/client/app/desktop/api/post.ts21
-rw-r--r--src/client/app/desktop/api/update-avatar.ts32
-rw-r--r--src/client/app/desktop/api/update-banner.ts32
-rw-r--r--src/client/app/desktop/assets/header-icon.dark.svg150
-rw-r--r--src/client/app/desktop/assets/header-icon.light.svg150
-rw-r--r--src/client/app/desktop/assets/header-logo-white.svg25
-rw-r--r--src/client/app/desktop/assets/header-logo.svg25
-rw-r--r--src/client/app/desktop/script.ts29
-rw-r--r--src/client/app/desktop/style.styl56
-rw-r--r--src/client/app/desktop/views/components/activity.calendar.vue6
-rw-r--r--src/client/app/desktop/views/components/activity.chart.vue14
-rw-r--r--src/client/app/desktop/views/components/analog-clock.vue108
-rw-r--r--src/client/app/desktop/views/components/calendar.vue54
-rw-r--r--src/client/app/desktop/views/components/choose-file-from-drive-window.vue11
-rw-r--r--src/client/app/desktop/views/components/choose-folder-from-drive-window.vue6
-rw-r--r--src/client/app/desktop/views/components/context-menu.menu.vue22
-rw-r--r--src/client/app/desktop/views/components/context-menu.vue22
-rw-r--r--src/client/app/desktop/views/components/crop-window.vue6
-rw-r--r--src/client/app/desktop/views/components/drive.file.vue62
-rw-r--r--src/client/app/desktop/views/components/drive.folder.vue38
-rw-r--r--src/client/app/desktop/views/components/drive.nav-folder.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.vue40
-rw-r--r--src/client/app/desktop/views/components/follow-button.vue76
-rw-r--r--src/client/app/desktop/views/components/followers-window.vue9
-rw-r--r--src/client/app/desktop/views/components/followers.vue2
-rw-r--r--src/client/app/desktop/views/components/following-window.vue9
-rw-r--r--src/client/app/desktop/views/components/following.vue2
-rw-r--r--src/client/app/desktop/views/components/friends-maker.vue10
-rw-r--r--src/client/app/desktop/views/components/game-window.vue8
-rw-r--r--src/client/app/desktop/views/components/home.vue126
-rw-r--r--src/client/app/desktop/views/components/index.ts2
-rw-r--r--src/client/app/desktop/views/components/input-dialog.vue4
-rw-r--r--src/client/app/desktop/views/components/media-image.vue2
-rw-r--r--src/client/app/desktop/views/components/mentions.vue125
-rw-r--r--src/client/app/desktop/views/components/messaging-room-window.vue2
-rw-r--r--src/client/app/desktop/views/components/note-detail.sub.vue123
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue64
-rw-r--r--src/client/app/desktop/views/components/note-preview.vue66
-rw-r--r--src/client/app/desktop/views/components/notes.note.sub.vue75
-rw-r--r--src/client/app/desktop/views/components/notes.note.vue138
-rw-r--r--src/client/app/desktop/views/components/notes.vue22
-rw-r--r--src/client/app/desktop/views/components/notifications.vue25
-rw-r--r--src/client/app/desktop/views/components/post-form-window.vue64
-rw-r--r--src/client/app/desktop/views/components/post-form.vue103
-rw-r--r--src/client/app/desktop/views/components/progress-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/received-follow-requests-window.vue72
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue6
-rw-r--r--src/client/app/desktop/views/components/settings-window.vue2
-rw-r--r--src/client/app/desktop/views/components/settings.2fa.vue18
-rw-r--r--src/client/app/desktop/views/components/settings.api.vue4
-rw-r--r--src/client/app/desktop/views/components/settings.password.vue10
-rw-r--r--src/client/app/desktop/views/components/settings.profile.vue31
-rw-r--r--src/client/app/desktop/views/components/settings.vue237
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue11
-rw-r--r--src/client/app/desktop/views/components/taskmanager.vue2
-rw-r--r--src/client/app/desktop/views/components/timeline.core.vue36
-rw-r--r--src/client/app/desktop/views/components/timeline.vue24
-rw-r--r--src/client/app/desktop/views/components/ui-notification.vue14
-rw-r--r--src/client/app/desktop/views/components/ui.header.account.vue31
-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.vue51
-rw-r--r--src/client/app/desktop/views/components/ui.header.notifications.vue39
-rw-r--r--src/client/app/desktop/views/components/ui.header.search.vue6
-rw-r--r--src/client/app/desktop/views/components/ui.header.vue20
-rw-r--r--src/client/app/desktop/views/components/ui.vue44
-rw-r--r--src/client/app/desktop/views/components/user-list-timeline.vue18
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue10
-rw-r--r--src/client/app/desktop/views/components/user-preview.vue10
-rw-r--r--src/client/app/desktop/views/components/users-list.item.vue2
-rw-r--r--src/client/app/desktop/views/components/users-list.vue10
-rw-r--r--src/client/app/desktop/views/components/widget-container.vue8
-rw-r--r--src/client/app/desktop/views/components/window.vue19
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column-core.vue35
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column.vue357
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.list-tl.vue123
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.note.sub.vue77
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.note.vue473
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notes.vue242
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notification.vue179
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notifications-column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notifications.vue229
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl-column.vue76
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl.vue152
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.vue221
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.widgets-column.vue182
-rw-r--r--src/client/app/desktop/views/pages/drive.vue7
-rw-r--r--src/client/app/desktop/views/pages/favorites.vue7
-rw-r--r--src/client/app/desktop/views/pages/home-customize.vue2
-rw-r--r--src/client/app/desktop/views/pages/home.vue33
-rw-r--r--src/client/app/desktop/views/pages/index.vue2
-rw-r--r--src/client/app/desktop/views/pages/messaging-room.vue16
-rw-r--r--src/client/app/desktop/views/pages/reversi.vue (renamed from src/client/app/desktop/views/pages/othello.vue)6
-rw-r--r--src/client/app/desktop/views/pages/search.vue2
-rw-r--r--src/client/app/desktop/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/desktop/views/pages/share.vue58
-rw-r--r--src/client/app/desktop/views/pages/tag.vue128
-rw-r--r--src/client/app/desktop/views/pages/user-list.users.vue6
-rw-r--r--src/client/app/desktop/views/pages/user/user.header.vue8
-rw-r--r--src/client/app/desktop/views/pages/user/user.home.vue2
-rw-r--r--src/client/app/desktop/views/pages/user/user.profile.vue2
-rw-r--r--src/client/app/desktop/views/pages/user/user.timeline.vue25
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue374
-rw-r--r--src/client/app/desktop/views/widgets/activity.vue2
-rw-r--r--src/client/app/desktop/views/widgets/channel.channel.form.vue67
-rw-r--r--src/client/app/desktop/views/widgets/channel.channel.note.vue65
-rw-r--r--src/client/app/desktop/views/widgets/channel.channel.vue106
-rw-r--r--src/client/app/desktop/views/widgets/channel.vue108
-rw-r--r--src/client/app/desktop/views/widgets/index.ts2
-rw-r--r--src/client/app/desktop/views/widgets/polls.vue2
-rw-r--r--src/client/app/desktop/views/widgets/post-form.vue15
-rw-r--r--src/client/app/desktop/views/widgets/profile.vue12
-rw-r--r--src/client/app/init.css40
-rw-r--r--src/client/app/init.ts94
-rw-r--r--src/client/app/mios.ts121
-rw-r--r--src/client/app/mobile/api/post.ts49
-rw-r--r--src/client/app/mobile/script.ts24
-rw-r--r--src/client/app/mobile/style.styl4
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue24
-rw-r--r--src/client/app/mobile/views/components/drive.file.vue2
-rw-r--r--src/client/app/mobile/views/components/drive.vue24
-rw-r--r--src/client/app/mobile/views/components/follow-button.vue106
-rw-r--r--src/client/app/mobile/views/components/friends-maker.vue10
-rw-r--r--src/client/app/mobile/views/components/index.ts2
-rw-r--r--src/client/app/mobile/views/components/media-image.vue12
-rw-r--r--src/client/app/mobile/views/components/note-detail.sub.vue101
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue59
-rw-r--r--src/client/app/mobile/views/components/note-preview.vue83
-rw-r--r--src/client/app/mobile/views/components/note.sub.vue120
-rw-r--r--src/client/app/mobile/views/components/note.vue164
-rw-r--r--src/client/app/mobile/views/components/notes.vue37
-rw-r--r--src/client/app/mobile/views/components/notification-preview.vue35
-rw-r--r--src/client/app/mobile/views/components/notification.vue32
-rw-r--r--src/client/app/mobile/views/components/notifications.vue2
-rw-r--r--src/client/app/mobile/views/components/post-form.vue107
-rw-r--r--src/client/app/mobile/views/components/sub-note-content.vue7
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue81
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue106
-rw-r--r--src/client/app/mobile/views/components/ui.vue6
-rw-r--r--src/client/app/mobile/views/components/user-card.vue23
-rw-r--r--src/client/app/mobile/views/components/user-list-timeline.vue36
-rw-r--r--src/client/app/mobile/views/components/user-preview.vue2
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue11
-rw-r--r--src/client/app/mobile/views/components/users-list.vue2
-rw-r--r--src/client/app/mobile/views/components/widget-container.vue22
-rw-r--r--src/client/app/mobile/views/pages/favorites.vue94
-rw-r--r--src/client/app/mobile/views/pages/followers.vue4
-rw-r--r--src/client/app/mobile/views/pages/following.vue4
-rw-r--r--src/client/app/mobile/views/pages/home.timeline.vue18
-rw-r--r--src/client/app/mobile/views/pages/home.vue46
-rw-r--r--src/client/app/mobile/views/pages/index.vue2
-rw-r--r--src/client/app/mobile/views/pages/messaging-room.vue19
-rw-r--r--src/client/app/mobile/views/pages/messaging.vue1
-rw-r--r--src/client/app/mobile/views/pages/notifications.vue4
-rw-r--r--src/client/app/mobile/views/pages/profile-setting.vue225
-rw-r--r--src/client/app/mobile/views/pages/received-follow-requests.vue78
-rw-r--r--src/client/app/mobile/views/pages/reversi.vue (renamed from src/client/app/mobile/views/pages/othello.vue)10
-rw-r--r--src/client/app/mobile/views/pages/search.vue2
-rw-r--r--src/client/app/mobile/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/mobile/views/pages/settings.vue275
-rw-r--r--src/client/app/mobile/views/pages/settings/settings.profile.vue153
-rw-r--r--src/client/app/mobile/views/pages/share.vue56
-rw-r--r--src/client/app/mobile/views/pages/signup.vue45
-rw-r--r--src/client/app/mobile/views/pages/tag.vue81
-rw-r--r--src/client/app/mobile/views/pages/user-list.vue70
-rw-r--r--src/client/app/mobile/views/pages/user-lists.vue68
-rw-r--r--src/client/app/mobile/views/pages/user.vue9
-rw-r--r--src/client/app/mobile/views/pages/user/home.vue2
-rw-r--r--src/client/app/mobile/views/pages/welcome.vue214
-rw-r--r--src/client/app/mobile/views/pages/widgets.vue (renamed from src/client/app/mobile/views/pages/dashboard.vue)98
-rw-r--r--src/client/app/mobile/views/widgets/activity.vue2
-rw-r--r--src/client/app/mobile/views/widgets/profile.vue6
-rw-r--r--src/client/app/reset.styl3
-rw-r--r--src/client/app/safe.js4
-rw-r--r--src/client/app/store.ts349
-rw-r--r--src/client/assets/manifest.json40
-rw-r--r--src/client/assets/pointer.pngbin0 -> 252860 bytes
-rw-r--r--src/client/assets/reversi-put-me.mp3 (renamed from src/client/assets/othello-put-me.mp3)bin15672 -> 15672 bytes
-rw-r--r--src/client/assets/reversi-put-you.mp3 (renamed from src/client/assets/othello-put-you.mp3)bin26121 -> 26121 bytes
-rw-r--r--src/client/assets/title.dark.svg140
-rw-r--r--src/client/assets/title.light.svg140
-rw-r--r--src/client/assets/title.svg25
-rw-r--r--src/client/assets/version.html5
-rw-r--r--src/client/assets/welcome-bg.dark.svg1
-rw-r--r--src/client/assets/welcome-bg.light.svg1
-rw-r--r--src/client/assets/welcome-bg.svg579
-rw-r--r--src/client/assets/welcome-fg.svg380
-rw-r--r--src/client/docs/api/gulpfile.ts4
-rw-r--r--src/client/docs/gulpfile.ts2
270 files changed, 9578 insertions, 5805 deletions
diff --git a/src/client/app/auth/assets/icon.svg b/src/client/app/auth/assets/icon.svg
new file mode 100644
index 0000000000..36f5d3e404
--- /dev/null
+++ b/src/client/app/auth/assets/icon.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 512 512" width="512" height="512"><defs><clipPath id="_clipPath_P6eAE2OaBltOJ3gHGVajfqsOnfv4xIns"><rect width="512" height="512"/></clipPath></defs><g clip-path="url(#_clipPath_P6eAE2OaBltOJ3gHGVajfqsOnfv4xIns)"><clipPath id="_clipPath_P6q7MZAUp3XpQhVgs2GuAbegX9v4gkom"><rect x="0" y="0" width="512" height="512" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_P6q7MZAUp3XpQhVgs2GuAbegX9v4gkom)"><g id="Group"><g id="g4502"><g id="g5125"><g id="text4489"><path d=" M 190.093 359.243 C 167.923 359.32 148.881 345.963 139.9 330.409 C 135.104 323.615 125.617 321.198 125.482 330.409 L 125.482 372.939 C 125.482 390.026 119.253 404.799 106.794 417.258 C 94.69 429.362 79.917 435.413 62.474 435.413 C 45.387 435.413 30.614 429.362 18.155 417.258 C 6.052 404.799 0 390.026 0 372.939 L 0 139.061 C 0 125.89 3.738 113.965 11.213 103.285 C 19.045 92.25 29.012 84.596 41.116 80.325 C 47.879 77.833 54.999 76.587 62.474 76.587 C 81.697 76.587 97.716 84.062 110.531 99.013 C 117.295 106.489 121.211 110.405 122.279 110.761 C 122.279 110.761 173.043 172.145 174.467 173.213 C 175.891 174.281 180.073 182.446 190.093 182.446 C 200.112 182.446 204.829 174.281 206.253 173.213 C 207.676 172.145 258.44 110.761 258.44 110.761 C 258.796 111.117 262.534 107.201 269.654 99.013 C 282.825 84.062 299.022 76.587 318.245 76.587 C 325.364 76.587 332.484 77.833 339.603 80.325 C 351.707 84.596 361.496 92.25 368.972 103.285 C 376.803 113.965 380.719 125.89 380.719 139.061 L 380.719 372.939 C 380.719 390.026 374.489 404.799 362.03 417.258 C 349.927 429.362 335.154 435.413 317.711 435.413 C 300.624 435.413 285.851 429.362 273.391 417.258 C 261.288 404.799 255.237 390.026 255.237 372.939 L 255.237 330.409 C 254.184 318.802 243.925 326.116 240.285 330.409 C 230.674 348.208 212.262 359.167 190.093 359.243 Z M 457.535 184.448 Q 435.109 184.448 419.09 168.963 Q 403.605 152.944 403.605 130.518 Q 403.605 108.091 419.09 92.606 Q 435.109 76.587 457.535 76.587 Q 479.962 76.587 495.981 92.606 Q 512 108.091 512 130.518 Q 512 152.944 495.981 168.963 Q 479.962 184.448 457.535 184.448 Z M 458.069 195.128 Q 480.496 195.128 495.981 211.147 Q 512 227.166 512 249.592 L 512 381.482 Q 512 403.909 495.981 419.928 Q 480.496 435.413 458.069 435.413 Q 435.643 435.413 419.624 419.928 Q 403.605 403.909 403.605 381.482 L 403.605 249.592 Q 403.605 227.166 419.624 211.147 Q 435.643 195.128 458.069 195.128 Z " fill-rule="evenodd" fill="rgb(157,157,157)"/></g></g></g></g></g></g></svg> \ No newline at end of file
diff --git a/src/client/app/auth/assets/logo.svg b/src/client/app/auth/assets/logo.svg
deleted file mode 100644
index 19b8a2737e..0000000000
--- a/src/client/app/auth/assets/logo.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="1024px" height="512px" viewBox="0 256 1024 512" enable-background="new 0 256 1024 512" xml:space="preserve">
-<polyline opacity="0.5" fill="none" stroke="#000000" stroke-width="34" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
- 896.5,608.5 800.5,416.5 704.5,608.5 608.5,416.5 512.5,608.5 416.5,416.5 320.5,608.5 224.5,416.5 128.5,608.5 "/>
-</svg>
diff --git a/src/client/app/auth/script.ts b/src/client/app/auth/script.ts
index 20f59bf033..fd985c46ad 100644
--- a/src/client/app/auth/script.ts
+++ b/src/client/app/auth/script.ts
@@ -20,6 +20,7 @@ init(launch => {
// Init router
const router = new VueRouter({
mode: 'history',
+ base: '/auth/',
routes: [
{ path: '/:token', component: Index },
]
diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
index 0fcd9bfe53..6d0ba3cda3 100644
--- a/src/client/app/auth/views/index.vue
+++ b/src/client/app/auth/views/index.vue
@@ -1,8 +1,9 @@
<template>
<div class="index">
- <main v-if="os.isSignedIn">
+ <main v-if="$store.getters.isSignedIn">
<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
<x-form
+ class="form"
ref="form"
v-if="state == 'waiting'"
:session="session"
@@ -22,11 +23,11 @@
<p>セッションが存在しません。</p>
</div>
</main>
- <main class="signin" v-if="!os.isSignedIn">
+ <main class="signin" v-if="!$store.getters.isSignedIn">
<h1>サインインしてください</h1>
<mk-signin/>
</main>
- <footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
+ <footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer>
</div>
</template>
@@ -51,7 +52,7 @@ export default Vue.extend({
}
},
mounted() {
- if (!this.$root.$data.os.isSignedIn) return;
+ if (!this.$store.getters.isSignedIn) return;
// Fetch session
(this as any).api('auth/session/show', {
@@ -62,7 +63,7 @@ export default Vue.extend({
// 既に連携していた場合
if (this.session.app.isAuthorized) {
- this.$root.$data.os.api('auth/accept', {
+ (this as any).api('auth/accept', {
token: this.session.token
}).then(() => {
this.accepted();
@@ -72,6 +73,7 @@ export default Vue.extend({
}
}).catch(error => {
this.state = 'fetch-session-error';
+ this.fetching = false;
});
},
methods: {
@@ -101,7 +103,7 @@ export default Vue.extend({
padding 32px
color #555
- > div
+ > div:not(.form)
padding 64px
> h1
@@ -142,8 +144,8 @@ export default Vue.extend({
> footer
> img
display block
- width 64px
- height 64px
- margin 0 auto
+ width 32px
+ height 32px
+ margin 16px auto
</style>
diff --git a/src/client/app/base.pug b/src/client/app/base.pug
index c182fd6f64..11b150bc67 100644
--- a/src/client/app/base.pug
+++ b/src/client/app/base.pug
@@ -19,7 +19,7 @@ html
| Misskey
block desc
- meta(name='description' content='A SNS')
+ meta(name='description' content='A planet of fediverse')
block meta
@@ -42,7 +42,7 @@ html
| JavaScriptを有効にしてください
br
| Please turn on your JavaScript
- div#ini: p
- span .
- span .
- span .
+ div#ini.
+ <svg viewBox="0 0 50 50">
+ <path fill=#{themeColor} d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
+ </svg>
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 35d02cf9c5..08c3fdeaee 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -18,22 +18,35 @@
return;
}
+ //#region Load settings
+ let settings = null;
+ const vuex = localStorage.getItem('vuex');
+ if (vuex) {
+ settings = JSON.parse(vuex);
+ }
+ //#endregion
+
// Get the current url information
const url = new URL(location.href);
//#region Detect app name
let app = null;
- if (url.pathname == '/docs') app = 'docs';
- if (url.pathname == '/dev') app = 'dev';
- if (url.pathname == '/auth') app = 'auth';
+ if (url.pathname == '/docs' || url.pathname.startsWith('/docs/')) app = 'docs';
+ if (url.pathname == '/dev' || url.pathname.startsWith('/dev/')) app = 'dev';
+ if (url.pathname == '/auth' || url.pathname.startsWith('/auth/')) app = 'auth';
//#endregion
- // Detect the user language
- // Note: The default language is Japanese
+ //#region Detect the user language
let lang = navigator.language.split('-')[0];
- if (!/^(en|ja)$/.test(lang)) lang = 'ja';
- if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
+
+ // The default language is English
+ if (!LANGS.includes(lang)) lang = 'en';
+
+ if (settings) {
+ if (settings.device.lang) lang = settings.device.lang;
+ }
+ //#endregion
// Detect the user agent
const ua = navigator.userAgent.toLowerCase();
@@ -61,20 +74,15 @@
}
// Dark/Light
- if (localStorage.getItem('darkmode') == 'true') {
- document.documentElement.setAttribute('data-darkmode', 'true');
+ if (settings) {
+ if (settings.device.darkmode) {
+ document.documentElement.setAttribute('data-darkmode', 'true');
+ }
}
// Script version
const ver = localStorage.getItem('v') || VERSION;
- // Whether in debug mode
- const isDebug = localStorage.getItem('debug') == 'true';
-
- // Whether use raw version script
- const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
- || ENV != 'production';
-
// Get salt query
const salt = localStorage.getItem('salt')
? '?salt=' + localStorage.getItem('salt')
@@ -84,7 +92,7 @@
// Note: 'async' make it possible to load the script asyncly.
// 'defer' make it possible to run the script when the dom loaded.
const script = document.createElement('script');
- script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js${salt}`);
+ script.setAttribute('src', `/assets/${app}.${ver}.${lang}.js${salt}`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
diff --git a/src/client/app/ch/script.ts b/src/client/app/ch/script.ts
deleted file mode 100644
index 4c6b6dfd1b..0000000000
--- a/src/client/app/ch/script.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Channels
- */
-
-// Style
-import './style.styl';
-
-require('./tags');
-import init from '../init';
-
-/**
- * init
- */
-init(() => {
-});
diff --git a/src/client/app/ch/style.styl b/src/client/app/ch/style.styl
deleted file mode 100644
index 21ca648cbe..0000000000
--- a/src/client/app/ch/style.styl
+++ /dev/null
@@ -1,10 +0,0 @@
-@import "../app"
-
-html
- padding 8px
- background #efefef
-
-#wait
- top auto
- bottom 15px
- left 15px
diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
deleted file mode 100644
index 74b1a9ba19..0000000000
--- a/src/client/app/ch/tags/channel.tag
+++ /dev/null
@@ -1,409 +0,0 @@
-<mk-channel>
- <mk-header/>
- <hr>
- <main v-if="!fetching">
- <h1>{ channel.title }</h1>
-
- <div v-if="$root.$data.os.isSignedIn">
- <p v-if="channel.isWatching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
- <p v-if="!channel.isWatching"><a @click="watch">このチャンネルをウォッチする</a></p>
- </div>
-
- <div class="share">
- <mk-twitter-button/>
- <mk-line-button/>
- </div>
-
- <div class="body">
- <p v-if="notesFetching">読み込み中<mk-ellipsis/></p>
- <div v-if="!notesFetching">
- <p v-if="notes == null || notes.length == 0">まだ投稿がありません</p>
- <template v-if="notes != null">
- <mk-channel-note each={ note in notes.slice().reverse() } note={ note } form={ parent.refs.form }/>
- </template>
- </div>
- </div>
- <hr>
- <mk-channel-form v-if="$root.$data.os.isSignedIn" channel={ channel } ref="form"/>
- <div v-if="!$root.$data.os.isSignedIn">
- <p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
- </div>
- <hr>
- <footer>
- <small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
- </footer>
- </main>
- <style lang="stylus" scoped>
- :scope
- display block
-
- > main
- > h1
- font-size 1.5em
- color #f00
-
- > .share
- > *
- margin-right 4px
-
- > .body
- margin 8px 0 0 0
-
- > mk-channel-form
- max-width 500px
-
- </style>
- <script lang="typescript">
- import Progress from '../../common/scripts/loading';
- import ChannelStream from '../../common/scripts/streaming/channel-stream';
-
- this.mixin('i');
- this.mixin('api');
-
- this.id = this.opts.id;
- this.fetching = true;
- this.notesFetching = true;
- this.channel = null;
- this.notes = null;
- this.connection = new ChannelStream(this.id);
- this.unreadCount = 0;
-
- this.on('mount', () => {
- document.documentElement.style.background = '#efefef';
-
- Progress.start();
-
- let fetched = false;
-
- // チャンネル概要読み込み
- this.$root.$data.os.api('channels/show', {
- channelId: this.id
- }).then(channel => {
- if (fetched) {
- Progress.done();
- } else {
- Progress.set(0.5);
- fetched = true;
- }
-
- this.update({
- fetching: false,
- channel: channel
- });
-
- document.title = channel.title + ' | Misskey'
- });
-
- // 投稿読み込み
- this.$root.$data.os.api('channels/notes', {
- channelId: this.id
- }).then(notes => {
- if (fetched) {
- Progress.done();
- } else {
- Progress.set(0.5);
- fetched = true;
- }
-
- this.update({
- notesFetching: false,
- notes: notes
- });
- });
-
- this.connection.on('note', this.onNote);
- document.addEventListener('visibilitychange', this.onVisibilitychange, false);
- });
-
- this.on('unmount', () => {
- this.connection.off('note', this.onNote);
- this.connection.close();
- document.removeEventListener('visibilitychange', this.onVisibilitychange);
- });
-
- this.onNote = note => {
- this.notes.unshift(note);
- this.update();
-
- if (document.hidden && this.$root.$data.os.isSignedIn && note.userId !== this.$root.$data.os.i.id) {
- this.unreadCount++;
- document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
- }
- };
-
- this.onVisibilitychange = () => {
- if (!document.hidden) {
- this.unreadCount = 0;
- document.title = this.channel.title + ' | Misskey'
- }
- };
-
- this.watch = () => {
- this.$root.$data.os.api('channels/watch', {
- channelId: this.id
- }).then(() => {
- this.channel.isWatching = true;
- this.update();
- }, e => {
- alert('error');
- });
- };
-
- this.unwatch = () => {
- this.$root.$data.os.api('channels/unwatch', {
- channelId: this.id
- }).then(() => {
- this.channel.isWatching = false;
- this.update();
- }, e => {
- alert('error');
- });
- };
- </script>
-</mk-channel>
-
-<mk-channel-note>
- <header>
- <a class="index" @click="reply">{ note.index }:</a>
- <a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(note.user) }</b></a>
- <mk-time time={ note.createdAt }/>
- <mk-time time={ note.createdAt } mode="detail"/>
- <span>ID:<i>{ acct }</i></span>
- </header>
- <div>
- <a v-if="note.reply">&gt;&gt;{ note.reply.index }</a>
- { note.text }
- <div class="media" v-if="note.media">
- <template each={ file in note.media }>
- <a href={ file.url } target="_blank">
- <img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
- </a>
- </template>
- </div>
- </div>
- <style lang="stylus" scoped>
- :scope
- display block
- margin 0
- padding 0
-
- > header
- position -webkit-sticky
- position sticky
- z-index 1
- top 0
- background rgba(239, 239, 239, 0.9)
-
- > .index
- margin-right 0.25em
- color #000
-
- > .name
- margin-right 0.5em
- color #008000
-
- > mk-time
- margin-right 0.5em
-
- &:first-of-type
- display none
-
- @media (max-width 600px)
- > mk-time
- &:first-of-type
- display initial
-
- &:last-of-type
- display none
-
- > div
- padding 0 0 1em 2em
-
- > .media
- > a
- display inline-block
-
- > img
- max-width 100%
- vertical-align bottom
-
- </style>
- <script lang="typescript">
- import getAcct from '../../../../acct/render';
- import getUserName from '../../../../renderers/get-user-name';
-
- this.note = this.opts.note;
- this.form = this.opts.form;
- this.acct = getAcct(this.note.user);
- this.name = getUserName(this.note.user);
-
- this.reply = () => {
- this.form.update({
- reply: this.note
- });
- };
- </script>
-</mk-channel-note>
-
-<mk-channel-form>
- <p v-if="reply"><b>&gt;&gt;{ reply.index }</b> ({ getUserName(reply.user) }): <a @click="clearReply">[x]</a></p>
- <textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
- <div class="actions">
- <button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
- <button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
- <button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="note">
- <template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:!ch.tags.mk-channel-form.posting%' : '%i18n:!ch.tags.mk-channel-form.note%' }<mk-ellipsis v-if="wait"/>
- </button>
- </div>
- <mk-uploader ref="uploader"/>
- <ol v-if="files">
- <li each={ files }>{ name }</li>
- </ol>
- <input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
- <style lang="stylus" scoped>
- :scope
- display block
-
- > textarea
- width 100%
- max-width 100%
- min-width 100%
- min-height 5em
-
- > .actions
- display flex
-
- > button
- > [data-fa]
- margin-right 0.25em
-
- &:last-child
- margin-left auto
-
- &.wait
- cursor wait
-
- > input[type='file']
- display none
-
- </style>
- <script lang="typescript">
- import getUserName from '../../../../renderers/get-user-name';
-
- this.mixin('api');
-
- this.channel = this.opts.channel;
- this.files = null;
-
- this.on('mount', () => {
- this.$refs.uploader.on('uploaded', file => {
- this.update({
- files: [file]
- });
- });
- });
-
- this.upload = file => {
- this.$refs.uploader.upload(file);
- };
-
- this.clearReply = () => {
- this.update({
- reply: null
- });
- };
-
- this.clear = () => {
- this.clearReply();
- this.update({
- files: null
- });
- this.$refs.text.value = '';
- };
-
- this.note = () => {
- this.update({
- wait: true
- });
-
- const files = this.files && this.files.length > 0
- ? this.files.map(f => f.id)
- : undefined;
-
- this.$root.$data.os.api('notes/create', {
- text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
- mediaIds: files,
- replyId: this.reply ? this.reply.id : undefined,
- channelId: this.channel.id
- }).then(data => {
- this.clear();
- }).catch(err => {
- alert('失敗した');
- }).then(() => {
- this.update({
- wait: false
- });
- });
- };
-
- this.changeFile = () => {
- Array.from(this.$refs.file.files).forEach(this.upload);
- };
-
- this.selectFile = () => {
- this.$refs.file.click();
- };
-
- this.drive = () => {
- window['cb'] = files => {
- this.update({
- files: files
- });
- };
-
- window.open(_URL_ + '/selectdrive?multiple=true',
- 'drive_window',
- 'height=500,width=800');
- };
-
- this.onkeydown = e => {
- if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
- };
-
- this.onpaste = e => {
- Array.from(e.clipboardData.items).forEach(item => {
- if (item.kind == 'file') {
- this.upload(item.getAsFile());
- }
- });
- };
-
- this.getUserName = getUserName;
- </script>
-</mk-channel-form>
-
-<mk-twitter-button>
- <a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
- <script lang="typescript">
- this.on('mount', () => {
- const head = document.getElementsByTagName('head')[0];
- const script = document.createElement('script');
- script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
- script.setAttribute('async', 'async');
- head.appendChild(script);
- });
- </script>
-</mk-twitter-button>
-
-<mk-line-button>
- <div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div>
- <script lang="typescript">
- this.on('mount', () => {
- const head = document.getElementsByTagName('head')[0];
- const script = document.createElement('script');
- script.setAttribute('src', 'https://d.line-scdn.net/r/web/social-plugin/js/thirdparty/loader.min.js');
- script.setAttribute('async', 'async');
- head.appendChild(script);
- });
- </script>
-</mk-line-button>
diff --git a/src/client/app/ch/tags/header.tag b/src/client/app/ch/tags/header.tag
deleted file mode 100644
index 901123d63b..0000000000
--- a/src/client/app/ch/tags/header.tag
+++ /dev/null
@@ -1,20 +0,0 @@
-<mk-header>
- <div>
- <a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
- </div>
- <div>
- <a v-if="!$root.$data.os.isSignedIn" href={ _URL_ }>ログイン(新規登録)</a>
- <a v-if="$root.$data.os.isSignedIn" href={ _URL_ + '/@' + I.username }>{ I.username }</a>
- </div>
- <style lang="stylus" scoped>
- :scope
- display flex
-
- > div:last-child
- margin-left auto
-
- </style>
- <script lang="typescript">
- this.mixin('i');
- </script>
-</mk-header>
diff --git a/src/client/app/ch/tags/index.tag b/src/client/app/ch/tags/index.tag
deleted file mode 100644
index 529b83b2c7..0000000000
--- a/src/client/app/ch/tags/index.tag
+++ /dev/null
@@ -1,37 +0,0 @@
-<mk-index>
- <mk-header/>
- <hr>
- <button @click="n">%i18n:ch.tags.mk-index.new%</button>
- <hr>
- <ul v-if="channels">
- <li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
- </ul>
- <style lang="stylus" scoped>
- :scope
- display block
-
- </style>
- <script lang="typescript">
- this.mixin('api');
-
- this.on('mount', () => {
- this.$root.$data.os.api('channels', {
- limit: 100
- }).then(channels => {
- this.update({
- channels: channels
- });
- });
- });
-
- this.n = () => {
- const title = window.prompt('%i18n:!ch.tags.mk-index.channel-title%');
-
- this.$root.$data.os.api('channels/create', {
- title: title
- }).then(channel => {
- location.href = '/' + channel.id;
- });
- };
- </script>
-</mk-index>
diff --git a/src/client/app/ch/tags/index.ts b/src/client/app/ch/tags/index.ts
deleted file mode 100644
index 12ffdaeb84..0000000000
--- a/src/client/app/ch/tags/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-require('./index.tag');
-require('./channel.tag');
-require('./header.tag');
diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 0b2bc36566..2fae28be72 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -9,9 +9,9 @@ export default function<T extends object>(data: {
widget: {
type: Object
},
- isMobile: {
- type: Boolean,
- default: false
+ platform: {
+ type: String,
+ required: true
},
isCustomizeMode: {
type: Boolean,
@@ -66,17 +66,10 @@ export default function<T extends object>(data: {
this.bakeProps();
- if (this.isMobile) {
- (this as any).api('i/update_mobile_home', {
- id: this.id,
- data: this.props
- });
- } else {
- (this as any).api('i/update_home', {
- id: this.id,
- data: this.props
- });
- }
+ (this as any).api('i/update_widget', {
+ id: this.id,
+ data: this.props
+ });
}
}
});
diff --git a/src/client/app/common/scripts/can-hide-text.ts b/src/client/app/common/scripts/can-hide-text.ts
new file mode 100644
index 0000000000..4a4be8d9d0
--- /dev/null
+++ b/src/client/app/common/scripts/can-hide-text.ts
@@ -0,0 +1,16 @@
+export default function(note) {
+ if (note.text == null) return true;
+
+ let txt = note.text;
+
+ if (note.media) {
+ note.media.forEach(file => {
+ txt = txt.replace(file.url, '');
+ if (file.src) txt = txt.replace(file.src, '');
+ });
+
+ if (txt == '') return true;
+ }
+
+ return false;
+}
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 1e303017eb..b5ba6916d1 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -23,7 +23,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
}
if (!silent) {
- alert('%i18n:!common.update-available%'.replace('{newer}', newer).replace('{current}', current));
+ alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current));
}
return newer;
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index c19b1c5ad0..cc28f75998 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -55,7 +55,7 @@ export default function(type, data): Notification {
icon: data.user.avatarUrl + '?thumbnail&size=64'
};
- case 'othello_invited':
+ case 'reversi_invited':
return {
title: '対局への招待があります',
body: `${getUserName(data.parent)}さんから`,
diff --git a/src/client/app/common/scripts/streaming/channel.ts b/src/client/app/common/scripts/streaming/channel.ts
deleted file mode 100644
index be68ec0997..0000000000
--- a/src/client/app/common/scripts/streaming/channel.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import Stream from './stream';
-import MiOS from '../../../mios';
-
-/**
- * Channel stream connection
- */
-export default class Connection extends Stream {
- constructor(os: MiOS, channelId) {
- super(os, 'channel', {
- channel: channelId
- });
- }
-}
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 32685f3c2c..dd18c70d70 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -1,5 +1,3 @@
-import * as merge from 'object-assign-deep';
-
import Stream from './stream';
import StreamManager from './stream-manager';
import MiOS from '../../../mios';
@@ -20,14 +18,36 @@ export class HomeStream extends Stream {
}, 1000 * 60);
// 自分の情報が更新されたとき
- this.on('i_updated', i => {
+ this.on('meUpdated', i => {
if (os.debug) {
console.log('I updated:', i);
}
- merge(me, i);
- // キャッシュ更新
- os.bakeMe();
+ 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 => {
@@ -38,20 +58,24 @@ export class HomeStream extends Stream {
});
this.on('home_updated', x => {
- if (x.home) {
- os.store.commit('settings/setHome', x.home);
- } else {
- os.store.commit('settings/setHomeWidget', {
- id: x.id,
- data: x.data
- });
- }
+ 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%');
+ alert('%i18n:common.my-token-regenerated%');
os.signout();
});
}
diff --git a/src/client/app/common/scripts/streaming/server.ts b/src/client/app/common/scripts/streaming/notes-stats.ts
index 2ea4239288..9e3e78a709 100644
--- a/src/client/app/common/scripts/streaming/server.ts
+++ b/src/client/app/common/scripts/streaming/notes-stats.ts
@@ -3,15 +3,15 @@ import StreamManager from './stream-manager';
import MiOS from '../../../mios';
/**
- * Server stream connection
+ * Notes stats stream connection
*/
-export class ServerStream extends Stream {
+export class NotesStatsStream extends Stream {
constructor(os: MiOS) {
- super(os, 'server');
+ super(os, 'notes-stats');
}
}
-export class ServerStreamManager extends StreamManager<ServerStream> {
+export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> {
private os: MiOS;
constructor(os: MiOS) {
@@ -22,7 +22,7 @@ export class ServerStreamManager extends StreamManager<ServerStream> {
public getConnection() {
if (this.connection == null) {
- this.connection = new ServerStream(this.os);
+ this.connection = new NotesStatsStream(this.os);
}
return this.connection;
diff --git a/src/client/app/common/scripts/streaming/othello-game.ts b/src/client/app/common/scripts/streaming/reversi-game.ts
index 9e36f647bb..5638b3013f 100644
--- a/src/client/app/common/scripts/streaming/othello-game.ts
+++ b/src/client/app/common/scripts/streaming/reversi-game.ts
@@ -1,9 +1,9 @@
import Stream from './stream';
import MiOS from '../../../mios';
-export class OthelloGameStream extends Stream {
+export class ReversiGameStream extends Stream {
constructor(os: MiOS, me, game) {
- super(os, 'othello-game', {
+ super(os, 'reversi-game', {
i: me ? me.token : null,
game: game.id
});
diff --git a/src/client/app/common/scripts/streaming/othello.ts b/src/client/app/common/scripts/streaming/reversi.ts
index 8f4f217e39..2e4395f0f1 100644
--- a/src/client/app/common/scripts/streaming/othello.ts
+++ b/src/client/app/common/scripts/streaming/reversi.ts
@@ -2,15 +2,15 @@ import StreamManager from './stream-manager';
import Stream from './stream';
import MiOS from '../../../mios';
-export class OthelloStream extends Stream {
+export class ReversiStream extends Stream {
constructor(os: MiOS, me) {
- super(os, 'othello', {
+ super(os, 'reversi', {
i: me.token
});
}
}
-export class OthelloStreamManager extends StreamManager<OthelloStream> {
+export class ReversiStreamManager extends StreamManager<ReversiStream> {
private me;
private os: MiOS;
@@ -23,7 +23,7 @@ export class OthelloStreamManager extends StreamManager<OthelloStream> {
public getConnection() {
if (this.connection == null) {
- this.connection = new OthelloStream(this.os, this.me);
+ this.connection = new ReversiStream(this.os, this.me);
}
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
new file mode 100644
index 0000000000..9983dfcaf0
--- /dev/null
+++ b/src/client/app/common/scripts/streaming/server-stats.ts
@@ -0,0 +1,30 @@
+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/views/components/acct.vue b/src/client/app/common/views/components/acct.vue
new file mode 100644
index 0000000000..1ad222afdd
--- /dev/null
+++ b/src/client/app/common/views/components/acct.vue
@@ -0,0 +1,19 @@
+<template>
+<span class="mk-acct">
+ <span class="name">@{{ user.username }}</span>
+ <span class="host" v-if="user.host">@{{ user.host }}</span>
+</span>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-acct
+ > .host
+ opacity 0.5
+</style>
diff --git a/src/client/app/common/views/components/analog-clock.vue b/src/client/app/common/views/components/analog-clock.vue
new file mode 100644
index 0000000000..53fb2a8dad
--- /dev/null
+++ b/src/client/app/common/views/components/analog-clock.vue
@@ -0,0 +1,127 @@
+<template>
+<svg class="mk-analog-clock" viewBox="0 0 10 10" preserveAspectRatio="none">
+ <circle v-for="angle, i in graduations"
+ :cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
+ :cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
+ :r="i % 5 == 0 ? 0.125 : 0.05"
+ :fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"/>
+
+ <line
+ :x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))"
+ :y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))"
+ :x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
+ :y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
+ :stroke="sHandColor"
+ stroke-width="0.05"/>
+ <line
+ :x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
+ :y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
+ :x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
+ :y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
+ :stroke="mHandColor"
+ stroke-width="0.1"/>
+ <line
+ :x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
+ :y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
+ :x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
+ :y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
+ :stroke="hHandColor"
+ stroke-width="0.1"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { themeColor } from '../../../config';
+
+export default Vue.extend({
+ props: {
+ dark: {
+ type: Boolean,
+ default: false
+ }
+ },
+
+ data() {
+ return {
+ now: new Date(),
+ clock: null,
+
+ graduationsPadding: 0.5,
+ handsPadding: 1,
+ handsTailLength: 0.7,
+ hHandLengthRatio: 0.75,
+ mHandLengthRatio: 1,
+ sHandLengthRatio: 1
+ };
+ },
+
+ computed: {
+ majorGraduationColor(): string {
+ return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
+ },
+ minorGraduationColor(): string {
+ return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+ },
+
+ sHandColor(): string {
+ return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
+ },
+ mHandColor(): string {
+ return this.dark ? '#fff' : '#777';
+ },
+ hHandColor(): string {
+ return themeColor;
+ },
+
+ s(): number {
+ return this.now.getSeconds();
+ },
+ m(): number {
+ return this.now.getMinutes();
+ },
+ h(): number {
+ return this.now.getHours();
+ },
+
+ hAngle(): number {
+ return Math.PI * (this.h % 12 + this.m / 60) / 6;
+ },
+ mAngle(): number {
+ return Math.PI * (this.m + this.s / 60) / 30;
+ },
+ sAngle(): number {
+ return Math.PI * this.s / 30;
+ },
+
+ graduations(): any {
+ const angles = [];
+ for (let i = 0; i < 60; i++) {
+ const angle = Math.PI * i / 30;
+ angles.push(angle);
+ }
+
+ return angles;
+ }
+ },
+
+ mounted() {
+ this.clock = setInterval(this.tick, 1000);
+ },
+
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+
+ methods: {
+ tick() {
+ this.now = new Date();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-analog-clock
+ display block
+</style>
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index a4648c272e..a65b62882f 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -21,11 +21,18 @@ export default Vue.extend({
}
},
computed: {
+ lightmode(): boolean {
+ return this.$store.state.device.lightmode;
+ },
style(): any {
return {
- backgroundColor: this.user.avatarColor ? `rgb(${ this.user.avatarColor.join(',') })` : null,
- backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
- borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
+ backgroundColor: this.lightmode
+ ? `rgb(${ this.user.avatarColor.slice(0, 3).join(',') })`
+ : this.user.avatarColor && this.user.avatarColor.length == 3
+ ? `rgb(${ this.user.avatarColor.join(',') })`
+ : null,
+ backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`,
+ borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
};
}
}
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 6a922676b7..6c23cc7969 100644
--- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
@@ -8,21 +8,21 @@
<template v-if="network">%fa:check%</template>
<template v-if="!network">%fa:times%</template>
</template>
- {{ network == null ? '%i18n:!@checking-network%' : '%i18n:!@network%' }}<mk-ellipsis v-if="network == null"/>
+ {{ network == null ? '%i18n:@checking-network%' : '%i18n:@network%' }}<mk-ellipsis v-if="network == null"/>
</p>
<p v-if="network == true" :data-wip="internet == null">
<template v-if="internet != null">
<template v-if="internet">%fa:check%</template>
<template v-if="!internet">%fa:times%</template>
</template>
- {{ internet == null ? '%i18n:!@checking-internet%' : '%i18n:!@internet%' }}<mk-ellipsis v-if="internet == null"/>
+ {{ internet == null ? '%i18n:@checking-internet%' : '%i18n:@internet%' }}<mk-ellipsis v-if="internet == null"/>
</p>
<p v-if="internet == true" :data-wip="server == null">
<template v-if="server != null">
<template v-if="server">%fa:check%</template>
<template v-if="!server">%fa:times%</template>
</template>
- {{ server == null ? '%i18n:!@checking-server%' : '%i18n:!@server%' }}<mk-ellipsis v-if="server == null"/>
+ {{ server == null ? '%i18n:@checking-server%' : '%i18n:@server%' }}<mk-ellipsis v-if="server == null"/>
</p>
</div>
<p v-if="!end">%i18n:@finding%<mk-ellipsis/></p>
diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue
index 6c194ff982..0f686926b0 100644
--- a/src/client/app/common/views/components/connect-failed.vue
+++ b/src/client/app/common/views/components/connect-failed.vue
@@ -3,9 +3,9 @@
<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
<h1>%i18n:@title%</h1>
<p class="text">
- <span>{{ '%i18n:!@description%'.substr(0, '%i18n:!@description%'.indexOf('{')) }}</span>
- <a @click="reload">{{ '%i18n:!@description%'.match(/\{(.+?)\}/)[1] }}</a>
- <span>{{ '%i18n:!@description%'.substr('%i18n:!@description%'.indexOf('}') + 1) }}</span>
+ <span>{{ '%i18n:@description%'.substr(0, '%i18n:@description%'.indexOf('{')) }}</span>
+ <a @click="reload">{{ '%i18n:@description%'.match(/\{(.+?)\}/)[1] }}</a>
+ <span>{{ '%i18n:@description%'.substr('%i18n:@description%'.indexOf('}') + 1) }}</span>
</p>
<button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:@troubleshoot%</button>
<x-troubleshooter v-if="troubleshooting"/>
@@ -28,7 +28,7 @@ export default Vue.extend({
},
mounted() {
document.title = 'Oops!';
- document.documentElement.style.background = '#f8f8f8';
+ document.documentElement.style.setProperty('background', '#f8f8f8', 'important');
},
methods: {
reload() {
diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue
index 2a463ebfd7..de627181ef 100644
--- a/src/client/app/common/views/components/forkit.vue
+++ b/src/client/app/common/views/components/forkit.vue
@@ -13,9 +13,6 @@
.a
display block
- position absolute
- top 0
- right 0
> svg
display block
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 69fed00c74..5b2fa084fb 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -1,8 +1,12 @@
import Vue from 'vue';
+import analogClock from './analog-clock.vue';
+import menu from './menu.vue';
+import noteHeader from './note-header.vue';
import signin from './signin.vue';
import signup from './signup.vue';
import forkit from './forkit.vue';
+import acct from './acct.vue';
import avatar from './avatar.vue';
import nav from './nav.vue';
import noteHtml from './note-html';
@@ -23,12 +27,24 @@ 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 Othello from './othello.vue';
+import Reversi from './reversi.vue';
import welcomeTimeline from './welcome-timeline.vue';
+import uiInput from './ui/input.vue';
+import uiButton from './ui/button.vue';
+import uiCard from './ui/card.vue';
+import uiForm from './ui/form.vue';
+import uiTextarea from './ui/textarea.vue';
+import uiSwitch from './ui/switch.vue';
+import uiRadio from './ui/radio.vue';
+import uiSelect from './ui/select.vue';
+Vue.component('mk-analog-clock', analogClock);
+Vue.component('mk-menu', menu);
+Vue.component('mk-note-header', noteHeader);
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit);
+Vue.component('mk-acct', acct);
Vue.component('mk-avatar', avatar);
Vue.component('mk-nav', nav);
Vue.component('mk-note-html', noteHtml);
@@ -49,5 +65,13 @@ 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-othello', Othello);
+Vue.component('mk-reversi', Reversi);
Vue.component('mk-welcome-timeline', welcomeTimeline);
+Vue.component('ui-input', uiInput);
+Vue.component('ui-button', uiButton);
+Vue.component('ui-card', uiCard);
+Vue.component('ui-form', uiForm);
+Vue.component('ui-textarea', uiTextarea);
+Vue.component('ui-switch', uiSwitch);
+Vue.component('ui-radio', uiRadio);
+Vue.component('ui-select', uiSelect);
diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
index ff9d5e1022..2f8a1943ad 100644
--- a/src/client/app/common/views/components/media-list.vue
+++ b/src/client/app/common/views/components/media-list.vue
@@ -1,9 +1,11 @@
<template>
-<div class="mk-media-list" :data-count="mediaList.length">
- <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>
+<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>
+ </div>
</div>
</template>
@@ -18,47 +20,60 @@ export default Vue.extend({
raw: {
default: false
}
+ },
+ mounted() {
+ // for Safari bug
+ this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
}
});
</script>
<style lang="stylus" scoped>
.mk-media-list
- display grid
- grid-gap 4px
- height 256px
+ width 100%
- @media (max-width 500px)
- height 192px
+ &: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
+
+ &[data-count="1"]
+ 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
+ :nth-child(1)
+ grid-row 1 / 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="1"]
- 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
:nth-child(1)
- grid-row 1 / 3
+ grid-column 1 / 2
+ 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(4)
grid-column 2 / 3
- grid-row 2/3
- &[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(2)
- grid-column 2 / 3
- grid-row 1 / 2
- :nth-child(3)
- grid-column 1 / 2
- grid-row 2 / 3
- :nth-child(4)
- grid-column 2 / 3
- grid-row 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
new file mode 100644
index 0000000000..9b16732b9a
--- /dev/null
+++ b/src/client/app/common/views/components/menu.vue
@@ -0,0 +1,196 @@
+<template>
+<div class="mk-menu">
+ <div class="backdrop" ref="backdrop" @click="close"></div>
+ <div class="popover" :class="{ hukidasi }" ref="popover">
+ <template v-for="item 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>
+ </template>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+ props: {
+ source: {
+ required: true
+ },
+ items: {
+ type: Array,
+ required: true
+ },
+ compact: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+ data() {
+ return {
+ hukidasi: !this.compact
+ };
+ },
+ mounted() {
+ this.$nextTick(() => {
+ const popover = this.$refs.popover as any;
+
+ const rect = this.source.getBoundingClientRect();
+ const width = popover.offsetWidth;
+ const height = popover.offsetHeight;
+
+ let left;
+ let top;
+
+ if (this.compact) {
+ const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+ const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+ left = (x - (width / 2));
+ top = (y - (height / 2));
+ } else {
+ const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+ const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+ left = (x - (width / 2));
+ top = y;
+ }
+
+ if (left + width - window.pageXOffset > window.innerWidth) {
+ left = window.innerWidth - width + window.pageXOffset;
+ this.hukidasi = false;
+ }
+
+ if (top + height - window.pageYOffset > window.innerHeight) {
+ top = window.innerHeight - height + window.pageYOffset;
+ this.hukidasi = false;
+ }
+
+ popover.style.left = left + 'px';
+ popover.style.top = top + 'px';
+
+ anime({
+ targets: this.$refs.backdrop,
+ opacity: 1,
+ duration: 100,
+ easing: 'linear'
+ });
+
+ anime({
+ targets: this.$refs.popover,
+ opacity: 1,
+ scale: [0.5, 1],
+ duration: 500
+ });
+ });
+ },
+ methods: {
+ clicked(fn) {
+ fn();
+ this.close();
+ },
+ close() {
+ (this.$refs.backdrop as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.backdrop,
+ opacity: 0,
+ duration: 200,
+ easing: 'linear'
+ });
+
+ (this.$refs.popover as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.popover,
+ opacity: 0,
+ scale: 0.5,
+ duration: 200,
+ easing: 'easeInBack',
+ complete: () => {
+ this.$emit('closed');
+ this.$destroy();
+ }
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+$border-color = rgba(27, 31, 35, 0.15)
+
+.mk-menu
+ position initial
+
+ > .backdrop
+ position fixed
+ top 0
+ left 0
+ z-index 10000
+ width 100%
+ height 100%
+ background rgba(#000, 0.1)
+ opacity 0
+
+ > .popover
+ position absolute
+ z-index 10001
+ padding 8px 0
+ background #fff
+ border 1px solid $border-color
+ border-radius 4px
+ box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
+ transform scale(0.5)
+ opacity 0
+
+ $balloon-size = 16px
+
+ &.hukidasi
+ margin-top $balloon-size
+ transform-origin center -($balloon-size)
+
+ &:before
+ &:after
+ content ""
+ display block
+ position absolute
+ pointer-events none
+
+ &:before
+ top -($balloon-size * 2)
+ 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 $border-color
+
+ &:after
+ 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 #fff
+
+ > button
+ display block
+ padding 8px 16px
+ width 100%
+
+ &:hover
+ color $theme-color-foreground
+ background $theme-color
+ text-decoration none
+
+ &:active
+ color $theme-color-foreground
+ background darken($theme-color, 10%)
+
+ > div
+ margin 8px 0
+ height 1px
+ background #eee
+
+</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 32a43ace57..050906cf44 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -197,7 +197,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-messaging-form
+root(isDark)
> textarea
cursor auto
display block
@@ -209,10 +209,10 @@ export default Vue.extend({
padding 8px
resize none
font-size 1em
- color #000
+ color isDark ? #fff : #000
outline none
border none
- border-top solid 1px #eee
+ border-top solid 1px isDark ? #4b5056 : #eee
border-radius 0
box-shadow none
background transparent
@@ -302,4 +302,10 @@ export default Vue.extend({
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 ba0ab3209f..a77b5f3658 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -8,7 +8,7 @@
<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
</button>
<div class="content" v-if="!message.isDeleted">
- <mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/>
+ <mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
<div class="file" v-if="message.file">
<a :href="message.file.url" target="_blank" :title="message.file.name">
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@@ -42,7 +42,7 @@ export default Vue.extend({
},
computed: {
isMe(): boolean {
- return this.message.userId == (this as any).os.i.id;
+ return this.message.userId == this.$store.state.i.id;
},
urls(): string[] {
if (this.message.text) {
@@ -59,8 +59,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.message
- $me-balloon-color = #23A7B6
+@import '~const.styl'
+
+root(isDark)
+ $me-balloon-color = $theme-color
padding 10px 12px 10px 12px
background-color transparent
@@ -126,7 +128,7 @@ export default Vue.extend({
bottom -4px
left -12px
margin 0
- color rgba(#000, 0.5)
+ color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5)
font-size 11px
> .content
@@ -187,7 +189,7 @@ export default Vue.extend({
display block
margin 2px 0 0 0
font-size 10px
- color rgba(#000, 0.4)
+ color isDark ? rgba(#fff, 0.4) : rgba(#000, 0.4)
> [data-fa]
margin-left 4px
@@ -200,8 +202,9 @@ export default Vue.extend({
padding-left 66px
> .balloon
+ $color = isDark ? #2d3338 : #eee
float left
- background #eee
+ background $color
&[data-no-text]
background transparent
@@ -209,10 +212,15 @@ export default Vue.extend({
&:not([data-no-text]):before
left -14px
border-top solid 8px transparent
- border-right solid 8px #eee
+ border-right solid 8px $color
border-bottom solid 8px transparent
border-left solid 8px transparent
+ > .content
+ > .text
+ if isDark
+ color #fff
+
> footer
text-align left
@@ -241,7 +249,7 @@ export default Vue.extend({
> .content
> p.is-deleted
- color rgba(255, 255, 255, 0.5)
+ color rgba(#fff, 0.5)
> .text >>>
&, *
@@ -254,4 +262,10 @@ export default Vue.extend({
> .baloon
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 a45114e6bb..b2831d6928 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -8,7 +8,7 @@
<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>
<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
- <template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
+ <template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
<template v-for="(message, i) in _messages">
<x-message :message="message" :key="message.id"/>
@@ -18,7 +18,11 @@
</template>
</div>
<footer>
- <div ref="notifications" class="notifications"></div>
+ <transition name="fade">
+ <div class="new-message" v-show="showIndicator">
+ <button @click="onIndicatorClick">%fa:arrow-circle-down%%i18n:@new-message%</button>
+ </div>
+ </transition>
<x-form :user="user" ref="form"/>
</footer>
</div>
@@ -45,7 +49,9 @@ export default Vue.extend({
fetchingMoreMessages: false,
messages: [],
existMoreMessages: false,
- connection: null
+ connection: null,
+ showIndicator: false,
+ timer: null
};
},
@@ -66,7 +72,7 @@ export default Vue.extend({
},
mounted() {
- this.connection = new MessagingStream((this as any).os, (this as any).os.i, this.user.id);
+ this.connection = new MessagingStream((this as any).os, this.$store.state.i, this.user.id);
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
@@ -149,16 +155,16 @@ export default Vue.extend({
onMessage(message) {
// サウンドを再生する
- if ((this as any).os.isEnableSounds) {
+ if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/message.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
const isBottom = this.isBottom();
this.messages.push(message);
- if (message.userId != (this as any).os.i.id && !document.hidden) {
+ if (message.userId != this.$store.state.i.id && !document.hidden) {
this.connection.send({
type: 'read',
id: message.id
@@ -170,9 +176,9 @@ export default Vue.extend({
this.$nextTick(() => {
this.scrollToBottom();
});
- } else if (message.userId != (this as any).os.i.id) {
+ } else if (message.userId != this.$store.state.i.id) {
// Notify
- this.notify('%i18n:!@new-message%');
+ this.notifyNewMessage();
}
},
@@ -205,25 +211,25 @@ export default Vue.extend({
}
},
- notify(message) {
- const n = document.createElement('p') as any;
- n.innerHTML = '%fa:arrow-circle-down%' + message;
- n.onclick = () => {
- this.scrollToBottom();
- n.parentNode.removeChild(n);
- };
- (this.$refs.notifications as any).appendChild(n);
+ onIndicatorClick() {
+ this.showIndicator = false;
+ this.scrollToBottom();
+ },
+
+ notifyNewMessage() {
+ this.showIndicator = true;
- setTimeout(() => {
- n.style.opacity = 0;
- setTimeout(() => n.parentNode.removeChild(n), 1000);
+ if (this.timer) clearTimeout(this.timer);
+
+ this.timer = setTimeout(() => {
+ this.showIndicator = false;
}, 4000);
},
onVisibilitychange() {
if (document.hidden) return;
this.messages.forEach(message => {
- if (message.userId !== (this as any).os.i.id && !message.isRead) {
+ if (message.userId !== this.$store.state.i.id && !message.isRead) {
this.connection.send({
type: 'read',
id: message.id
@@ -238,11 +244,12 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-messaging-room
+root(isDark)
display flex
flex 1
flex-direction column
height 100%
+ background isDark ? #191b22 : #fff
> .stream
width 100%
@@ -256,7 +263,7 @@ export default Vue.extend({
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
- color rgba(#000, 0.4)
+ color rgba(isDark ? #fff : #000, 0.4)
[data-fa]
margin-right 4px
@@ -267,7 +274,7 @@ export default Vue.extend({
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
- color rgba(#000, 0.4)
+ color rgba(isDark ? #fff : #000, 0.4)
[data-fa]
margin-right 4px
@@ -278,7 +285,7 @@ export default Vue.extend({
padding 16px
text-align center
font-size 0.8em
- color rgba(#000, 0.4)
+ color rgba(isDark ? #fff : #000, 0.4)
[data-fa]
margin-right 4px
@@ -322,7 +329,7 @@ export default Vue.extend({
left 0
right 0
margin 0 auto
- background rgba(#000, 0.1)
+ background rgba(isDark ? #fff : #000, 0.1)
> span
display inline-block
@@ -330,8 +337,8 @@ export default Vue.extend({
padding 0 16px
//font-weight bold
line-height 32px
- color rgba(#000, 0.3)
- background #fff
+ color rgba(isDark ? #fff : #000, 0.3)
+ background isDark ? #191b22 : #fff
> footer
position -webkit-sticky
@@ -342,30 +349,32 @@ export default Vue.extend({
max-width 600px
margin 0 auto
padding 0
- background rgba(255, 255, 255, 0.95)
+ background rgba(isDark ? #282c37 : #fff, 0.95)
background-clip content-box
- > .notifications
+ > .new-message
position absolute
top -48px
width 100%
padding 8px 0
text-align center
- &:empty
- display none
-
- > p
+ > button
display inline-block
margin 0
- padding 0 12px 0 28px
+ padding 0 12px 0 30px
cursor pointer
line-height 32px
font-size 12px
color $theme-color-foreground
background $theme-color
border-radius 16px
- transition opacity 1s ease
+
+ &:hover
+ background lighten($theme-color, 10%)
+
+ &:active
+ background darken($theme-color, 10%)
> [data-fa]
position absolute
@@ -374,4 +383,17 @@ export default Vue.extend({
line-height 32px
font-size 16px
+.fade-enter-active, .fade-leave-active
+ transition opacity 0.1s
+
+.fade-enter, .fade-leave-to
+ 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 11f9c366d4..2ddec29984 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -95,7 +95,7 @@ export default Vue.extend({
methods: {
getAcct,
isMe(message) {
- return message.userId == (this as any).os.i.id;
+ return message.userId == this.$store.state.i.id;
},
onMessage(message) {
this.messages = this.messages.filter(m => !(
diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue
new file mode 100644
index 0000000000..6e64a6a6d3
--- /dev/null
+++ b/src/client/app/common/views/components/note-header.vue
@@ -0,0 +1,117 @@
+<template>
+<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
+ <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
+ <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
+ <span class="is-admin" v-if="note.user.isAdmin">admin</span>
+ <span class="is-bot" v-if="note.user.isBot">bot</span>
+ <span class="is-cat" v-if="note.user.isCat">cat</span>
+ <span class="username"><mk-acct :user="note.user"/></span>
+ <div class="info">
+ <span class="app" v-if="note.app && !mini">via <b>{{ note.app.name }}</b></span>
+ <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
+ <router-link class="created-at" :to="note | notePage">
+ <mk-time :time="note.createdAt"/>
+ </router-link>
+ <span class="visibility" v-if="note.visibility != 'public'">
+ <template v-if="note.visibility == 'home'">%fa:home%</template>
+ <template v-if="note.visibility == 'followers'">%fa:unlock%</template>
+ <template v-if="note.visibility == 'specified'">%fa:envelope%</template>
+ <template v-if="note.visibility == 'private'">%fa:lock%</template>
+ </span>
+ </div>
+</header>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ note: {
+ type: Object,
+ required: true
+ },
+ mini: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ display flex
+ align-items baseline
+ white-space nowrap
+
+ > .avatar
+ flex-shrink 0
+ margin-right 8px
+ width 20px
+ height 20px
+ border-radius 100%
+
+ > .name
+ display block
+ margin 0 .5em 0 0
+ padding 0
+ overflow hidden
+ color isDark ? #fff : #627079
+ font-size 1em
+ font-weight bold
+ text-decoration none
+ text-overflow ellipsis
+
+ &:hover
+ text-decoration underline
+
+ > .is-admin
+ > .is-bot
+ > .is-cat
+ align-self center
+ margin 0 .5em 0 0
+ padding 1px 6px
+ font-size 80%
+ color isDark ? #758188 : #aaa
+ border solid 1px isDark ? #57616f : #ddd
+ border-radius 3px
+
+ &.is-admin
+ border-color isDark ? #d42c41 : #f56a7b
+ color isDark ? #d42c41 : #f56a7b
+
+ > .username
+ margin 0 .5em 0 0
+ overflow hidden
+ text-overflow ellipsis
+ color isDark ? #606984 : #ccc
+
+ > .info
+ margin-left auto
+ font-size 0.9em
+
+ > *
+ color isDark ? #606984 : #c0c0c0
+
+ > .mobile
+ margin-right 8px
+
+ > .app
+ margin-right 8px
+ padding-right 8px
+ border-right solid 1px isDark ? #1c2023 : #eaeaea
+
+ > .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-html.ts b/src/client/app/common/views/components/note-html.ts
index f86b50659e..8fa5f380dd 100644
--- a/src/client/app/common/views/components/note-html.ts
+++ b/src/client/app/common/views/components/note-html.ts
@@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
ast = this.ast;
}
+ if (ast.filter(x => x.type != 'hashtag').length == 0) {
+ return;
+ }
+
+ while (ast[ast.length - 1] && (
+ ast[ast.length - 1].type == 'hashtag' ||
+ (ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
+ (ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) {
+ ast.pop();
+ }
+
// Parse ast to DOM
const els = flatten(ast.map(token => {
switch (token.type) {
@@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
case 'hashtag':
return createElement('a', {
attrs: {
- href: `${url}/search?q=${token.content}`,
+ href: `${url}/tags/${token.hashtag}`,
target: '_blank'
}
}, token.content);
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 88dc22aaf4..27a49a6536 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -1,54 +1,45 @@
<template>
-<div class="mk-note-menu">
- <div class="backdrop" ref="backdrop" @click="close"></div>
- <div class="popover" :class="{ compact }" ref="popover">
- <button @click="favorite">%i18n:@favorite%</button>
- <button v-if="note.userId == os.i.id" @click="pin">%i18n:@pin%</button>
- <a v-if="note.uri" :href="note.uri" target="_blank">%i18n:@remote%</a>
- </div>
+<div style="position:initial">
+ <mk-menu :source="source" :compact="compact" :items="items" @closed="closed"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
-import * as anime from 'animejs';
export default Vue.extend({
props: ['note', 'source', 'compact'],
- mounted() {
- this.$nextTick(() => {
- const popover = this.$refs.popover as any;
-
- const rect = this.source.getBoundingClientRect();
- const width = popover.offsetWidth;
- const height = popover.offsetHeight;
-
- if (this.compact) {
- const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
- const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
- popover.style.left = (x - (width / 2)) + 'px';
- popover.style.top = (y - (height / 2)) + 'px';
- } else {
- const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
- const y = rect.top + window.pageYOffset + this.source.offsetHeight;
- popover.style.left = (x - (width / 2)) + 'px';
- popover.style.top = y + 'px';
- }
-
- anime({
- targets: this.$refs.backdrop,
- opacity: 1,
- duration: 100,
- easing: 'linear'
+ computed: {
+ items() {
+ const items = [];
+ items.push({
+ icon: '%fa:star%',
+ text: '%i18n:@favorite%',
+ action: this.favorite
});
-
- anime({
- targets: this.$refs.popover,
- opacity: 1,
- scale: [0.5, 1],
- duration: 500
- });
- });
+ if (this.note.userId == this.$store.state.i.id) {
+ items.push({
+ icon: '%fa:thumbtack%',
+ text: '%i18n:@pin%',
+ action: this.pin
+ });
+ 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%',
+ text: '%i18n:@remote%',
+ action: () => {
+ window.open(this.note.uri, '_blank');
+ }
+ });
+ }
+ return items;
+ }
},
methods: {
pin() {
@@ -59,107 +50,28 @@ export default Vue.extend({
});
},
- favorite() {
- (this as any).api('notes/favorites/create', {
+ del() {
+ if (!window.confirm('%i18n:@delete-confirm%')) return;
+ (this as any).api('notes/delete', {
noteId: this.note.id
}).then(() => {
this.$destroy();
});
},
- close() {
- (this.$refs.backdrop as any).style.pointerEvents = 'none';
- anime({
- targets: this.$refs.backdrop,
- opacity: 0,
- duration: 200,
- easing: 'linear'
+ favorite() {
+ (this as any).api('notes/favorites/create', {
+ noteId: this.note.id
+ }).then(() => {
+ this.$destroy();
});
+ },
- (this.$refs.popover as any).style.pointerEvents = 'none';
- anime({
- targets: this.$refs.popover,
- opacity: 0,
- scale: 0.5,
- duration: 200,
- easing: 'easeInBack',
- complete: () => this.$destroy()
+ closed() {
+ this.$nextTick(() => {
+ this.$destroy();
});
}
}
});
</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-$border-color = rgba(27, 31, 35, 0.15)
-
-.mk-note-menu
- position initial
-
- > .backdrop
- position fixed
- top 0
- left 0
- z-index 10000
- width 100%
- height 100%
- background rgba(#000, 0.1)
- opacity 0
-
- > .popover
- position absolute
- z-index 10001
- padding 8px 0
- background #fff
- border 1px solid $border-color
- border-radius 4px
- box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
- transform scale(0.5)
- opacity 0
-
- $balloon-size = 16px
-
- &:not(.compact)
- margin-top $balloon-size
- transform-origin center -($balloon-size)
-
- &:before
- content ""
- display block
- position absolute
- top -($balloon-size * 2)
- 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 $border-color
-
- &: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 #fff
-
- > button
- > a
- display block
- padding 8px 16px
- width 100%
-
- &:hover
- color $theme-color-foreground
- background $theme-color
- text-decoration none
-
- &:active
- color $theme-color-foreground
- background darken($theme-color, 10%)
-
-</style>
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 95bcba996e..115c934c8b 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -5,7 +5,7 @@
</p>
<ul ref="choices">
<li v-for="(choice, i) in choices">
- <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:!@choice-n%'.replace('{}', i + 1)">
+ <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:@choice-n%'.replace('{}', i + 1)">
<button @click="remove(i)" title="%i18n:@remove%">
%fa:times%
</button>
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 46e41cbcdb..660247edbc 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -1,19 +1,19 @@
<template>
<div class="mk-poll" :data-is-voted="isVoted">
<ul>
- <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:!@vote-to%'.replace('{}', choice.text) : ''">
+ <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:@vote-to%'.replace('{}', choice.text) : ''">
<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
<span>
<template v-if="choice.isVoted">%fa:check%</template>
<span>{{ choice.text }}</span>
- <span class="votes" v-if="showResult">({{ '%i18n:!@vote-count%'.replace('{}', choice.votes) }})</span>
+ <span class="votes" v-if="showResult">({{ '%i18n:@vote-count%'.replace('{}', choice.votes) }})</span>
</span>
</li>
</ul>
<p v-if="total > 0">
- <span>{{ '%i18n:!@total-users%'.replace('{}', total) }}</span>
+ <span>{{ '%i18n:@total-users%'.replace('{}', total) }}</span>
<span>・</span>
- <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:!@vote%' : '%i18n:!@show-result%' }}</a>
+ <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:@vote%' : '%i18n:@show-result%' }}</a>
<span v-if="isVoted">%i18n:@voted%</span>
</p>
</div>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index e2c8a6ed3f..0db6f66b37 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -22,7 +22,7 @@
import Vue from 'vue';
import * as anime from 'animejs';
-const placeholder = '%i18n:!@choose-reaction%';
+const placeholder = '%i18n:@choose-reaction%';
export default Vue.extend({
props: ['note', 'source', 'compact', 'cb'],
diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/reversi.game.vue
index 8c646cce07..dc79c95bb8 100644
--- a/src/client/app/common/views/components/othello.game.vue
+++ b/src/client/app/common/views/components/reversi.game.vue
@@ -43,7 +43,7 @@
<script lang="ts">
import Vue from 'vue';
import * as CRC32 from 'crc-32';
-import Othello, { Color } from '../../../../../othello/core';
+import Reversi, { Color } from '../../../../../reversi/core';
import { url } from '../../../config';
export default Vue.extend({
@@ -52,7 +52,7 @@ export default Vue.extend({
data() {
return {
game: null,
- o: null as Othello,
+ o: null as Reversi,
logs: [],
logPos: 0,
pollingClock: null
@@ -61,13 +61,13 @@ export default Vue.extend({
computed: {
iAmPlayer(): boolean {
- if (!(this as any).os.isSignedIn) return false;
- return this.game.user1Id == (this as any).os.i.id || this.game.user2Id == (this as any).os.i.id;
+ if (!this.$store.getters.isSignedIn) return false;
+ return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id;
},
myColor(): Color {
if (!this.iAmPlayer) return null;
- if (this.game.user1Id == (this as any).os.i.id && this.game.black == 1) return true;
- if (this.game.user2Id == (this as any).os.i.id && this.game.black == 2) return true;
+ if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true;
+ if (this.game.user2Id == this.$store.state.i.id && this.game.black == 2) return true;
return false;
},
opColor(): Color {
@@ -91,14 +91,14 @@ export default Vue.extend({
},
isMyTurn(): boolean {
if (this.turnUser == null) return null;
- return this.turnUser.id == (this as any).os.i.id;
+ return this.turnUser.id == this.$store.state.i.id;
}
},
watch: {
logPos(v) {
if (!this.game.isEnded) return;
- this.o = new Othello(this.game.settings.map, {
+ this.o = new Reversi(this.game.settings.map, {
isLlotheo: this.game.settings.isLlotheo,
canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard
@@ -115,7 +115,7 @@ export default Vue.extend({
created() {
this.game = this.initGame;
- this.o = new Othello(this.game.settings.map, {
+ this.o = new Reversi(this.game.settings.map, {
isLlotheo: this.game.settings.isLlotheo,
canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard
@@ -162,9 +162,9 @@ export default Vue.extend({
this.o.put(this.myColor, pos);
// サウンドを再生する
- if ((this as any).os.isEnableSounds) {
- const sound = new Audio(`${url}/assets/othello-put-me.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ if (this.$store.state.device.enableSounds) {
+ const sound = new Audio(`${url}/assets/reversi-put-me.mp3`);
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
@@ -186,9 +186,9 @@ export default Vue.extend({
this.$forceUpdate();
// サウンドを再生する
- if ((this as any).os.isEnableSounds && x.color != this.myColor) {
- const sound = new Audio(`${url}/assets/othello-put-you.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ if (this.$store.state.device.enableSounds && x.color != this.myColor) {
+ const sound = new Audio(`${url}/assets/reversi-put-you.mp3`);
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
},
@@ -213,7 +213,7 @@ export default Vue.extend({
onRescue(game) {
this.game = game;
- this.o = new Othello(this.game.settings.map, {
+ this.o = new Reversi(this.game.settings.map, {
isLlotheo: this.game.settings.isLlotheo,
canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard
diff --git a/src/client/app/common/views/components/othello.gameroom.vue b/src/client/app/common/views/components/reversi.gameroom.vue
index dba9ccd16d..7ce0112451 100644
--- a/src/client/app/common/views/components/othello.gameroom.vue
+++ b/src/client/app/common/views/components/reversi.gameroom.vue
@@ -7,9 +7,9 @@
<script lang="ts">
import Vue from 'vue';
-import XGame from './othello.game.vue';
-import XRoom from './othello.room.vue';
-import { OthelloGameStream } from '../../scripts/streaming/othello-game';
+import XGame from './reversi.game.vue';
+import XRoom from './reversi.room.vue';
+import { ReversiGameStream } from '../../scripts/streaming/reversi-game';
export default Vue.extend({
components: {
@@ -25,7 +25,7 @@ export default Vue.extend({
},
created() {
this.g = this.game;
- this.connection = new OthelloGameStream((this as any).os, (this as any).os.i, this.game);
+ this.connection = new ReversiGameStream((this as any).os, this.$store.state.i, this.game);
this.connection.on('started', this.onStarted);
},
beforeDestroy() {
diff --git a/src/client/app/common/views/components/othello.room.vue b/src/client/app/common/views/components/reversi.room.vue
index 86368b3cc3..5074845758 100644
--- a/src/client/app/common/views/components/othello.room.vue
+++ b/src/client/app/common/views/components/reversi.room.vue
@@ -94,7 +94,7 @@
<script lang="ts">
import Vue from 'vue';
-import * as maps from '../../../../../othello/maps';
+import * as maps from '../../../../../reversi/maps';
export default Vue.extend({
props: ['game', 'connection'],
@@ -116,13 +116,13 @@ export default Vue.extend({
return categories.filter((item, pos) => categories.indexOf(item) == pos);
},
isAccepted(): boolean {
- if (this.game.user1Id == (this as any).os.i.id && this.game.user1Accepted) return true;
- if (this.game.user2Id == (this as any).os.i.id && this.game.user2Accepted) return true;
+ if (this.game.user1Id == this.$store.state.i.id && this.game.user1Accepted) return true;
+ if (this.game.user2Id == this.$store.state.i.id && this.game.user2Accepted) return true;
return false;
},
isOpAccepted(): boolean {
- if (this.game.user1Id != (this as any).os.i.id && this.game.user1Accepted) return true;
- if (this.game.user2Id != (this as any).os.i.id && this.game.user2Accepted) return true;
+ if (this.game.user1Id != this.$store.state.i.id && this.game.user1Accepted) return true;
+ if (this.game.user2Id != this.$store.state.i.id && this.game.user2Accepted) return true;
return false;
}
},
@@ -133,8 +133,8 @@ export default Vue.extend({
this.connection.on('init-form', this.onInitForm);
this.connection.on('message', this.onMessage);
- if (this.game.user1Id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
- if (this.game.user2Id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
+ if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
+ if (this.game.user2Id != this.$store.state.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
},
beforeDestroy() {
@@ -185,12 +185,12 @@ export default Vue.extend({
},
onInitForm(x) {
- if (x.userId == (this as any).os.i.id) return;
+ if (x.userId == this.$store.state.i.id) return;
this.form = x.form;
},
onMessage(x) {
- if (x.userId == (this as any).os.i.id) return;
+ if (x.userId == this.$store.state.i.id) return;
this.messages.unshift(x.message);
},
diff --git a/src/client/app/common/views/components/othello.vue b/src/client/app/common/views/components/reversi.vue
index a0971c45b4..e4d7740bde 100644
--- a/src/client/app/common/views/components/othello.vue
+++ b/src/client/app/common/views/components/reversi.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mk-othello">
+<div class="mk-reversi">
<div v-if="game">
<x-gameroom :game="game"/>
</div>
@@ -11,14 +11,14 @@
</div>
<div class="index" v-else>
<h1>Misskey %fa:circle%thell%fa:circle R%</h1>
- <p>他のMisskeyユーザーとオセロで対戦しよう</p>
+ <p>他のMisskeyユーザーとリバーシで対戦しよう</p>
<div class="play">
<el-button round>フリーマッチ(準備中)</el-button>
<el-button type="primary" round @click="match">指名</el-button>
<details>
<summary>遊び方</summary>
<div>
- <p>オセロは、相手と交互に石をボードに置いてゆき、相手の石を挟んでひっくり返しながら、最終的に残った石が多い方が勝ちというボードゲームです。</p>
+ <p>リバーシは、相手と交互に石をボードに置いてゆき、相手の石を挟んでひっくり返しながら、最終的に残った石が多い方が勝ちというボードゲームです。</p>
<dl>
<dt><b>フリーマッチ</b></dt>
<dd>ランダムなユーザーと対戦するモードです。</dd>
@@ -39,7 +39,7 @@
</section>
<section v-if="myGames.length > 0">
<h2>自分の対局</h2>
- <a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
+ <a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
@@ -48,7 +48,7 @@
</section>
<section v-if="games.length > 0">
<h2>みんなの対局</h2>
- <a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
+ <a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
@@ -61,7 +61,7 @@
<script lang="ts">
import Vue from 'vue';
-import XGameroom from './othello.gameroom.vue';
+import XGameroom from './reversi.gameroom.vue';
export default Vue.extend({
components: {
@@ -93,24 +93,24 @@ export default Vue.extend({
}
},
mounted() {
- this.connection = (this as any).os.streams.othelloStream.getConnection();
- this.connectionId = (this as any).os.streams.othelloStream.use();
+ this.connection = (this as any).os.streams.reversiStream.getConnection();
+ this.connectionId = (this as any).os.streams.reversiStream.use();
this.connection.on('matched', this.onMatched);
this.connection.on('invited', this.onInvited);
- (this as any).api('othello/games', {
+ (this as any).api('reversi/games', {
my: true
}).then(games => {
this.myGames = games;
});
- (this as any).api('othello/games').then(games => {
+ (this as any).api('reversi/games').then(games => {
this.games = games;
this.gamesFetching = false;
});
- (this as any).api('othello/invitations').then(invitations => {
+ (this as any).api('reversi/invitations').then(invitations => {
this.invitations = this.invitations.concat(invitations);
});
@@ -126,13 +126,13 @@ export default Vue.extend({
beforeDestroy() {
this.connection.off('matched', this.onMatched);
this.connection.off('invited', this.onInvited);
- (this as any).os.streams.othelloStream.dispose(this.connectionId);
+ (this as any).os.streams.reversiStream.dispose(this.connectionId);
clearInterval(this.pingClock);
},
methods: {
go(game) {
- (this as any).api('othello/games/show', {
+ (this as any).api('reversi/games/show', {
gameId: game.id
}).then(game => {
this.matching = null;
@@ -146,7 +146,7 @@ export default Vue.extend({
(this as any).api('users/show', {
username
}).then(user => {
- (this as any).api('othello/match', {
+ (this as any).api('reversi/match', {
userId: user.id
}).then(res => {
if (res == null) {
@@ -160,10 +160,10 @@ export default Vue.extend({
},
cancel() {
this.matching = null;
- (this as any).api('othello/match/cancel');
+ (this as any).api('reversi/match/cancel');
},
accept(invitation) {
- (this as any).api('othello/match', {
+ (this as any).api('reversi/match', {
userId: invitation.parent.id
}).then(game => {
if (game) {
@@ -186,7 +186,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-othello
+.mk-reversi
color #677f84
background #fff
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 7fb9fc3fd4..58241cef09 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -1,24 +1,33 @@
<template>
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
- <label class="user-name">
- <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="%i18n:@username%" autofocus required @change="onUsernameChange"/>%fa:at%
- </label>
- <label class="password">
- <input v-model="password" type="password" placeholder="%i18n:@password%" required/>%fa:lock%
- </label>
- <label class="token" v-if="user && user.twoFactorEnabled">
- <input v-model="token" type="number" placeholder="%i18n:@token%" required/>%fa:lock%
- </label>
- <button type="submit" :disabled="signing">{{ signing ? '%i18n:!@signing-in%' : '%i18n:!@signin%' }}</button>
- もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
+ <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">
+ <span>%i18n:@username%</span>
+ <span slot="prefix">@</span>
+ <span slot="suffix">@{{ host }}</span>
+ </ui-input>
+ <ui-input v-model="password" type="password" required>
+ <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-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
+ <p style="margin: 8px 0;">または<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a></p>
</form>
</template>
<script lang="ts">
import Vue from 'vue';
-import { apiUrl } from '../../../config';
+import { apiUrl, host } from '../../../config';
export default Vue.extend({
+ props: {
+ withAvatar: {
+ type: Boolean,
+ required: false,
+ default: true
+ }
+ },
data() {
return {
signing: false,
@@ -27,6 +36,7 @@ export default Vue.extend({
password: '',
token: '',
apiUrl,
+ host
};
},
methods: {
@@ -35,6 +45,8 @@ export default Vue.extend({
username: this.username
}).then(user => {
this.user = user;
+ }, () => {
+ this.user = null;
});
},
onSubmit() {
@@ -59,84 +71,19 @@ export default Vue.extend({
@import '~const.styl'
.mk-signin
+ color #555
+
&.signing
&, *
cursor wait !important
- label
- display block
- margin 12px 0
-
- [data-fa]
- display block
- pointer-events none
- position absolute
- bottom 0
- top 0
- left 0
- z-index 1
- margin auto
- padding 0 16px
- height 1em
- color #898786
-
- input[type=text]
- input[type=password]
- input[type=number]
- user-select text
- display inline-block
- cursor auto
- padding 0 0 0 38px
- margin 0
- width 100%
- line-height 44px
- font-size 1em
- color rgba(#000, 0.7)
- background #fff
- outline none
- border solid 1px #eee
- border-radius 4px
-
- &:hover
- background rgba(255, 255, 255, 0.7)
- border-color #ddd
-
- & + i
- color #797776
-
- &:focus
- background #fff
- border-color #ccc
-
- & + i
- color #797776
-
- [type=submit]
- cursor pointer
- padding 16px
- margin -6px 0 0 0
- width 100%
- font-size 1.2em
- color rgba(#000, 0.5)
- outline none
- border none
- border-radius 0
- background transparent
- transition all .5s ease
-
- &:hover
- color $theme-color
- transition all .2s ease
-
- &:focus
- color $theme-color
- transition all .2s ease
-
- &:active
- color darken($theme-color, 30%)
- transition all .2s ease
-
- &:disabled
- opacity 0.7
+ > .avatar
+ margin 16px auto 0 auto
+ width 64px
+ height 64px
+ background #ddd
+ background-position center
+ background-size cover
+ border-radius 100%
</style>
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 516979acd0..62373a59ec 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -1,60 +1,58 @@
<template>
-<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
- <label class="username">
- <p class="caption">%fa:at%%i18n:@username%</p>
- <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/>
- <p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/@${username}` }}</p>
- <p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:@checking%</p>
- <p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:@available%</p>
- <p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@unavailable%</p>
- <p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@error%</p>
- <p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@invalid-format%</p>
- <p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-short%</p>
- <p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-long%</p>
- </label>
- <label class="password">
- <p class="caption">%fa:lock%%i18n:@password%</p>
- <input v-model="password" type="password" placeholder="%i18n:@password-placeholder%" autocomplete="off" required @input="onChangePassword"/>
- <div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
- <div class="value" ref="passwordMetar"></div>
+<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
+ <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
+ <span>%i18n:@username%</span>
+ <span slot="prefix">@</span>
+ <span slot="suffix">@{{ host }}</span>
+ <p slot="text" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw% %i18n:@checking%</p>
+ <p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw% %i18n:@available%</p>
+ <p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@unavailable%</p>
+ <p slot="text" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@error%</p>
+ <p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@invalid-format%</p>
+ <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">
+ <span>%i18n:@password%</span>
+ <span slot="prefix">%fa:lock%</span>
+ <div slot="text">
+ <p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@weak-password%</p>
+ <p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw% %i18n:@normal-password%</p>
+ <p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
</div>
- <p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@weak-password%</p>
- <p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:@normal-password%</p>
- <p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:@strong-password%</p>
- </label>
- <label class="retype-password">
- <p class="caption">%fa:lock%%i18n:@password%(%i18n:@retype%)</p>
- <input v-model="retypedPassword" type="password" placeholder="%i18n:@retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/>
- <p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:@password-matched%</p>
- <p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@password-not-matched%</p>
- </label>
- <label class="recaptcha">
- <p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:@recaptcha%</p>
- <div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
- </label>
- <label class="agree-tou">
- <input name="agree-tou" type="checkbox" autocomplete="off" required/>
+ </ui-input>
+ <ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
+ <span>%i18n:@password% (%i18n:@retype%)</span>
+ <span slot="prefix">%fa:lock%</span>
+ <div slot="text">
+ <p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw% %i18n:@password-matched%</p>
+ <p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p>
+ </div>
+ </ui-input>
+ <div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div>
+ <label class="agree-tou" style="display: block; margin: 16px 0;">
+ <input name="agree-tou" type="checkbox" required/>
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
</label>
- <button type="submit">%i18n:@create%</button>
+ <ui-button type="submit">%i18n:@create%</ui-button>
</form>
</template>
<script lang="ts">
import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength');
-import { url, docsUrl, lang, recaptchaSitekey } from '../../../config';
+import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config';
export default Vue.extend({
data() {
return {
+ host,
username: '',
password: '',
retypedPassword: '',
url,
touUrl: `${docsUrl}/${lang}/tou`,
recaptchaSitekey,
- recaptchaed: false,
usernameState: null,
passwordStrength: '',
passwordRetypeState: null
@@ -104,7 +102,6 @@ export default Vue.extend({
const strength = getPasswordStrength(this.password);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
- (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
},
onChangePasswordRetype() {
if (this.retypedPassword == '') {
@@ -127,22 +124,12 @@ export default Vue.extend({
location.href = '/';
});
}).catch(() => {
- alert('%i18n:!@some-error%');
+ alert('%i18n:@some-error%');
(window as any).grecaptcha.reset();
- this.recaptchaed = false;
});
}
},
- created() {
- (window as any).onRecaptchaed = () => {
- this.recaptchaed = true;
- };
-
- (window as any).onRecaptchaExpired = () => {
- this.recaptchaed = false;
- };
- },
mounted() {
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
@@ -158,100 +145,6 @@ export default Vue.extend({
.mk-signup
min-width 302px
- label
- display block
- margin 0 0 16px 0
-
- > .caption
- margin 0 0 4px 0
- color #828888
- font-size 0.95em
-
- > [data-fa]
- margin-right 0.25em
- color #96adac
-
- > .info
- display block
- margin 4px 0
- font-size 0.8em
-
- > [data-fa]
- margin-right 0.3em
-
- &.username
- .profile-page-url-preview
- display block
- margin 4px 8px 0 4px
- font-size 0.8em
- color #888
-
- &:empty
- display none
-
- &:not(:empty) + .info
- margin-top 0
-
- &.password
- .meter
- display block
- margin-top 8px
- width 100%
- height 8px
-
- &[data-strength='']
- display none
-
- &[data-strength='low']
- > .value
- background #d73612
-
- &[data-strength='medium']
- > .value
- background #d7ca12
-
- &[data-strength='high']
- > .value
- background #61bb22
-
- > .value
- display block
- width 0%
- height 100%
- background transparent
- border-radius 4px
- transition all 0.1s ease
-
- [type=text], [type=password]
- user-select text
- display inline-block
- cursor auto
- padding 0 12px
- margin 0
- width 100%
- line-height 44px
- font-size 1em
- color #333 !important
- background #fff !important
- outline none
- border solid 1px rgba(#000, 0.1)
- border-radius 4px
- box-shadow 0 0 0 114514px #fff inset
- transition all .3s ease
-
- &:hover
- border-color rgba(#000, 0.2)
- transition all .1s ease
-
- &:focus
- color $theme-color !important
- border-color $theme-color
- box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
- transition all 0s ease
-
- &:disabled
- opacity 0.5
-
.agree-tou
padding 4px
border-radius 4px
@@ -269,19 +162,4 @@ export default Vue.extend({
display inline
color #555
- button
- margin 0
- padding 16px
- width 100%
- font-size 1em
- color #fff
- background $theme-color
- border-radius 3px
-
- &:hover
- background lighten($theme-color, 5%)
-
- &:active
- background darken($theme-color, 5%)
-
</style>
diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue
index 533958697c..700643ff05 100644
--- a/src/client/app/common/views/components/time.vue
+++ b/src/client/app/common/views/components/time.vue
@@ -44,32 +44,35 @@ export default Vue.extend({
const time = this._time;
const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
return (
- ago >= 31536000 ? '%i18n:!common.time.years_ago%' .replace('{}', (~~(ago / 31536000)).toString()) :
- ago >= 2592000 ? '%i18n:!common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) :
- ago >= 604800 ? '%i18n:!common.time.weeks_ago%' .replace('{}', (~~(ago / 604800)).toString()) :
- ago >= 86400 ? '%i18n:!common.time.days_ago%' .replace('{}', (~~(ago / 86400)).toString()) :
- ago >= 3600 ? '%i18n:!common.time.hours_ago%' .replace('{}', (~~(ago / 3600)).toString()) :
- ago >= 60 ? '%i18n:!common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) :
- ago >= 10 ? '%i18n:!common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) :
- ago >= 0 ? '%i18n:!common.time.just_now%' :
- ago < 0 ? '%i18n:!common.time.future%' :
- '%i18n:!common.time.unknown%');
+ ago >= 31536000 ? '%i18n:common.time.years_ago%' .replace('{}', (~~(ago / 31536000)).toString()) :
+ ago >= 2592000 ? '%i18n:common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) :
+ ago >= 604800 ? '%i18n:common.time.weeks_ago%' .replace('{}', (~~(ago / 604800)).toString()) :
+ ago >= 86400 ? '%i18n:common.time.days_ago%' .replace('{}', (~~(ago / 86400)).toString()) :
+ ago >= 3600 ? '%i18n:common.time.hours_ago%' .replace('{}', (~~(ago / 3600)).toString()) :
+ ago >= 60 ? '%i18n:common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) :
+ ago >= 10 ? '%i18n:common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) :
+ ago >= 0 ? '%i18n:common.time.just_now%' :
+ ago < 0 ? '%i18n:common.time.future%' :
+ '%i18n:common.time.unknown%');
}
},
created() {
if (this.mode == 'relative' || this.mode == 'detail') {
- this.tick();
- this.tickId = setInterval(this.tick, 1000);
+ this.tickId = window.requestAnimationFrame(this.tick);
}
},
destroyed() {
if (this.mode === 'relative' || this.mode === 'detail') {
- clearInterval(this.tickId);
+ window.clearTimeout(this.tickId);
}
},
methods: {
tick() {
this.now = new Date();
+
+ this.tickId = setTimeout(() => {
+ window.requestAnimationFrame(this.tick);
+ }, 10000);
}
}
});
diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
index ab07e6d09a..d1cb78c544 100644
--- a/src/client/app/common/views/components/twitter-setting.vue
+++ b/src/client/app/common/views/components/twitter-setting.vue
@@ -1,13 +1,13 @@
<template>
<div class="mk-twitter-setting">
<p>%i18n:@description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:@detail%</a></p>
- <p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.userId}`">%i18n:@connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p>
+ <p class="account" v-if="$store.state.i.twitter" :title="`Twitter ID: ${$store.state.i.twitter.userId}`">%i18n:@connected-to%: <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" @click.prevent="connect">{{ os.i.twitter ? '%i18n:!@reconnect%' : '%i18n:!@connect%' }}</a>
- <span v-if="os.i.twitter"> or </span>
- <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter" @click.prevent="disconnect">%i18n:@disconnect%</a>
+ <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ $store.state.i.twitter ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
+ <span v-if="$store.state.i.twitter"> or </span>
+ <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter" @click.prevent="disconnect">%i18n:@disconnect%</a>
</p>
- <p class="id" v-if="os.i.twitter">Twitter ID: {{ os.i.twitter.userId }}</p>
+ <p class="id" v-if="$store.state.i.twitter">Twitter ID: {{ $store.state.i.twitter.userId }}</p>
</div>
</template>
@@ -24,8 +24,8 @@ export default Vue.extend({
};
},
mounted() {
- this.$watch('os.i', () => {
- if ((this as any).os.i.twitter) {
+ this.$watch('$store.state.i', () => {
+ if (this.$store.state.i.twitter) {
if (this.form) this.form.close();
}
}, {
diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue
new file mode 100644
index 0000000000..e778750354
--- /dev/null
+++ b/src/client/app/common/views/components/ui/button.vue
@@ -0,0 +1,82 @@
+<template>
+<div class="ui-button" :class="[styl]">
+ <button :type="type" @click="$emit('click')">
+ <slot></slot>
+ </button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ props: {
+ type: {
+ type: String,
+ required: false
+ }
+ },
+ data() {
+ return {
+ styl: 'fill'
+ };
+ },
+ inject: {
+ isCardChild: { default: false }
+ },
+ created() {
+ if (this.isCardChild) {
+ this.styl = 'line';
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark, fill)
+ > button
+ display block
+ width 100%
+ margin 0
+ padding 0
+ 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
+
+ &:hover
+ background lighten($theme-color, 5%)
+
+ &:active
+ background darken($theme-color, 5%)
+ else
+ color $theme-color
+ background none
+
+ &:hover
+ color darken($theme-color, 5%)
+
+ &:active
+ background rgba($theme-color, 0.3)
+
+.ui-button[data-darkmode]
+ &.fill
+ root(true, true)
+ &:not(.fill)
+ root(true, false)
+
+.ui-button:not([data-darkmode])
+ &.fill
+ root(false, true)
+ &:not(.fill)
+ root(false, false)
+
+</style>
diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue
new file mode 100644
index 0000000000..05c51bca6b
--- /dev/null
+++ b/src/client/app/common/views/components/ui/card.vue
@@ -0,0 +1,46 @@
+<template>
+<div class="ui-card">
+ <header>
+ <slot name="title"></slot>
+ </header>
+
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ provide() {
+ return {
+ isCardChild: true
+ };
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ margin 16px
+ padding 16px
+ color isDark ? #fff : #000
+ background isDark ? #282C37 : #fff
+ 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
+
+.ui-card[data-darkmode]
+ root(true)
+
+.ui-card:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/common/views/components/ui/form.vue b/src/client/app/common/views/components/ui/form.vue
new file mode 100644
index 0000000000..fc8fdad9c4
--- /dev/null
+++ b/src/client/app/common/views/components/ui/form.vue
@@ -0,0 +1,30 @@
+<template>
+<div class="ui-form">
+ <fieldset :disabled="disabled">
+ <slot></slot>
+ </fieldset>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.ui-form
+ > fieldset
+ margin 0
+ padding 0
+ border none
+
+</style>
diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue
new file mode 100644
index 0000000000..ce28bfb12a
--- /dev/null
+++ b/src/client/app/common/views/components/ui/input.vue
@@ -0,0 +1,350 @@
+<template>
+<div class="ui-input" :class="[{ focused, filled }, styl]">
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input">
+ <div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
+ <div class="value" ref="passwordMetar"></div>
+ </div>
+ <span class="label" ref="label"><slot></slot></span>
+ <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
+ <template v-if="type != 'file'">
+ <input ref="input"
+ :type="type"
+ v-model="v"
+ :required="required"
+ :readonly="readonly"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ @focus="focused = true"
+ @blur="focused = false">
+ </template>
+ <template v-else>
+ <input ref="input"
+ type="text"
+ :value="placeholder"
+ readonly
+ @click="chooseFile">
+ <input ref="file"
+ type="file"
+ :value="value"
+ @change="onChangeFile">
+ </template>
+ <div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
+ </div>
+ <div class="text"><slot name="text"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+const getPasswordStrength = require('syuilo-password-strength');
+
+export default Vue.extend({
+ props: {
+ value: {
+ required: false
+ },
+ type: {
+ type: String,
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ readonly: {
+ type: Boolean,
+ required: false
+ },
+ pattern: {
+ type: String,
+ required: false
+ },
+ autocomplete: {
+ required: false
+ },
+ spellcheck: {
+ required: false
+ },
+ withPasswordMeter: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+ data() {
+ return {
+ v: this.value,
+ focused: false,
+ passwordStrength: '',
+ styl: 'fill'
+ };
+ },
+ computed: {
+ filled(): boolean {
+ return this.v != '' && this.v != null;
+ },
+ placeholder(): string {
+ if (this.type != 'file') return null;
+ if (this.v == null) return null;
+
+ if (typeof this.v == 'string') return this.v;
+
+ if (Array.isArray(this.v)) {
+ return this.v.map(file => file.name).join(', ');
+ } else {
+ return this.v.name;
+ }
+ }
+ },
+ watch: {
+ value(v) {
+ this.v = v;
+ },
+ v(v) {
+ this.$emit('input', v);
+
+ if (this.withPasswordMeter) {
+ if (v == '') {
+ this.passwordStrength = '';
+ return;
+ }
+
+ const strength = getPasswordStrength(v);
+ this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
+ (this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
+ }
+ }
+ },
+ 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';
+ if (this.$refs.prefix.offsetWidth) {
+ this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px';
+ }
+ }
+ if (this.$refs.suffix) {
+ if (this.$refs.suffix.offsetWidth) {
+ this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px';
+ }
+ }
+ },
+ methods: {
+ focus() {
+ this.$refs.input.focus();
+ },
+ chooseFile() {
+ this.$refs.file.click();
+ },
+ onChangeFile() {
+ this.v = Array.from((this.$refs.file as any).files);
+ this.$emit('input', this.v);
+ this.$emit('change', this.v);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark, fill)
+ margin 32px 0
+
+ > .icon
+ position absolute
+ top 0
+ left 0
+ width 24px
+ text-align center
+ line-height 32px
+ color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+
+ &:not(:empty) + .input
+ margin-left 28px
+
+ > .input
+
+ if !fill
+ &:before
+ content ''
+ display block
+ position absolute
+ bottom 0
+ left 0
+ right 0
+ height 1px
+ background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
+
+ &:after
+ content ''
+ display block
+ position absolute
+ bottom 0
+ left 0
+ right 0
+ height 2px
+ background $theme-color
+ 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)
+ will-change border opacity transform
+
+ > .password-meter
+ position absolute
+ top 0
+ left 0
+ width 100%
+ height 100%
+ border-radius 6px
+ overflow hidden
+ opacity 0.3
+
+ &[data-strength='']
+ display none
+
+ &[data-strength='low']
+ > .value
+ background #d73612
+
+ &[data-strength='medium']
+ > .value
+ background #d7ca12
+
+ &[data-strength='high']
+ > .value
+ background #61bb22
+
+ > .value
+ display block
+ width 0%
+ height 100%
+ background transparent
+ border-radius 6px
+ transition all 0.1s ease
+
+ > .label
+ position absolute
+ z-index 1
+ top fill ? 6px : 0
+ left 0
+ pointer-events none
+ transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
+ transition-duration 0.3s
+ font-size 16px
+ line-height 32px
+ color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ pointer-events none
+ //will-change transform
+ transform-origin top left
+ transform scale(1)
+
+ > input
+ display block
+ width 100%
+ margin 0
+ padding 0
+ font inherit
+ font-weight fill ? bold : normal
+ font-size 16px
+ line-height 32px
+ color isDark ? #fff : #000
+ background transparent
+ border none
+ border-radius 0
+ outline none
+ box-shadow none
+
+ if fill
+ padding 6px 12px
+ background rgba(#000, 0.035)
+ border-radius 6px
+
+ &[type='file']
+ display none
+
+ > .prefix
+ > .suffix
+ display block
+ position absolute
+ z-index 1
+ top 0
+ font-size 16px
+ line-height fill ? 44px : 32px
+ color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ pointer-events none
+
+ &:empty
+ display none
+
+ > *
+ display block
+ min-width 16px
+ max-width 150px
+ overflow hidden
+ white-space nowrap
+ text-overflow ellipsis
+
+ > .prefix
+ left 0
+ padding-right 4px
+
+ if fill
+ padding-left 12px
+
+ > .suffix
+ right 0
+ padding-left 4px
+
+ if fill
+ padding-right 12px
+
+ > .text
+ margin 6px 0
+ font-size 13px
+
+ *
+ margin 0
+
+ &.focused
+ > .input
+ if fill
+ background rgba(#000, 0.05)
+ else
+ &:after
+ opacity 1
+ transform scaleX(1)
+
+ > .label
+ color $theme-color
+
+ &.focused
+ &.filled
+ > .input
+ > .label
+ top fill ? -24px : -17px
+ 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])
+ &.fill
+ root(false, true)
+ &:not(.fill)
+ root(false, false)
+
+</style>
diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue
new file mode 100644
index 0000000000..04a46c5a96
--- /dev/null
+++ b/src/client/app/common/views/components/ui/radio.vue
@@ -0,0 +1,120 @@
+<template>
+<div
+ class="ui-radio"
+ :class="{ disabled, checked }"
+ :aria-checked="checked"
+ :aria-disabled="disabled"
+ @click="toggle"
+>
+ <input type="radio"
+ :disabled="disabled"
+ >
+ <span class="button">
+ <span></span>
+ </span>
+ <span class="label"><slot></slot></span>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ model: {
+ prop: 'model',
+ event: 'change'
+ },
+ props: {
+ model: {
+ type: String,
+ required: false
+ },
+ value: {
+ type: String,
+ required: false
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+ },
+ computed: {
+ checked(): boolean {
+ return this.model === this.value;
+ }
+ },
+ methods: {
+ toggle() {
+ this.$emit('change', this.value);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ display inline-block
+ margin 32px 32px 32px 0
+ cursor pointer
+ transition all 0.3s
+
+ > *
+ user-select none
+
+ &.disabled
+ opacity 0.6
+ cursor not-allowed
+
+ &.checked
+ > .button
+ border-color $theme-color
+
+ &:after
+ background-color $theme-color
+ transform scale(1)
+ opacity 1
+
+ > input
+ position absolute
+ width 0
+ height 0
+ opacity 0
+ margin 0
+
+ > .button
+ position absolute
+ width 20px
+ height 20px
+ background none
+ border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ border-radius 100%
+ transition inherit
+
+ &:after
+ content ''
+ display block
+ position absolute
+ top 3px
+ right 3px
+ bottom 3px
+ left 3px
+ border-radius 100%
+ opacity 0
+ transform scale(0)
+ transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
+
+ > .label
+ margin-left 28px
+ display block
+ font-size 16px
+ 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
new file mode 100644
index 0000000000..4273a4a0de
--- /dev/null
+++ b/src/client/app/common/views/components/ui/select.vue
@@ -0,0 +1,215 @@
+<template>
+<div class="ui-select" :class="[{ focused, filled }, styl]">
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input" @click="focus">
+ <span class="label" ref="label"><slot name="label"></slot></span>
+ <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
+ <select ref="input"
+ :value="v"
+ :required="required"
+ @input="$emit('input', $event.target.value)"
+ @focus="focused = true"
+ @blur="focused = false">
+ <slot></slot>
+ </select>
+ <div class="suffix"><slot name="suffix"></slot></div>
+ </div>
+ <div class="text"><slot name="text"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ value: {
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ }
+ },
+ data() {
+ return {
+ v: this.value,
+ focused: false,
+ styl: 'fill'
+ };
+ },
+ computed: {
+ filled(): boolean {
+ return this.v != '' && this.v != null;
+ }
+ },
+ watch: {
+ value(v) {
+ 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';
+ }
+ },
+ methods: {
+ focus() {
+ this.$refs.input.focus();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark, fill)
+ margin 32px 0
+
+ > .icon
+ position absolute
+ top 0
+ left 0
+ width 24px
+ text-align center
+ line-height 32px
+ color rgba(#000, 0.54)
+
+ &:not(:empty) + .input
+ margin-left 28px
+
+ > .input
+ display flex
+
+ if fill
+ padding 6px 12px
+ background rgba(#000, 0.035)
+ border-radius 6px
+ else
+ &:before
+ content ''
+ display block
+ position absolute
+ bottom 0
+ left 0
+ right 0
+ height 1px
+ background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
+
+ &:after
+ content ''
+ display block
+ position absolute
+ bottom 0
+ left 0
+ right 0
+ height 2px
+ background $theme-color
+ 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)
+ will-change border opacity transform
+
+ > .label
+ position absolute
+ top fill ? 6px : 0
+ left 0
+ pointer-events none
+ transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
+ transition-duration 0.3s
+ font-size 16px
+ line-height 32px
+ color rgba(#000, 0.54)
+ pointer-events none
+ //will-change transform
+ transform-origin top left
+ transform scale(1)
+
+ > select
+ display block
+ flex 1
+ width 100%
+ padding 0
+ font inherit
+ font-weight fill ? bold : normal
+ font-size 16px
+ height 32px
+ color isDark ? #fff : #000
+ background transparent
+ border none
+ border-radius 0
+ outline none
+ box-shadow none
+
+ *
+ color #000
+
+ > .prefix
+ > .suffix
+ display block
+ align-self center
+ justify-self center
+ font-size 16px
+ line-height 32px
+ color rgba(#000, 0.54)
+ pointer-events none
+
+ > *
+ display block
+ min-width 16px
+
+ > .prefix
+ padding-right 4px
+
+ > .suffix
+ padding-left 4px
+
+ > .text
+ margin 6px 0
+ font-size 13px
+
+ *
+ margin 0
+
+ &.focused
+ > .input
+ if fill
+ background rgba(#000, 0.05)
+ else
+ &:after
+ opacity 1
+ transform scaleX(1)
+
+ > .label
+ color $theme-color
+
+ &.focused
+ &.filled
+ > .input
+ > .label
+ top fill ? -24px : -17px
+ 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])
+ &.fill
+ root(false, true)
+ &:not(.fill)
+ root(false, false)
+
+</style>
diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue
new file mode 100644
index 0000000000..a9e00d73d2
--- /dev/null
+++ b/src/client/app/common/views/components/ui/switch.vue
@@ -0,0 +1,135 @@
+<template>
+<div
+ class="ui-switch"
+ :class="{ disabled, checked }"
+ role="switch"
+ :aria-checked="checked"
+ :aria-disabled="disabled"
+ @click="toggle"
+>
+ <input
+ type="checkbox"
+ ref="input"
+ :disabled="disabled"
+ @keydown.enter="toggle"
+ >
+ <span class="button">
+ <span></span>
+ </span>
+ <span class="label">
+ <span :aria-hidden="!checked"><slot></slot></span>
+ <p :aria-hidden="!checked">
+ <slot name="text"></slot>
+ </p>
+ </span>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ model: {
+ prop: 'value',
+ event: 'change'
+ },
+ props: {
+ value: {
+ type: Boolean,
+ default: false
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+ },
+ computed: {
+ checked(): boolean {
+ return this.value;
+ }
+ },
+ methods: {
+ toggle() {
+ this.$emit('change', !this.checked);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ display flex
+ margin 32px 0
+ cursor pointer
+ transition all 0.3s
+
+ > *
+ user-select none
+
+ &.disabled
+ opacity 0.6
+ cursor not-allowed
+
+ &.checked
+ > .button
+ background-color rgba($theme-color, 0.4)
+ border-color rgba($theme-color, 0.4)
+
+ > *
+ background-color $theme-color
+ transform translateX(14px)
+
+ > input
+ position absolute
+ width 0
+ height 0
+ opacity 0
+ margin 0
+
+ > .button
+ display inline-block
+ margin 3px 0 0 0
+ width 34px
+ height 14px
+ background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25)
+ outline none
+ border-radius 14px
+ transition inherit
+
+ > *
+ position absolute
+ top -3px
+ left 0
+ border-radius 100%
+ transition background-color 0.3s, transform 0.3s
+ width 20px
+ height 20px
+ background-color #fff
+ box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12)
+
+ > .label
+ margin-left 8px
+ display block
+ font-size 16px
+ cursor pointer
+ transition inherit
+
+ > span
+ display block
+ line-height 20px
+ color isDark ? #c4ccd2 : rgba(#000, 0.75)
+ 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)
+
+</style>
diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue
new file mode 100644
index 0000000000..60fe1cdd82
--- /dev/null
+++ b/src/client/app/common/views/components/ui/textarea.vue
@@ -0,0 +1,174 @@
+<template>
+<div class="ui-textarea" :class="{ focused, filled }">
+ <div class="input">
+ <span class="label" ref="label"><slot></slot></span>
+ <textarea ref="input"
+ :value="value"
+ :required="required"
+ :readonly="readonly"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ @input="$emit('input', $event.target.value)"
+ @focus="focused = true"
+ @blur="focused = false">
+ </textarea>
+ </div>
+ <div class="text"><slot name="text"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+const getPasswordStrength = require('syuilo-password-strength');
+
+export default Vue.extend({
+ props: {
+ value: {
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ readonly: {
+ type: Boolean,
+ required: false
+ },
+ pattern: {
+ type: String,
+ required: false
+ },
+ autocomplete: {
+ type: String,
+ required: false
+ }
+ },
+ data() {
+ return {
+ focused: false,
+ passwordStrength: ''
+ }
+ },
+ computed: {
+ filled(): boolean {
+ return this.value != '' && this.value != null;
+ }
+ },
+ methods: {
+ focus() {
+ this.$refs.input.focus();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark, fill)
+ margin 42px 0 32px 0
+
+ > .input
+ padding 12px
+
+ if fill
+ background rgba(#000, 0.035)
+ border-radius 6px
+ else
+ &:before
+ content ''
+ display block
+ position absolute
+ top 0
+ bottom 0
+ left 0
+ right 0
+ background none
+ border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
+ border-radius 3px
+ pointer-events none
+
+ &:after
+ content ''
+ display block
+ position absolute
+ top 0
+ bottom 0
+ left 0
+ right 0
+ background none
+ border solid 2px $theme-color
+ border-radius 3px
+ opacity 0
+ transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
+ pointer-events none
+
+ > .label
+ position absolute
+ top 6px
+ left 12px
+ pointer-events none
+ transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
+ transition-duration 0.3s
+ font-size 16px
+ line-height 32px
+ color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
+ pointer-events none
+ //will-change transform
+ transform-origin top left
+ transform scale(1)
+
+ > textarea
+ display block
+ width 100%
+ min-height 100px
+ padding 0
+ font inherit
+ font-weight fill ? bold : normal
+ font-size 16px
+ color isDark ? #fff : #000
+ background transparent
+ border none
+ border-radius 0
+ outline none
+ box-shadow none
+
+ > .text
+ margin 6px 0
+ font-size 13px
+
+ *
+ margin 0
+
+ &.focused
+ > .input
+ if fill
+ background rgba(#000, 0.05)
+ else
+ &:after
+ opacity 1
+
+ > .label
+ color $theme-color
+
+ &.focused
+ &.filled
+ > .input
+ > .label
+ top -24px
+ left 0 !important
+ transform scale(0.75)
+
+.ui-textarea[data-darkmode]
+ &.fill
+ root(true, true)
+ &:not(.fill)
+ root(true, false)
+
+.ui-textarea:not([data-darkmode])
+ &.fill
+ root(false, true)
+ &:not(.fill)
+ root(false, false)
+
+</style>
diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
index a6caa80f3c..f4797d89f7 100644
--- a/src/client/app/common/views/components/uploader.vue
+++ b/src/client/app/common/views/components/uploader.vue
@@ -50,7 +50,7 @@ export default Vue.extend({
reader.readAsDataURL(file);
const data = new FormData();
- data.append('i', (this as any).os.i.token);
+ data.append('i', this.$store.state.i.token);
data.append('file', file);
if (folder) data.append('folderId', folder);
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 3bae6e5078..38979871c1 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -68,7 +68,7 @@ iframe
root(isDark)
> a
display block
- font-size 16px
+ font-size 14px
border solid 1px isDark ? #191b1f : #eee
border-radius 4px
overflow hidden
@@ -126,20 +126,44 @@ root(isDark)
line-height 16px
vertical-align top
- @media (max-width 500px)
- font-size 8px
- border none
-
+ @media (max-width 700px)
> .thumbnail
- width 70px
+ position relative
+ width 100%
+ height 100px
& + article
- left 70px
- width calc(100% - 70px)
+ left 0
+ width 100%
+
+ @media (max-width 550px)
+ font-size 12px
+
+ > .thumbnail
+ height 80px
+
+ > article
+ padding 12px
+
+ @media (max-width 500px)
+ font-size 10px
+
+ > .thumbnail
+ height 70px
> article
padding 8px
+ > header
+ margin-bottom 4px
+
+ > footer
+ margin-top 4px
+
+ > img
+ width 12px
+ height 12px
+
.mk-url-preview[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 50f0877ae9..cc9c75095e 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -5,34 +5,34 @@
<div @click="choose('public')" :class="{ active: v == 'public' }">
<div>%fa:globe%</div>
<div>
- <span>公開</span>
+ <span>%i18n:@public%</span>
</div>
</div>
<div @click="choose('home')" :class="{ active: v == 'home' }">
<div>%fa:home%</div>
<div>
- <span>ホーム</span>
- <span>ホームタイムラインにのみ公開</span>
+ <span>%i18n:@home%</span>
+ <span>%i18n:@home-desc%</span>
</div>
</div>
<div @click="choose('followers')" :class="{ active: v == 'followers' }">
<div>%fa:unlock%</div>
<div>
- <span>フォロワー</span>
- <span>自分のフォロワーにのみ公開</span>
+ <span>%i18n:@followers%</span>
+ <span>%i18n:@followers-desc%</span>
</div>
</div>
<div @click="choose('specified')" :class="{ active: v == 'specified' }">
<div>%fa:envelope%</div>
<div>
- <span>ダイレクト</span>
- <span>指定したユーザーにのみ公開</span>
+ <span>%i18n:@specified%</span>
+ <span>%i18n:@specified-desc%</span>
</div>
</div>
<div @click="choose('private')" :class="{ active: v == 'private' }">
<div>%fa:lock%</div>
<div>
- <span>非公開</span>
+ <span>%i18n:@private%</span>
</div>
</div>
</div>
@@ -203,6 +203,7 @@ root(isDark)
justify-content center
align-items center
margin-right 10px
+ width 16px
> *:last-child
flex 1 1 auto
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 6fadb030c3..f3372bf062 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -13,7 +13,7 @@
</div>
</header>
<div class="text">
- <mk-note-html :text="note.text"/>
+ <mk-note-html v-if="note.text" :text="note.text"/>
</div>
</div>
</div>
@@ -37,6 +37,7 @@ export default Vue.extend({
fetch(cb?) {
this.fetching = true;
(this as any).api('notes', {
+ local: true,
reply: false,
renote: false,
media: false,
@@ -52,15 +53,15 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-welcome-timeline
- background #fff
+root(isDark)
+ background isDark ? #282C37 : #fff
> div
padding 16px
overflow-wrap break-word
font-size .9em
- color #4C4C4C
- border-bottom 1px solid rgba(#000, 0.05)
+ color isDark ? #fff : #4C4C4C
+ border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
&:after
content ""
@@ -95,17 +96,26 @@ export default Vue.extend({
overflow hidden
font-weight bold
text-overflow ellipsis
- color #627079
+ color isDark ? #fff : #627079
> .username
margin 0 .5em 0 0
- color #ccc
+ color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
> .created-at
- color #c0c0c0
+ color isDark ? #606984 : #c0c0c0
+
+ > .text
+ text-align left
+
+.mk-welcome-timeline[data-darkmode]
+ root(true)
+
+.mk-welcome-timeline:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/widgets/access-log.vue b/src/client/app/common/views/widgets/access-log.vue
deleted file mode 100644
index 8652e35645..0000000000
--- a/src/client/app/common/views/widgets/access-log.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<template>
-<div class="mkw-access-log">
- <mk-widget-container :show-header="props.design == 0">
- <template slot="header">%fa:server%%i18n:@title%</template>
-
- <div :class="$style.logs" ref="log">
- <p v-for="req in requests">
- <span :class="$style.ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span>
- <b>{{ req.method }}</b>
- <span>{{ req.path }}</span>
- </p>
- </div>
- </mk-widget-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import * as seedrandom from 'seedrandom';
-
-export default define({
- name: 'broadcast',
- props: () => ({
- design: 0
- })
-}).extend({
- data() {
- return {
- requests: [],
- connection: null,
- connectionId: null
- };
- },
- mounted() {
- this.connection = (this as any).os.streams.requestsStream.getConnection();
- this.connectionId = (this as any).os.streams.requestsStream.use();
- this.connection.on('request', this.onRequest);
- },
- beforeDestroy() {
- this.connection.off('request', this.onRequest);
- (this as any).os.streams.requestsStream.dispose(this.connectionId);
- },
- methods: {
- onRequest(request) {
- const random = seedrandom(request.ip);
- const r = Math.floor(random() * 255);
- const g = Math.floor(random() * 255);
- const b = Math.floor(random() * 255);
- const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings
- request.bg = `rgb(${r}, ${g}, ${b})`;
- request.fg = luma >= 165 ? '#000' : '#fff';
-
- this.requests.push(request);
- if (this.requests.length > 30) this.requests.shift();
-
- (this.$refs.log as any).scrollTop = (this.$refs.log as any).scrollHeight;
- },
- func() {
- if (this.props.design == 1) {
- this.props.design = 0;
- } else {
- this.props.design++;
- }
- this.save();
- }
- }
-});
-</script>
-
-<style lang="stylus" module>
-.logs
- max-height 250px
- overflow auto
-
- > p
- margin 0
- padding 8px
- font-size 0.8em
- color #555
-
- &:nth-child(odd)
- background rgba(#000, 0.025)
-
- > b
- margin-right 4px
-
-.ip
- margin-right 4px
- padding 0 4px
-
-</style>
diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue
new file mode 100644
index 0000000000..b1177d4ddf
--- /dev/null
+++ b/src/client/app/common/views/widgets/analog-clock.vue
@@ -0,0 +1,41 @@
+<template>
+<div class="mkw-analog-clock">
+ <mk-widget-container :naked="props.naked" :show-header="false">
+ <div class="mkw-analog-clock--body">
+ <mk-analog-clock :dark="$store.state.device.darkmode"/>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../common/define-widget';
+export default define({
+ name: 'analog-clock',
+ props: () => ({
+ naked: false
+ })
+}).extend({
+ methods: {
+ func() {
+ this.props.naked = !this.props.naked;
+ this.save();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ .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 75b1d60524..69b2a54fe9 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -2,7 +2,7 @@
<div class="mkw-broadcast"
:data-found="broadcasts.length != 0"
:data-melt="props.design == 1"
- :data-mobile="isMobile"
+ :data-mobile="platform == 'mobile'"
>
<div class="icon">
<svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
@@ -14,7 +14,7 @@
</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>
+ <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>
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 41e9253784..333b56f629 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -1,37 +1,37 @@
<template>
-<div class="mkw-calendar"
- :data-melt="props.design == 1"
- :data-special="special"
- :data-mobile="isMobile"
->
- <div class="calendar" :data-is-holiday="isHoliday">
- <p class="month-and-year">
- <span class="year">{{ year }}年</span>
- <span class="month">{{ month }}月</span>
- </p>
- <p class="day">{{ day }}日</p>
- <p class="week-day">{{ weekDay }}曜日</p>
- </div>
- <div class="info">
- <div>
- <p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
- <div class="meter">
- <div class="val" :style="{ width: `${dayP}%` }"></div>
+<div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'">
+ <mk-widget-container :naked="props.design == 1" :show-header="false">
+ <div class="mkw-calendar--body">
+ <div class="calendar" :data-is-holiday="isHoliday">
+ <p class="month-and-year">
+ <span class="year">{{ year }}年</span>
+ <span class="month">{{ month }}月</span>
+ </p>
+ <p class="day">{{ day }}日</p>
+ <p class="week-day">{{ weekDay }}曜日</p>
</div>
- </div>
- <div>
- <p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
- <div class="meter">
- <div class="val" :style="{ width: `${monthP}%` }"></div>
- </div>
- </div>
- <div>
- <p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
- <div class="meter">
- <div class="val" :style="{ width: `${yearP}%` }"></div>
+ <div class="info">
+ <div>
+ <p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${dayP}%` }"></div>
+ </div>
+ </div>
+ <div>
+ <p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${monthP}%` }"></div>
+ </div>
+ </div>
+ <div>
+ <p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
+ <div class="meter">
+ <div class="val" :style="{ width: `${yearP}%` }"></div>
+ </div>
+ </div>
</div>
</div>
- </div>
+ </mk-widget-container>
</div>
</template>
@@ -67,7 +67,7 @@ export default define({
},
methods: {
func() {
- if (this.isMobile) return;
+ if (this.platform == 'mobile') return;
if (this.props.design == 2) {
this.props.design = 0;
} else {
@@ -111,93 +111,82 @@ export default define({
@import '~const.styl'
root(isDark)
- padding 16px 0
- color isDark ? #c5ced6 :#777
- background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
-
&[data-special='on-new-years-day']
border-color #ef95a0
- &[data-melt]
- background transparent
- border none
-
- &[data-mobile]
- border none
- border-radius 8px
- box-shadow 0 0 0 1px rgba(#000, 0.2)
+ .mkw-calendar--body
+ padding 16px 0
+ color isDark ? #c5ced6 : #777
- &:after
- content ""
- display block
- clear both
+ &:after
+ content ""
+ display block
+ clear both
- > .calendar
- float left
- width 60%
- text-align center
+ > .calendar
+ float left
+ width 60%
+ text-align center
- &[data-is-holiday]
- > .day
- color #ef95a0
+ &[data-is-holiday]
+ > .day
+ color #ef95a0
- > p
- margin 0
- line-height 18px
- font-size 14px
+ > p
+ margin 0
+ line-height 18px
+ font-size 14px
- > span
- margin 0 4px
+ > span
+ margin 0 4px
- > .day
- margin 10px 0
- line-height 32px
- font-size 28px
+ > .day
+ margin 10px 0
+ line-height 32px
+ font-size 28px
- > .info
- display block
- float left
- width 40%
- padding 0 16px 0 0
+ > .info
+ display block
+ float left
+ width 40%
+ padding 0 16px 0 0
- > div
- margin-bottom 8px
+ > div
+ margin-bottom 8px
- &:last-child
- margin-bottom 4px
+ &:last-child
+ margin-bottom 4px
- > p
- margin 0 0 2px 0
- font-size 12px
- line-height 18px
- color isDark ? #7a8692 : #888
+ > p
+ margin 0 0 2px 0
+ font-size 12px
+ line-height 18px
+ color isDark ? #7a8692 : #888
- > b
- margin-left 2px
+ > b
+ margin-left 2px
- > .meter
- width 100%
- overflow hidden
- background isDark ? #1c1f25 : #eee
- border-radius 8px
+ > .meter
+ width 100%
+ overflow hidden
+ background isDark ? #1c1f25 : #eee
+ border-radius 8px
- > .val
- height 4px
- background $theme-color
+ > .val
+ height 4px
+ background $theme-color
- &:nth-child(1)
- > .meter > .val
- background #f7796c
+ &:nth-child(1)
+ > .meter > .val
+ background #f7796c
- &:nth-child(2)
- > .meter > .val
- background #a1de41
+ &:nth-child(2)
+ > .meter > .val
+ background #a1de41
- &:nth-child(3)
- > .meter > .val
- background #41ddde
+ &:nth-child(3)
+ > .meter > .val
+ background #41ddde
.mkw-calendar[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index e35462611d..470576d5e6 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -1,11 +1,11 @@
<template>
-<div class="mkw-donation" :data-mobile="isMobile">
+<div class="mkw-donation" :data-mobile="platform == 'mobile'">
<article>
<h1>%fa:heart%%i18n:@title%</h1>
<p>
- {{ '%i18n:!@text%'.substr(0, '%i18n:!@text%'.indexOf('{')) }}
+ {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
<a href="https://syuilo.com">@syuilo</a>
- {{ '%i18n:!@text%'.substr('%i18n:!@text%'.indexOf('}') + 1) }}
+ {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
</p>
</article>
</div>
diff --git a/src/client/app/common/views/widgets/hashtags.chart.vue b/src/client/app/common/views/widgets/hashtags.chart.vue
new file mode 100644
index 0000000000..723a3947f8
--- /dev/null
+++ b/src/client/app/common/views/widgets/hashtags.chart.vue
@@ -0,0 +1,89 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
+ <defs>
+ <linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
+ <stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
+ <stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
+ </linearGradient>
+ <mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+ <polygon
+ :points="polygonPoints"
+ fill="#fff"
+ fill-opacity="0.5"/>
+ <polyline
+ :points="polylinePoints"
+ fill="none"
+ stroke="#fff"
+ stroke-width="2"/>
+ <circle
+ :cx="headX"
+ :cy="headY"
+ r="3"
+ fill="#fff"/>
+ </mask>
+ </defs>
+ <rect
+ x="-10" y="-10"
+ :width="viewBoxX + 20" :height="viewBoxY + 20"
+ :style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ props: {
+ src: {
+ type: Array,
+ required: true
+ }
+ },
+ data() {
+ return {
+ viewBoxX: 50,
+ viewBoxY: 30,
+ gradientId: uuid(),
+ maskId: uuid(),
+ polylinePoints: '',
+ polygonPoints: '',
+ headX: null,
+ headY: null,
+ clock: null
+ };
+ },
+ watch: {
+ src() {
+ this.draw();
+ }
+ },
+ created() {
+ this.draw();
+
+ // Vueが何故かWatchを発動させない場合があるので
+ this.clock = setInterval(this.draw, 1000);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ draw() {
+ const stats = this.src.slice().reverse();
+ const peak = Math.max.apply(null, stats) || 1;
+
+ const polylinePoints = stats.map((n, i) => [
+ i * (this.viewBoxX / (stats.length - 1)),
+ (1 - (n / peak)) * this.viewBoxY
+ ]);
+
+ this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+
+ this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+
+ this.headX = polylinePoints[polylinePoints.length - 1][0];
+ this.headY = polylinePoints[polylinePoints.length - 1][1];
+ }
+ }
+});
+</script>
diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue
new file mode 100644
index 0000000000..9ab855d927
--- /dev/null
+++ b/src/client/app/common/views/widgets/hashtags.vue
@@ -0,0 +1,118 @@
+<template>
+<div class="mkw-hashtags">
+ <mk-widget-container :show-header="!props.compact">
+ <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 v-for="stat in stats" :key="stat.tag">
+ <div class="tag">
+ <router-link :to="`/tags/${ 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>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../common/define-widget';
+import XChart from './hashtags.chart.vue';
+
+export default define({
+ name: 'hashtags',
+ props: () => ({
+ 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/index.ts b/src/client/app/common/views/widgets/index.ts
index e41030e85a..7d548ef353 100644
--- a/src/client/app/common/views/widgets/index.ts
+++ b/src/client/app/common/views/widgets/index.ts
@@ -1,9 +1,11 @@
import Vue from 'vue';
-import wAccessLog from './access-log.vue';
+import wAnalogClock from './analog-clock.vue';
import wVersion from './version.vue';
import wRss from './rss.vue';
import wServer from './server.vue';
+import wPostsMonitor from './posts-monitor.vue';
+import wMemo from './memo.vue';
import wBroadcast from './broadcast.vue';
import wCalendar from './calendar.vue';
import wPhotoStream from './photo-stream.vue';
@@ -11,7 +13,9 @@ import wSlideshow from './slideshow.vue';
import wTips from './tips.vue';
import wDonation from './donation.vue';
import wNav from './nav.vue';
+import wHashtags from './hashtags.vue';
+Vue.component('mkw-analog-clock', wAnalogClock);
Vue.component('mkw-nav', wNav);
Vue.component('mkw-calendar', wCalendar);
Vue.component('mkw-photo-stream', wPhotoStream);
@@ -20,6 +24,8 @@ Vue.component('mkw-tips', wTips);
Vue.component('mkw-donation', wDonation);
Vue.component('mkw-broadcast', wBroadcast);
Vue.component('mkw-server', wServer);
+Vue.component('mkw-posts-monitor', wPostsMonitor);
+Vue.component('mkw-memo', wMemo);
Vue.component('mkw-rss', wRss);
Vue.component('mkw-version', wVersion);
-Vue.component('mkw-access-log', wAccessLog);
+Vue.component('mkw-hashtags', wHashtags);
diff --git a/src/client/app/common/views/widgets/memo.vue b/src/client/app/common/views/widgets/memo.vue
new file mode 100644
index 0000000000..30f0d3b009
--- /dev/null
+++ b/src/client/app/common/views/widgets/memo.vue
@@ -0,0 +1,111 @@
+<template>
+<div class="mkw-memo">
+ <mk-widget-container :show-header="!props.compact">
+ <template slot="header">%fa:R sticky-note%%i18n:@title%</template>
+
+ <div class="mkw-memo--body">
+ <textarea v-model="text" placeholder="%i18n:@memo%" @input="onChange"></textarea>
+ <button @click="saveMemo" :disabled="!changed">%i18n:@save%</button>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../define-widget';
+
+export default define({
+ name: 'memo',
+ props: () => ({
+ compact: false
+ })
+}).extend({
+ data() {
+ return {
+ text: null,
+ changed: false
+ };
+ },
+
+ created() {
+ this.text = this.$store.state.settings.memo;
+
+ this.$watch('$store.state.settings.memo', text => {
+ this.text = text;
+ });
+ },
+
+ methods: {
+ func() {
+ this.props.compact = !this.props.compact;
+ this.save();
+ },
+
+ onChange() {
+ this.changed = true;
+ },
+
+ saveMemo() {
+ this.$store.dispatch('settings/set', {
+ key: 'memo',
+ value: this.text
+ });
+ this.changed = false;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ .mkw-memo--body
+ padding-bottom 28px + 16px
+
+ > textarea
+ display block
+ width 100%
+ max-width 100%
+ min-width 100%
+ padding 16px
+ color isDark ? #fff : #222
+ background isDark ? #282c37 : #fff
+ border none
+ border-bottom solid 1px isDark ? #1c2023 : #eee
+ border-radius 0
+
+ > button
+ display block
+ position absolute
+ bottom 8px
+ right 8px
+ margin 0
+ padding 0 10px
+ height 28px
+ color $theme-color-foreground
+ background $theme-color !important
+ outline none
+ border none
+ border-radius 4px
+ transition background 0.1s ease
+ cursor pointer
+
+ &:hover
+ background lighten($theme-color, 10%) !important
+
+ &:active
+ background darken($theme-color, 10%) !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/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue
new file mode 100644
index 0000000000..801307be54
--- /dev/null
+++ b/src/client/app/common/views/widgets/posts-monitor.vue
@@ -0,0 +1,211 @@
+<template>
+<div class="mkw-posts-monitor">
+ <mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
+ <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">
+ <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2">
+ <defs>
+ <linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0">
+ <stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
+ <stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
+ </linearGradient>
+ <mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+ <polygon
+ :points="localPolygonPoints"
+ fill="#fff"
+ fill-opacity="0.5"/>
+ <polyline
+ :points="localPolylinePoints"
+ fill="none"
+ stroke="#fff"
+ stroke-width="1"/>
+ <circle
+ :cx="localHeadX"
+ :cy="localHeadY"
+ r="1.5"
+ fill="#fff"/>
+ </mask>
+ </defs>
+ <rect
+ x="-2" y="-2"
+ :width="viewBoxX + 4" :height="viewBoxY + 4"
+ :style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/>
+ <text x="1" y="5">Local</text>
+ </svg>
+ <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 1">
+ <defs>
+ <linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0">
+ <stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
+ <stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
+ </linearGradient>
+ <mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+ <polygon
+ :points="fediPolygonPoints"
+ fill="#fff"
+ fill-opacity="0.5"/>
+ <polyline
+ :points="fediPolylinePoints"
+ fill="none"
+ stroke="#fff"
+ stroke-width="1"/>
+ <circle
+ :cx="fediHeadX"
+ :cy="fediHeadY"
+ r="1.5"
+ fill="#fff"/>
+ </mask>
+ </defs>
+ <rect
+ x="-2" y="-2"
+ :width="viewBoxX + 4" :height="viewBoxY + 4"
+ :style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/>
+ <text x="1" y="5">Fedi</text>
+ </svg>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../common/define-widget';
+import * as uuid from 'uuid';
+
+export default define({
+ name: 'server',
+ props: () => ({
+ design: 0,
+ view: 0
+ })
+}).extend({
+ data() {
+ return {
+ connection: null,
+ connectionId: null,
+ viewBoxY: 30,
+ stats: [],
+ fediGradientId: uuid(),
+ fediMaskId: uuid(),
+ localGradientId: uuid(),
+ localMaskId: uuid(),
+ fediPolylinePoints: '',
+ localPolylinePoints: '',
+ fediPolygonPoints: '',
+ localPolygonPoints: '',
+ fediHeadX: null,
+ fediHeadY: null,
+ localHeadX: null,
+ localHeadY: null
+ };
+ },
+ computed: {
+ viewBoxX(): number {
+ return this.props.view == 0 ? 50 : 100;
+ }
+ },
+ watch: {
+ viewBoxX() {
+ this.draw();
+ }
+ },
+ mounted() {
+ this.connection = (this as any).os.streams.notesStatsStream.getConnection();
+ this.connectionId = (this as any).os.streams.notesStatsStream.use();
+
+ this.connection.on('stats', this.onStats);
+ this.connection.on('statsLog', this.onStatsLog);
+ this.connection.send({
+ type: 'requestLog',
+ id: Math.random().toString()
+ });
+ },
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ this.connection.off('statsLog', this.onStatsLog);
+ (this as any).os.streams.notesStatsStream.dispose(this.connectionId);
+ },
+ methods: {
+ toggle() {
+ if (this.props.view == 2) {
+ this.props.view = 0;
+ } else {
+ this.props.view++;
+ }
+ this.save();
+ },
+ func() {
+ if (this.props.design == 2) {
+ this.props.design = 0;
+ } else {
+ this.props.design++;
+ }
+ this.save();
+ },
+ draw() {
+ const stats = this.props.view == 0 ? this.stats.slice(-50) : this.stats;
+ const fediPeak = Math.max.apply(null, stats.map(x => x.all)) || 1;
+ const localPeak = Math.max.apply(null, stats.map(x => x.local)) || 1;
+
+ const fediPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.all / fediPeak)) * this.viewBoxY]);
+ const localPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.local / localPeak)) * this.viewBoxY]);
+ this.fediPolylinePoints = fediPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ this.localPolylinePoints = localPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+
+ this.fediPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+ this.localPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+
+ this.fediHeadX = fediPolylinePoints[fediPolylinePoints.length - 1][0];
+ this.fediHeadY = fediPolylinePoints[fediPolylinePoints.length - 1][1];
+ this.localHeadX = localPolylinePoints[localPolylinePoints.length - 1][0];
+ this.localHeadY = localPolylinePoints[localPolylinePoints.length - 1][1];
+ },
+ onStats(stats) {
+ this.stats.push(stats);
+ if (this.stats.length > 100) this.stats.shift();
+ this.draw();
+ },
+ onStatsLog(statsLog) {
+ statsLog.forEach(stats => this.onStats(stats));
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ &.dual
+ > svg
+ width 50%
+ float left
+
+ &:first-child
+ padding-right 5px
+
+ &:last-child
+ padding-left 5px
+
+ > svg
+ display block
+ padding 10px
+ width 100%
+
+ > text
+ font-size 5px
+ fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
+
+ > tspan
+ opacity 0.5
+
+ &:after
+ content ""
+ 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 b5339add0b..a777388cdb 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -1,10 +1,10 @@
<template>
-<div class="mkw-rss" :data-mobile="isMobile">
+<div class="mkw-rss">
<mk-widget-container :show-header="!props.compact">
<template slot="header">%fa:rss-square%RSS</template>
<button slot="func" title="設定" @click="setting">%fa:cog%</button>
- <div class="mkw-rss--body">
+ <div class="mkw-rss--body" :data-mobile="platform == 'mobile'">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div class="feed" v-else>
<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
@@ -19,12 +19,12 @@ import define from '../../../common/define-widget';
export default define({
name: 'rss',
props: () => ({
- compact: false
+ compact: false,
+ url: 'http://news.yahoo.co.jp/pickup/rss.xml'
})
}).extend({
data() {
return {
- url: 'http://news.yahoo.co.jp/pickup/rss.xml',
items: [],
fetching: true,
clock: null
@@ -43,7 +43,7 @@ export default define({
this.save();
},
fetch() {
- fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, {
+ fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
cache: 'no-cache'
}).then(res => {
res.json().then(feed => {
@@ -53,7 +53,12 @@ export default define({
});
},
setting() {
- alert('not implemented yet');
+ const url = window.prompt('URL', this.props.url);
+ if (url && url != '') {
+ this.props.url = url;
+ this.save();
+ this.fetch();
+ }
}
}
});
@@ -85,15 +90,17 @@ root(isDark)
margin-right 4px
&[data-mobile]
+ background isDark ? #21242f : #f3f3f3
+
.feed
padding 0
- font-size 1em
> a
padding 8px 16px
+ border-bottom none
&:nth-child(even)
- background rgba(#000, 0.05)
+ background isDark ? rgba(#000, 0.05) : rgba(#fff, 0.7)
.mkw-rss[data-darkmode]
root(true)
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 fbd36b255a..da6b9f799f 100644
--- a/src/client/app/common/views/widgets/server.cpu-memory.vue
+++ b/src/client/app/common/views/widgets/server.cpu-memory.vue
@@ -1,6 +1,6 @@
<template>
<div class="cpu-memory">
- <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
+ <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
@@ -16,15 +16,20 @@
fill="none"
stroke="#fff"
stroke-width="1"/>
+ <circle
+ :cx="cpuHeadX"
+ :cy="cpuHeadY"
+ r="1.5"
+ fill="#fff"/>
</mask>
</defs>
<rect
- x="-1" y="-1"
- :width="viewBoxX + 2" :height="viewBoxY + 2"
+ x="-2" y="-2"
+ :width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
</svg>
- <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
+ <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
@@ -40,11 +45,16 @@
fill="none"
stroke="#fff"
stroke-width="1"/>
+ <circle
+ :cx="memHeadX"
+ :cy="memHeadY"
+ r="1.5"
+ fill="#fff"/>
</mask>
</defs>
<rect
- x="-1" y="-1"
- :width="viewBoxX + 2" :height="viewBoxY + 2"
+ x="-2" y="-2"
+ :width="viewBoxX + 4" :height="viewBoxY + 4"
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
</svg>
@@ -70,15 +80,25 @@ export default Vue.extend({
memPolylinePoints: '',
cpuPolygonPoints: '',
memPolygonPoints: '',
+ cpuHeadX: null,
+ cpuHeadY: null,
+ memHeadX: null,
+ memHeadY: null,
cpuP: '',
memP: ''
};
},
mounted() {
this.connection.on('stats', this.onStats);
+ this.connection.on('statsLog', this.onStatsLog);
+ this.connection.send({
+ type: 'requestLog',
+ id: Math.random().toString()
+ });
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
+ this.connection.off('statsLog', this.onStatsLog);
},
methods: {
onStats(stats) {
@@ -86,14 +106,24 @@ export default Vue.extend({
this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift();
- this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' ');
- this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' ');
+ const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]);
+ const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]);
+ this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+ this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
+ this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
+ this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
+ this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
+
this.cpuP = (stats.cpu_usage * 100).toFixed(0);
this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
+ },
+ onStatsLog(statsLog) {
+ statsLog.forEach(stats => this.onStats(stats));
}
}
});
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
index 2fdd60499b..d796a3ae05 100644
--- a/src/client/app/common/views/widgets/server.vue
+++ b/src/client/app/common/views/widgets/server.vue
@@ -55,11 +55,11 @@ export default define({
this.fetching = false;
});
- this.connection = (this as any).os.streams.serverStream.getConnection();
- this.connectionId = (this as any).os.streams.serverStream.use();
+ this.connection = (this as any).os.streams.serverStatsStream.getConnection();
+ this.connectionId = (this as any).os.streams.serverStatsStream.use();
},
beforeDestroy() {
- (this as any).os.streams.serverStream.dispose(this.connectionId);
+ (this as any).os.streams.serverStatsStream.dispose(this.connectionId);
},
methods: {
toggle() {
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index 459b24a32f..e1c28f5115 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mkw-slideshow" :data-mobile="isMobile">
+<div class="mkw-slideshow" :data-mobile="platform == 'mobile'">
<div @click="choose">
<p v-if="props.folder === undefined">
<template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template>
diff --git a/src/client/app/config.ts b/src/client/app/config.ts
index 522d7ff056..c6efe26cd5 100644
--- a/src/client/app/config.ts
+++ b/src/client/app/config.ts
@@ -1,6 +1,8 @@
declare const _HOST_: string;
declare const _HOSTNAME_: string;
declare const _URL_: string;
+declare const _NAME_: string;
+declare const _DESCRIPTION_: string;
declare const _API_URL_: string;
declare const _WS_URL_: string;
declare const _DOCS_URL_: string;
@@ -8,6 +10,7 @@ declare const _STATS_URL_: string;
declare const _STATUS_URL_: string;
declare const _DEV_URL_: string;
declare const _LANG_: string;
+declare const _LANGS_: string;
declare const _RECAPTCHA_SITEKEY_: string;
declare const _SW_PUBLICKEY_: string;
declare const _THEME_COLOR_: string;
@@ -16,10 +19,13 @@ declare const _VERSION_: string;
declare const _CODENAME_: string;
declare const _LICENSE_: string;
declare const _GOOGLE_MAPS_API_KEY_: string;
+declare const _WELCOME_BG_URL_: string;
export const host = _HOST_;
export const hostname = _HOSTNAME_;
export const url = _URL_;
+export const name = _NAME_;
+export const description = _DESCRIPTION_;
export const apiUrl = _API_URL_;
export const wsUrl = _WS_URL_;
export const docsUrl = _DOCS_URL_;
@@ -27,6 +33,7 @@ export const statsUrl = _STATS_URL_;
export const statusUrl = _STATUS_URL_;
export const devUrl = _DEV_URL_;
export const lang = _LANG_;
+export const langs = _LANGS_;
export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
export const swPublickey = _SW_PUBLICKEY_;
export const themeColor = _THEME_COLOR_;
@@ -35,3 +42,4 @@ export const version = _VERSION_;
export const codename = _CODENAME_;
export const license = _LICENSE_;
export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
+export const welcomeBgUrl = _WELCOME_BG_URL_;
diff --git a/src/client/app/desktop/api/choose-drive-file.ts b/src/client/app/desktop/api/choose-drive-file.ts
index fbda600e6e..a362a1289b 100644
--- a/src/client/app/desktop/api/choose-drive-file.ts
+++ b/src/client/app/desktop/api/choose-drive-file.ts
@@ -1,18 +1,17 @@
+import OS from '../../mios';
import { url } from '../../config';
import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue';
-export default function(opts) {
+export default (os: OS) => opts => {
return new Promise((res, rej) => {
const o = opts || {};
if (document.body.clientWidth > 800) {
- const w = new MkChooseFileFromDriveWindow({
- propsData: {
- title: o.title,
- multiple: o.multiple,
- initFolder: o.currentFolder
- }
- }).$mount();
+ const w = os.new(MkChooseFileFromDriveWindow, {
+ title: o.title,
+ multiple: o.multiple,
+ initFolder: o.currentFolder
+ });
w.$once('selected', file => {
res(file);
});
@@ -22,9 +21,9 @@ export default function(opts) {
res(file);
};
- window.open(url + '/selectdrive',
+ window.open(url + `/selectdrive?multiple=${o.multiple}`,
'choose_drive_window',
'height=500, width=800');
}
});
-}
+};
diff --git a/src/client/app/desktop/api/choose-drive-folder.ts b/src/client/app/desktop/api/choose-drive-folder.ts
index 9b33a20d9a..68dc7988b5 100644
--- a/src/client/app/desktop/api/choose-drive-folder.ts
+++ b/src/client/app/desktop/api/choose-drive-folder.ts
@@ -1,17 +1,16 @@
+import OS from '../../mios';
import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue';
-export default function(opts) {
+export default (os: OS) => opts => {
return new Promise((res, rej) => {
const o = opts || {};
- const w = new MkChooseFolderFromDriveWindow({
- propsData: {
- title: o.title,
- initFolder: o.currentFolder
- }
- }).$mount();
+ const w = os.new(MkChooseFolderFromDriveWindow, {
+ title: o.title,
+ initFolder: o.currentFolder
+ });
w.$once('selected', folder => {
res(folder);
});
document.body.appendChild(w.$el);
});
-}
+};
diff --git a/src/client/app/desktop/api/contextmenu.ts b/src/client/app/desktop/api/contextmenu.ts
index b70d7122d3..c92f087551 100644
--- a/src/client/app/desktop/api/contextmenu.ts
+++ b/src/client/app/desktop/api/contextmenu.ts
@@ -1,16 +1,15 @@
+import OS from '../../mios';
import Ctx from '../views/components/context-menu.vue';
-export default function(e, menu, opts?) {
+export default (os: OS) => (e, menu, opts?) => {
const o = opts || {};
- const vm = new Ctx({
- propsData: {
- menu,
- x: e.pageX - window.pageXOffset,
- y: e.pageY - window.pageYOffset,
- }
- }).$mount();
+ const vm = os.new(Ctx, {
+ menu,
+ x: e.pageX - window.pageXOffset,
+ y: e.pageY - window.pageYOffset,
+ });
vm.$once('closed', () => {
if (o.closed) o.closed();
});
document.body.appendChild(vm.$el);
-}
+};
diff --git a/src/client/app/desktop/api/dialog.ts b/src/client/app/desktop/api/dialog.ts
index 07935485b0..23f35b7aa9 100644
--- a/src/client/app/desktop/api/dialog.ts
+++ b/src/client/app/desktop/api/dialog.ts
@@ -1,19 +1,18 @@
+import OS from '../../mios';
import Dialog from '../views/components/dialog.vue';
-export default function(opts) {
+export default (os: OS) => opts => {
return new Promise<string>((res, rej) => {
const o = opts || {};
- const d = new Dialog({
- propsData: {
- title: o.title,
- text: o.text,
- modal: o.modal,
- buttons: o.actions
- }
- }).$mount();
+ const d = os.new(Dialog, {
+ title: o.title,
+ text: o.text,
+ modal: o.modal,
+ buttons: o.actions
+ });
d.$once('clicked', id => {
res(id);
});
document.body.appendChild(d.$el);
});
-}
+};
diff --git a/src/client/app/desktop/api/input.ts b/src/client/app/desktop/api/input.ts
index ce26a8112f..bd7bfa0129 100644
--- a/src/client/app/desktop/api/input.ts
+++ b/src/client/app/desktop/api/input.ts
@@ -1,20 +1,19 @@
+import OS from '../../mios';
import InputDialog from '../views/components/input-dialog.vue';
-export default function(opts) {
+export default (os: OS) => opts => {
return new Promise<string>((res, rej) => {
const o = opts || {};
- const d = new InputDialog({
- propsData: {
- title: o.title,
- placeholder: o.placeholder,
- default: o.default,
- type: o.type || 'text',
- allowEmpty: o.allowEmpty
- }
- }).$mount();
+ const d = os.new(InputDialog, {
+ title: o.title,
+ placeholder: o.placeholder,
+ default: o.default,
+ type: o.type || 'text',
+ allowEmpty: o.allowEmpty
+ });
d.$once('done', text => {
res(text);
});
document.body.appendChild(d.$el);
});
-}
+};
diff --git a/src/client/app/desktop/api/notify.ts b/src/client/app/desktop/api/notify.ts
index 1f89f40ce6..72e5827607 100644
--- a/src/client/app/desktop/api/notify.ts
+++ b/src/client/app/desktop/api/notify.ts
@@ -1,10 +1,9 @@
+import OS from '../../mios';
import Notification from '../views/components/ui-notification.vue';
-export default function(message) {
- const vm = new Notification({
- propsData: {
- message
- }
- }).$mount();
+export default (os: OS) => message => {
+ const vm = os.new(Notification, {
+ message
+ });
document.body.appendChild(vm.$el);
-}
+};
diff --git a/src/client/app/desktop/api/post.ts b/src/client/app/desktop/api/post.ts
index b569610e1d..cfc78e50fa 100644
--- a/src/client/app/desktop/api/post.ts
+++ b/src/client/app/desktop/api/post.ts
@@ -1,21 +1,18 @@
+import OS from '../../mios';
import PostFormWindow from '../views/components/post-form-window.vue';
import RenoteFormWindow from '../views/components/renote-form-window.vue';
-export default function(opts) {
+export default (os: OS) => opts => {
const o = opts || {};
if (o.renote) {
- const vm = new RenoteFormWindow({
- propsData: {
- renote: o.renote
- }
- }).$mount();
+ const vm = os.new(RenoteFormWindow, {
+ note: o.renote
+ });
document.body.appendChild(vm.$el);
} else {
- const vm = new PostFormWindow({
- propsData: {
- reply: o.reply
- }
- }).$mount();
+ const vm = os.new(PostFormWindow, {
+ reply: o.reply
+ });
document.body.appendChild(vm.$el);
}
-}
+};
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
index 8ddaebc072..887367a24e 100644
--- a/src/client/app/desktop/api/update-avatar.ts
+++ b/src/client/app/desktop/api/update-avatar.ts
@@ -6,17 +6,15 @@ import ProgressDialog from '../views/components/progress-dialog.vue';
export default (os: OS) => (cb, file = null) => {
const fileSelected = file => {
- const w = new CropWindow({
- propsData: {
- image: file,
- title: 'アバターとして表示する部分を選択',
- aspectRatio: 1 / 1
- }
- }).$mount();
+ const w = os.new(CropWindow, {
+ image: file,
+ title: 'アバターとして表示する部分を選択',
+ aspectRatio: 1 / 1
+ });
w.$once('cropped', blob => {
const data = new FormData();
- data.append('i', os.i.token);
+ data.append('i', os.store.state.i.token);
data.append('file', blob, file.name + '.cropped.png');
os.api('drive/folders/find', {
@@ -42,11 +40,9 @@ export default (os: OS) => (cb, file = null) => {
};
const upload = (data, folder) => {
- const dialog = new ProgressDialog({
- propsData: {
- title: '新しいアバターをアップロードしています'
- }
- }).$mount();
+ const dialog = os.new(ProgressDialog, {
+ title: '新しいアバターをアップロードしています'
+ });
document.body.appendChild(dialog.$el);
if (folder) data.append('folderId', folder.id);
@@ -70,8 +66,14 @@ export default (os: OS) => (cb, file = null) => {
os.api('i/update', {
avatarId: file.id
}).then(i => {
- os.i.avatarId = i.avatarId;
- os.i.avatarUrl = i.avatarUrl;
+ os.store.commit('updateIKeyValue', {
+ key: 'avatarId',
+ value: i.avatarId
+ });
+ os.store.commit('updateIKeyValue', {
+ key: 'avatarUrl',
+ value: i.avatarUrl
+ });
os.apis.dialog({
title: '%fa:info-circle%アバターを更新しました',
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index 1a5da272bd..4e6dd4e2c7 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -6,17 +6,15 @@ import ProgressDialog from '../views/components/progress-dialog.vue';
export default (os: OS) => {
const cropImage = file => new Promise((resolve, reject) => {
- const w = new CropWindow({
- propsData: {
- image: file,
- title: 'バナーとして表示する部分を選択',
- aspectRatio: 16 / 9
- }
- }).$mount();
+ const w = os.new(CropWindow, {
+ image: file,
+ title: 'バナーとして表示する部分を選択',
+ aspectRatio: 16 / 9
+ });
w.$once('cropped', blob => {
const data = new FormData();
- data.append('i', os.i.token);
+ data.append('i', os.store.state.i.token);
data.append('file', blob, file.name + '.cropped.png');
os.api('drive/folders/find', {
@@ -44,11 +42,9 @@ export default (os: OS) => {
});
const upload = (data, folder) => new Promise((resolve, reject) => {
- const dialog = new ProgressDialog({
- propsData: {
- title: '新しいバナーをアップロードしています'
- }
- }).$mount();
+ const dialog = os.new(ProgressDialog, {
+ title: '新しいバナーをアップロードしています'
+ });
document.body.appendChild(dialog.$el);
if (folder) data.append('folderId', folder.id);
@@ -73,8 +69,14 @@ export default (os: OS) => {
return os.api('i/update', {
bannerId: file.id
}).then(i => {
- os.i.bannerId = i.bannerId;
- os.i.bannerUrl = i.bannerUrl;
+ os.store.commit('updateIKeyValue', {
+ key: 'bannerId',
+ value: i.bannerId
+ });
+ os.store.commit('updateIKeyValue', {
+ key: 'bannerUrl',
+ value: i.bannerUrl
+ });
os.apis.dialog({
title: '%fa:info-circle%バナーを更新しました',
diff --git a/src/client/app/desktop/assets/header-icon.dark.svg b/src/client/app/desktop/assets/header-icon.dark.svg
new file mode 100644
index 0000000000..fa42856fa5
--- /dev/null
+++ b/src/client/app/desktop/assets/header-icon.dark.svg
@@ -0,0 +1,150 @@
+<?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.dark.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></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:#ffffff;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"
+ 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"
+ 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"
+ 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.light.svg b/src/client/app/desktop/assets/header-icon.light.svg
new file mode 100644
index 0000000000..61e2026243
--- /dev/null
+++ b/src/client/app/desktop/assets/header-icon.light.svg
@@ -0,0 +1,150 @@
+<?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-logo-white.svg b/src/client/app/desktop/assets/header-logo-white.svg
deleted file mode 100644
index 8082edb30d..0000000000
--- a/src/client/app/desktop/assets/header-logo-white.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle fill="#FFFFFF" cx="128" cy="153.6" r="19.201"/>
-<circle fill="#FFFFFF" cx="51.2" cy="153.6" r="19.2"/>
-<circle fill="#FFFFFF" cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#FFFFFF" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6
- 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle fill="#FFFFFF" cx="89.6" cy="102.4" r="19.2"/>
-<circle fill="#FFFFFF" cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/client/app/desktop/assets/header-logo.svg b/src/client/app/desktop/assets/header-logo.svg
deleted file mode 100644
index 3a2207954a..0000000000
--- a/src/client/app/desktop/assets/header-logo.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle cx="128" cy="153.6" r="19.201"/>
-<circle cx="51.2" cy="153.6" r="19.2"/>
-<circle cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#000000" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6
- 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle cx="89.6" cy="102.4" r="19.2"/>
-<circle cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index 2658a86b95..201ab0a83d 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -2,7 +2,6 @@
* Desktop Client
*/
-import Vue from 'vue';
import VueRouter from 'vue-router';
// Style
@@ -24,6 +23,7 @@ import updateAvatar from './api/update-avatar';
import updateBanner from './api/update-banner';
import MkIndex from './views/pages/index.vue';
+import MkDeck from './views/pages/deck/deck.vue';
import MkUser from './views/pages/user/user.vue';
import MkFavorites from './views/pages/favorites.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
@@ -33,7 +33,9 @@ import MkHomeCustomize from './views/pages/home-customize.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
import MkSearch from './views/pages/search.vue';
-import MkOthello from './views/pages/othello.vue';
+import MkTag from './views/pages/tag.vue';
+import MkReversi from './views/pages/reversi.vue';
+import MkShare from './views/pages/share.vue';
/**
* init
@@ -51,6 +53,7 @@ init(async (launch) => {
mode: 'history',
routes: [
{ path: '/', name: 'index', component: MkIndex },
+ { path: '/deck', name: 'deck', component: MkDeck },
{ path: '/i/customize-home', component: MkHomeCustomize },
{ path: '/i/favorites', component: MkFavorites },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
@@ -59,8 +62,10 @@ init(async (launch) => {
{ path: '/i/lists/:list', component: MkUserList },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
- { path: '/othello', component: MkOthello },
- { path: '/othello/:game', component: MkOthello },
+ { path: '/tags/:tag', component: MkTag },
+ { path: '/share', component: MkShare },
+ { path: '/reversi', component: MkReversi },
+ { path: '/reversi/:game', component: MkReversi },
{ path: '/@:user', component: MkUser },
{ path: '/notes/:note', component: MkNote }
]
@@ -68,12 +73,12 @@ init(async (launch) => {
// Launch the app
const [, os] = launch(router, os => ({
- chooseDriveFolder,
- chooseDriveFile,
- dialog,
- input,
- post,
- notify,
+ chooseDriveFolder: chooseDriveFolder(os),
+ chooseDriveFile: chooseDriveFile(os),
+ dialog: dialog(os),
+ input: input(os),
+ post: post(os),
+ notify: notify(os),
updateAvatar: updateAvatar(os),
updateBanner: updateBanner(os)
}));
@@ -161,8 +166,8 @@ function registerNotifications(stream: HomeStreamManager) {
setTimeout(n.close.bind(n), 7000);
});
- connection.on('othello_invited', matching => {
- const _n = composeNotification('othello_invited', matching);
+ connection.on('reversi_invited', matching => {
+ const _n = composeNotification('reversi_invited', 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 ea48fbee3d..3cd36482e4 100644
--- a/src/client/app/desktop/style.styl
+++ b/src/client/app/desktop/style.styl
@@ -6,43 +6,26 @@
*::input-placeholder
color #D8CBC5
-*
- &:focus
- outline none
+*:focus
+ outline none
- &::scrollbar
- width 5px
- background transparent
-
- &:horizontal
- height 5px
-
- &::scrollbar-button
- width 0
- height 0
- background rgba(0, 0, 0, 0.2)
-
- &::scrollbar-piece
- background transparent
-
- &:start
- background transparent
-
- &::scrollbar-thumb
- background rgba(0, 0, 0, 0.2)
+html
+ height 100%
+ background #f7f7f7
- &:hover
- background rgba(0, 0, 0, 0.4)
+ &, *
+ &::-webkit-scrollbar
+ width 6px
+ height 6px
- &:active
- background $theme-color
+ &::-webkit-scrollbar-thumb
+ background rgba(0, 0, 0, 0.2)
- &::scrollbar-corner
- background rgba(0, 0, 0, 0.2)
+ &:hover
+ background rgba(0, 0, 0, 0.4)
-html
- height 100%
- background #f7f7f7
+ &:active
+ background $theme-color
&[data-darkmode]
background #191B22
@@ -51,10 +34,6 @@ html
&::-webkit-scrollbar-track
background-color #282C37
- &::-webkit-scrollbar
- width 6px
- height 6px
-
&::-webkit-scrollbar-thumb
background-color #454954
@@ -63,8 +42,3 @@ html
&:active
background-color $theme-color
-
-body
- display flex
- flex-direction column
- min-height 100%
diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue
index e488571070..1a88d1a994 100644
--- a/src/client/app/desktop/views/components/activity.calendar.vue
+++ b/src/client/app/desktop/views/components/activity.calendar.vue
@@ -1,5 +1,5 @@
<template>
-<svg viewBox="0 0 21 7" preserveAspectRatio="none">
+<svg viewBox="0 0 21 7">
<rect v-for="record in data" class="day"
width="1" height="1"
:x="record.x" :y="record.date.weekday"
@@ -15,7 +15,7 @@
style="pointer-events: none;"/>
<rect class="today"
width="1" height="1"
- :x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday"
+ :x="data[0].x" :y="data[0].date.weekday"
rx="1" ry="1"
fill="none"
stroke-width="0.1"
@@ -33,7 +33,7 @@ export default Vue.extend({
const peak = Math.max.apply(null, this.data.map(d => d.total));
let x = 0;
- this.data.reverse().forEach(d => {
+ this.data.slice().reverse().forEach(d => {
d.x = x;
d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
diff --git a/src/client/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue
index 175c5d37ed..202eabe90d 100644
--- a/src/client/app/desktop/views/components/activity.chart.vue
+++ b/src/client/app/desktop/views/components/activity.chart.vue
@@ -1,6 +1,6 @@
<template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown">
- <title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
+ <title>%i18n:@total%<br/>%i18n:@notes%<br/>%i18n:@replies%<br/>%i18n:@renotes%</title>
<polyline
:points="pointsNote"
fill="none"
@@ -55,7 +55,6 @@ export default Vue.extend({
};
},
created() {
- this.data.reverse();
this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
this.render();
},
@@ -63,10 +62,11 @@ export default Vue.extend({
render() {
const peak = Math.max.apply(null, this.data.map(d => d.total));
if (peak != 0) {
- this.pointsNote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
- this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
- this.pointsRenote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
- this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+ const data = this.data.slice().reverse();
+ this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
}
},
onMousedown(e) {
diff --git a/src/client/app/desktop/views/components/analog-clock.vue b/src/client/app/desktop/views/components/analog-clock.vue
deleted file mode 100644
index 81eec81598..0000000000
--- a/src/client/app/desktop/views/components/analog-clock.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<template>
-<canvas class="mk-analog-clock" ref="canvas" width="256" height="256"></canvas>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { themeColor } from '../../../config';
-
-const Vec2 = function(this: any, x, y) {
- this.x = x;
- this.y = y;
-};
-
-export default Vue.extend({
- data() {
- return {
- clock: null
- };
- },
- mounted() {
- this.tick();
- this.clock = setInterval(this.tick, 1000);
- },
- beforeDestroy() {
- clearInterval(this.clock);
- },
- methods: {
- tick() {
- const canv = this.$refs.canvas as any;
-
- const now = new Date();
- const s = now.getSeconds();
- const m = now.getMinutes();
- const h = now.getHours();
-
- const ctx = canv.getContext('2d');
- const canvW = canv.width;
- const canvH = canv.height;
- ctx.clearRect(0, 0, canvW, canvH);
-
- { // 背景
- const center = Math.min((canvW / 2), (canvH / 2));
- const lineStart = center * 0.90;
- const shortLineEnd = center * 0.87;
- const longLineEnd = center * 0.84;
- for (let i = 0; i < 60; i++) {
- const angle = Math.PI * i / 30;
- const uv = new Vec2(Math.sin(angle), -Math.cos(angle));
- ctx.beginPath();
- ctx.lineWidth = 1;
- ctx.moveTo((canvW / 2) + uv.x * lineStart, (canvH / 2) + uv.y * lineStart);
- if (i % 5 == 0) {
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
- ctx.lineTo((canvW / 2) + uv.x * longLineEnd, (canvH / 2) + uv.y * longLineEnd);
- } else {
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
- ctx.lineTo((canvW / 2) + uv.x * shortLineEnd, (canvH / 2) + uv.y * shortLineEnd);
- }
- ctx.stroke();
- }
- }
-
- { // 分
- const angle = Math.PI * (m + s / 60) / 30;
- const length = Math.min(canvW, canvH) / 2.6;
- const uv = new Vec2(Math.sin(angle), -Math.cos(angle));
- ctx.beginPath();
- ctx.strokeStyle = '#ffffff';
- ctx.lineWidth = 2;
- ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5);
- ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length);
- ctx.stroke();
- }
-
- { // 時
- const angle = Math.PI * (h % 12 + m / 60) / 6;
- const length = Math.min(canvW, canvH) / 4;
- const uv = new Vec2(Math.sin(angle), -Math.cos(angle));
- ctx.beginPath();
- ctx.strokeStyle = themeColor;
- ctx.lineWidth = 2;
- ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5);
- ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length);
- ctx.stroke();
- }
-
- { // 秒
- const angle = Math.PI * s / 30;
- const length = Math.min(canvW, canvH) / 2.6;
- const uv = new Vec2(Math.sin(angle), -Math.cos(angle));
- ctx.beginPath();
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
- ctx.lineWidth = 1;
- ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5);
- ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length);
- ctx.stroke();
- }
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-analog-clock
- display block
- width 256px
- height 256px
-</style>
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
index 1d8cc4f3a9..3b0330cf61 100644
--- a/src/client/app/desktop/views/components/calendar.vue
+++ b/src/client/app/desktop/views/components/calendar.vue
@@ -2,7 +2,7 @@
<div class="mk-calendar" :data-melt="design == 4 || design == 5">
<template v-if="design == 0 || design == 1">
<button @click="prev" title="%i18n:@prev%">%fa:chevron-circle-left%</button>
- <p class="title">{{ '%i18n:!@title%'.replace('{1}', year).replace('{2}', month) }}</p>
+ <p class="title">{{ '%i18n:@title%'.replace('{1}', year).replace('{2}', month) }}</p>
<button @click="next" title="%i18n:@next%">%fa:chevron-circle-right%</button>
</template>
@@ -21,7 +21,7 @@
:data-is-out-of-range="isOutOfRange(i + 1)"
:data-is-donichi="isDonichi(i + 1)"
@click="go(i + 1)"
- :title="isOutOfRange(i + 1) ? null : '%i18n:!@go%'"
+ :title="isOutOfRange(i + 1) ? null : '%i18n:@go%'"
>
<div>{{ i + 1 }}</div>
</div>
@@ -58,13 +58,13 @@ export default Vue.extend({
month: new Date().getMonth() + 1,
selected: new Date(),
weekdayText: [
- '%i18n:!common.weekday-short.sunday%',
- '%i18n:!common.weekday-short.monday%',
- '%i18n:!common.weekday-short.tuesday%',
- '%i18n:!common.weekday-short.wednesday%',
- '%i18n:!common.weekday-short.thursday%',
- '%i18n:!common.weekday-short.friday%',
- '%i18n:!common.weekday-short.satruday%'
+ '%i18n:common.weekday-short.sunday%',
+ '%i18n:common.weekday-short.monday%',
+ '%i18n:common.weekday-short.tuesday%',
+ '%i18n:common.weekday-short.wednesday%',
+ '%i18n:common.weekday-short.thursday%',
+ '%i18n:common.weekday-short.friday%',
+ '%i18n:common.weekday-short.saturday%'
]
};
},
@@ -138,6 +138,7 @@ root(isDark)
background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
+ overflow hidden
&[data-melt]
background transparent !important
@@ -151,9 +152,12 @@ root(isDark)
line-height 42px
font-size 0.9em
font-weight bold
- color #888
+ color isDark ? #c5ced6 : #888
box-shadow 0 1px rgba(#000, 0.07)
+ if isDark
+ background #313543
+
> [data-fa]
margin-right 4px
@@ -165,13 +169,13 @@ root(isDark)
width 42px
font-size 0.9em
line-height 42px
- color #ccc
+ color isDark ? #9baec8 : #ccc
&:hover
- color #aaa
+ color isDark ? #b2c1d5 : #aaa
&:active
- color #999
+ color isDark ? #b2c1d5 : #999
&:first-of-type
left 0
@@ -194,49 +198,49 @@ root(isDark)
font-size 14px
&.weekday
- color #19a2a9
+ color isDark ? #43d5dc : #19a2a9
&[data-is-donichi]
- color #ef95a0
+ color isDark ? #ff6679 : #ef95a0
&[data-today]
- box-shadow 0 0 0 1px #19a2a9 inset
+ box-shadow 0 0 0 1px isDark ? #43d5dc : #19a2a9 inset
border-radius 6px
&[data-is-donichi]
- box-shadow 0 0 0 1px #ef95a0 inset
+ box-shadow 0 0 0 1px isDark ? #ff6679 : #ef95a0 inset
&.day
cursor pointer
- color #777
+ color isDark ? #c5ced6 : #777
> div
border-radius 6px
&:hover > div
- background rgba(#000, 0.025)
+ background rgba(#000, isDark ? 0.1 : 0.025)
&:active > div
- background rgba(#000, 0.05)
+ background rgba(#000, isDark ? 0.2 : 0.05)
&[data-is-donichi]
- color #ef95a0
+ color isDark ? #ff6679 : #ef95a0
&[data-is-out-of-range]
cursor default
- color rgba(#777, 0.5)
+ color rgba(isDark ? #c5ced6 : #777, 0.5)
&[data-is-donichi]
- color rgba(#ef95a0, 0.5)
+ color rgba(isDark ? #ff6679 : #ef95a0, 0.5)
&[data-selected]
font-weight bold
> div
- background rgba(#000, 0.025)
+ background rgba(#000, isDark ? 0.1 : 0.025)
&:active > div
- background rgba(#000, 0.05)
+ background rgba(#000, isDark ? 0.2 : 0.05)
&[data-today]
> div
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 9a1e9c958a..30e59429d2 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
@@ -2,7 +2,7 @@
<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
<span slot="header">
<span v-html="title" :class="$style.title"></span>
- <span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}ファイル選択中)</span>
+ <span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
</span>
<mk-drive
@@ -13,9 +13,9 @@
@change-selection="onChangeSelection"
/>
<div :class="$style.footer">
- <button :class="$style.upload" title="PCからドライブにファイルをアップロード" @click="upload">%fa:upload%</button>
- <button :class="$style.cancel" @click="cancel">キャンセル</button>
- <button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">決定</button>
+ <button :class="$style.upload" title="%i18n:@upload%" @click="upload">%fa:upload%</button>
+ <button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
+ <button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">%i18n:@ok%</button>
</div>
</mk-window>
</template>
@@ -28,7 +28,7 @@ export default Vue.extend({
default: false
},
title: {
- default: '%fa:R file%ファイルを選択'
+ default: '%fa:R file%%i18n:@choose-prompt%s'
}
},
data() {
@@ -177,4 +177,3 @@ export default Vue.extend({
border-color #dcdcdc
</style>
-
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 f99533176d..0c4643fdcb 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
@@ -10,8 +10,8 @@
:multiple="false"
/>
<div :class="$style.footer">
- <button :class="$style.cancel" @click="cancel">キャンセル</button>
- <button :class="$style.ok" @click="ok">決定</button>
+ <button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
+ <button :class="$style.ok" @click="ok">%i18n:@ok%</button>
</div>
</mk-window>
</template>
@@ -21,7 +21,7 @@ import Vue from 'vue';
export default Vue.extend({
props: {
title: {
- default: '%fa:R folder%フォルダを選択'
+ default: '%fa:R folder%%i18n:@choose-prompt%'
}
},
methods: {
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 843604a059..e7deec675e 100644
--- a/src/client/app/desktop/views/components/context-menu.menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.menu.vue
@@ -1,15 +1,17 @@
<template>
<ul class="menu">
- <li v-for="(item, i) in menu" :class="item.type">
- <template v-if="item.type == 'item'">
- <p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
- </template>
- <template v-if="item.type == 'link'">
- <a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
- </template>
- <template v-else-if="item.type == 'nest'">
- <p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
- <me-nu :menu="item.menu" @x="click"/>
+ <li v-for="(item, i) in menu" :class="item ? item.type : item === null ? 'divider' : null">
+ <template v-if="item">
+ <template v-if="item.type == null || item.type == 'item'">
+ <p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
+ </template>
+ <template v-else-if="item.type == 'link'">
+ <a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
+ </template>
+ <template v-else-if="item.type == 'nest'">
+ <p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
+ <me-nu :menu="item.menu" @x="click"/>
+ </template>
</template>
</li>
</ul>
diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
index 60a33f9c93..afb6838eb6 100644
--- a/src/client/app/desktop/views/components/context-menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.vue
@@ -1,5 +1,5 @@
<template>
-<div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}">
+<div class="context-menu" @contextmenu.prevent="() => {}">
<x-menu :menu="menu" @x="click"/>
</div>
</template>
@@ -17,6 +17,23 @@ export default Vue.extend({
props: ['x', 'y', 'menu'],
mounted() {
this.$nextTick(() => {
+ const width = this.$el.offsetWidth;
+ const height = this.$el.offsetHeight;
+
+ let x = this.x;
+ let y = this.y;
+
+ if (x + width - window.pageXOffset > window.innerWidth) {
+ x = window.innerWidth - width + window.pageXOffset;
+ }
+
+ if (y + height - window.pageYOffset > window.innerHeight) {
+ y = window.innerHeight - height + window.pageYOffset;
+ }
+
+ this.$el.style.left = x + 'px';
+ this.$el.style.top = y + 'px';
+
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.addEventListener('mousedown', this.onMousedown);
});
@@ -38,7 +55,7 @@ export default Vue.extend({
return false;
},
click(item) {
- if (item.onClick) item.onClick();
+ if (item.action) item.action();
this.close();
},
close() {
@@ -59,7 +76,6 @@ root(isDark)
$item-height = 38px
$padding = 10px
- display none
position fixed
top 0
left 0
diff --git a/src/client/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue
index eb6a55d959..4fa258549f 100644
--- a/src/client/app/desktop/views/components/crop-window.vue
+++ b/src/client/app/desktop/views/components/crop-window.vue
@@ -10,9 +10,9 @@
/>
</div>
<div :class="$style.actions">
- <button :class="$style.skip" @click="skip">クロップをスキップ</button>
- <button :class="$style.cancel" @click="cancel">キャンセル</button>
- <button :class="$style.ok" @click="ok">決定</button>
+ <button :class="$style.skip" @click="skip">%i18n:@skip%</button>
+ <button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
+ <button :class="$style.ok" @click="ok">%i18n:@ok%</button>
</div>
</mk-window>
</template>
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index 39881711fa..86addb1318 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -9,10 +9,10 @@
@contextmenu.prevent.stop="onContextmenu"
:title="title"
>
- <div class="label" v-if="os.i.avatarId == file.id"><img src="/assets/label.svg"/>
+ <div class="label" v-if="$store.state.i.avatarId == file.id"><img src="/assets/label.svg"/>
<p>%i18n:@avatar%</p>
</div>
- <div class="label" v-if="os.i.bannerId == file.id"><img src="/assets/label.svg"/>
+ <div class="label" v-if="$store.state.i.bannerId == file.id"><img src="/assets/label.svg"/>
<p>%i18n:@banner%</p>
</div>
<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
@@ -50,7 +50,7 @@ export default Vue.extend({
return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
},
background(): string {
- return this.file.properties.avgColor
+ return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
? `rgb(${this.file.properties.avgColor.join(',')})`
: 'transparent';
}
@@ -62,49 +62,45 @@ export default Vue.extend({
onContextmenu(e) {
this.isContextmenuShowing = true;
- contextmenu(e, [{
+ contextmenu((this as any).os)(e, [{
type: 'item',
- text: '%i18n:!@contextmenu.rename%',
+ text: '%i18n:@contextmenu.rename%',
icon: '%fa:i-cursor%',
- onClick: this.rename
+ action: this.rename
}, {
type: 'item',
- text: '%i18n:!@contextmenu.copy-url%',
+ text: '%i18n:@contextmenu.copy-url%',
icon: '%fa:link%',
- onClick: this.copyUrl
+ action: this.copyUrl
}, {
type: 'link',
href: `${this.file.url}?download`,
- text: '%i18n:!@contextmenu.download%',
+ text: '%i18n:@contextmenu.download%',
icon: '%fa:download%',
- }, {
- type: 'divider',
- }, {
+ }, null, {
type: 'item',
- text: '%i18n:!common.delete%',
+ text: '%i18n:common.delete%',
icon: '%fa:R trash-alt%',
- onClick: this.deleteFile
- }, {
- type: 'divider',
- }, {
+ action: this.deleteFile
+ }, null, {
type: 'nest',
- text: '%i18n:!@contextmenu.else-files%',
+ text: '%i18n:@contextmenu.else-files%',
menu: [{
type: 'item',
- text: '%i18n:!@contextmenu.set-as-avatar%',
- onClick: this.setAsAvatar
+ text: '%i18n:@contextmenu.set-as-avatar%',
+ action: this.setAsAvatar
}, {
type: 'item',
- text: '%i18n:!@contextmenu.set-as-banner%',
- onClick: this.setAsBanner
+ text: '%i18n:@contextmenu.set-as-banner%',
+ action: this.setAsBanner
}]
}, {
type: 'nest',
- text: '%i18n:!@contextmenu.open-in-app%',
+ text: '%i18n:@contextmenu.open-in-app%',
menu: [{
type: 'item',
- text: '%i18n:!@contextmenu.add-app%...',
- onClick: this.addApp
+ text: '%i18n:@contextmenu.add-app%...',
+ action: this.addApp
}]
}], {
closed: () => {
@@ -129,7 +125,7 @@ export default Vue.extend({
},
onThumbnailLoaded() {
- if (this.file.properties.avgColor) {
+ if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
@@ -141,15 +137,15 @@ export default Vue.extend({
rename() {
(this as any).apis.input({
- title: '%i18n:!@contextmenu.rename-file%',
- placeholder: '%i18n:!@contextmenu.input-new-file-name%',
+ title: '%i18n:@contextmenu.rename-file%',
+ placeholder: '%i18n:@contextmenu.input-new-file-name%',
default: this.file.name,
allowEmpty: false
}).then(name => {
(this as any).api('drive/files/update', {
fileId: this.file.id,
name: name
- })
+ });
});
},
@@ -157,9 +153,9 @@ export default Vue.extend({
copyToClipboard(this.file.url);
(this as any).apis.dialog({
title: '%fa:check%%i18n:@contextmenu.copied%',
- text: '%i18n:!@contextmenu.copied-url-to-clipboard%',
+ text: '%i18n:@contextmenu.copied-url-to-clipboard%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
},
@@ -177,7 +173,9 @@ export default Vue.extend({
},
deleteFile() {
- alert('not implemented yet');
+ (this as any).api('drive/files/delete', {
+ fileId: this.file.id
+ });
}
}
});
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index 0761ffb1a1..fc0f353f47 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -52,30 +52,26 @@ export default Vue.extend({
onContextmenu(e) {
this.isContextmenuShowing = true;
- contextmenu(e, [{
+ contextmenu((this as any).os)(e, [{
type: 'item',
- text: '%i18n:!@contextmenu.move-to-this-folder%',
+ text: '%i18n:@contextmenu.move-to-this-folder%',
icon: '%fa:arrow-right%',
- onClick: this.go
+ action: this.go
}, {
type: 'item',
- text: '%i18n:!@contextmenu.show-in-new-window%',
+ text: '%i18n:@contextmenu.show-in-new-window%',
icon: '%fa:R window-restore%',
- onClick: this.newWindow
- }, {
- type: 'divider',
- }, {
+ action: this.newWindow
+ }, null, {
type: 'item',
- text: '%i18n:!@contextmenu.rename%',
+ text: '%i18n:@contextmenu.rename%',
icon: '%fa:i-cursor%',
- onClick: this.rename
- }, {
- type: 'divider',
- }, {
+ action: this.rename
+ }, null, {
type: 'item',
- text: '%i18n:!common.delete%',
+ text: '%i18n:common.delete%',
icon: '%fa:R trash-alt%',
- onClick: this.deleteFolder
+ action: this.deleteFolder
}], {
closed: () => {
this.isContextmenuShowing = false;
@@ -159,15 +155,15 @@ export default Vue.extend({
switch (err) {
case 'detected-circular-definition':
(this as any).apis.dialog({
- title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%',
- text: '%i18n:!@circular-reference-detected%',
+ title: '%fa:exclamation-triangle%%i18n:@unable-to-process%',
+ text: '%i18n:@circular-reference-detected%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
break;
default:
- alert('%i18n:!@unhandled-error% ' + err);
+ alert('%i18n:@unhandled-error% ' + err);
}
});
}
@@ -199,8 +195,8 @@ export default Vue.extend({
rename() {
(this as any).apis.input({
- title: '%i18n:!@contextmenu.rename-folder%',
- placeholder: '%i18n:!@contextmenu.input-new-folder-name%',
+ title: '%i18n:@contextmenu.rename-folder%',
+ placeholder: '%i18n:@contextmenu.input-new-folder-name%',
default: this.folder.name
}).then(name => {
(this as any).api('drive/folders/update', {
diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/app/desktop/views/components/drive.nav-folder.vue
index 71b2e419d9..40f620875e 100644
--- a/src/client/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/client/app/desktop/views/components/drive.nav-folder.vue
@@ -8,7 +8,7 @@
@drop.stop="onDrop"
>
<template v-if="folder == null">%fa:cloud%</template>
- <span>{{ folder == null ? '%i18n:!@drive%' : folder.name }}</span>
+ <span>{{ folder == null ? '%i18n:@drive%' : folder.name }}</span>
</div>
</template>
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index 973df1014d..df141b6d6c 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -118,6 +118,7 @@ export default Vue.extend({
this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
+ this.connection.on('file_deleted', this.onStreamDriveFileDeleted);
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
@@ -130,27 +131,28 @@ 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);
},
methods: {
onContextmenu(e) {
- contextmenu(e, [{
+ contextmenu((this as any).os)(e, [{
type: 'item',
- text: '%i18n:!@contextmenu.create-folder%',
+ text: '%i18n:@contextmenu.create-folder%',
icon: '%fa:R folder%',
- onClick: this.createFolder
+ action: this.createFolder
}, {
type: 'item',
- text: '%i18n:!@contextmenu.upload%',
+ text: '%i18n:@contextmenu.upload%',
icon: '%fa:upload%',
- onClick: this.selectLocalFile
+ action: this.selectLocalFile
}, {
type: 'item',
- text: '%i18n:!@contextmenu.url-upload%',
+ text: '%i18n:@contextmenu.url-upload%',
icon: '%fa:cloud-upload-alt%',
- onClick: this.urlUpload
+ action: this.urlUpload
}]);
},
@@ -167,6 +169,10 @@ export default Vue.extend({
}
},
+ onStreamDriveFileDeleted(fileId) {
+ this.removeFile(fileId);
+ },
+
onStreamDriveFolderCreated(folder) {
this.addFolder(folder, true);
},
@@ -306,15 +312,15 @@ export default Vue.extend({
switch (err) {
case 'detected-circular-definition':
(this as any).apis.dialog({
- title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%',
- text: '%i18n:!@circular-reference-detected%',
+ title: '%fa:exclamation-triangle%%i18n:@unable-to-process%',
+ text: '%i18n:@circular-reference-detected%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
break;
default:
- alert('%i18n:!@unhandled-error% ' + err);
+ alert('%i18n:@unhandled-error% ' + err);
}
});
}
@@ -327,8 +333,8 @@ export default Vue.extend({
urlUpload() {
(this as any).apis.input({
- title: '%i18n:!@url-upload%',
- placeholder: '%i18n:!@url-of-file%'
+ title: '%i18n:@url-upload%',
+ placeholder: '%i18n:@url-of-file%'
}).then(url => {
(this as any).api('drive/files/upload_from_url', {
url: url,
@@ -337,9 +343,9 @@ export default Vue.extend({
(this as any).apis.dialog({
title: '%fa:check%%i18n:@url-upload-requested%',
- text: '%i18n:!@may-take-time%',
+ text: '%i18n:@may-take-time%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
});
@@ -347,8 +353,8 @@ export default Vue.extend({
createFolder() {
(this as any).apis.input({
- title: '%i18n:!@create-folder%',
- placeholder: '%i18n:!@folder-name%'
+ title: '%i18n:@create-folder%',
+ placeholder: '%i18n:@folder-name%'
}).then(name => {
(this as any).api('drive/folders/create', {
name: name,
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index 60c6129f61..62742a8f39 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -1,19 +1,16 @@
<template>
<button class="mk-follow-button"
- :class="{ wait, follow: !user.isFollowing, unfollow: user.isFollowing, big: size == 'big' }"
+ :class="{ wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou, big: size == 'big' }"
@click="onClick"
:disabled="wait"
- :title="user.isFollowing ? 'フォロー解除' : 'フォローする'"
>
- <template v-if="!wait && user.isFollowing">
- <template v-if="size == 'compact'">%fa:minus%</template>
- <template v-if="size == 'big'">%fa:minus%フォロー解除</template>
+ <template v-if="!wait">
+ <template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half%<template v-if="size == 'big'"> %i18n:@request-pending%</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>
</template>
- <template v-if="!wait && !user.isFollowing">
- <template v-if="size == 'compact'">%fa:plus%</template>
- <template v-if="size == 'big'">%fa:plus%フォロー</template>
- </template>
- <template v-if="wait">%fa:spinner .pulse .fw%</template>
+ <template v-else>%fa:spinner .pulse .fw%</template>
</button>
</template>
@@ -34,6 +31,7 @@ export default Vue.extend({
data() {
return {
+ u: this.user,
wait: false,
connection: null,
connectionId: null
@@ -56,39 +54,44 @@ export default Vue.extend({
methods: {
onFollow(user) {
- if (user.id == this.user.id) {
+ if (user.id == this.u.id) {
this.user.isFollowing = user.isFollowing;
}
},
onUnfollow(user) {
- if (user.id == this.user.id) {
+ if (user.id == this.u.id) {
this.user.isFollowing = user.isFollowing;
}
},
- onClick() {
+ async onClick() {
this.wait = true;
- if (this.user.isFollowing) {
- (this as any).api('following/delete', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = false;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
- } else {
- (this as any).api('following/create', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = true;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
+
+ try {
+ if (this.u.isFollowing) {
+ this.u = await (this as any).api('following/delete', {
+ userId: this.u.id
+ });
+ } else {
+ if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ this.u = await (this as any).api('following/requests/cancel', {
+ userId: this.u.id
+ });
+ } else if (this.u.isLocked) {
+ this.u = await (this as any).api('following/create', {
+ userId: this.u.id
+ });
+ } else {
+ this.u = await (this as any).api('following/create', {
+ userId: this.user.id
+ });
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.wait = false;
}
}
}
@@ -124,7 +127,7 @@ root(isDark)
border 2px solid rgba($theme-color, 0.3)
border-radius 8px
- &.follow
+ &: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
@@ -137,7 +140,7 @@ root(isDark)
background isDark ? #22262f : #ececec
border-color isDark ? #151a1d : #dcdcdc
- &.unfollow
+ &.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%)
@@ -162,9 +165,6 @@ root(isDark)
height 38px
line-height 38px
- i
- margin-right 8px
-
.mk-follow-button[data-darkmode]
root(true)
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index 16206299d7..7ed31315f1 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
<template>
<mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header">
- <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロワー
+ <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
</span>
<mk-followers :user="user"/>
</mk-window>
@@ -11,7 +11,12 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['user']
+ props: ['user'],
+ computed: {
+ name(): string {
+ return Vue.filter('userName')(this.user);
+ }
+ }
});
</script>
diff --git a/src/client/app/desktop/views/components/followers.vue b/src/client/app/desktop/views/components/followers.vue
index a1b98995d8..1ef9f69771 100644
--- a/src/client/app/desktop/views/components/followers.vue
+++ b/src/client/app/desktop/views/components/followers.vue
@@ -4,7 +4,7 @@
:count="user.followersCount"
:you-know-count="user.followersYouKnowCount"
>
- フォロワーはいないようです。
+ %i18n:@empty%
</mk-users-list>
</template>
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index cc3d77198e..b97f21e2a3 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
<template>
<mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header">
- <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロー
+ <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
</span>
<mk-following :user="user"/>
</mk-window>
@@ -11,7 +11,12 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['user']
+ props: ['user'],
+ computed: {
+ name(): string {
+ return Vue.filter('userName')(this.user);
+ }
+ }
});
</script>
diff --git a/src/client/app/desktop/views/components/following.vue b/src/client/app/desktop/views/components/following.vue
index b7aedda84f..d55ce1c0d4 100644
--- a/src/client/app/desktop/views/components/following.vue
+++ b/src/client/app/desktop/views/components/following.vue
@@ -4,7 +4,7 @@
:count="user.followingCount"
:you-know-count="user.followingYouKnowCount"
>
- フォロー中のユーザーはいないようです。
+ %i18n:@empty%
</mk-users-list>
</template>
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 3c1f8b8257..7dfd9e4359 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -1,6 +1,6 @@
<template>
<div class="mk-friends-maker">
- <p class="title">気になるユーザーをフォロー:</p>
+ <p class="title">%i18n:@title%</p>
<div class="users" v-if="!fetching && users.length > 0">
<div class="user" v-for="user in users" :key="user.id">
<mk-avatar class="avatar" :user="user" target="_blank"/>
@@ -11,10 +11,10 @@
<mk-follow-button :user="user"/>
</div>
</div>
- <p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
- <a class="refresh" @click="refresh">もっと見る</a>
- <button class="close" @click="$destroy()" title="閉じる">%fa:times%</button>
+ <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>
</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 3c8bf40e12..7c6cb9cd40 100644
--- a/src/client/app/desktop/views/components/game-window.vue
+++ b/src/client/app/desktop/views/components/game-window.vue
@@ -1,7 +1,7 @@
<template>
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
- <span slot="header" :class="$style.header">%fa:gamepad%オセロ</span>
- <mk-othello :class="$style.content" @gamed="g => game = g"/>
+ <span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
+ <mk-reversi :class="$style.content" @gamed="g => game = g"/>
</mk-window>
</template>
@@ -18,8 +18,8 @@ export default Vue.extend({
computed: {
popout(): string {
return this.game
- ? `${url}/othello/${this.game.id}`
- : `${url}/othello`;
+ ? `${url}/reversi/${this.game.id}`
+ : `${url}/reversi`;
}
}
});
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index cae6233cd8..ba48ce24e8 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -1,34 +1,36 @@
<template>
<div class="mk-home" :data-customize="customize">
<div class="customize" v-if="customize">
- <router-link to="/">%fa:check%完了</router-link>
+ <router-link to="/">%fa:check%%i18n:@done%</router-link>
<div>
<div class="adder">
- <p>ウィジェットを追加:</p>
+ <p>%i18n:@add-widget%</p>
<select v-model="widgetAdderSelected">
- <option value="profile">プロフィール</option>
- <option value="calendar">カレンダー</option>
- <option value="timemachine">カレンダー(タイムマシン)</option>
- <option value="activity">アクティビティ</option>
- <option value="rss">RSSリーダー</option>
- <option value="trends">トレンド</option>
- <option value="photo-stream">フォトストリーム</option>
- <option value="slideshow">スライドショー</option>
- <option value="version">バージョン</option>
- <option value="broadcast">ブロードキャスト</option>
- <option value="notifications">通知</option>
- <option value="users">おすすめユーザー</option>
- <option value="polls">投票</option>
- <option value="post-form">投稿フォーム</option>
- <option value="messaging">メッセージ</option>
- <option value="channel">チャンネル</option>
- <option value="access-log">アクセスログ</option>
- <option value="server">サーバー情報</option>
- <option value="donation">寄付のお願い</option>
- <option value="nav">ナビゲーション</option>
- <option value="tips">ヒント</option>
+ <option value="profile">%i18n:common.widgets.profile%</option>
+ <option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
+ <option value="calendar">%i18n:common.widgets.calendar%</option>
+ <option value="timemachine">%i18n:common.widgets.timemachine%</option>
+ <option value="activity">%i18n:common.widgets.activity%</option>
+ <option value="rss">%i18n:common.widgets.rss%</option>
+ <option value="trends">%i18n:common.widgets.trends%</option>
+ <option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
+ <option value="slideshow">%i18n:common.widgets.slideshow%</option>
+ <option value="version">%i18n:common.widgets.version%</option>
+ <option value="broadcast">%i18n:common.widgets.broadcast%</option>
+ <option value="notifications">%i18n:common.widgets.notifications%</option>
+ <option value="users">%i18n:common.widgets.users%</option>
+ <option value="polls">%i18n:common.widgets.polls%</option>
+ <option value="post-form">%i18n:common.widgets.post-form%</option>
+ <option value="messaging">%i18n:common.widgets.messaging%</option>
+ <option value="memo">%i18n:common.widgets.memo%</option>
+ <option value="hashtags">%i18n:common.widgets.hashtags%</option>
+ <option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
+ <option value="server">%i18n:common.widgets.server%</option>
+ <option value="donation">%i18n:common.widgets.donation%</option>
+ <option value="nav">%i18n:common.widgets.nav%</option>
+ <option value="tips">%i18n:common.widgets.tips%</option>
</select>
- <button @click="addWidget">追加</button>
+ <button @click="addWidget">%i18n:@add%</button>
</div>
<div class="trash">
<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
@@ -47,25 +49,24 @@
:key="place"
>
<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
- <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
+ <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/>
</div>
</x-draggable>
<div class="main">
<a @click="hint">カスタマイズのヒント</a>
<div>
- <mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
+ <mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/>
<mk-timeline ref="tl" @loaded="onTlLoaded"/>
</div>
</div>
</template>
<template v-else>
<div v-for="place in ['left', 'right']" :class="place">
- <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
+ <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp" platform="desktop"/>
</div>
<div class="main">
- <mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
- <mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
- <mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
+ <mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
+ <mk-timeline class="tl" cref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
</div>
</template>
</div>
@@ -77,6 +78,50 @@ import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
import * as uuid from 'uuid';
+const defaultDesktopHomeWidgets = {
+ left: [
+ 'profile',
+ 'calendar',
+ 'activity',
+ 'rss',
+ 'hashtags',
+ 'photo-stream',
+ 'version'
+ ],
+ right: [
+ 'broadcast',
+ 'notifications',
+ 'users',
+ 'polls',
+ 'server',
+ 'donation',
+ 'nav',
+ 'tips'
+ ]
+};
+
+//#region Construct home data
+const _defaultDesktopHomeWidgets = [];
+
+defaultDesktopHomeWidgets.left.forEach(widget => {
+ _defaultDesktopHomeWidgets.push({
+ name: widget,
+ id: uuid(),
+ place: 'left',
+ data: {}
+ });
+});
+
+defaultDesktopHomeWidgets.right.forEach(widget => {
+ _defaultDesktopHomeWidgets.push({
+ name: widget,
+ id: uuid(),
+ place: 'right',
+ data: {}
+ });
+});
+//#endregion
+
export default Vue.extend({
components: {
XDraggable
@@ -104,7 +149,7 @@ export default Vue.extend({
computed: {
home(): any[] {
- return this.$store.state.settings.data.home;
+ return this.$store.state.settings.home || [];
},
left(): any[] {
return this.home.filter(w => w.place == 'left');
@@ -120,6 +165,16 @@ export default Vue.extend({
}
},
+ created() {
+ if (this.$store.state.settings.home == null) {
+ this.api('i/update_home', {
+ home: _defaultDesktopHomeWidgets
+ }).then(() => {
+ this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets);
+ });
+ }
+ },
+
mounted() {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -299,11 +354,18 @@ root(isDark)
width calc(100% - 275px * 2)
order 2
- .mk-post-form
+ > .form
margin-bottom 16px
border solid 1px rgba(#000, 0.075)
border-radius 4px
+ @media (max-width 700px)
+ padding 0
+
+ > .tl
+ border none
+ border-radius 0
+
> *:not(.main)
width 275px
padding 16px 0 16px 0
diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
index f58d0706df..7b7a38afa2 100644
--- a/src/client/app/desktop/views/components/index.ts
+++ b/src/client/app/desktop/views/components/index.ts
@@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
import window from './window.vue';
import noteFormWindow from './post-form-window.vue';
import renoteFormWindow from './renote-form-window.vue';
-import analogClock from './analog-clock.vue';
import ellipsisIcon from './ellipsis-icon.vue';
import mediaImage from './media-image.vue';
import mediaImageDialog from './media-image-dialog.vue';
@@ -40,7 +39,6 @@ Vue.component('mk-sub-note-content', subNoteContent);
Vue.component('mk-window', window);
Vue.component('mk-post-form-window', noteFormWindow);
Vue.component('mk-renote-form-window', renoteFormWindow);
-Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-ellipsis-icon', ellipsisIcon);
Vue.component('mk-media-image', mediaImage);
Vue.component('mk-media-image-dialog', mediaImageDialog);
diff --git a/src/client/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue
index e939fc1903..e2cf4e48fd 100644
--- a/src/client/app/desktop/views/components/input-dialog.vue
+++ b/src/client/app/desktop/views/components/input-dialog.vue
@@ -8,8 +8,8 @@
<input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/>
</div>
<div :class="$style.actions">
- <button :class="$style.cancel" @click="cancel">キャンセル</button>
- <button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">決定</button>
+ <button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
+ <button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">%i18n:@ok%</button>
</div>
</mk-window>
</template>
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index e5803cc36e..b98a4707ec 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
computed: {
style(): any {
return {
- 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
+ 'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
};
}
diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue
deleted file mode 100644
index 66bdab5c08..0000000000
--- a/src/client/app/desktop/views/components/mentions.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<template>
-<div class="mk-mentions">
- <header>
- <span :data-active="mode == 'all'" @click="mode = 'all'">すべて</span>
- <span :data-active="mode == 'following'" @click="mode = 'following'">フォロー中</span>
- </header>
- <div class="fetching" v-if="fetching">
- <mk-ellipsis-icon/>
- </div>
- <p class="empty" v-if="notes.length == 0 && !fetching">
- %fa:R comments%
- <span v-if="mode == 'all'">あなた宛ての投稿はありません。</span>
- <span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span>
- </p>
- <mk-notes :notes="notes" ref="timeline"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
- data() {
- return {
- fetching: true,
- moreFetching: false,
- mode: 'all',
- notes: []
- };
- },
- watch: {
- mode() {
- this.fetch();
- }
- },
- mounted() {
- document.addEventListener('keydown', this.onDocumentKeydown);
- window.addEventListener('scroll', this.onScroll);
-
- this.fetch(() => this.$emit('loaded'));
- },
- beforeDestroy() {
- document.removeEventListener('keydown', this.onDocumentKeydown);
- window.removeEventListener('scroll', this.onScroll);
- },
- methods: {
- onDocumentKeydown(e) {
- if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
- if (e.which == 84) { // t
- (this.$refs.timeline as any).focus();
- }
- }
- },
- onScroll() {
- const current = window.scrollY + window.innerHeight;
- if (current > document.body.offsetHeight - 8) this.more();
- },
- fetch(cb?) {
- this.fetching = true;
- this.notes = [];
- (this as any).api('notes/mentions', {
- following: this.mode == 'following'
- }).then(notes => {
- this.notes = notes;
- this.fetching = false;
- if (cb) cb();
- });
- },
- more() {
- if (this.moreFetching || this.fetching || this.notes.length == 0) return;
- this.moreFetching = true;
- (this as any).api('notes/mentions', {
- following: this.mode == 'following',
- untilId: this.notes[this.notes.length - 1].id
- }).then(notes => {
- this.notes = this.notes.concat(notes);
- this.moreFetching = false;
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.mk-mentions
- background #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
-
- > header
- padding 8px 16px
- border-bottom solid 1px #eee
-
- > span
- margin-right 16px
- line-height 27px
- font-size 18px
- color #555
-
- &:not([data-active])
- color $theme-color
- cursor pointer
-
- &:hover
- text-decoration underline
-
- > .fetching
- padding 64px 0
-
- > .empty
- display block
- margin 0 auto
- padding 32px
- max-width 400px
- text-align center
- color #999
-
- > [data-fa]
- display block
- margin-bottom 16px
- font-size 3em
- color #ccc
-
-</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 dbe3266734..cbb58b5e99 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,6 +1,6 @@
<template>
<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
- <span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user | userName }}</span>
+ <span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
<mk-messaging-room :user="user" :class="$style.content"/>
</mk-window>
</template>
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
deleted file mode 100644
index 24550c4e94..0000000000
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-<template>
-<div class="sub" :title="title">
- <mk-avatar class="avatar" :user="note.user"/>
- <div class="main">
- <header>
- <div class="left">
- <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- </div>
- <div class="right">
- <router-link class="time" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
- </div>
- </header>
- <div class="body">
- <div class="text">
- <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
- <mk-note-html v-if="note.text" :text="note.text" :i="os.i"/>
- </div>
- <div class="media" v-if="note.mediaIds.length > 0">
- <mk-media-list :media-list="note.media"/>
- </div>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import dateStringify from '../../../common/scripts/date-stringify';
-
-export default Vue.extend({
- props: ['note'],
- computed: {
- title(): string {
- return dateStringify(this.note.createdAt);
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-root(isDark)
- margin 0
- padding 20px 32px
- background isDark ? #21242d : #fdfdfd
-
- &:after
- content ""
- display block
- clear both
-
- &:hover
- > .main > footer > button
- color #888
-
- > .avatar
- display block
- float left
- margin 0 16px 0 0
- width 44px
- height 44px
- border-radius 4px
-
- > .main
- float left
- width calc(100% - 60px)
-
- > header
- margin-bottom 4px
- white-space nowrap
-
- &:after
- content ""
- display block
- clear both
-
- > .left
- float left
-
- > .name
- display inline
- margin 0
- padding 0
- color isDark ? #fff : #777
- font-size 1em
- font-weight 700
- text-align left
- text-decoration none
-
- &:hover
- text-decoration underline
-
- > .username
- text-align left
- margin 0 0 0 8px
- color isDark ? #606984 : #ccc
-
- > .right
- float right
-
- > .time
- font-size 0.9em
- color isDark ? #606984 : #c0c0c0
-
- > .body
- > .text
- cursor default
- display block
- margin 0
- padding 0
- overflow-wrap break-word
- font-size 1em
- color isDark ? #959ba7 : #717171
-
-.sub[data-darkmode]
- root(true)
-
-.sub:not([data-darkmode])
- root(false)
-
-</style>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index a0e3915149..4b5e5bebdf 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -2,16 +2,16 @@
<div class="mk-note-detail" :title="title">
<button
class="read-more"
- v-if="p.reply && p.reply.replyId && context.length == 0"
- title="会話をもっと読み込む"
- @click="fetchContext"
- :disabled="contextFetching"
+ v-if="p.reply && p.reply.replyId && conversation.length == 0"
+ title="%i18n:@more%"
+ @click="fetchConversation"
+ :disabled="conversationFetching"
>
- <template v-if="!contextFetching">%fa:ellipsis-v%</template>
- <template v-if="contextFetching">%fa:spinner .pulse%</template>
+ <template v-if="!conversationFetching">%fa:ellipsis-v%</template>
+ <template v-if="conversationFetching">%fa:spinner .pulse%</template>
</button>
- <div class="context">
- <x-sub v-for="note in context" :key="note.id" :note="note"/>
+ <div class="conversation">
+ <x-sub v-for="note in conversation" :key="note.id" :note="note"/>
</div>
<div class="reply-to" v-if="p.reply">
<x-sub :note="p.reply"/>
@@ -21,22 +21,26 @@
<mk-avatar class="avatar" :user="note.user"/>
%fa:retweet%
<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
- がRenote
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+ <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+ <mk-time :time="note.createdAt"/>
</p>
</div>
<article>
<mk-avatar class="avatar" :user="p.user"/>
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
- <span class="username">@{{ p.user | acct }}</span>
+ <span class="username"><mk-acct :user="p.user"/></span>
<router-link class="time" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>
</header>
<div class="body">
<div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
+ <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
+ <mk-note-html 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"/>
@@ -44,9 +48,9 @@
<mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
- <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+ <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
- <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+ <a class="location" v-if="p.geo" :href="`http://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"/>
@@ -54,15 +58,15 @@
</div>
<footer>
<mk-reactions-viewer :note="p"/>
- <button @click="reply" title="返信">
+ <button @click="reply" title="">
<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 @click="renote" title="Renote">
+ <button @click="renote" title="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
</button>
- <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション">
+ <button :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">
@@ -85,7 +89,7 @@ import MkPostFormWindow from './post-form-window.vue';
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 './note-detail.sub.vue';
+import XSub from './notes.note.sub.vue';
export default Vue.extend({
components: {
@@ -104,8 +108,8 @@ export default Vue.extend({
data() {
return {
- context: [],
- contextFetching: false,
+ conversation: [],
+ conversationFetching: false,
replies: []
};
},
@@ -155,7 +159,7 @@ export default Vue.extend({
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
+ 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]);
@@ -173,15 +177,15 @@ export default Vue.extend({
},
methods: {
- fetchContext() {
- this.contextFetching = true;
+ fetchConversation() {
+ this.conversationFetching = true;
- // Fetch context
- (this as any).api('notes/context', {
+ // Fetch conversation
+ (this as any).api('notes/conversation', {
noteId: this.p.replyId
- }).then(context => {
- this.contextFetching = false;
- this.context = context.reverse();
+ }).then(conversation => {
+ this.conversationFetching = false;
+ this.conversation = conversation.reverse();
});
},
reply() {
@@ -214,8 +218,6 @@ export default Vue.extend({
@import '~const.styl'
root(isDark)
- margin 0 auto
- padding 0
overflow hidden
text-align left
background isDark ? #282C37 : #fff
@@ -246,7 +248,7 @@ root(isDark)
&:disabled
color isDark ? #21242b : #ccc
- > .context
+ > .conversation
> *
border-bottom 1px solid isDark ? #1c2023 : #eef0f2
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index d04abfc5a7..2a49557247 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,14 +1,8 @@
<template>
<div class="mk-note-preview" :title="title">
- <mk-avatar class="avatar" :user="note.user"/>
+ <mk-avatar class="avatar" :user="note.user" v-if="!mini"/>
<div class="main">
- <header>
- <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- <router-link class="time" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
- </header>
+ <mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@@ -21,7 +15,17 @@ import Vue from 'vue';
import dateStringify from '../../../common/scripts/date-stringify';
export default Vue.extend({
- props: ['note'],
+ props: {
+ note: {
+ type: Object,
+ required: true
+ },
+ mini: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
computed: {
title(): string {
return dateStringify(this.note.createdAt);
@@ -32,49 +36,20 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
+ display flex
font-size 0.9em
- &:after
- content ""
- display block
- clear both
-
> .avatar
+ flex-shrink 0
display block
- float left
- margin 0 16px 0 0
- width 52px
- height 52px
+ margin 0 12px 0 0
+ width 48px
+ height 48px
border-radius 8px
> .main
- float left
- width calc(100% - 68px)
-
- > header
- display flex
- align-items baseline
- white-space nowrap
-
- > .name
- margin 0 .5em 0 0
- padding 0
- color isDark ? #fff : #607073
- font-size 1em
- font-weight bold
- text-decoration none
- white-space normal
-
- &:hover
- text-decoration underline
-
- > .username
- margin 0 .5em 0 0
- color isDark ? #606984 : #d1d8da
-
- > .time
- margin-left auto
- color isDark ? #606984 : #b2b8bb
+ flex 1
+ min-width 0
> .body
@@ -82,7 +57,6 @@ root(isDark)
cursor default
margin 0
padding 0
- font-size 1.1em
color isDark ? #959ba7 : #717171
.mk-note-preview[data-darkmode]
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 575d605203..a8186fb7e4 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -2,22 +2,7 @@
<div class="sub" :title="title">
<mk-avatar class="avatar" :user="note.user"/>
<div class="main">
- <header>
- <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- <div class="info">
- <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
- <router-link class="created-at" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
- <span class="visibility" v-if="note.visibility != 'public'">
- <template v-if="note.visibility == 'home'">%fa:home%</template>
- <template v-if="note.visibility == 'followers'">%fa:unlock%</template>
- <template v-if="note.visibility == 'specified'">%fa:envelope%</template>
- <template v-if="note.visibility == 'private'">%fa:lock%</template>
- </span>
- </div>
- </header>
+ <mk-note-header class="header" :note="note"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@@ -41,75 +26,33 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
+ display flex
margin 0
padding 16px 32px
font-size 0.9em
background isDark ? #21242d : #fcfcfc
- &:after
- content ""
- display block
- clear both
-
> .avatar
+ flex-shrink 0
display block
- float left
- margin 0 14px 0 0
- width 52px
- height 52px
+ margin 0 12px 0 0
+ width 48px
+ height 48px
border-radius 8px
> .main
- float left
- width calc(100% - 66px)
+ flex 1
+ min-width 0
- > header
- display flex
- align-items baseline
+ > .header
margin-bottom 2px
- white-space nowrap
- line-height 21px
-
- > .name
- display block
- margin 0 .5em 0 0
- padding 0
- overflow hidden
- color isDark ? #fff : #607073
- font-size 1em
- font-weight bold
- text-decoration none
- text-overflow ellipsis
-
- &:hover
- text-decoration underline
-
- > .username
- margin 0 .5em 0 0
- color isDark ? #606984 : #d1d8da
-
- > .info
- margin-left auto
- font-size 0.9em
-
- > *
- color isDark ? #606984 : #b2b8bb
-
- > .mobile
- margin-right 6px
-
- > .visibility
- margin-left 6px
> .body
- max-height 128px
- overflow hidden
> .text
cursor default
margin 0
padding 0
- font-size 1.1em
color isDark ? #959ba7 : #717171
pre
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 057c3c0956..ee11fcc55f 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -1,50 +1,31 @@
<template>
<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
- <div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
+ <div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/>
%fa:retweet%
- <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
- <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/>
</div>
<article>
<mk-avatar class="avatar" :user="p.user"/>
<div class="main">
- <header>
- <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
- <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
- <span class="username">@{{ p.user | acct }}</span>
- <div class="info">
- <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
- <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
- <router-link class="created-at" :to="p | notePage">
- <mk-time :time="p.createdAt"/>
- </router-link>
- <span class="visibility" v-if="p.visibility != 'public'">
- <template v-if="p.visibility == 'home'">%fa:home%</template>
- <template v-if="p.visibility == 'followers'">%fa:unlock%</template>
- <template v-if="p.visibility == 'specified'">%fa:envelope%</template>
- <template v-if="p.visibility == 'private'">%fa:lock%</template>
- </span>
- </div>
- </header>
+ <mk-note-header class="header" :note="p"/>
<div class="body">
- <p class="channel" v-if="p.channel">
- <a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
- </p>
<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 ? '隠す' : 'もっと見る' }}</span>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
<a class="reply" v-if="p.reply">%fa:reply%</a>
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+ <mk-note-html v-if="p.text && !canHideText(p)" :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">
@@ -52,7 +33,7 @@
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
- <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+ <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://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>
@@ -94,6 +75,7 @@
<script lang="ts">
import Vue from 'vue';
import dateStringify from '../../../common/scripts/date-stringify';
+import canHideText from '../../../common/scripts/can-hide-text';
import parse from '../../../../../text/parse';
import MkPostFormWindow from './post-form-window.vue';
@@ -130,16 +112,17 @@ export default Vue.extend({
},
computed: {
-
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.mediaIds.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)
@@ -147,9 +130,11 @@ export default Vue.extend({
.reduce((a, b) => a + b)
: 0;
},
+
title(): string {
return dateStringify(this.p.createdAt);
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -163,7 +148,7 @@ export default Vue.extend({
},
created() {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
}
@@ -172,13 +157,13 @@ export default Vue.extend({
mounted() {
this.capture(true);
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.on('_connected_', this.onStreamConnected);
}
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
+ 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]);
@@ -198,15 +183,17 @@ export default Vue.extend({
beforeDestroy() {
this.decapture(true);
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.off('_connected_', this.onStreamConnected);
(this as any).os.stream.dispose(this.connectionId);
}
},
methods: {
+ canHideText,
+
capture(withHandler = false) {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.send({
type: 'capture',
id: this.p.id
@@ -214,8 +201,9 @@ export default Vue.extend({
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
}
},
+
decapture(withHandler = false) {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.send({
type: 'decapture',
id: this.p.id
@@ -223,9 +211,11 @@ export default Vue.extend({
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
}
},
+
onStreamConnected() {
this.capture();
},
+
onStreamNoteUpdated(data) {
const note = data.note;
if (note.id == this.note.id) {
@@ -234,28 +224,33 @@ export default Vue.extend({
this.note.renote = note;
}
},
+
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,
note: this.p
});
},
+
onKeydown(e) {
let shouldBeCancel = true;
@@ -334,8 +329,9 @@ root(isDark)
> .renote
display flex
align-items center
- padding 16px 32px
+ 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%)
@@ -372,20 +368,16 @@ root(isDark)
padding-top 8px
> article
+ display flex
padding 28px 32px 18px 32px
- &:after
- content ""
- display block
- clear both
-
&:hover
> .main > footer > button
color isDark ? #707b97 : #888
> .avatar
+ flex-shrink 0
display block
- float left
margin 0 16px 10px 0
width 58px
height 58px
@@ -395,60 +387,11 @@ root(isDark)
//top 74px
> .main
- float left
- width calc(100% - 74px)
+ flex 1
+ min-width 0
- > header
- display flex
- align-items baseline
+ > .header
margin-bottom 4px
- white-space nowrap
-
- > .name
- display block
- margin 0 .5em 0 0
- padding 0
- overflow hidden
- color isDark ? #fff : #627079
- font-size 1em
- font-weight bold
- text-decoration none
- text-overflow ellipsis
-
- &:hover
- text-decoration underline
-
- > .is-bot
- margin 0 .5em 0 0
- padding 1px 6px
- font-size 12px
- color isDark ? #758188 : #aaa
- border solid 1px isDark ? #57616f : #ddd
- border-radius 3px
-
- > .username
- margin 0 .5em 0 0
- overflow hidden
- text-overflow ellipsis
- color isDark ? #606984 : #ccc
-
- > .info
- margin-left auto
- font-size 0.9em
-
- > *
- color isDark ? #606984 : #c0c0c0
-
- > .mobile
- margin-right 8px
-
- > .app
- margin-right 8px
- padding-right 8px
- border-right solid 1px #eaeaea
-
- > .visibility
- margin-left 8px
> .body
@@ -458,7 +401,6 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- font-size 1.1em
color isDark ? #fff : #717171
> .text
@@ -485,7 +427,6 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- font-size 1.1em
color isDark ? #fff : #717171
>>> .title
@@ -536,7 +477,7 @@ root(isDark)
padding 2px 8px 2px 16px
font-size 90%
color #8d969e
- background #edf0f3
+ background isDark ? #313543 : #edf0f3
border-radius 4px
&:before
@@ -549,7 +490,7 @@ root(isDark)
width 8px
height 8px
margin auto 0
- background #fff
+ background isDark ? #282c37 : #fff
border-radius 100%
&:hover
@@ -559,9 +500,6 @@ root(isDark)
.mk-url-preview
margin-top 8px
- > .channel
- margin 0
-
> .mk-poll
font-size 80%
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index 7e80e6f74a..69f3739f79 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -5,8 +5,8 @@
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
<div v-if="!fetching && requestInitPromise != null">
- <p>読み込みに失敗しました。</p>
- <button @click="resolveInitPromise">リトライ</button>
+ <p>%i18n:@error%</p>
+ <button @click="resolveInitPromise">%i18n:@retry%</button>
</div>
<transition-group name="mk-notes" class="transition">
@@ -74,7 +74,7 @@ export default Vue.extend({
mounted() {
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
- window.addEventListener('scroll', this.onScroll);
+ window.addEventListener('scroll', this.onScroll, { passive: true });
},
beforeDestroy() {
@@ -118,24 +118,24 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
- const isMyNote = note.userId == (this as any).os.i.id;
+ const isMyNote = note.userId == this.$store.state.i.id;
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
- if ((this as any).clientSettings.showMyRenotes === false) {
+ if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
return;
}
}
- if ((this as any).clientSettings.showRenotedMyNotes === false) {
- if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
+ if (this.$store.state.settings.showRenotedMyNotes === false) {
+ if (isPureRenote && (note.renote.userId == this.$store.state.i.id)) {
return;
}
}
//#endregion
// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
- if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) {
+ if ((document.hidden || !this.isScrollTop()) && note.userId !== this.$store.state.i.id) {
this.unreadCount++;
document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
}
@@ -145,9 +145,9 @@ export default Vue.extend({
this.notes.unshift(note);
// サウンドを再生する
- if ((this as any).os.isEnableSounds && !silent) {
+ if (this.$store.state.device.enableSounds && !silent) {
const sound = new Audio(`${url}/assets/post.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
@@ -199,7 +199,7 @@ export default Vue.extend({
this.clearNotification();
}
- if ((this as any).clientSettings.fetchOnScroll !== false) {
+ if (this.$store.state.settings.fetchOnScroll !== false) {
const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 8) this.loadMore();
}
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 7923d1a62d..e479ffadbf 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -5,6 +5,7 @@
<template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>
+
<template v-if="notification.type == 'reaction'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
@@ -17,6 +18,7 @@
</router-link>
</div>
</template>
+
<template v-if="notification.type == 'renote'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -28,6 +30,7 @@
</router-link>
</div>
</template>
+
<template v-if="notification.type == 'quote'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -37,6 +40,7 @@
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
+
<template v-if="notification.type == 'follow'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
@@ -45,6 +49,16 @@
</p>
</div>
</template>
+
+ <template v-if="notification.type == 'receiveFollowRequest'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div class="text">
+ <p>%fa:user-clock%
+ <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
+ </p>
+ </div>
+ </template>
+
<template v-if="notification.type == 'reply'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -54,6 +68,7 @@
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
+
<template v-if="notification.type == 'mention'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -63,6 +78,7 @@
<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
</div>
</template>
+
<template v-if="notification.type == 'poll_vote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
@@ -73,6 +89,7 @@
</div>
</template>
</div>
+
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
<span>%fa:angle-up%{{ notification._datetext }}</span>
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
@@ -81,7 +98,7 @@
</transition-group>
</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%' }}
+ <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
@@ -189,7 +206,7 @@ root(isDark)
margin 0
padding 16px
overflow-wrap break-word
- font-size 0.9em
+ font-size 13px
border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
&:last-child
@@ -251,6 +268,10 @@ root(isDark)
.text p i
color #53c7ce
+ &.receiveFollowRequest
+ .text p i
+ color #888
+
&.reply, &.mention
.text p i
color #555
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 1f0fbff760..51a416e281 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -1,21 +1,23 @@
<template>
-<mk-window ref="window" is-modal @closed="$destroy">
- <span slot="header">
- <span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</span>
+<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy">
+ <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="$style.count" v-if="media.length != 0">{{ '%i18n:!@attaches%'.replace('{}', media.length) }}</span>
- <span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:!@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
+ <span class="count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span>
+ <span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
</span>
- <mk-note-preview v-if="reply" :class="$style.notePreview" :note="reply"/>
- <mk-post-form ref="form"
- :reply="reply"
- @posted="onPosted"
- @change-uploadings="onChangeUploadings"
- @change-attached-media="onChangeMedia"
- @geo-attached="onGeoAttached"
- @geo-dettached="onGeoDettached"/>
+ <div class="mk-post-form-window--body">
+ <mk-note-preview v-if="reply" class="notePreview" :note="reply"/>
+ <mk-post-form ref="form"
+ :reply="reply"
+ @posted="onPosted"
+ @change-uploadings="onChangeUploadings"
+ @change-attached-media="onChangeMedia"
+ @geo-attached="onGeoAttached"
+ @geo-dettached="onGeoDettached"/>
+ </div>
</mk-window>
</template>
@@ -56,21 +58,33 @@ export default Vue.extend({
});
</script>
-<style lang="stylus" module>
-.icon
- margin-right 8px
+<style lang="stylus" scoped>
+root(isDark)
+ .mk-post-form-window--header
+ .icon
+ margin-right 8px
-.count
- margin-left 8px
- opacity 0.8
+ .count
+ margin-left 8px
+ opacity 0.8
- &:before
- content '('
+ &:before
+ content '('
- &:after
- content ')'
+ &:after
+ content ')'
-.notePreview
- margin 16px 22px
+ .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 984fc9866c..33f2288e06 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -37,7 +37,7 @@
<button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
<p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
- {{ posting ? '%i18n:!@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
+ {{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
</button>
<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
<div class="dropzone" v-if="draghover"></div>
@@ -49,6 +49,8 @@ import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
+import parse from '../../../../../text/parse';
+import { host } from '../../../config';
export default Vue.extend({
components: {
@@ -56,7 +58,25 @@ export default Vue.extend({
MkVisibilityChooser
},
- props: ['reply', 'renote'],
+ props: {
+ reply: {
+ type: Object,
+ required: false
+ },
+ renote: {
+ type: Object,
+ required: false
+ },
+ initialText: {
+ type: String,
+ required: false
+ },
+ instant: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
data() {
return {
@@ -85,19 +105,29 @@ export default Vue.extend({
},
placeholder(): string {
+ const xs = [
+ '%i18n:common.note-placeholders.a%',
+ '%i18n:common.note-placeholders.b%',
+ '%i18n:common.note-placeholders.c%',
+ '%i18n:common.note-placeholders.d%',
+ '%i18n:common.note-placeholders.e%',
+ '%i18n:common.note-placeholders.f%'
+ ];
+ const x = xs[Math.floor(Math.random() * xs.length)];
+
return this.renote
- ? '%i18n:!@quote-placeholder%'
+ ? '%i18n:@quote-placeholder%'
: this.reply
- ? '%i18n:!@reply-placeholder%'
- : '%i18n:!@note-placeholder%';
+ ? '%i18n:@reply-placeholder%'
+ : x;
},
submitText(): string {
return this.renote
- ? '%i18n:!@renote%'
+ ? '%i18n:@renote%'
: this.reply
- ? '%i18n:!@reply%'
- : '%i18n:!@note%';
+ ? '%i18n:@reply%'
+ : '%i18n:@submit%';
},
canPost(): boolean {
@@ -106,23 +136,46 @@ export default Vue.extend({
},
mounted() {
+ if (this.initialText) {
+ this.text = this.initialText;
+ }
+
if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
}
+ if (this.reply && this.reply.text != null) {
+ const ast = parse(this.reply.text);
+
+ ast.filter(t => t.type == 'mention').forEach(x => {
+ const mention = x.host ? `@${x.username}@${x.host}` : `@${x.username}`;
+
+ // 自分は除外
+ if (this.$store.state.i.username == x.username && x.host == null) return;
+ if (this.$store.state.i.username == x.username && x.host == host) return;
+
+ // 重複は除外
+ if (this.text.indexOf(`${mention} `) != -1) return;
+
+ this.text += `${mention} `;
+ });
+ }
+
this.$nextTick(() => {
// 書きかけの投稿を復元
- const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
- if (draft) {
- this.text = draft.data.text;
- this.files = draft.data.files;
- if (draft.data.poll) {
- this.poll = true;
- this.$nextTick(() => {
- (this.$refs.poll as any).set(draft.data.poll);
- });
+ if (!this.instant) {
+ const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
+ if (draft) {
+ this.text = draft.data.text;
+ this.files = draft.data.files;
+ if (draft.data.poll) {
+ this.poll = true;
+ this.$nextTick(() => {
+ (this.$refs.poll as any).set(draft.data.poll);
+ });
+ }
+ this.$emit('change-attached-media', this.files);
}
- this.$emit('change-attached-media', this.files);
}
this.$nextTick(() => this.watch());
@@ -304,22 +357,24 @@ export default Vue.extend({
this.deleteDraft();
this.$emit('posted');
(this as any).apis.notify(this.renote
- ? '%i18n:!@reposted%'
+ ? '%i18n:@reposted%'
: this.reply
- ? '%i18n:!@replied%'
- : '%i18n:!@posted%');
+ ? '%i18n:@replied%'
+ : '%i18n:@posted%');
}).catch(err => {
(this as any).apis.notify(this.renote
- ? '%i18n:!@renote-failed%'
+ ? '%i18n:@renote-failed%'
: this.reply
- ? '%i18n:!@reply-failed%'
- : '%i18n:!@note-failed%');
+ ? '%i18n:@reply-failed%'
+ : '%i18n:@note-failed%');
}).then(() => {
this.posting = false;
});
},
saveDraft() {
+ if (this.instant) return;
+
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftId] = {
diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue
index a4292e1aec..2f59733d99 100644
--- a/src/client/app/desktop/views/components/progress-dialog.vue
+++ b/src/client/app/desktop/views/components/progress-dialog.vue
@@ -2,7 +2,7 @@
<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
<span slot="header">{{ title }}<mk-ellipsis/></span>
<div :class="$style.body">
- <p :class="$style.init" v-if="isNaN(value)">待機中<mk-ellipsis/></p>
+ <p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
<p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p>
<progress :class="$style.progress"
v-if="!isNaN(value) && value < max"
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
new file mode 100644
index 0000000000..26b7ec2590
--- /dev/null
+++ b/src/client/app/desktop/views/components/received-follow-requests-window.vue
@@ -0,0 +1,72 @@
+<template>
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+ <span slot="header">%fa:envelope R% %i18n:@title%</span>
+
+ <div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">
+ <div v-for="req in requests">
+ <router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
+ <span>
+ <a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
+ </span>
+ </div>
+ </div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ requests: []
+ };
+ },
+ mounted() {
+ (this as any).api('following/requests/list').then(requests => {
+ this.fetching = false;
+ this.requests = requests;
+ });
+ },
+ methods: {
+ accept(user) {
+ (this as any).api('following/requests/accept', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ },
+ reject(user) {
+ (this as any).api('following/requests/reject', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ },
+ close() {
+ (this as any).$refs.window.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+
+root(isDark)
+ padding 16px
+
+ > button
+ margin-bottom 16px
+
+ > div
+ display flex
+ padding 16px
+ border solid 1px isDark ? #1c2023 : #eee
+ 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.vue b/src/client/app/desktop/views/components/renote-form.vue
index 9c0154211b..38eab3362f 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -5,7 +5,7 @@
<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>
+ <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:@reposting%' : '%i18n:@renote%' }}</button>
</footer>
</template>
<template v-if="quote">
@@ -32,9 +32,9 @@ export default Vue.extend({
renoteId: this.note.id
}).then(data => {
this.$emit('posted');
- (this as any).apis.notify('%i18n:!@success%');
+ (this as any).apis.notify('%i18n:@success%');
}).catch(err => {
- (this as any).apis.notify('%i18n:!@failure%');
+ (this as any).apis.notify('%i18n:@failure%');
}).then(() => {
this.wait = false;
});
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
index d5be177dcc..deb865b102 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,6 +1,6 @@
<template>
<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
- <span slot="header" :class="$style.header">%fa:cog%設定</span>
+ <span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
<mk-settings @done="close"/>
</mk-window>
</template>
diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue
index 99b6cb947c..3e8c860eba 100644
--- a/src/client/app/desktop/views/components/settings.2fa.vue
+++ b/src/client/app/desktop/views/components/settings.2fa.vue
@@ -2,8 +2,8 @@
<div class="2fa">
<p>%i18n:@intro%<a href="%i18n:@url%" target="_blank">%i18n:@detail%</a></p>
<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:@caution%</p></div>
- <p v-if="!data && !os.i.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:@register%</button></p>
- <template v-if="os.i.twoFactorEnabled">
+ <p v-if="!data && !$store.state.i.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:@register%</button></p>
+ <template v-if="$store.state.i.twoFactorEnabled">
<p>%i18n:@already-registered%</p>
<button @click="unregister" class="ui">%i18n:@unregister%</button>
</template>
@@ -34,7 +34,7 @@ export default Vue.extend({
methods: {
register() {
(this as any).apis.input({
- title: '%i18n:!@enter-password%',
+ title: '%i18n:@enter-password%',
type: 'password'
}).then(password => {
(this as any).api('i/2fa/register', {
@@ -47,14 +47,14 @@ export default Vue.extend({
unregister() {
(this as any).apis.input({
- title: '%i18n:!@enter-password%',
+ title: '%i18n:@enter-password%',
type: 'password'
}).then(password => {
(this as any).api('i/2fa/unregister', {
password: password
}).then(() => {
- (this as any).apis.notify('%i18n:!@unregistered%');
- (this as any).os.i.twoFactorEnabled = false;
+ (this as any).apis.notify('%i18n:@unregistered%');
+ this.$store.state.i.twoFactorEnabled = false;
});
});
},
@@ -63,10 +63,10 @@ export default Vue.extend({
(this as any).api('i/2fa/done', {
token: this.token
}).then(() => {
- (this as any).apis.notify('%i18n:!@success%');
- (this as any).os.i.twoFactorEnabled = true;
+ (this as any).apis.notify('%i18n:@success%');
+ this.$store.state.i.twoFactorEnabled = true;
}).catch(() => {
- (this as any).apis.notify('%i18n:!@failed%');
+ (this as any).apis.notify('%i18n:@failed%');
});
}
}
diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
index 377f2e689b..113764c3e1 100644
--- a/src/client/app/desktop/views/components/settings.api.vue
+++ b/src/client/app/desktop/views/components/settings.api.vue
@@ -1,6 +1,6 @@
<template>
<div class="root api">
- <p>Token: <code>{{ os.i.token }}</code></p>
+ <p>%i18n:@token% <code>{{ $store.state.i.token }}</code></p>
<p>%i18n:@intro%</p>
<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:@caution%</p></div>
<p>%i18n:@regeneration-of-token%</p>
@@ -15,7 +15,7 @@ export default Vue.extend({
methods: {
regenerateToken() {
(this as any).apis.input({
- title: '%i18n:!@enter-password%',
+ title: '%i18n:@enter-password%',
type: 'password'
}).then(password => {
(this as any).api('i/regenerate_token', {
diff --git a/src/client/app/desktop/views/components/settings.password.vue b/src/client/app/desktop/views/components/settings.password.vue
index 9e89bc0f6e..39896daf67 100644
--- a/src/client/app/desktop/views/components/settings.password.vue
+++ b/src/client/app/desktop/views/components/settings.password.vue
@@ -11,21 +11,21 @@ export default Vue.extend({
methods: {
reset() {
(this as any).apis.input({
- title: '%i18n:!@enter-current-password%',
+ title: '%i18n:@enter-current-password%',
type: 'password'
}).then(currentPassword => {
(this as any).apis.input({
- title: '%i18n:!@enter-new-password%',
+ title: '%i18n:@enter-new-password%',
type: 'password'
}).then(newPassword => {
(this as any).apis.input({
- title: '%i18n:!@enter-new-password-again%',
+ title: '%i18n:@enter-new-password-again%',
type: 'password'
}).then(newPassword2 => {
if (newPassword !== newPassword2) {
(this as any).apis.dialog({
title: null,
- text: '%i18n:!@not-match%',
+ text: '%i18n:@not-match%',
actions: [{
text: 'OK'
}]
@@ -36,7 +36,7 @@ export default Vue.extend({
currentPasword: currentPassword,
newPassword: newPassword
}).then(() => {
- (this as any).apis.notify('%i18n:!@changed%');
+ (this as any).apis.notify('%i18n:@changed%');
});
});
});
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 84b09eb988..0b3a25f389 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -2,7 +2,7 @@
<div class="profile">
<label class="avatar ui from group">
<p>%i18n:@avatar%</p>
- <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <img class="avatar" :src="`${$store.state.i.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
<button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button>
</label>
<label class="ui from group">
@@ -23,8 +23,13 @@
</label>
<button class="ui primary" @click="save">%i18n:@save%</button>
<section>
- <h2>その他</h2>
- <mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/>
+ <h2>%i18n:@locked-account%</h2>
+ <mk-switch v-model="$store.state.i.isLocked" @change="onChangeIsLocked" text="%i18n:@is-locked%"/>
+ </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%"/>
</section>
</div>
</template>
@@ -42,10 +47,10 @@ export default Vue.extend({
};
},
created() {
- this.name = (this as any).os.i.name || '';
- this.location = (this as any).os.i.profile.location;
- this.description = (this as any).os.i.description;
- this.birthday = (this as any).os.i.profile.birthday;
+ this.name = this.$store.state.i.name || '';
+ this.location = this.$store.state.i.profile.location;
+ this.description = this.$store.state.i.description;
+ this.birthday = this.$store.state.i.profile.birthday;
},
methods: {
updateAvatar() {
@@ -61,9 +66,19 @@ export default Vue.extend({
(this as any).apis.notify('プロフィールを更新しました');
});
},
+ onChangeIsLocked() {
+ (this as any).api('i/update', {
+ isLocked: this.$store.state.i.isLocked
+ });
+ },
onChangeIsBot() {
(this as any).api('i/update', {
- isBot: (this as any).os.i.isBot
+ isBot: this.$store.state.i.isBot
+ });
+ },
+ onChangeIsCat() {
+ (this as any).api('i/update', {
+ isCat: this.$store.state.i.isCat
});
}
}
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 9439ded2fc..c660c2869a 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -19,90 +19,91 @@
</section>
<section class="web" v-show="page == 'web'">
- <h1>動作</h1>
- <mk-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
- <span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
+ <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="ウィンドウの自動ポップアウト">
- <span>ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。</span>
+ <mk-switch v-model="autoPopout" text="%i18n:@auto-popout%">
+ <span>%i18n:@auto-popout-desc%</span>
</mk-switch>
<details>
- <summary>詳細設定</summary>
- <mk-switch v-model="apiViaStream" text="ストリームを経由したAPIリクエスト">
- <span>この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。</span>
+ <summary>%i18n:@advanced%</summary>
+ <mk-switch v-model="apiViaStream" text="%i18n:@api-via-stream%">
+ <span>%i18n:@api-via-stream-desc%</span>
</mk-switch>
</details>
</section>
<section class="web" v-show="page == 'web'">
- <h1>デザインと表示</h1>
+ <h1>%i18n:@display%</h1>
<div class="div">
- <button class="ui button" @click="customizeHome" style="margin-bottom: 16px">ホームをカスタマイズ</button>
+ <button class="ui button" @click="customizeHome" style="margin-bottom: 16px">%i18n:@customize%</button>
</div>
<div class="div">
- <mk-switch v-model="darkmode" text="ダークモード"/>
- <mk-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons" text="円形のアイコンを使用"/>
- <mk-switch v-model="clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+ <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%"/>
</div>
- <mk-switch v-model="clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
- <mk-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/>
- <mk-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/>
- <mk-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/>
- <mk-switch v-model="clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
- <span>位置情報が添付された投稿のマップを自動的に展開します。</span>
+ <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
+ <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.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%">
+ <span>%i18n:@show-maps-desc%</span>
</mk-switch>
</section>
<section class="web" v-show="page == 'web'">
- <h1>サウンド</h1>
- <mk-switch v-model="enableSounds" text="サウンドを有効にする">
- <span>投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。</span>
+ <h1>%i18n:@sound%</h1>
+ <mk-switch v-model="enableSounds" text="%i18n:@enable-sounds%">
+ <span>%i18n:@enable-sounds-desc%</span>
</mk-switch>
- <label>ボリューム</label>
+ <label>%i18n:@volume%</label>
<el-slider
v-model="soundVolume"
:show-input="true"
- :format-tooltip="v => `${v}%`"
+ :format-tooltip="v => `${v * 100}%`"
:disabled="!enableSounds"
+ :max="1"
+ :step="0.1"
/>
- <button class="ui button" @click="soundTest">%fa:volume-up% テスト</button>
+ <button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button>
</section>
<section class="web" v-show="page == 'web'">
- <h1>モバイル</h1>
- <mk-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+ <h1>%i18n:@mobile%</h1>
+ <mk-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile" text="%i18n:@disable-via-mobile%"/>
</section>
<section class="web" v-show="page == 'web'">
- <h1>言語</h1>
- <el-select v-model="lang" placeholder="言語を選択">
- <el-option-group label="推奨">
- <el-option label="自動" value=""/>
+ <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="言語を指定">
- <el-option label="ja-JP" value="ja"/>
- <el-option label="en-US" value="en"/>
- <el-option label="fr" value="fr"/>
- <el-option label="pl" value="pl"/>
+ <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>
<div class="none ui info">
- <p>%fa:info-circle%変更はページの再度読み込み後に反映されます。</p>
+ <p>%fa:info-circle%%i18n:@language-desc%</p>
</div>
</section>
<section class="web" v-show="page == 'web'">
- <h1>キャッシュ</h1>
- <button class="ui button" @click="clean">クリーンアップ</button>
+ <h1>%i18n:@cache%</h1>
+ <button class="ui button" @click="clean">%i18n:@clean-cache%</button>
<div class="none ui info warn">
- <p>%fa:exclamation-triangle%クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。</p>
+ <p>%fa:exclamation-triangle%%i18n:@cache-warn%</p>
</div>
</section>
<section class="notification" v-show="page == 'notification'">
- <h1>通知</h1>
- <mk-switch v-model="os.i.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
- <span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
+ <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>
</section>
@@ -117,7 +118,7 @@
</section>
<section class="apps" v-show="page == 'apps'">
- <h1>アプリケーション</h1>
+ <h1>%i18n:@apps%</h1>
<x-apps/>
</section>
@@ -137,7 +138,7 @@
</section>
<section class="signin" v-show="page == 'security'">
- <h1>サインイン履歴</h1>
+ <h1>%i18n:@signin%</h1>
<x-signins/>
</section>
@@ -147,57 +148,49 @@
</section>
<section class="other" v-show="page == 'other'">
- <h1>Misskeyについて</h1>
- <p v-if="meta">このサーバーの運営者: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p>
+ <h1>%i18n:@about%</h1>
+ <p v-if="meta">%i18n:@operator%: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p>
</section>
<section class="other" v-show="page == 'other'">
- <h1>Misskey Update</h1>
+ <h1>%i18n:@update%</h1>
<p>
- <span>バージョン: <i>{{ version }}</i></span>
+ <span>%i18n:@version% <i>{{ version }}</i></span>
<template v-if="latestVersion !== undefined">
<br>
- <span>最新のバージョン: <i>{{ latestVersion ? latestVersion : version }}</i></span>
+ <span>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></span>
</template>
</p>
<button class="ui button block" @click="checkForUpdate" :disabled="checkingForUpdate">
- <template v-if="checkingForUpdate">アップデートを確認中<mk-ellipsis/></template>
- <template v-else>アップデートを確認</template>
+ <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
+ <template v-else>%i18n:@do-update%</template>
</button>
<details>
- <summary>詳細設定</summary>
- <mk-switch v-model="preventUpdate" text="アップデートを延期する(非推奨)">
- <span>この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。</span>
+ <summary>%i18n:@update-settings%</summary>
+ <mk-switch v-model="preventUpdate" text="%i18n:@prevent-update%">
+ <span>%i18n:@prevent-update-desc%</span>
</mk-switch>
</details>
</section>
<section class="other" v-show="page == 'other'">
- <h1>高度な設定</h1>
- <mk-switch v-model="debug" text="デバッグモードを有効にする">
- <span>この設定はブラウザに記憶されます。</span>
+ <h1>%i18n:@advanced-settings%</h1>
+ <mk-switch v-model="debug" text="%i18n:@debug-mode%">
+ <span>%i18n:@debug-mode-desc%</span>
</mk-switch>
- <template v-if="debug">
- <mk-switch v-model="useRawScript" text="生のスクリプトを読み込む">
- <span>圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。</span>
- </mk-switch>
- <div class="none ui info">
- <p>%fa:info-circle%Misskeyはソースマップも提供しています。</p>
- </div>
- </template>
- <mk-switch v-model="enableExperimental" text="実験的機能を有効にする">
- <span>実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。</span>
+ <mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%">
+ <span>%i18n:@experimental-desc%</span>
</mk-switch>
<details v-if="debug">
- <summary>ツール</summary>
- <button class="ui button block" @click="taskmngr">タスクマネージャ</button>
+ <summary>%i18n:@tools%</summary>
+ <button class="ui button block" @click="taskmngr">%i18n:@task-manager%</button>
</details>
</section>
<section class="other" v-show="page == 'other'">
<h1>%i18n:@license%</h1>
<div v-html="license"></div>
- <a :href="licenseUrl" target="_blank">サードパーティ</a>
+ <a :href="licenseUrl" target="_blank">%i18n:@third-parties%</a>
</section>
</div>
</div>
@@ -213,7 +206,7 @@ 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 { url, docsUrl, license, lang, version } from '../../../config';
+import { url, docsUrl, license, lang, langs, version } from '../../../config';
import checkForUpdate from '../../../common/scripts/check-for-update';
import MkTaskManager from './taskmanager.vue';
@@ -234,55 +227,59 @@ export default Vue.extend({
meta: null,
license,
version,
+ langs,
latestVersion: undefined,
- checkingForUpdate: false,
- darkmode: localStorage.getItem('darkmode') == 'true',
- enableSounds: localStorage.getItem('enableSounds') == 'true',
- autoPopout: localStorage.getItem('autoPopout') == 'true',
- apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true,
- soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 50,
- lang: localStorage.getItem('lang') || '',
- preventUpdate: localStorage.getItem('preventUpdate') == 'true',
- debug: localStorage.getItem('debug') == 'true',
- useRawScript: localStorage.getItem('useRawScript') == 'true',
- enableExperimental: localStorage.getItem('enableExperimental') == 'true'
+ checkingForUpdate: false
};
},
computed: {
licenseUrl(): string {
return `${docsUrl}/${lang}/license`;
- }
- },
- watch: {
- autoPopout() {
- localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false');
},
- apiViaStream() {
- localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false');
+
+ apiViaStream: {
+ get() { return this.$store.state.device.apiViaStream; },
+ set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
},
- darkmode() {
- (this as any)._updateDarkmode_(this.darkmode);
+
+ autoPopout: {
+ get() { return this.$store.state.device.autoPopout; },
+ set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); }
},
- enableSounds() {
- localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
+
+ darkmode: {
+ get() { return this.$store.state.device.darkmode; },
+ set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
},
- soundVolume() {
- localStorage.setItem('soundVolume', this.soundVolume.toString());
+
+ enableSounds: {
+ get() { return this.$store.state.device.enableSounds; },
+ set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
},
- lang() {
- localStorage.setItem('lang', this.lang);
+
+ soundVolume: {
+ get() { return this.$store.state.device.soundVolume; },
+ set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); }
},
- preventUpdate() {
- localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false');
+
+ lang: {
+ get() { return this.$store.state.device.lang; },
+ set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
},
- debug() {
- localStorage.setItem('debug', this.debug ? 'true' : 'false');
+
+ preventUpdate: {
+ get() { return this.$store.state.device.preventUpdate; },
+ set(value) { this.$store.commit('device/set', { key: 'preventUpdate', value }); }
},
- useRawScript() {
- localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false');
+
+ debug: {
+ get() { return this.$store.state.device.debug; },
+ set(value) { this.$store.commit('device/set', { key: 'debug', value }); }
},
- enableExperimental() {
- localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false');
+
+ enableExperimentalFeatures: {
+ get() { return this.$store.state.device.enableExperimentalFeatures; },
+ set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); }
}
},
created() {
@@ -298,6 +295,20 @@ export default Vue.extend({
this.$router.push('/i/customize-home');
this.$emit('done');
},
+ updateWallpaper() {
+ (this as any).apis.chooseDriveFile({
+ multiple: false
+ }).then(file => {
+ (this as any).api('i/update', {
+ wallpaperId: file.id
+ });
+ });
+ },
+ deleteWallpaper() {
+ (this as any).api('i/update', {
+ wallpaperId: null
+ });
+ },
onChangeFetchOnScroll(v) {
this.$store.dispatch('settings/set', {
key: 'fetchOnScroll',
@@ -370,13 +381,13 @@ export default Vue.extend({
this.latestVersion = newer;
if (newer == null) {
(this as any).apis.dialog({
- title: '利用可能な更新はありません',
- text: 'お使いのMisskeyは最新です。'
+ title: '%i18n:@no-updates%',
+ text: '%i18n:@no-updates-desc%'
});
} else {
(this as any).apis.dialog({
- title: '新しいバージョンが利用可能です',
- text: 'ページを再度読み込みすると更新が適用されます。'
+ title: '%i18n:@update-available%',
+ text: '%i18n:@update-available-desc%'
});
}
});
@@ -384,13 +395,13 @@ export default Vue.extend({
clean() {
localStorage.clear();
(this as any).apis.dialog({
- title: 'キャッシュを削除しました',
- text: 'ページを再度読み込みしてください。'
+ title: '%i18n:@cache-cleared%',
+ text: '%i18n:@caache-cleared-desc%'
});
},
soundTest() {
const sound = new Audio(`${url}/assets/message.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
}
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 dd4012039b..45ce6a6f8f 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -1,17 +1,18 @@
<template>
<div class="mk-sub-note-content">
<div class="body">
- <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <span v-if="note.isHidden" style="opacity: 0.5">%i18n:@private%</span>
+ <span v-if="note.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
<a class="reply" v-if="note.replyId">%fa:reply%</a>
- <mk-note-html :text="note.text" :i="os.i"/>
- <a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a>
+ <mk-note-html 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>({{ note.media.length }}つのメディア)</summary>
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
<mk-media-list :media-list="note.media"/>
</details>
<details v-if="note.poll">
- <summary>投票</summary>
+ <summary>%i18n:@poll%</summary>
<mk-poll :note="note"/>
</details>
</div>
diff --git a/src/client/app/desktop/views/components/taskmanager.vue b/src/client/app/desktop/views/components/taskmanager.vue
index a00fabb047..1f1385add8 100644
--- a/src/client/app/desktop/views/components/taskmanager.vue
+++ b/src/client/app/desktop/views/components/taskmanager.vue
@@ -1,6 +1,6 @@
<template>
<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager">
- <span slot="header" :class="$style.header">%fa:stethoscope%タスクマネージャ</span>
+ <span slot="header" :class="$style.header">%fa:stethoscope%%i18n:@title%</span>
<el-tabs :class="$style.content">
<el-tab-pane label="Requests">
<el-table
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 254a5b9d63..1728dad286 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -5,7 +5,7 @@
<mk-ellipsis-icon/>
</div>
- <mk-notes ref="timeline" :more="canFetchMore ? more : null">
+ <mk-notes ref="timeline" :more="existMore ? more : null">
<p :class="$style.empty" slot="empty">
%fa:R comments%%i18n:@empty%
</p>
@@ -15,7 +15,6 @@
<script lang="ts">
import Vue from 'vue';
-import getNoteSummary from '../../../../../renderers/get-note-summary';
const fetchLimit = 10;
@@ -34,14 +33,13 @@ export default Vue.extend({
existMore: false,
connection: null,
connectionId: null,
- unreadCount: 0,
date: null
};
},
computed: {
alone(): boolean {
- return (this as any).os.i.followingCount == 0;
+ return this.$store.state.i.followingCount == 0;
},
stream(): any {
@@ -76,7 +74,6 @@ export default Vue.extend({
}
document.addEventListener('keydown', this.onKeydown);
- document.addEventListener('visibilitychange', this.onVisibilitychange, false);
this.fetch();
},
@@ -90,7 +87,6 @@ export default Vue.extend({
this.stream.dispose(this.connectionId);
document.removeEventListener('keydown', this.onKeydown);
- document.removeEventListener('visibilitychange', this.onVisibilitychange);
},
methods: {
@@ -101,8 +97,8 @@ export default Vue.extend({
(this as any).api(this.endpoint, {
limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -120,12 +116,14 @@ export default Vue.extend({
this.moreFetching = true;
- (this as any).api(this.endpoint, {
+ const promise = (this as any).api(this.endpoint, {
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
- }).then(notes => {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ });
+
+ promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
@@ -134,14 +132,11 @@ export default Vue.extend({
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
+
+ return promise;
},
onNote(note) {
- if (document.hidden && note.userId !== (this as any).os.i.id) {
- this.unreadCount++;
- document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
- }
-
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
@@ -159,13 +154,6 @@ export default Vue.extend({
this.fetch();
},
- onVisibilitychange() {
- if (!document.hidden) {
- this.unreadCount = 0;
- document.title = 'Misskey';
- }
- },
-
onKeydown(e) {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
if (e.which == 84) { // t
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index a776e40a24..0728b78aa9 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -31,8 +31,23 @@ export default Vue.extend({
};
},
+ watch: {
+ src() {
+ this.saveSrc();
+ },
+
+ list() {
+ this.saveSrc();
+ }
+ },
+
created() {
- if ((this as any).os.i.followingCount == 0) {
+ 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.$store.state.i.followingCount == 0) {
this.src = 'local';
}
},
@@ -44,6 +59,13 @@ export default Vue.extend({
},
methods: {
+ saveSrc() {
+ this.$store.commit('device/setTl', {
+ src: this.src,
+ arg: this.list
+ });
+ },
+
warp(date) {
(this.$refs.tl as any).warp(date);
},
diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue
index 9983f02c5e..68413914c0 100644
--- a/src/client/app/desktop/views/components/ui-notification.vue
+++ b/src/client/app/desktop/views/components/ui-notification.vue
@@ -36,7 +36,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-ui-notification
+root(isDark)
display block
position fixed
z-index 10000
@@ -46,10 +46,10 @@ export default Vue.extend({
margin 0 auto
padding 128px 0 0 0
width 500px
- color rgba(#000, 0.6)
- background rgba(#fff, 0.9)
+ color rgba(isDark ? #fff : #000, 0.6)
+ background rgba(isDark ? #282C37 : #fff, 0.9)
border-radius 0 0 8px 8px
- box-shadow 0 2px 4px rgba(#000, 0.2)
+ box-shadow 0 2px 4px rgba(#000, isDark ? 0.4 : 0.2)
transform translateY(-64px)
opacity 0
@@ -58,4 +58,10 @@ export default Vue.extend({
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 fd15ea6006..4e0fc1cf1a 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -1,14 +1,14 @@
<template>
<div class="account">
<button class="header" :data-active="isOpen" @click="toggle">
- <span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
- <mk-avatar class="avatar" :user="os.i"/>
+ <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"/>
</button>
<transition name="zoom-in-top">
<div class="menu" v-if="isOpen">
<ul>
<li>
- <router-link :to="`/@${ os.i.username }`">%fa:user%<span>%i18n:@profile%</span>%fa:angle-right%</router-link>
+ <router-link :to="`/@${ $store.state.i.username }`">%fa:user%<span>%i18n:@profile%</span>%fa:angle-right%</router-link>
</li>
<li @click="drive">
<p>%fa:cloud%<span>%i18n:@drive%</span>%fa:angle-right%</p>
@@ -19,6 +19,9 @@
<li @click="list">
<p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p>
</li>
+ <li @click="followRequests" v-if="$store.state.i.isLocked">
+ <p>%fa:envelope R%<span>%i18n:@follow-requests%<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>%fa:angle-right%</p>
+ </li>
</ul>
<ul>
<li>
@@ -35,7 +38,7 @@
</ul>
<ul>
<li @click="dark">
- <p><span>%i18n:@dark%</span><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
+ <p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
</li>
</ul>
</div>
@@ -46,6 +49,7 @@
<script lang="ts">
import Vue from 'vue';
import MkUserListsWindow from './user-lists-window.vue';
+import MkFollowRequestsWindow from './received-follow-requests-window.vue';
import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import contains from '../../../common/scripts/contains';
@@ -91,6 +95,10 @@ export default Vue.extend({
this.$router.push(`i/lists/${ list.id }`);
});
},
+ followRequests() {
+ this.close();
+ (this as any).os.new(MkFollowRequestsWindow);
+ },
settings() {
this.close();
(this as any).os.new(MkSettingsWindow);
@@ -99,7 +107,10 @@ export default Vue.extend({
(this as any).os.signout();
},
dark() {
- (this as any)._updateDarkmode_(!(this as any)._darkmode_);
+ this.$store.commit('device/set', {
+ key: 'darkmode',
+ value: !this.$store.state.device.darkmode
+ });
}
}
});
@@ -222,6 +233,16 @@ root(isDark)
> span:first-child
padding-left 22px
+ > span:nth-child(2)
+ > i
+ margin-left 4px
+ padding 2px 8px
+ font-size 90%
+ font-style normal
+ background $theme-color
+ color $theme-color-foreground
+ border-radius 8px
+
> [data-fa]:first-child
margin-right 6px
width 16px
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 cd23a67506..1c3f12f2f2 100644
--- a/src/client/app/desktop/views/components/ui.header.clock.vue
+++ b/src/client/app/desktop/views/components/ui.header.clock.vue
@@ -8,7 +8,7 @@
</time>
</div>
<div class="content">
- <mk-analog-clock/>
+ <mk-analog-clock :dark="true"/>
</div>
</div>
</template>
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 0800d96eb6..42211b57fe 100644
--- a/src/client/app/desktop/views/components/ui.header.nav.vue
+++ b/src/client/app/desktop/views/components/ui.header.nav.vue
@@ -1,18 +1,24 @@
<template>
<div class="nav">
<ul>
- <template v-if="os.isSignedIn">
+ <template v-if="$store.getters.isSignedIn">
<li class="home" :class="{ active: $route.name == 'index' }">
<router-link to="/">
%fa:home%
<p>%i18n:@home%</p>
</router-link>
</li>
+ <li class="deck" :class="{ active: $route.name == 'deck' }">
+ <router-link to="/deck">
+ %fa:columns%
+ <p>%i18n:@deck% <small>(beta)</small></p>
+ </router-link>
+ </li>
<li class="messaging">
<a @click="messaging">
%fa:comments%
<p>%i18n:@messaging%</p>
- <template v-if="hasUnreadMessagingMessages">%fa:circle%</template>
+ <template v-if="hasUnreadMessagingMessage">%fa:circle%</template>
</a>
</li>
<li class="game">
@@ -35,53 +41,38 @@ import MkGameWindow from './game-window.vue';
export default Vue.extend({
data() {
return {
- hasUnreadMessagingMessages: false,
hasGameInvitations: false,
connection: null,
connectionId: null
};
},
+ computed: {
+ hasUnreadMessagingMessage(): boolean {
+ return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
+ }
+ },
mounted() {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
- this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
- this.connection.on('othello_invited', this.onOthelloInvited);
- this.connection.on('othello_no_invites', this.onOthelloNoInvites);
-
- // Fetch count of unread messaging messages
- (this as any).api('messaging/unread').then(res => {
- if (res.count > 0) {
- this.hasUnreadMessagingMessages = true;
- }
- });
+ this.connection.on('reversi_invited', this.onReversiInvited);
+ this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
beforeDestroy() {
- if ((this as any).os.isSignedIn) {
- this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
- this.connection.off('othello_invited', this.onOthelloInvited);
- this.connection.off('othello_no_invites', this.onOthelloNoInvites);
+ 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);
}
},
methods: {
- onUnreadMessagingMessage() {
- this.hasUnreadMessagingMessages = true;
- },
-
- onReadAllMessagingMessages() {
- this.hasUnreadMessagingMessages = false;
- },
-
- onOthelloInvited() {
+ onReversiInvited() {
this.hasGameInvitations = true;
},
- onOthelloNoInvites() {
+ onReversiNoInvites() {
this.hasGameInvitations = false;
},
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 ea814dd7a3..59a16df9ec 100644
--- a/src/client/app/desktop/views/components/ui.header.notifications.vue
+++ b/src/client/app/desktop/views/components/ui.header.notifications.vue
@@ -1,7 +1,7 @@
<template>
<div class="notifications">
<button :data-active="isOpen" @click="toggle" title="%i18n:@title%">
- %fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template>
+ %fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template>
</button>
<div class="pop" v-if="isOpen">
<mk-notifications/>
@@ -16,44 +16,15 @@ import contains from '../../../common/scripts/contains';
export default Vue.extend({
data() {
return {
- isOpen: false,
- hasUnreadNotifications: false,
- connection: null,
- connectionId: null
+ isOpen: false
};
},
- mounted() {
- if ((this as any).os.isSignedIn) {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
-
- this.connection.on('read_all_notifications', this.onReadAllNotifications);
- this.connection.on('unread_notification', this.onUnreadNotification);
-
- // Fetch count of unread notifications
- (this as any).api('notifications/get_unread_count').then(res => {
- if (res.count > 0) {
- this.hasUnreadNotifications = true;
- }
- });
- }
- },
- beforeDestroy() {
- if ((this as any).os.isSignedIn) {
- this.connection.off('read_all_notifications', this.onReadAllNotifications);
- this.connection.off('unread_notification', this.onUnreadNotification);
- (this as any).os.stream.dispose(this.connectionId);
+ computed: {
+ hasUnreadNotification(): boolean {
+ return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
}
},
methods: {
- onReadAllNotifications() {
- this.hasUnreadNotifications = false;
- },
-
- onUnreadNotification() {
- this.hasUnreadNotifications = true;
- },
-
toggle() {
this.isOpen ? this.close() : this.open();
},
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 1ed28ba3a8..b6149a1878 100644
--- a/src/client/app/desktop/views/components/ui.header.search.vue
+++ b/src/client/app/desktop/views/components/ui.header.search.vue
@@ -17,7 +17,11 @@ export default Vue.extend({
},
methods: {
onSubmit() {
- location.href = `/search?q=${encodeURIComponent(this.q)}`;
+ if (this.q.startsWith('#')) {
+ this.$router.push(`/tags/${encodeURIComponent(this.q.substr(1))}`);
+ } else {
+ this.$router.push(`/search?q=${encodeURIComponent(this.q)}`);
+ }
}
}
});
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 7729575b56..7045790054 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -4,16 +4,16 @@
<div class="main" ref="main">
<div class="backdrop"></div>
<div class="main">
- <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p>
+ <p ref="welcomeback" v-if="$store.getters.isSignedIn">おかえりなさい、<b>{{ $store.state.i | userName }}</b>さん</p>
<div class="container" ref="mainContainer">
<div class="left">
<x-nav/>
</div>
<div class="right">
<x-search/>
- <x-account v-if="os.isSignedIn"/>
- <x-notifications v-if="os.isSignedIn"/>
- <x-post v-if="os.isSignedIn"/>
+ <x-account v-if="$store.getters.isSignedIn"/>
+ <x-notifications v-if="$store.getters.isSignedIn"/>
+ <x-post v-if="$store.getters.isSignedIn"/>
<x-clock/>
</div>
</div>
@@ -45,11 +45,11 @@ export default Vue.extend({
mounted() {
this.$store.commit('setUiHeaderHeight', 48);
- if ((this as any).os.isSignedIn) {
- const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000;
+ if (this.$store.getters.isSignedIn) {
+ const ago = (new Date().getTime() - new Date(this.$store.state.i.lastUsedAt).getTime()) / 1000;
const isHisasiburi = ago >= 3600;
- (this as any).os.i.lastUsedAt = new Date();
- (this as any).os.bakeMe();
+ 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';
@@ -150,8 +150,8 @@ root(isDark)
display block
width 100%
height 48px
- background-image url(/assets/desktop/header-logo.svg)
- background-size 46px
+ 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
diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
index 87f932ff14..d410c3d980 100644
--- a/src/client/app/desktop/views/components/ui.vue
+++ b/src/client/app/desktop/views/components/ui.vue
@@ -1,10 +1,10 @@
<template>
-<div>
- <x-header/>
+<div class="mk-ui" :style="style">
+ <x-header class="header" v-show="!zenMode"/>
<div class="content">
<slot></slot>
</div>
- <mk-stream-indicator v-if="os.isSignedIn"/>
+ <mk-stream-indicator v-if="$store.getters.isSignedIn"/>
</div>
</template>
@@ -16,6 +16,20 @@ 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 {};
+ return {
+ 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 })`
+ };
+ }
+ },
mounted() {
document.addEventListener('keydown', this.onKeydown);
},
@@ -30,8 +44,32 @@ export default Vue.extend({
e.preventDefault();
(this as any).apis.post();
}
+
+ if (e.which == 90) { // z
+ e.preventDefault();
+ this.zenMode = !this.zenMode;
+ }
}
}
});
</script>
+<style lang="stylus" scoped>
+.mk-ui
+ display flex
+ flex-direction column
+ flex 1
+ background-size cover
+ background-position center
+ background-attachment fixed
+
+ > .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 59d6abbbc1..03ac81a4a1 100644
--- a/src/client/app/desktop/views/components/user-list-timeline.vue
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -32,7 +32,7 @@ export default Vue.extend({
methods: {
init() {
if (this.connection) this.connection.close();
- this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
+ this.connection = new UserListStream((this as any).os, this.$store.state.i, this.list.id);
this.connection.on('note', this.onNote);
this.connection.on('userAdded', this.onUserAdded);
this.connection.on('userRemoved', this.onUserRemoved);
@@ -46,8 +46,8 @@ export default Vue.extend({
(this as any).api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -62,13 +62,15 @@ export default Vue.extend({
more() {
this.moreFetching = true;
- (this as any).api('notes/user-list-timeline', {
+ const promise = (this as any).api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
- }).then(notes => {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ });
+
+ promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
@@ -77,6 +79,8 @@ export default Vue.extend({
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
+
+ return promise;
},
onNote(note) {
// Prepend a note
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 d082610132..47648c287d 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -1,9 +1,9 @@
<template>
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
- <span slot="header">%fa:list% リスト</span>
+ <span slot="header">%fa:list% %i18n:@title%</span>
- <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_">
- <button class="ui" @click="add">リストを作成</button>
+ <div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">
+ <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>
</mk-window>
@@ -60,10 +60,10 @@ root(isDark)
border solid 1px isDark ? #1c2023 : #eee
border-radius 4px
-[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"][data-darkmode]
+.xkxvokkjlptzyewouewmceqcxhpgzprp[data-darkmode]
root(true)
-[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"]:not([data-darkmode])
+.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 cc5e021390..788881ead5 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -5,21 +5,21 @@
<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
<div class="title">
<router-link class="name" :to="u | userPage">{{ u | userName }}</router-link>
- <p class="username">@{{ u | acct }}</p>
+ <p class="username"><mk-acct :user="u"/></p>
</div>
<div class="description">{{ u.description }}</div>
<div class="status">
<div>
- <p>投稿</p><a>{{ u.notesCount }}</a>
+ <p>%i18n:@notes%</p><a>{{ u.notesCount }}</a>
</div>
<div>
- <p>フォロー</p><a>{{ u.followingCount }}</a>
+ <p>%i18n:@following%</p><a>{{ u.followingCount }}</a>
</div>
<div>
- <p>フォロワー</p><a>{{ u.followersCount }}</a>
+ <p>%i18n:@followers%</p><a>{{ u.followersCount }}</a>
</div>
</div>
- <mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/>
+ <mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="u"/>
</template>
</div>
</template>
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 dbad295178..262fd38cd1 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -7,7 +7,7 @@
<span class="username">@{{ user | acct }}</span>
</header>
<div class="body">
- <p class="followed" v-if="user.isFollowed">フォローされています</p>
+ <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
<div class="description">{{ user.description }}</div>
</div>
</div>
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index 13d0d07bbc..0423db8ed7 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -2,8 +2,8 @@
<div class="mk-users-list">
<nav>
<div>
- <span :data-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
- <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
+ <span v-if="$store.getters.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@iknow%<span>{{ youKnowCount }}</span></span>
</div>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
@@ -12,13 +12,13 @@
</div>
</div>
<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
- <span v-if="!moreFetching">もっと</span>
- <span v-if="moreFetching">読み込み中<mk-ellipsis/></span>
+ <span v-if="!moreFetching">%i18n:@load-more%</span>
+ <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
</button>
<p class="no" v-if="!fetching && users.length == 0">
<slot></slot>
</p>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p>
</div>
</template>
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index ab8327d39e..7cfcd68eba 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -23,9 +23,9 @@ export default Vue.extend({
},
computed: {
withGradient(): boolean {
- return (this as any).os.isSignedIn
- ? (this as any).clientSettings.gradientWindowHeader != null
- ? (this as any).clientSettings.gradientWindowHeader
+ return this.$store.getters.isSignedIn
+ ? this.$store.state.settings.gradientWindowHeader != null
+ ? this.$store.state.settings.gradientWindowHeader
: false
: false;
}
@@ -36,7 +36,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
background isDark ? #282C37 : #fff
- border solid 1px rgba(#000, 0.075)
+ border solid 1px rgba(#000, isDark ? 0.2 : 0.075)
border-radius 6px
overflow hidden
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index 2e7eb557b4..ec044ad27e 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -4,13 +4,13 @@
<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: clientSettings.gradientWindowHeader }"
+ :class="{ withGradient: $store.state.settings.gradientWindowHeader }"
@contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown"
>
<h1><slot name="header"></slot></h1>
<div>
- <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
- <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button>
+ <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="%i18n:@popout%">%fa:R window-restore%</button>
+ <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="%i18n:@close%">%fa:times%</button>
</div>
</header>
<div class="content">
@@ -95,7 +95,7 @@ export default Vue.extend({
},
created() {
- if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) {
+ if (this.$store.state.device.autoPopout && this.popoutUrl) {
this.popout();
this.preventMount = true;
} else {
@@ -488,7 +488,10 @@ root(isDark)
&:focus
&:not([data-is-modal])
> .body
- box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(#000, 0.2)
+ 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)
> .handle
$size = 8px
@@ -556,7 +559,11 @@ root(isDark)
overflow hidden
background isDark ? #282C37 : #fff
border-radius 6px
- box-shadow 0 2px 6px 0 rgba(#000, 0.2)
+
+ if isDark
+ box-shadow 0 2px 12px 0 rgba(#000, 0.5)
+ else
+ box-shadow 0 2px 6px 0 rgba(#000, 0.2)
> header
$header-height = 40px
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
new file mode 100644
index 0000000000..28e7f13650
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.column-core.vue
@@ -0,0 +1,35 @@
+<template>
+<x-widgets-column v-if="column.type == 'widgets'" :column="column" :is-stacked="isStacked"/>
+<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked"/>
+<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked"/>
+<x-tl-column v-else-if="column.type == 'local'" :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"/>
+</template>
+
+<script lang="ts">
+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';
+
+export default Vue.extend({
+ components: {
+ XTlColumn,
+ XNotificationsColumn,
+ XWidgetsColumn
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue
new file mode 100644
index 0000000000..d59d430da6
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.column.vue
@@ -0,0 +1,357 @@
+<template>
+<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow, active, isStacked, draghover, dragging, dropready }"
+ @dragover.prevent.stop="onDragover"
+ @dragenter.prevent="onDragenter"
+ @dragleave="onDragleave"
+ @drop.prevent.stop="onDrop"
+>
+ <header :class="{ indicate: count > 0 }"
+ draggable="true"
+ @click="toggleActive"
+ @dragstart="onDragstart"
+ @dragend="onDragend"
+ @contextmenu.prevent.stop="onContextmenu"
+ >
+ <slot name="header"></slot>
+ <span class="count" v-if="count > 0">({{ count }})</span>
+ <button ref="menu" @click.stop="showMenu">%fa:caret-down%</button>
+ </header>
+ <div ref="body" v-show="active">
+ <slot></slot>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Menu from '../../../../common/views/components/menu.vue';
+import contextmenu from '../../../api/contextmenu';
+
+export default Vue.extend({
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ },
+ name: {
+ type: String,
+ required: false
+ },
+ menu: {
+ type: Array,
+ required: false,
+ default: null
+ },
+ naked: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ narrow: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ inject: {
+ getColumnVm: { from: 'getColumnVm' }
+ },
+
+ data() {
+ return {
+ count: 0,
+ active: true,
+ dragging: false,
+ draghover: false,
+ dropready: false
+ };
+ },
+
+ watch: {
+ active(v) {
+ if (v && this.isScrollTop()) {
+ this.$emit('top');
+ }
+ },
+ dragging(v) {
+ this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd');
+ }
+ },
+
+ provide() {
+ return {
+ column: this,
+ isScrollTop: this.isScrollTop,
+ count: v => this.count = v
+ };
+ },
+
+ mounted() {
+ this.$refs.body.addEventListener('scroll', this.onScroll, { passive: true });
+ this.$root.$on('deck.column.dragStart', this.onOtherDragStart);
+ this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd);
+ },
+
+ beforeDestroy() {
+ this.$refs.body.removeEventListener('scroll', this.onScroll);
+ this.$root.$off('deck.column.dragStart', this.onOtherDragStart);
+ this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd);
+ },
+
+ methods: {
+ onOtherDragStart() {
+ this.dropready = true;
+ },
+
+ onOtherDragEnd() {
+ this.dropready = false;
+ },
+
+ 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;
+ this.active = !this.active;
+ },
+
+ isScrollTop() {
+ return this.active && this.$refs.body.scrollTop == 0;
+ },
+
+ onScroll() {
+ if (this.isScrollTop()) {
+ this.$emit('top');
+ }
+
+ if (this.$store.state.settings.fetchOnScroll !== false) {
+ const current = this.$refs.body.scrollTop + this.$refs.body.clientHeight;
+ if (current > this.$refs.body.scrollHeight - 1) this.$emit('bottom');
+ }
+ },
+
+ getMenu() {
+ const items = [{
+ icon: '%fa:pencil-alt%',
+ text: '%i18n:common.deck.rename%',
+ action: () => {
+ (this as any).apis.input({
+ title: '%i18n:common.deck.rename%',
+ default: this.name,
+ allowEmpty: false
+ }).then(name => {
+ this.$store.dispatch('settings/renameDeckColumn', { id: this.column.id, name });
+ });
+ }
+ }, null, {
+ icon: '%fa:arrow-left%',
+ text: '%i18n:common.deck.swap-left%',
+ action: () => {
+ this.$store.dispatch('settings/swapLeftDeckColumn', this.column.id);
+ }
+ }, {
+ icon: '%fa:arrow-right%',
+ text: '%i18n:common.deck.swap-right%',
+ action: () => {
+ this.$store.dispatch('settings/swapRightDeckColumn', this.column.id);
+ }
+ }, this.isStacked ? {
+ icon: '%fa:arrow-up%',
+ text: '%i18n:common.deck.swap-up%',
+ action: () => {
+ this.$store.dispatch('settings/swapUpDeckColumn', this.column.id);
+ }
+ } : undefined, this.isStacked ? {
+ icon: '%fa:arrow-down%',
+ text: '%i18n:common.deck.swap-down%',
+ action: () => {
+ this.$store.dispatch('settings/swapDownDeckColumn', this.column.id);
+ }
+ } : undefined, null, {
+ icon: '%fa:window-restore R%',
+ text: '%i18n:common.deck.stack-left%',
+ action: () => {
+ this.$store.dispatch('settings/stackLeftDeckColumn', this.column.id);
+ }
+ }, this.isStacked ? {
+ icon: '%fa:window-maximize R%',
+ text: '%i18n:common.deck.pop-right%',
+ action: () => {
+ this.$store.dispatch('settings/popRightDeckColumn', this.column.id);
+ }
+ } : undefined, null, {
+ icon: '%fa:trash-alt R%',
+ text: '%i18n:common.deck.remove%',
+ action: () => {
+ this.$store.dispatch('settings/removeDeckColumn', this.column.id);
+ }
+ }];
+
+ if (this.menu) {
+ items.unshift(null);
+ this.menu.reverse().forEach(i => items.unshift(i));
+ }
+
+ return items;
+ },
+
+ onContextmenu(e) {
+ contextmenu((this as any).os)(e, this.getMenu());
+ },
+
+ showMenu() {
+ this.os.new(Menu, {
+ source: this.$refs.menu,
+ compact: false,
+ items: this.getMenu()
+ });
+ },
+
+ onDragstart(e) {
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('mk-deck-column', this.column.id);
+ this.dragging = true;
+ },
+
+ onDragend(e) {
+ this.dragging = false;
+ },
+
+ onDragover(e) {
+ // 自分自身がドラッグされている場合
+ if (this.dragging) {
+ // 自分自身にはドロップさせない
+ e.dataTransfer.dropEffect = 'none';
+ return;
+ }
+
+ const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column';
+
+ e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
+ },
+
+ onDragenter() {
+ if (!this.dragging) this.draghover = true;
+ },
+
+ onDragleave() {
+ this.draghover = false;
+ },
+
+ onDrop(e) {
+ this.draghover = false;
+ this.$root.$emit('deck.column.dragEnd');
+
+ const id = e.dataTransfer.getData('mk-deck-column');
+ if (id != null && id != '') {
+ this.$store.dispatch('settings/swapDeckColumn', {
+ a: this.column.id,
+ b: id
+ });
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ $header-height = 42px
+
+ width 330px
+ min-width 330px
+ height 100%
+ background isDark ? #282C37 : #fff
+ border-radius 6px
+ box-shadow 0 2px 16px rgba(#000, 0.1)
+ overflow hidden
+
+ &.draghover
+ box-shadow 0 0 0 2px rgba($theme-color, 0.8)
+
+ &.dragging
+ box-shadow 0 0 0 2px rgba($theme-color, 0.4)
+
+ &.dropready
+ *
+ pointer-events none
+
+ &:not(.active)
+ flex-basis $header-height
+ min-height $header-height
+
+ &:not(.isStacked).narrow
+ width 285px
+ min-width 285px
+
+ &.naked
+ background rgba(#000, isDark ? 0.25 : 0.1)
+
+ > header
+ background transparent
+ box-shadow none
+
+ if !isDark
+ > button
+ color #bbb
+
+ > header
+ z-index 1
+ line-height $header-height
+ padding 0 16px
+ font-size 14px
+ color isDark ? #e3e5e8 : #888
+ background isDark ? #313543 : #fff
+ box-shadow 0 1px rgba(#000, 0.15)
+ cursor pointer
+
+ &, *
+ user-select none
+
+ *:not(button)
+ pointer-events none
+
+ &.indicate
+ box-shadow 0 3px 0 0 $theme-color
+
+ > span
+ [data-fa]
+ margin-right 8px
+
+ > .count
+ margin-left 4px
+ opacity 0.5
+
+ > button
+ position absolute
+ top 0
+ right 0
+ width $header-height
+ line-height $header-height
+ font-size 16px
+ color isDark ? #9baec8 : #ccc
+
+ &:hover
+ color isDark ? #b2c1d5 : #aaa
+
+ &:active
+ color isDark ? #b2c1d5 : #999
+
+ > 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.list-tl.vue b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
new file mode 100644
index 0000000000..d2f46bd8be
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
@@ -0,0 +1,123 @@
+<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';
+import { UserListStream } from '../../../../common/scripts/streaming/user-list';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ list: {
+ 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 = new UserListStream((this as any).os, this.$store.state.i, this.list.id);
+ this.connection.on('note', this.onNote);
+ this.connection.on('userAdded', this.onUserAdded);
+ this.connection.on('userRemoved', this.onUserRemoved);
+
+ 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/user-list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ mediaOnly: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ }).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/user-list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ mediaOnly: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ });
+
+ 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.media.length == 0) return;
+
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+ onUserAdded() {
+ this.fetch();
+ },
+ onUserRemoved() {
+ this.fetch();
+ }
+ }
+});
+</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
new file mode 100644
index 0000000000..3ba9ae914e
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.note.sub.vue
@@ -0,0 +1,77 @@
+<template>
+<div class="fnlfosztlhtptnongximhlbykxblytcq">
+ <mk-avatar class="avatar" :user="note.user"/>
+ <div class="main">
+ <mk-note-header class="header" :note="note" :mini="true"/>
+ <div class="body">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ note: {
+ type: Object,
+ required: true
+ },
+ // TODO
+ truncate: {
+ type: Boolean,
+ default: true
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ display flex
+ padding 16px
+ font-size 10px
+ background isDark ? #21242d : #fcfcfc
+
+ &.smart
+ > .main
+ width 100%
+
+ > header
+ align-items center
+
+ > .avatar
+ flex-shrink 0
+ display block
+ margin 0 8px 0 0
+ width 38px
+ height 38px
+ border-radius 8px
+
+ > .main
+ flex 1
+ min-width 0
+
+ > .header
+ margin-bottom 2px
+
+ > .body
+
+ > .text
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
+
+ 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
new file mode 100644
index 0000000000..5a8dc2ea65
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.note.vue
@@ -0,0 +1,473 @@
+<template>
+<div v-if="!mediaView" class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }">
+ <div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
+ <x-sub :note="p.reply"/>
+ </div>
+ <div class="renote" v-if="isRenote">
+ <mk-avatar class="avatar" :user="note.user"/>
+ %fa:retweet%
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+ <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+ <mk-time :time="note.createdAt"/>
+ </div>
+ <article>
+ <mk-avatar class="avatar" :user="p.user"/>
+ <div class="main">
+ <mk-note-header class="header" :note="p" :mini="true"/>
+ <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>
+ </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>
+ <a class="reply" v-if="p.reply">%fa:reply%</a>
+ <mk-note-html v-if="p.text && !canHideText(p)" :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>
+ <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+ <div class="tags" v-if="p.tags && p.tags.length > 0">
+ <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
+ </div>
+ <a class="location" v-if="p.geo" :href="`http://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>
+ </div>
+ <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+ </div>
+ <footer>
+ <mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+ <button @click="reply">
+ <template v-if="p.reply">%fa:reply-all%</template>
+ <template v-else>%fa:reply%</template>
+ </button>
+ <button @click="renote" title="Renote">%fa:retweet%</button>
+ <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">%fa:plus%</button>
+ <button class="menu" @click="menu" ref="menuButton">%fa:ellipsis-h%</button>
+ </footer>
+ </div>
+ </article>
+</div>
+<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi">
+ <div v-if="note.media.length > 0">
+ <mk-media-list :media-list="note.media"/>
+ </div>
+ <div v-if="note.renote && note.renote.media.length > 0">
+ <mk-media-list :media-list="note.renote.media"/>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parse from '../../../../../../text/parse';
+import canHideText from '../../../../common/scripts/can-hide-text';
+
+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';
+
+export default Vue.extend({
+ components: {
+ XSub
+ },
+
+ props: {
+ note: {
+ type: Object,
+ required: true
+ },
+ mediaView: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ data() {
+ return {
+ showContent: false,
+ connection: null,
+ connectionId: null
+ };
+ },
+
+ computed: {
+ isRenote(): boolean {
+ return (this.note.renote &&
+ this.note.text == null &&
+ this.note.mediaIds.length == 0 &&
+ this.note.poll == null);
+ },
+
+ p(): any {
+ return this.isRenote ? this.note.renote : this.note;
+ },
+
+ urls(): string[] {
+ if (this.p.text) {
+ const ast = parse(this.p.text);
+ return ast
+ .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+ .map(t => t.url);
+ } else {
+ return null;
+ }
+ }
+ },
+
+ 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: {
+ canHideText,
+
+ 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
+ });
+ },
+
+ renote() {
+ (this as any).apis.post({
+ renote: this.p
+ });
+ },
+
+ react() {
+ (this as any).os.new(MkReactionPicker, {
+ source: this.$refs.reactButton,
+ note: this.p,
+ compact: true
+ });
+ },
+
+ menu() {
+ (this as any).os.new(MkNoteMenu, {
+ source: this.$refs.menuButton,
+ note: this.p,
+ compact: true
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+mediaRoot(isDark)
+ font-size 13px
+ margin 4px 12px
+
+ &:first-child
+ margin-top 12px
+
+ &:last-child
+ margin-bottom 12px
+
+root(isDark)
+ font-size 13px
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+
+ &:last-of-type
+ border-bottom none
+
+ &.smart
+ > article
+ > .main
+ > header
+ align-items center
+ margin-bottom 4px
+
+ > .renote
+ display flex
+ align-items center
+ 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%)
+
+ .avatar
+ flex-shrink 0
+ display inline-block
+ width 20px
+ height 20px
+ margin 0 8px 0 0
+ border-radius 6px
+
+ [data-fa]
+ margin-right 4px
+
+ > span
+ flex-shrink 0
+
+ &:last-of-type
+ margin-right 8px
+
+ .name
+ overflow hidden
+ flex-shrink 1
+ text-overflow ellipsis
+ white-space nowrap
+ font-weight bold
+
+ > .mk-time
+ display block
+ margin-left auto
+ flex-shrink 0
+ font-size 0.9em
+
+ & + article
+ padding-top 8px
+
+ > article
+ display flex
+ padding 16px 16px 4px
+
+ > .avatar
+ flex-shrink 0
+ display block
+ margin 0 10px 8px 0
+ width 42px
+ height 42px
+ border-radius 6px
+ //position -webkit-sticky
+ //position sticky
+ //top 62px
+
+ > .main
+ flex 1
+ min-width 0
+
+ > .body
+
+ > .cw
+ cursor default
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .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
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ >>> .title
+ display block
+ margin-bottom 4px
+ padding 4px
+ font-size 90%
+ text-align center
+ background isDark ? #2f3944 : #eef1f3
+ border-radius 4px
+
+ >>> .code
+ margin 8px 0
+
+ >>> .quote
+ margin 8px
+ padding 6px 12px
+ color isDark ? #6f808e : #aaa
+ border-left solid 3px isDark ? #637182 : #eee
+
+ > .reply
+ margin-right 8px
+ color isDark ? #99abbf : #717171
+
+ > .rp
+ margin-left 4px
+ font-style oblique
+ color #a0bf46
+
+ [data-is-me]:after
+ content "you"
+ padding 0 4px
+ margin-left 4px
+ font-size 80%
+ color $theme-color-foreground
+ background $theme-color
+ border-radius 4px
+
+ .mk-url-preview
+ margin-top 8px
+
+ > .tags
+ margin 4px 0 0 0
+
+ > *
+ display inline-block
+ margin 0 8px 0 0
+ padding 2px 8px 2px 16px
+ font-size 90%
+ color #8d969e
+ background isDark ? #313543 : #edf0f3
+ border-radius 4px
+
+ &:before
+ content ""
+ display block
+ position absolute
+ top 0
+ bottom 0
+ left 4px
+ width 8px
+ height 8px
+ margin auto 0
+ background isDark ? #282c37 : #fff
+ border-radius 100%
+
+ > .media
+ > img
+ display block
+ max-width 100%
+
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
+
+ > .map
+ width 100%
+ height 200px
+
+ &:empty
+ display none
+
+ > .mk-poll
+ font-size 80%
+
+ > .renote
+ margin 8px 0
+
+ > .mk-note-preview
+ padding 16px
+ border dashed 1px isDark ? #4e945e : #c0dac6
+ border-radius 8px
+
+ > .app
+ font-size 12px
+ color #ccc
+
+ > footer
+ > button
+ margin 0
+ padding 4px 8px 8px 8px
+ background transparent
+ border none
+ box-shadow none
+ font-size 1em
+ color isDark ? #606984 : #ddd
+ cursor pointer
+
+ &:not(:last-child)
+ margin-right 28px
+
+ &:hover
+ color isDark ? #9198af : #666
+
+ > .count
+ display inline
+ margin 0 0 0 8px
+ 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)
+
+</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
new file mode 100644
index 0000000000..8862b0e0fc
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue
@@ -0,0 +1,242 @@
+<template>
+<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu">
+ <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
+
+ <div v-if="!fetching && requestInitPromise != null">
+ <p>%i18n:@error%</p>
+ <button @click="resolveInitPromise">%i18n:@retry%</button>
+ </div>
+
+ <transition-group name="mk-notes" class="transition">
+ <template v-for="(note, i) in _notes">
+ <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView"/>
+ <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>
+ </transition-group>
+
+ <footer v-if="more">
+ <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <template v-if="!moreFetching">%i18n:@load-more%</template>
+ <template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+ </button>
+ </footer>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+import XNote from './deck.note.vue';
+
+const displayLimit = 20;
+
+export default Vue.extend({
+ components: {
+ XNote
+ },
+
+ inject: ['column', 'isScrollTop', 'count'],
+
+ props: {
+ more: {
+ type: Function,
+ required: false
+ },
+ mediaView: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ data() {
+ return {
+ rootEl: null,
+ requestInitPromise: null as () => Promise<any[]>,
+ notes: [],
+ queue: [],
+ fetching: true,
+ moreFetching: false
+ };
+ },
+
+ computed: {
+ _notes(): any[] {
+ return (this.notes as any).map(note => {
+ const date = new Date(note.createdAt).getDate();
+ const month = new Date(note.createdAt).getMonth() + 1;
+ note._date = date;
+ note._datetext = `${month}月 ${date}日`;
+ return note;
+ });
+ }
+ },
+
+ watch: {
+ queue(q) {
+ this.count(q.length);
+ }
+ },
+
+ created() {
+ this.column.$on('top', this.onTop);
+ this.column.$on('bottom', this.onBottom);
+ },
+
+ beforeDestroy() {
+ this.column.$off('top', this.onTop);
+ this.column.$off('bottom', this.onBottom);
+ },
+
+ methods: {
+ focus() {
+ (this.$el as any).children[0].focus();
+ },
+
+ onNoteUpdated(i, note) {
+ Vue.set((this as any).notes, i, note);
+ },
+
+ init(promiseGenerator: () => Promise<any[]>) {
+ this.requestInitPromise = promiseGenerator;
+ this.resolveInitPromise();
+ },
+
+ resolveInitPromise() {
+ this.queue = [];
+ this.notes = [];
+ this.fetching = true;
+
+ const promise = this.requestInitPromise();
+
+ promise.then(notes => {
+ this.notes = notes;
+ this.requestInitPromise = null;
+ this.fetching = false;
+ }, e => {
+ this.fetching = false;
+ });
+ },
+
+ 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;
+
+ if (this.$store.state.settings.showMyRenotes === false) {
+ if (isMyNote && isPureRenote) {
+ return;
+ }
+ }
+
+ if (this.$store.state.settings.showRenotedMyNotes === false) {
+ if (isPureRenote && (note.renote.userId == this.$store.state.i.id)) {
+ return;
+ }
+ }
+ //#endregion
+
+ if (this.isScrollTop()) {
+ // Prepend the note
+ this.notes.unshift(note);
+
+ // オーバーフローしたら古い投稿は捨てる
+ if (this.notes.length >= displayLimit) {
+ this.notes = this.notes.slice(0, displayLimit);
+ }
+ } else {
+ this.queue.push(note);
+ }
+ },
+
+ append(note) {
+ this.notes.push(note);
+ },
+
+ tail() {
+ return this.notes[this.notes.length - 1];
+ },
+
+ releaseQueue() {
+ this.queue.forEach(n => this.prepend(n, true));
+ this.queue = [];
+ },
+
+ async loadMore() {
+ if (this.more == null) return;
+ if (this.moreFetching) return;
+
+ this.moreFetching = true;
+ await this.more();
+ this.moreFetching = false;
+ },
+
+ onTop() {
+ this.releaseQueue();
+ },
+
+ onBottom() {
+ this.loadMore();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ .transition
+ .mk-notes-enter
+ .mk-notes-leave-to
+ opacity 0
+ transform translateY(-30px)
+
+ > *
+ transition transform .3s ease, opacity .3s ease
+
+ > .date
+ display block
+ margin 0
+ line-height 32px
+ font-size 14px
+ text-align center
+ color isDark ? #666b79 : #aaa
+ background isDark ? #242731 : #fdfdfd
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+
+ span
+ margin 0 16px
+
+ [data-fa]
+ margin-right 8px
+
+ > footer
+ > button
+ display block
+ margin 0
+ padding 16px
+ width 100%
+ text-align center
+ color #ccc
+ background isDark ? #282C37 : #fff
+ border-top solid 1px isDark ? #1c2023 : #eaeaea
+ border-bottom-left-radius 6px
+ border-bottom-right-radius 6px
+
+ &:hover
+ background isDark ? #2e3440 : #f5f5f5
+
+ &:active
+ background isDark ? #21242b : #eee
+
+.eamppglmnmimdhrlzhplwpvyeaqmmhxu[data-darkmode]
+ root(true)
+
+.eamppglmnmimdhrlzhplwpvyeaqmmhxu:not([data-darkmode])
+ root(false)
+
+</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
new file mode 100644
index 0000000000..a379adc69e
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.notification.vue
@@ -0,0 +1,179 @@
+<template>
+<div class="dsfykdcjpuwfvpefwufddclpjhzktmpw">
+ <div class="notification reaction" v-if="notification.type == 'reaction'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
+ <mk-reaction-icon :reaction="notification.reaction"/>
+ <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
+ <mk-time :time="notification.createdAt"/>
+ </header>
+ <router-link class="note-ref" :to="notification.note | notePage">
+ %fa:quote-left%{{ getNoteSummary(notification.note) }}
+ %fa:quote-right%
+ </router-link>
+ </div>
+ </div>
+
+ <div class="notification renote" v-if="notification.type == 'renote'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
+ %fa:retweet%
+ <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
+ <mk-time :time="notification.createdAt"/>
+ </header>
+ <router-link class="note-ref" :to="notification.note | notePage">
+ %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
+ </router-link>
+ </div>
+ </div>
+
+ <div class="notification follow" v-if="notification.type == 'follow'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
+ %fa:user-plus%
+ <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
+ <mk-time :time="notification.createdAt"/>
+ </header>
+ </div>
+ </div>
+
+ <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
+ %fa:user-clock%
+ <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
+ <mk-time :time="notification.createdAt"/>
+ </header>
+ </div>
+ </div>
+
+ <div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
+ %fa:chart-pie%
+ <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
+ <mk-time :time="notification.createdAt"/>
+ </header>
+ <router-link class="note-ref" :to="notification.note | notePage">
+ %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
+ </router-link>
+ </div>
+ </div>
+
+ <template v-if="notification.type == 'quote'">
+ <x-note :note="notification.note" @update:note="onNoteUpdated"/>
+ </template>
+
+ <template v-if="notification.type == 'reply'">
+ <x-note :note="notification.note" @update:note="onNoteUpdated"/>
+ </template>
+
+ <template v-if="notification.type == 'mention'">
+ <x-note :note="notification.note" @update:note="onNoteUpdated"/>
+ </template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import getNoteSummary from '../../../../../../renderers/get-note-summary';
+import XNote from './deck.note.vue';
+
+export default Vue.extend({
+ components: {
+ XNote
+ },
+ props: ['notification'],
+ data() {
+ return {
+ getNoteSummary
+ };
+ },
+ methods: {
+ onNoteUpdated(note) {
+ switch (this.notification.type) {
+ case 'quote':
+ case 'reply':
+ case 'mention':
+ Vue.set(this.notification, 'note', note);
+ break;
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ > .notification
+ padding 16px
+ font-size 13px
+ overflow-wrap break-word
+
+ &:after
+ content ""
+ display block
+ clear both
+
+ > .avatar
+ display block
+ float left
+ width 36px
+ height 36px
+ border-radius 6px
+
+ > div
+ float right
+ width calc(100% - 36px)
+ padding-left 8px
+
+ > header
+ display flex
+ align-items baseline
+ white-space nowrap
+
+ i, .mk-reaction-icon
+ margin-right 4px
+
+ > .mk-time
+ margin-left auto
+ color isDark ? #606984 : #c0c0c0
+ font-size 0.9em
+
+ > .note-preview
+ color isDark ? #fff : #717171
+
+ > .note-ref
+ color isDark ? #fff : #717171
+
+ [data-fa]
+ font-size 1em
+ font-weight normal
+ font-style normal
+ display inline-block
+ margin-right 3px
+
+ &.renote
+ > div > header i
+ color #77B255
+
+ &.follow
+ > div > header i
+ color #53c7ce
+
+ &.receiveFollowRequest
+ > 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-column.vue b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
new file mode 100644
index 0000000000..220e938a46
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
@@ -0,0 +1,38 @@
+<template>
+<x-column :name="name" :column="column" :is-stacked="isStacked">
+ <span slot="header">%fa:bell R%{{ name }}</span>
+
+ <x-notifications/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import XNotifications from './deck.notifications.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XNotifications
+ },
+
+ 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.notifications%';
+ }
+ },
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
new file mode 100644
index 0000000000..f54ad1a3cd
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
@@ -0,0 +1,229 @@
+<template>
+<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
+ <transition-group 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'">
+ <span>%fa:angle-up%{{ notification._datetext }}</span>
+ <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
+ </p>
+ </template>
+ </transition-group>
+ <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>
+ <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
+ <p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotification from './deck.notification.vue';
+
+const displayLimit = 20;
+
+export default Vue.extend({
+ components: {
+ XNotification
+ },
+
+ inject: ['column', 'isScrollTop', 'count'],
+
+ data() {
+ return {
+ fetching: true,
+ fetchingMoreNotifications: false,
+ notifications: [],
+ queue: [],
+ moreNotifications: false,
+ connection: null,
+ connectionId: null
+ };
+ },
+
+ computed: {
+ _notifications(): any[] {
+ return (this.notifications as any).map(notification => {
+ const date = new Date(notification.createdAt).getDate();
+ const month = new Date(notification.createdAt).getMonth() + 1;
+ notification._date = date;
+ notification._datetext = `${month}月 ${date}日`;
+ return notification;
+ });
+ }
+ },
+
+ watch: {
+ queue(q) {
+ this.count(q.length);
+ }
+ },
+
+ mounted() {
+ this.connection = (this as any).os.stream.getConnection();
+ this.connectionId = (this as any).os.stream.use();
+
+ this.connection.on('notification', this.onNotification);
+
+ this.column.$on('top', this.onTop);
+ this.column.$on('bottom', this.onBottom);
+
+ const max = 10;
+
+ (this as any).api('i/notifications', {
+ limit: max + 1
+ }).then(notifications => {
+ if (notifications.length == max + 1) {
+ this.moreNotifications = true;
+ notifications.pop();
+ }
+
+ this.notifications = notifications;
+ this.fetching = false;
+ });
+ },
+
+ beforeDestroy() {
+ this.connection.off('notification', this.onNotification);
+ (this as any).os.stream.dispose(this.connectionId);
+
+ this.column.$off('top', this.onTop);
+ this.column.$off('bottom', this.onBottom);
+ },
+
+ methods: {
+ fetchMoreNotifications() {
+ this.fetchingMoreNotifications = true;
+
+ const max = 20;
+
+ (this as any).api('i/notifications', {
+ limit: max + 1,
+ untilId: this.notifications[this.notifications.length - 1].id
+ }).then(notifications => {
+ if (notifications.length == max + 1) {
+ this.moreNotifications = true;
+ notifications.pop();
+ } else {
+ this.moreNotifications = false;
+ }
+ this.notifications = this.notifications.concat(notifications);
+ this.fetchingMoreNotifications = false;
+ });
+ },
+
+ onNotification(notification) {
+ // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+ this.connection.send({
+ type: 'read_notification',
+ id: notification.id
+ });
+
+ this.prepend(notification);
+ },
+
+ prepend(notification) {
+ if (this.isScrollTop()) {
+ // Prepend the notification
+ this.notifications.unshift(notification);
+
+ // オーバーフローしたら古い通知は捨てる
+ if (this.notifications.length >= displayLimit) {
+ this.notifications = this.notifications.slice(0, displayLimit);
+ }
+ } else {
+ this.queue.push(notification);
+ }
+ },
+
+ releaseQueue() {
+ this.queue.forEach(n => this.prepend(n));
+ this.queue = [];
+ },
+
+ onTop() {
+ this.releaseQueue();
+ },
+
+ onBottom() {
+ this.fetchMoreNotifications();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+
+ .transition
+ .mk-notifications-enter
+ .mk-notifications-leave-to
+ opacity 0
+ transform translateY(-30px)
+
+ > *
+ transition transform .3s ease, opacity .3s ease
+
+ > .notifications
+
+ > .notification:not(:last-child)
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+
+ > .date
+ display block
+ margin 0
+ 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
+
+ span
+ margin 0 16px
+
+ i
+ margin-right 8px
+
+ > .more
+ display block
+ width 100%
+ padding 16px
+ color #555
+ border-top solid 1px rgba(#000, 0.05)
+
+ &:hover
+ background rgba(#000, 0.025)
+
+ &:active
+ background rgba(#000, 0.05)
+
+ &.fetching
+ cursor wait
+
+ > [data-fa]
+ margin-right 4px
+
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > .loading
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [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
new file mode 100644
index 0000000000..ffe1da670b
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
@@ -0,0 +1,76 @@
+<template>
+<x-column :menu="menu" :name="name" :column="column" :is-stacked="isStacked">
+ <span slot="header">
+ <template v-if="column.type == 'home'">%fa:home%</template>
+ <template v-if="column.type == 'local'">%fa:R comments%</template>
+ <template v-if="column.type == 'global'">%fa:globe%</template>
+ <template v-if="column.type == 'list'">%fa:list%</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%"/>
+ </div>
+ <x-list-tl v-if="column.type == 'list'" :list="column.list" :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>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import XTl from './deck.tl.vue';
+import XListTl from './deck.list-tl.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XTl,
+ XListTl
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ edit: false,
+ menu: [{
+ icon: '%fa:cog%',
+ text: '%i18n:@edit%',
+ action: () => {
+ this.edit = !this.edit;
+ }
+ }]
+ }
+ },
+
+ computed: {
+ name(): string {
+ if (this.column.name) return this.column.name;
+
+ switch (this.column.type) {
+ case 'home': return '%i18n:common.deck.home%';
+ case 'local': return '%i18n:common.deck.local%';
+ case 'global': return '%i18n:common.deck.global%';
+ case 'list': return this.column.list.title;
+ }
+ }
+ },
+
+ methods: {
+ onChangeSettings(v) {
+ this.$store.dispatch('settings/saveDeck');
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue
new file mode 100644
index 0000000000..8e05f09c5d
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue
@@ -0,0 +1,152 @@
+<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: {
+ src: {
+ type: String,
+ required: false,
+ default: 'home'
+ },
+ mediaOnly: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ mediaView: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null,
+ connectionId: null
+ };
+ },
+
+ watch: {
+ mediaOnly() {
+ this.fetch();
+ }
+ },
+
+ computed: {
+ stream(): any {
+ return this.src == 'home'
+ ? (this as any).os.stream
+ : this.src == 'local'
+ ? (this as any).os.streams.localTimelineStream
+ : (this as any).os.streams.globalTimelineStream;
+ },
+
+ endpoint(): string {
+ return this.src == 'home'
+ ? 'notes/timeline'
+ : this.src == 'local'
+ ? 'notes/local-timeline'
+ : 'notes/global-timeline';
+ }
+ },
+
+ mounted() {
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
+
+ this.connection.on('note', this.onNote);
+ if (this.src == 'home') {
+ this.connection.on('follow', this.onChangeFollowing);
+ this.connection.on('unfollow', this.onChangeFollowing);
+ }
+
+ 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);
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api(this.endpoint, {
+ limit: fetchLimit + 1,
+ mediaOnly: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ }).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(this.endpoint, {
+ limit: fetchLimit + 1,
+ mediaOnly: this.mediaOnly,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ });
+
+ 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.media.length == 0) return;
+
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+
+ onChangeFollowing() {
+ this.fetch();
+ },
+
+ focus() {
+ (this.$refs.timeline as any).focus();
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue
new file mode 100644
index 0000000000..da4acb8cca
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.vue
@@ -0,0 +1,221 @@
+<template>
+<mk-ui :class="$style.root">
+ <div class="qlvquzbjribqcaozciifydkngcwtyzje" :data-darkmode="$store.state.device.darkmode">
+ <template v-for="ids in layout">
+ <div v-if="ids.length > 1" class="folder">
+ <template v-for="id, i in ids">
+ <x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true"/>
+ </template>
+ </div>
+ <x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])"/>
+ </template>
+ <button ref="add" @click="add" title="%i18n:common.deck.add-column%">%fa:plus%</button>
+ </div>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumnCore from './deck.column-core.vue';
+import Menu from '../../../../common/views/components/menu.vue';
+import MkUserListsWindow from '../../components/user-lists-window.vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ components: {
+ XColumnCore
+ },
+
+ computed: {
+ columns(): any[] {
+ if (this.$store.state.settings.deck == null) return [];
+ return this.$store.state.settings.deck.columns;
+ },
+ layout(): any[] {
+ 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;
+ }
+ },
+
+ provide() {
+ return {
+ getColumnVm: this.getColumnVm
+ };
+ },
+
+ created() {
+ if (this.$store.state.settings.deck == null) {
+ const deck = {
+ columns: [/*{
+ type: 'widgets',
+ widgets: []
+ }, */{
+ id: uuid(),
+ type: 'home'
+ }, {
+ id: uuid(),
+ type: 'notifications'
+ }, {
+ id: uuid(),
+ type: 'local'
+ }, {
+ id: uuid(),
+ type: 'global'
+ }]
+ };
+
+ deck.layout = deck.columns.map(c => [c.id]);
+
+ this.$store.dispatch('settings/set', {
+ key: 'deck',
+ value: deck
+ });
+ }
+
+ // 互換性のため
+ if (this.$store.state.settings.deck != null && this.$store.state.settings.deck.layout == null) {
+ this.$store.dispatch('settings/set', {
+ key: 'deck',
+ value: Object.assign({}, this.$store.state.settings.deck, {
+ layout: this.$store.state.settings.deck.columns.map(c => [c.id])
+ })
+ });
+ }
+ },
+
+ mounted() {
+ document.documentElement.style.overflow = 'hidden';
+ },
+
+ beforeDestroy() {
+ document.documentElement.style.overflow = 'auto';
+ },
+
+ methods: {
+ getColumnVm(id) {
+ return this.$refs[id][0];
+ },
+
+ add() {
+ this.os.new(Menu, {
+ source: this.$refs.add,
+ compact: true,
+ items: [{
+ icon: '%fa:home%',
+ text: '%i18n:common.deck.home%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'home'
+ });
+ }
+ }, {
+ icon: '%fa:comments R%',
+ text: '%i18n:common.deck.local%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'local'
+ });
+ }
+ }, {
+ icon: '%fa:globe%',
+ text: '%i18n:common.deck.global%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'global'
+ });
+ }
+ }, {
+ icon: '%fa:list%',
+ text: '%i18n:common.deck.list%',
+ action: () => {
+ const w = (this as any).os.new(MkUserListsWindow);
+ w.$once('choosen', list => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'list',
+ list: list
+ });
+ w.close();
+ });
+ }
+ }, {
+ icon: '%fa:bell R%',
+ text: '%i18n:common.deck.notifications%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'notifications'
+ });
+ }
+ }, {
+ icon: '%fa:calculator%',
+ text: '%i18n:common.deck.widgets%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'widgets',
+ widgets: []
+ });
+ }
+ }]
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" module>
+.root
+ height 100vh
+</style>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ display flex
+ flex 1
+ padding 16px 0 16px 16px
+ overflow auto
+
+ > div
+ margin-right 8px
+
+ &:last-of-type
+ margin-right 0
+
+ &.folder
+ display flex
+ flex-direction column
+
+ > *:not(:last-child)
+ margin-bottom 8px
+
+ > *
+ &:first-child
+ margin-left auto
+
+ &:last-child
+ margin-right auto
+
+ > button
+ padding 0 16px
+ color isDark ? #93a0a5 : #888
+
+ &:hover
+ color isDark ? #b8c5ca : #777
+
+ &:active
+ color isDark ? #fff : #555
+
+.qlvquzbjribqcaozciifydkngcwtyzje[data-darkmode]
+ root(true)
+
+.qlvquzbjribqcaozciifydkngcwtyzje:not([data-darkmode])
+ root(false)
+
+</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
new file mode 100644
index 0000000000..15397232e0
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
@@ -0,0 +1,182 @@
+<template>
+<x-column :menu="menu" :naked="true" :narrow="true" :name="name" :column="column" :is-stacked="isStacked" class="wtdtxvecapixsepjtcupubtsmometobz">
+ <span slot="header">%fa:calculator%{{ name }}</span>
+
+ <div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
+ <template v-if="edit">
+ <header>
+ <select v-model="widgetAdderSelected" @change="addWidget">
+ <option value="profile">%i18n:common.widgets.profile%</option>
+ <option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
+ <option value="calendar">%i18n:common.widgets.calendar%</option>
+ <option value="timemachine">%i18n:common.widgets.timemachine%</option>
+ <option value="activity">%i18n:common.widgets.activity%</option>
+ <option value="rss">%i18n:common.widgets.rss%</option>
+ <option value="trends">%i18n:common.widgets.trends%</option>
+ <option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
+ <option value="slideshow">%i18n:common.widgets.slideshow%</option>
+ <option value="version">%i18n:common.widgets.version%</option>
+ <option value="broadcast">%i18n:common.widgets.broadcast%</option>
+ <option value="notifications">%i18n:common.widgets.notifications%</option>
+ <option value="users">%i18n:common.widgets.users%</option>
+ <option value="polls">%i18n:common.widgets.polls%</option>
+ <option value="post-form">%i18n:common.widgets.post-form%</option>
+ <option value="messaging">%i18n:common.widgets.messaging%</option>
+ <option value="memo">%i18n:common.widgets.memo%</option>
+ <option value="hashtags">%i18n:common.widgets.hashtags%</option>
+ <option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
+ <option value="server">%i18n:common.widgets.server%</option>
+ <option value="donation">%i18n:common.widgets.donation%</option>
+ <option value="nav">%i18n:common.widgets.nav%</option>
+ <option value="tips">%i18n:common.widgets.tips%</option>
+ </select>
+ </header>
+ <x-draggable
+ :list="column.widgets"
+ :options="{ animation: 150 }"
+ @sort="onWidgetSort"
+ >
+ <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
+ <button class="remove" @click="removeWidget(widget)">%fa:times%</button>
+ <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
+ </div>
+ </x-draggable>
+ </template>
+ <template v-else>
+ <component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck"/>
+ </template>
+ </div>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import * as XDraggable from 'vuedraggable';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XDraggable
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ edit: false,
+ menu: null,
+ widgetAdderSelected: null
+ }
+ },
+
+ computed: {
+ name(): string {
+ if (this.column.name) return this.column.name;
+ return '%i18n:common.deck.widgets%';
+ }
+ },
+
+ created() {
+ this.menu = [{
+ icon: '%fa:cog%',
+ text: '%i18n:@edit%',
+ action: () => {
+ this.edit = !this.edit;
+ }
+ }];
+ },
+
+ methods: {
+ widgetFunc(id) {
+ const w = this.$refs[id][0];
+ if (w.func) w.func();
+ },
+
+ onWidgetSort() {
+ this.saveWidgets();
+ },
+
+ addWidget() {
+ this.$store.dispatch('settings/addDeckWidget', {
+ id: this.column.id,
+ widget: {
+ name: this.widgetAdderSelected,
+ id: uuid(),
+ data: {}
+ }
+ });
+
+ this.widgetAdderSelected = null;
+ },
+
+ removeWidget(widget) {
+ this.$store.dispatch('settings/removeDeckWidget', {
+ id: this.column.id,
+ widget
+ });
+ },
+
+ saveWidgets() {
+ this.$store.dispatch('settings/saveDeck');
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+ .gqpwvtwtprsbmnssnbicggtwqhmylhnq
+ > header
+ padding 16px
+
+ > *
+ width 100%
+ padding 4px
+
+ .widget, .customize-container
+ margin 8px
+
+ &:first-of-type
+ margin-top 0
+
+ .customize-container
+ cursor move
+
+ > *:not(.remove)
+ pointer-events none
+
+ > .remove
+ position absolute
+ z-index 1
+ top 8px
+ right 8px
+ width 32px
+ height 32px
+ color #fff
+ 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 353f59b703..217dcb7751 100644
--- a/src/client/app/desktop/views/pages/drive.vue
+++ b/src/client/app/desktop/views/pages/drive.vue
@@ -16,11 +16,11 @@ export default Vue.extend({
this.folder = this.$route.params.folder;
},
mounted() {
- document.title = 'Misskey Drive';
+ document.title = '%i18n:@title%';
},
methods: {
onMoveRoot() {
- const title = 'Misskey Drive';
+ const title = '%i18n:@title%';
// Rewrite URL
history.pushState(null, title, '/i/drive');
@@ -28,7 +28,7 @@ export default Vue.extend({
document.title = title;
},
onOpenFolder(folder) {
- const title = folder.name + ' | Misskey Drive';
+ const title = folder.name + ' | %i18n:@title%';
// Rewrite URL
history.pushState(null, title, '/i/drive/folder/' + folder.id);
@@ -49,4 +49,3 @@ export default Vue.extend({
> .mk-drive
height 100%
</style>
-
diff --git a/src/client/app/desktop/views/pages/favorites.vue b/src/client/app/desktop/views/pages/favorites.vue
index d908c08f7c..8adb9412f2 100644
--- a/src/client/app/desktop/views/pages/favorites.vue
+++ b/src/client/app/desktop/views/pages/favorites.vue
@@ -2,9 +2,9 @@
<mk-ui>
<main v-if="!fetching">
<template v-for="favorite in favorites">
- <mk-note-detail :note="favorite.note" :key="favorite.note.id"/>
+ <mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
</template>
- <a v-if="existMore" @click="more">さらに読み込む</a>
+ <a v-if="existMore" @click="more">%i18n:@more%</a>
</main>
</mk-ui>
</template>
@@ -70,4 +70,7 @@ main
margin 0 auto
padding 16px
max-width 700px
+
+ > .post
+ margin-bottom 16px
</style>
diff --git a/src/client/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue
index 8aa06be57f..da5f15bb69 100644
--- a/src/client/app/desktop/views/pages/home-customize.vue
+++ b/src/client/app/desktop/views/pages/home-customize.vue
@@ -6,7 +6,7 @@
import Vue from 'vue';
export default Vue.extend({
mounted() {
- document.title = 'Misskey - ホームのカスタマイズ';
+ document.title = 'Misskey - %i18n:@title%';
}
});
</script>
diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
index e4caa2022e..60b257edb7 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -7,7 +7,6 @@
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
-import getNoteSummary from '../../../../../renderers/get-note-summary';
export default Vue.extend({
props: {
@@ -16,46 +15,14 @@ export default Vue.extend({
default: 'timeline'
}
},
- data() {
- return {
- connection: null,
- connectionId: null,
- unreadCount: 0
- };
- },
mounted() {
document.title = 'Misskey';
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
-
- this.connection.on('note', this.onStreamNote);
- document.addEventListener('visibilitychange', this.onVisibilitychange, false);
-
Progress.start();
},
- beforeDestroy() {
- this.connection.off('note', this.onStreamNote);
- (this as any).os.stream.dispose(this.connectionId);
- document.removeEventListener('visibilitychange', this.onVisibilitychange);
- },
methods: {
loaded() {
Progress.done();
- },
-
- onStreamNote(note) {
- if (document.hidden && note.userId != (this as any).os.i.id) {
- this.unreadCount++;
- document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
- }
- },
-
- onVisibilitychange() {
- if (!document.hidden) {
- this.unreadCount = 0;
- document.title = 'Misskey';
- }
}
}
});
diff --git a/src/client/app/desktop/views/pages/index.vue b/src/client/app/desktop/views/pages/index.vue
index 0ea47d913b..5d11fc5423 100644
--- a/src/client/app/desktop/views/pages/index.vue
+++ b/src/client/app/desktop/views/pages/index.vue
@@ -1,5 +1,5 @@
<template>
-<component :is="os.isSignedIn ? 'home' : 'welcome'"></component>
+<component :is="$store.getters.isSignedIn ? 'home' : 'welcome'"></component>
</template>
<script lang="ts">
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 1cc8d8a778..06c32776c9 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -21,10 +21,21 @@ export default Vue.extend({
$route: 'fetch'
},
created() {
+ const applyBg = v =>
+ document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
+
+ applyBg(this.$store.state.device.darkmode);
+
+ this.unwatchDarkmode = this.$store.watch(s => {
+ return s.device.darkmode;
+ }, applyBg);
+
this.fetch();
},
- mounted() {
- document.documentElement.style.background = '#fff';
+ beforeDestroy() {
+ document.documentElement.style.removeProperty('background');
+ document.documentElement.style.removeProperty('background-color'); // for safari's bug
+ this.unwatchDarkmode();
},
methods: {
fetch() {
@@ -50,6 +61,5 @@ export default Vue.extend({
flex 1
flex-direction column
min-height 100%
- background #fff
</style>
diff --git a/src/client/app/desktop/views/pages/othello.vue b/src/client/app/desktop/views/pages/reversi.vue
index 0d8e987dd9..098fc41f1c 100644
--- a/src/client/app/desktop/views/pages/othello.vue
+++ b/src/client/app/desktop/views/pages/reversi.vue
@@ -1,6 +1,6 @@
<template>
<component :is="ui ? 'mk-ui' : 'div'">
- <mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/>
+ <mk-reversi v-if="!fetching" :init-game="game" @gamed="onGamed"/>
</component>
</template>
@@ -33,7 +33,7 @@ export default Vue.extend({
Progress.start();
this.fetching = true;
- (this as any).api('othello/games/show', {
+ (this as any).api('reversi/games/show', {
gameId: this.$route.params.game
}).then(game => {
this.game = game;
@@ -43,7 +43,7 @@ export default Vue.extend({
});
},
onGamed(game) {
- history.pushState(null, null, '/othello/' + game.id);
+ history.pushState(null, null, '/reversi/' + game.id);
}
}
});
diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue
index 67e1e3bfe0..e79ac1c739 100644
--- a/src/client/app/desktop/views/pages/search.vue
+++ b/src/client/app/desktop/views/pages/search.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
},
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
- window.addEventListener('scroll', this.onScroll);
+ window.addEventListener('scroll', this.onScroll, { passive: true });
this.fetch();
},
diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue
index 7a00896640..c846f2418f 100644
--- a/src/client/app/desktop/views/pages/selectdrive.vue
+++ b/src/client/app/desktop/views/pages/selectdrive.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
}
},
mounted() {
- document.title = '%i18n:!@title%';
+ document.title = '%i18n:@title%';
},
methods: {
onSelected(file) {
diff --git a/src/client/app/desktop/views/pages/share.vue b/src/client/app/desktop/views/pages/share.vue
new file mode 100644
index 0000000000..e60434074a
--- /dev/null
+++ b/src/client/app/desktop/views/pages/share.vue
@@ -0,0 +1,58 @@
+<template>
+<div class="pptjhabgjtt7kwskbfv4y3uml6fpuhmr">
+ <h1>Misskeyで共有</h1>
+ <div>
+ <mk-signin v-if="!$store.getters.isSignedIn"/>
+ <mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
+ <p v-if="posted" class="posted">%fa:check%</p>
+ </div>
+ <button v-if="posted" class="ui button" @click="close">閉じる</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ data() {
+ return {
+ posted: false,
+ text: new URLSearchParams(location.search).get('text')
+ };
+ },
+ methods: {
+ close() {
+ window.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.pptjhabgjtt7kwskbfv4y3uml6fpuhmr
+ padding 16px
+
+ > h1
+ margin 0 0 8px 0
+ color #555
+ font-size 20px
+ text-align center
+
+ > div
+ max-width 500px
+ margin 0 auto
+ background #fff
+ border solid 1px rgba(#000, 0.1)
+ border-radius 6px
+ overflow hidden
+
+ > .posted
+ display block
+ margin 0
+ padding 64px
+ text-align center
+
+ > button
+ display block
+ margin 16px auto
+</style>
diff --git a/src/client/app/desktop/views/pages/tag.vue b/src/client/app/desktop/views/pages/tag.vue
new file mode 100644
index 0000000000..0b8fd81ac1
--- /dev/null
+++ b/src/client/app/desktop/views/pages/tag.vue
@@ -0,0 +1,128 @@
+<template>
+<mk-ui>
+ <header :class="$style.header">
+ <h1>#{{ $route.params.tag }}</h1>
+ </header>
+ <div :class="$style.loading" v-if="fetching">
+ <mk-ellipsis-icon/>
+ </div>
+ <p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
+ <mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+const limit = 20;
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ offset: 0,
+ empty: false
+ };
+ },
+ watch: {
+ $route: 'fetch'
+ },
+ mounted() {
+ document.addEventListener('keydown', this.onDocumentKeydown);
+ window.addEventListener('scroll', this.onScroll, { passive: true });
+
+ this.fetch();
+ },
+ beforeDestroy() {
+ document.removeEventListener('keydown', this.onDocumentKeydown);
+ window.removeEventListener('scroll', this.onScroll);
+ },
+ methods: {
+ onDocumentKeydown(e) {
+ if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
+ if (e.which == 84) { // t
+ (this.$refs.timeline as any).focus();
+ }
+ }
+ },
+ fetch() {
+ this.fetching = true;
+ Progress.start();
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/search_by_tag', {
+ limit: limit + 1,
+ offset: this.offset,
+ tag: this.$route.params.tag
+ }).then(notes => {
+ if (notes.length == 0) this.empty = true;
+ if (notes.length == limit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ Progress.done();
+ }, rej);
+ }));
+ },
+ more() {
+ this.offset += limit;
+
+ const promise = (this as any).api('notes/search_by_tag', {
+ limit: limit + 1,
+ offset: this.offset,
+ tag: this.$route.params.tag
+ });
+
+ promise.then(notes => {
+ if (notes.length == limit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" module>
+.header
+ width 100%
+ max-width 600px
+ margin 0 auto
+ color #555
+
+.notes
+ width 600px
+ margin 0 auto
+ border solid 1px rgba(#000, 0.075)
+ border-radius 6px
+ overflow hidden
+
+.loading
+ padding 64px 0
+
+.empty
+ display block
+ margin 0 auto
+ padding 32px
+ max-width 400px
+ text-align center
+ color #999
+
+ > [data-fa]
+ display block
+ margin-bottom 16px
+ font-size 3em
+ color #ccc
+
+</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
index 4236cdbb14..517fe89750 100644
--- a/src/client/app/desktop/views/pages/user-list.users.vue
+++ b/src/client/app/desktop/views/pages/user-list.users.vue
@@ -1,8 +1,8 @@
<template>
<div>
<mk-widget-container>
- <template slot="header">%fa:users% ユーザー</template>
- <button slot="func" title="ユーザーを追加" @click="add">%fa:plus%</button>
+ <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>
@@ -48,7 +48,7 @@ export default Vue.extend({
methods: {
add() {
(this as any).apis.input({
- title: 'ユーザー名',
+ title: '%i18n:@username%',
}).then(async username => {
const user = await (this as any).api('users/show', {
username
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 60dc15b15d..d52c6b762c 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -10,7 +10,7 @@
<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<div class="title">
<p class="name">{{ user | userName }}</p>
- <p class="username">@{{ user | acct }}</p>
+ <p class="username"><mk-acct :user="user"/></p>
<p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
</div>
<footer>
@@ -29,7 +29,7 @@ export default Vue.extend({
style(): any {
if (this.user.bannerUrl == null) return {};
return {
- backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}
@@ -37,7 +37,7 @@ export default Vue.extend({
mounted() {
if (this.user.bannerUrl) {
window.addEventListener('load', this.onScroll);
- window.addEventListener('scroll', this.onScroll);
+ window.addEventListener('scroll', this.onScroll, { passive: true });
window.addEventListener('resize', this.onScroll);
}
},
@@ -63,7 +63,7 @@ export default Vue.extend({
},
onBannerClick() {
- if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
+ if (!this.$store.getters.isSignedIn || this.$store.state.i.id != this.user.id) return;
(this as any).apis.updateBanner().then(i => {
this.user.bannerUrl = i.bannerUrl;
diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue
index 6b242a6129..afaf97dc9e 100644
--- a/src/client/app/desktop/views/pages/user/user.home.vue
+++ b/src/client/app/desktop/views/pages/user/user.home.vue
@@ -4,7 +4,7 @@
<div ref="left">
<x-profile :user="user"/>
<x-photos :user="user"/>
- <x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
+ <x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
<p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
</div>
</div>
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 29e49f36a6..5aa08f7c85 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -1,6 +1,6 @@
<template>
<div class="profile">
- <div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id">
+ <div class="friend-form" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id">
<mk-follow-button :user="user" size="big"/>
<p class="followed" v-if="user.isFollowed">%i18n:@follows-you%</p>
<p class="stalk" v-if="user.isFollowing">
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 9c9840c190..812b5b4229 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -1,15 +1,15 @@
<template>
<div class="timeline">
<header>
- <span :data-active="mode == 'default'" @click="mode = 'default'">投稿</span>
- <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
- <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span>
+ <span :data-active="mode == 'default'" @click="mode = 'default'">%i18n:@default%</span>
+ <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%i18n:@with-replies%</span>
+ <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%i18n:@with-media%</span>
</header>
<div class="loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<mk-notes ref="timeline" :more="existMore ? more : null">
- <p class="empty" slot="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
+ <p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p>
</mk-notes>
</div>
</template>
@@ -21,6 +21,7 @@ const fetchLimit = 10;
export default Vue.extend({
props: ['user'],
+
data() {
return {
fetching: true,
@@ -31,19 +32,23 @@ export default Vue.extend({
date: null
};
},
+
watch: {
mode() {
this.fetch();
}
},
+
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
this.fetch(() => this.$emit('loaded'));
},
+
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
},
+
methods: {
onDocumentKeydown(e) {
if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
@@ -52,6 +57,7 @@ export default Vue.extend({
}
}
},
+
fetch(cb?) {
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
@@ -72,15 +78,19 @@ export default Vue.extend({
}, rej);
}));
},
+
more() {
this.moreFetching = true;
- (this as any).api('users/notes', {
+
+ const promise = (this as any).api('users/notes', {
userId: this.user.id,
limit: fetchLimit + 1,
includeReplies: this.mode == 'with-replies',
withMedia: this.mode == 'with-media',
untilId: (this.$refs.timeline as any).tail().id
- }).then(notes => {
+ });
+
+ promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
@@ -89,7 +99,10 @@ export default Vue.extend({
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
+
+ return promise;
},
+
warp(date) {
this.date = date;
this.fetch();
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 898b6b2179..70fa0123af 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -1,105 +1,91 @@
<template>
<div class="mk-welcome">
- <main>
- <div class="top">
- <div>
- <div>
- <h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1>
- <p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
- <p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
- <div class="users">
- <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
- </div>
+ <img ref="pointer" class="pointer" src="/assets/pointer.png" alt="">
+ <button @click="dark">
+ <template v-if="$store.state.device.darkmode">%fa:moon%</template>
+ <template v-else>%fa:R moon%</template>
+ </button>
+ <div class="body" :style="{ backgroundImage: `url('${ welcomeBgUrl }')` }">
+ <div class="container">
+ <main>
+ <div class="about">
+ <h1 v-if="name">{{ name }}</h1>
+ <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey"></h1>
+ <p class="powerd-by" v-if="name">powerd by <b>Misskey</b></p>
+ <p class="desc" v-html="description || '%i18n:common.about%'"></p>
+ <a ref="signup" @click="signup">%i18n:@signup%</a>
</div>
- <div>
- <div>
- <header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header>
- <mk-welcome-timeline/>
- </div>
+ <div class="login">
+ <mk-signin/>
</div>
+ </main>
+ <div class="info">
+ <span>%i18n:common.misskey% <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>
+ <mk-nav class="nav"/>
</div>
- </main>
- <mk-forkit/>
- <footer>
- <div>
- <mk-nav :class="$style.nav"/>
- <p class="c">{{ copyright }}</p>
- </div>
- </footer>
+ <mk-forkit class="forkit"/>
+ <img src="assets/title.dark.svg" alt="Misskey">
+ </div>
+ <div class="tl">
+ <mk-welcome-timeline/>
+ </div>
<modal name="signup" width="500px" height="auto" scrollable>
- <header :class="$style.signupFormHeader">新規登録</header>
+ <header :class="$style.signupFormHeader">%i18n:@signup%</header>
<mk-signup :class="$style.signupForm"/>
</modal>
- <modal name="signin" width="500px" height="auto" scrollable>
- <header :class="$style.signinFormHeader">ログイン</header>
- <mk-signin :class="$style.signinForm"/>
- </modal>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
-import { docsUrl, copyright, lang } from '../../../config';
-
-const shares = [
- 'Everything!',
- 'Webpages',
- 'Photos',
- 'Interests',
- 'Favorites'
-];
+import { host, name, description, copyright, welcomeBgUrl } from '../../../config';
export default Vue.extend({
data() {
return {
- aboutUrl: `${docsUrl}/${lang}/about`,
+ stats: null,
copyright,
- users: [],
- clock: null,
- i: 0
+ welcomeBgUrl,
+ host,
+ name,
+ description,
+ pointerInterval: null
};
},
- mounted() {
- (this as any).api('users', {
- sort: '+follower',
- limit: 20
- }).then(users => {
- this.users = users;
+ created() {
+ (this as any).api('stats').then(stats => {
+ this.stats = stats;
});
-
- this.clock = setInterval(() => {
- if (++this.i == shares.length) this.i = 0;
- const speed = 70;
- const text = (this.$refs.share as any).innerText;
- for (let i = 0; i < text.length; i++) {
- setTimeout(() => {
- if (this.$refs.share) {
- (this.$refs.share as any).innerText = text.substr(0, text.length - i);
- }
- }, i * speed)
- }
- setTimeout(() => {
- const newText = shares[this.i];
- for (let i = 0; i <= newText.length; i++) {
- setTimeout(() => {
- if (this.$refs.share) {
- (this.$refs.share as any).innerText = newText.substr(0, i);
- }
- }, i * speed)
- }
- }, text.length * speed);
- }, 4000);
+ },
+ mounted() {
+ this.point();
+ this.pointerInterval = setInterval(this.point, 100);
},
beforeDestroy() {
- clearInterval(this.clock);
+ 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';
+ },
signup() {
this.$modal.show('signup');
},
signin() {
this.$modal.show('signin');
+ },
+ dark() {
+ this.$store.commit('device/set', {
+ key: 'darkmode',
+ value: !this.$store.state.device.darkmode
+ });
}
}
});
@@ -115,167 +101,145 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-@import url('https://fonts.googleapis.com/css?family=Sarpanch:700')
-
-.mk-welcome
+root(isDark)
display flex
- flex-direction column
- flex 1
- $width = 1000px
+ min-height 100vh
- background linear-gradient(to bottom, #1e1d65, #bd6659)
- //background-image url('/assets/welcome-bg.svg')
- background-size cover
- background-position top center
-
- &:before
- content ""
+ > .pointer
display block
+ 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
- bottom 0
+ z-index 1
+ top 0
left 0
- width 100%
- height 100%
- background-image url('/assets/welcome-fg.svg')
- background-size cover
- background-position bottom center
-
- > main
- display flex
- flex 1
+ padding 16px
+ font-size 18px
+ color #fff
- > .top
- display flex
- width 100%
+ display none // TODO
- > div
- display flex
- max-width $width + 64px
- margin 0 auto
- padding 80px 32px 0 32px
-
- > *
- margin-bottom 48px
-
- > div:first-child
- margin-right 48px
- color #fff
- text-shadow 0 0 12px #172062
-
- > h1
- margin 0
- font-weight bold
- //font-variant small-caps
- letter-spacing 12px
- font-family 'Sarpanch', sans-serif
- font-size 42px
- line-height 48px
-
- > .cursor
- animation cursor 1s infinite linear both
-
- @keyframes cursor
- 0%
- opacity 1
- 50%
- opacity 0
-
- > p
- margin 1em 0
- line-height 2em
+ > .body
+ flex 1
+ padding 64px 0 0 0
+ text-align center
+ background #578394
+ background-position center
+ background-size cover
- button
- padding 8px 16px
- font-size inherit
+ &:before
+ content ''
+ display block
+ position absolute
+ top 0
+ left 0
+ right 0
+ bottom 0
+ background rgba(#000, 0.5)
- .signup
- color $theme-color
- border solid 2px $theme-color
- border-radius 4px
+ > .forkit
+ position absolute
+ top 0
+ right 0
- &:focus
- box-shadow 0 0 0 3px rgba($theme-color, 0.2)
+ > img
+ position absolute
+ bottom 16px
+ right 16px
+ width 150px
- &:hover
- color $theme-color-foreground
- background $theme-color
+ > .container
+ $aboutWidth = 380px
+ $loginWidth = 340px
+ $width = $aboutWidth + $loginWidth
- &:active
- color $theme-color-foreground
- background darken($theme-color, 10%)
- border-color darken($theme-color, 10%)
+ > main
+ display flex
+ margin auto
+ width $width
+ border-radius 8px
+ overflow hidden
+ box-shadow 0 2px 8px rgba(#000, 0.3)
- .signin
- &:hover
- color #fff
+ > .about
+ width $aboutWidth
+ color #444
+ background #fff
- > .users
- margin 16px 0 0 0
+ > h1
+ margin 0 0 16px 0
+ padding 32px 32px 0 32px
+ color #444
- > *
- display inline-block
- margin 4px
- width 38px
- height 38px
- border-radius 6px
+ > img
+ width 170px
+ vertical-align bottom
- > div:last-child
+ > .powerd-by
+ margin 16px
+ opacity 0.7
- > div
- width 410px
- background #fff
- border-radius 8px
- box-shadow 0 0 0 12px rgba(#000, 0.1)
- overflow hidden
+ > .desc
+ margin 0
+ padding 0 32px 16px 32px
- > header
- z-index 1
- padding 12px 16px
- color #888d94
- box-shadow 0 1px 0px rgba(#000, 0.1)
+ > a
+ display inline-block
+ margin 0 0 32px 0
+ font-weight bold
- > div
- position absolute
- top 0
- right 0
- padding inherit
+ > .login
+ width $loginWidth
+ padding 16px 32px 32px 32px
+ background #f5f5f5
- > span
- display inline-block
- height 11px
- width 11px
- margin-left 6px
- background #ccc
- border-radius 100%
- vertical-align middle
+ > .info
+ margin 16px auto
+ padding 12px
+ width $width
+ font-size 14px
+ color #fff
+ background rgba(#000, 0.2)
+ border-radius 8px
- &:nth-child(1)
- background #5BCC8B
+ > .stats
+ margin-left 16px
+ padding-left 16px
+ border-left solid 1px #fff
- &:nth-child(2)
- background #E6BB46
+ > *
+ margin-right 16px
- &:nth-child(3)
- background #DF7065
+ > .nav
+ display block
+ margin 16px 0
+ font-size 14px
+ color #fff
- > .mk-welcome-timeline
- max-height 350px
- overflow auto
+ > .tl
+ margin 0
+ width 410px
+ height 100vh
+ text-align left
+ background isDark ? #313543 : #fff
- > footer
- font-size 12px
- color #949ea5
+ > *
+ max-height 100%
+ overflow auto
- > div
- max-width $width
- margin 0 auto
- padding 0 0 42px 0
- text-align center
+.mk-welcome[data-darkmode]
+ root(true)
- > .c
- margin 16px 0 0 0
- font-size 10px
- opacity 0.7
+.mk-welcome:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue
index 1be87f590c..73c6d0ef64 100644
--- a/src/client/app/desktop/views/widgets/activity.vue
+++ b/src/client/app/desktop/views/widgets/activity.vue
@@ -2,7 +2,7 @@
<mk-activity
:design="props.design"
:init-view="props.view"
- :user="os.i"
+ :user="$store.state.i"
@view-changed="viewChanged"/>
</template>
diff --git a/src/client/app/desktop/views/widgets/channel.channel.form.vue b/src/client/app/desktop/views/widgets/channel.channel.form.vue
deleted file mode 100644
index f2744268bb..0000000000
--- a/src/client/app/desktop/views/widgets/channel.channel.form.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<template>
-<div class="form">
- <input v-model="text" :disabled="wait" @keydown="onKeydown" placeholder="書いて">
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
- data() {
- return {
- text: '',
- wait: false
- };
- },
- methods: {
- onKeydown(e) {
- if (e.which == 10 || e.which == 13) this.post();
- },
- post() {
- this.wait = true;
-
- let reply = null;
-
- if (/^>>([0-9]+) /.test(this.text)) {
- const index = this.text.match(/^>>([0-9]+) /)[1];
- reply = (this.$parent as any).notes.find(p => p.index.toString() == index);
- this.text = this.text.replace(/^>>([0-9]+) /, '');
- }
-
- (this as any).api('notes/create', {
- text: this.text,
- replyId: reply ? reply.id : undefined,
- channelId: (this.$parent as any).channel.id
- }).then(data => {
- this.text = '';
- }).catch(err => {
- alert('失敗した');
- }).then(() => {
- this.wait = false;
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.form
- width 100%
- height 38px
- padding 4px
- border-top solid 1px #ddd
-
- > input
- padding 0 8px
- width 100%
- height 100%
- font-size 14px
- color #55595c
- border solid 1px #dadada
- border-radius 4px
-
- &:hover
- &:focus
- border-color #aeaeae
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/channel.channel.note.vue b/src/client/app/desktop/views/widgets/channel.channel.note.vue
deleted file mode 100644
index 7767919066..0000000000
--- a/src/client/app/desktop/views/widgets/channel.channel.note.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div class="note">
- <header>
- <a class="index" @click="reply">{{ note.index }}:</a>
- <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link>
- <span>ID:<i>{{ note.user | acct }}</i></span>
- </header>
- <div>
- <a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
- {{ note.text }}
- <div class="media" v-if="note.media">
- <a v-for="file in note.media" :href="file.url" target="_blank">
- <img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/>
- </a>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- props: ['note'],
- methods: {
- reply() {
- this.$emit('reply', this.note);
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.note
- margin 0
- padding 0
- color #444
-
- > header
- position -webkit-sticky
- position sticky
- z-index 1
- top 0
- padding 8px 4px 4px 16px
- background rgba(255, 255, 255, 0.9)
-
- > .index
- margin-right 0.25em
-
- > .name
- margin-right 0.5em
- color #008000
-
- > div
- padding 0 16px 16px 16px
-
- > .media
- > a
- display inline-block
-
- > img
- max-width 100%
- vertical-align bottom
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/channel.channel.vue b/src/client/app/desktop/views/widgets/channel.channel.vue
deleted file mode 100644
index ea4d8f8454..0000000000
--- a/src/client/app/desktop/views/widgets/channel.channel.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-<div class="channel">
- <p v-if="fetching">読み込み中<mk-ellipsis/></p>
- <div v-if="!fetching" ref="notes" class="notes">
- <p v-if="notes.length == 0">まだ投稿がありません</p>
- <x-note class="note" v-for="note in notes.slice().reverse()" :note="note" :key="note.id" @reply="reply"/>
- </div>
- <x-form class="form" ref="form"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import ChannelStream from '../../../common/scripts/streaming/channel';
-import XForm from './channel.channel.form.vue';
-import XNote from './channel.channel.note.vue';
-
-export default Vue.extend({
- components: {
- XForm,
- XNote
- },
- props: ['channel'],
- data() {
- return {
- fetching: true,
- notes: [],
- connection: null
- };
- },
- watch: {
- channel() {
- this.zap();
- }
- },
- mounted() {
- this.zap();
- },
- beforeDestroy() {
- this.disconnect();
- },
- methods: {
- zap() {
- this.fetching = true;
-
- (this as any).api('channels/notes', {
- channelId: this.channel.id
- }).then(notes => {
- this.notes = notes;
- this.fetching = false;
-
- this.$nextTick(() => {
- this.scrollToBottom();
- });
-
- this.disconnect();
- this.connection = new ChannelStream((this as any).os, this.channel.id);
- this.connection.on('note', this.onNote);
- });
- },
- disconnect() {
- if (this.connection) {
- this.connection.off('note', this.onNote);
- this.connection.close();
- }
- },
- onNote(note) {
- this.notes.unshift(note);
- this.scrollToBottom();
- },
- scrollToBottom() {
- (this.$refs.notes as any).scrollTop = (this.$refs.notes as any).scrollHeight;
- },
- reply(note) {
- (this.$refs.form as any).text = `>>${ note.index } `;
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.channel
-
- > p
- margin 0
- padding 16px
- text-align center
- color #aaa
-
- > .notes
- height calc(100% - 38px)
- overflow auto
- font-size 0.9em
-
- > .note
- border-bottom solid 1px #eee
-
- &:last-child
- border-bottom none
-
- > .form
- position absolute
- left 0
- bottom 0
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue
deleted file mode 100644
index d21aed40fd..0000000000
--- a/src/client/app/desktop/views/widgets/channel.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<template>
-<div class="mkw-channel">
- <template v-if="!props.compact">
- <p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:!@title%' }}</p>
- <button @click="settings" title="%i18n:@settings%">%fa:cog%</button>
- </template>
- <p class="get-started" v-if="props.channel == null">%i18n:@get-started%</p>
- <x-channel class="channel" :channel="channel" v-if="channel != null"/>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import XChannel from './channel.channel.vue';
-
-export default define({
- name: 'server',
- props: () => ({
- channel: null,
- compact: false
- })
-}).extend({
- components: {
- XChannel
- },
- data() {
- return {
- fetching: true,
- channel: null
- };
- },
- mounted() {
- if (this.props.channel) {
- this.zap();
- }
- },
- methods: {
- func() {
- this.props.compact = !this.props.compact;
- this.save();
- },
- settings() {
- const id = window.prompt('チャンネルID');
- if (!id) return;
- this.props.channel = id;
- this.zap();
- },
- zap() {
- this.fetching = true;
-
- (this as any).api('channels/show', {
- channelId: this.props.channel
- }).then(channel => {
- this.channel = channel;
- this.fetching = false;
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-channel
- background #fff
- border solid 1px rgba(#000, 0.075)
- border-radius 6px
- overflow hidden
-
- > .title
- z-index 2
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- box-shadow 0 1px rgba(#000, 0.07)
-
- > [data-fa]
- margin-right 4px
-
- > button
- position absolute
- z-index 2
- top 0
- right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color #ccc
-
- &:hover
- color #aaa
-
- &:active
- color #999
-
- > .get-started
- margin 0
- padding 16px
- text-align center
- color #aaa
-
- > .channel
- height 200px
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/index.ts b/src/client/app/desktop/views/widgets/index.ts
index 77d771d6b3..7c074080c1 100644
--- a/src/client/app/desktop/views/widgets/index.ts
+++ b/src/client/app/desktop/views/widgets/index.ts
@@ -8,7 +8,6 @@ import wUsers from './users.vue';
import wPolls from './polls.vue';
import wPostForm from './post-form.vue';
import wMessaging from './messaging.vue';
-import wChannel from './channel.vue';
import wProfile from './profile.vue';
Vue.component('mkw-notifications', wNotifications);
@@ -19,5 +18,4 @@ Vue.component('mkw-users', wUsers);
Vue.component('mkw-polls', wPolls);
Vue.component('mkw-post-form', wPostForm);
Vue.component('mkw-messaging', wMessaging);
-Vue.component('mkw-channel', wChannel);
Vue.component('mkw-profile', wProfile);
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 36fcc20636..7421a81102 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="_darkmode_">
+ <div class="mkw-polls--body" :data-darkmode="$store.state.device.darkmode">
<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>
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index 69b21ad37a..3c4ade0e81 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -3,7 +3,7 @@
<template v-if="props.design == 0">
<p class="title">%fa:pencil-alt%%i18n:@title%</p>
</template>
- <textarea :disabled="posting" v-model="text" @keydown="onKeydown" placeholder="%i18n:@placeholder%"></textarea>
+ <textarea :disabled="posting" v-model="text" @keydown="onKeydown" :placeholder="placeholder"></textarea>
<button @click="post" :disabled="posting">%i18n:@note%</button>
</div>
</template>
@@ -22,6 +22,19 @@ export default define({
text: ''
};
},
+ computed: {
+ placeholder(): string {
+ const xs = [
+ '%i18n:common.note-placeholders.a%',
+ '%i18n:common.note-placeholders.b%',
+ '%i18n:common.note-placeholders.c%',
+ '%i18n:common.note-placeholders.d%',
+ '%i18n:common.note-placeholders.e%',
+ '%i18n:common.note-placeholders.f%'
+ ];
+ return xs[Math.floor(Math.random() * xs.length)];
+ }
+ },
methods: {
func() {
if (this.props.design == 1) {
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 3b01ed034d..7b0fea3729 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -4,16 +4,16 @@
:data-melt="props.design == 2"
>
<div class="banner"
- :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
- title="クリックでバナー編集"
+ :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl}?thumbnail&size=256)` : ''"
+ title="%i18n:@update-banner%"
@click="os.apis.updateBanner"
></div>
- <mk-avatar class="avatar" :user="os.i"
+ <mk-avatar class="avatar" :user="$store.state.i"
@click="os.apis.updateAvatar"
- title="クリックでアバター編集"
+ title="%i18n:@update-avatar%"
/>
- <router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
- <p class="username">@{{ os.i | acct }}</p>
+ <router-link class="name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
+ <p class="username">@{{ $store.state.i | acct }}</p>
</div>
</template>
diff --git a/src/client/app/init.css b/src/client/app/init.css
index fa59195f71..6ee25d64e2 100644
--- a/src/client/app/init.css
+++ b/src/client/app/init.css
@@ -32,42 +32,30 @@ body > noscript {
left: 0;
width: 100%;
height: 100%;
- text-align: center;
background: #fff;
cursor: wait;
}
- #ini > p {
- display: block;
- user-select: none;
- margin: 32px;
- font-size: 4em;
- color: #555;
+ #ini > svg {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ width: 64px;
+ height: 64px;
+ animation: ini 0.6s infinite linear;
}
- #ini > p > span {
- animation: ini 1.4s infinite ease-in-out both;
- }
- #ini > p > span:nth-child(1) {
- animation-delay: 0s;
- }
- #ini > p > span:nth-child(2) {
- animation-delay: 0.16s;
- }
- #ini > p > span:nth-child(3) {
- animation-delay: 0.32s;
- }
html[data-darkmode] #ini {
background: #191b22;
}
- html[data-darkmode] #ini > p {
- color: #fff;
- }
@keyframes ini {
- 0%, 80%, 100% {
- opacity: 1;
+ from {
+ transform: rotate(0deg);
}
- 40% {
- opacity: 0;
+ to {
+ transform: rotate(360deg);
}
}
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 4908b73b23..043f26d0bc 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -49,48 +49,6 @@ Vue.mixin({
}
});
-// Dark/Light
-const bus = new Vue();
-Vue.mixin({
- data() {
- return {
- _darkmode_: localStorage.getItem('darkmode') == 'true'
- };
- },
- beforeCreate() {
- // なぜか警告が出るので
- this._darkmode_ = localStorage.getItem('darkmode') == 'true';
- },
- beforeDestroy() {
- bus.$off('updated', this._onDarkmodeUpdated_);
- },
- mounted() {
- this._onDarkmodeUpdated_(this._darkmode_);
- bus.$on('updated', this._onDarkmodeUpdated_);
- },
- methods: {
- _updateDarkmode_(v) {
- localStorage.setItem('darkmode', v.toString());
- if (v) {
- document.documentElement.setAttribute('data-darkmode', 'true');
- } else {
- document.documentElement.removeAttribute('data-darkmode');
- }
- bus.$emit('updated', v);
- },
- _onDarkmodeUpdated_(v) {
- if (!this.$el || !this.$el.setAttribute) return;
- if (v) {
- this.$el.setAttribute('data-darkmode', 'true');
- } else {
- this.$el.removeAttribute('data-darkmode');
- }
- this._darkmode_ = v;
- this.$forceUpdate();
- }
- }
-});
-
/**
* APP ENTRY POINT!
*/
@@ -109,14 +67,6 @@ const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
-//#region Set description meta tag
-const head = document.getElementsByTagName('head')[0];
-const meta = document.createElement('meta');
-meta.setAttribute('name', 'description');
-meta.setAttribute('content', '%i18n:!common.misskey%');
-head.appendChild(meta);
-//#endregion
-
// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする
try {
localStorage.setItem('kyoppie', 'yuppie');
@@ -141,13 +91,51 @@ 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_();
+ }
+ });
+
+ os.store.watch(s => {
+ return s.device.darkmode;
+ }, v => {
+ if (v) {
+ document.documentElement.setAttribute('data-darkmode', 'true');
+ } else {
+ document.documentElement.removeAttribute('data-darkmode');
+ }
+ });
+ //#endregion
+
Vue.mixin({
data() {
return {
os,
api: os.api,
- apis: os.apis,
- clientSettings: os.store.state.settings.data
+ apis: os.apis
};
}
});
@@ -173,7 +161,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
}
//#region 更新チェック
- const preventUpdate = localStorage.getItem('preventUpdate') == 'true';
+ const preventUpdate = os.store.state.device.preventUpdate;
if (!preventUpdate) {
setTimeout(() => {
checkForUpdate(os);
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 2373b0d8d2..9a8d19adbd 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -1,17 +1,17 @@
import Vue from 'vue';
import { EventEmitter } from 'eventemitter3';
-import * as merge from 'object-assign-deep';
import * as uuid from 'uuid';
import initStore from './store';
-import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from './config';
+import { apiUrl, swPublickey, version, lang, googleMapsApiKey } 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 { ServerStreamManager } from './common/scripts/streaming/server';
+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 { OthelloStreamManager } from './common/scripts/streaming/othello';
+import { ReversiStreamManager } from './common/scripts/streaming/reversi';
import Err from './common/views/components/connect-failed.vue';
import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline';
@@ -74,38 +74,19 @@ export default class MiOS extends EventEmitter {
public app: Vue;
public new(vm, props) {
- const w = new vm({
+ const x = new vm({
parent: this.app,
propsData: props
}).$mount();
- document.body.appendChild(w.$el);
- return w;
- }
-
- /**
- * A signing user
- */
- public i: { [x: string]: any };
-
- /**
- * Whether signed in
- */
- public get isSignedIn() {
- return this.i != null;
+ document.body.appendChild(x.$el);
+ return x;
}
/**
* Whether is debug mode
*/
public get debug() {
- return localStorage.getItem('debug') == 'true';
- }
-
- /**
- * Whether enable sounds
- */
- public get isEnableSounds() {
- return localStorage.getItem('enableSounds') == 'true';
+ return this.store ? this.store.state.device.debug : false;
}
public store: ReturnType<typeof initStore>;
@@ -124,16 +105,18 @@ export default class MiOS extends EventEmitter {
localTimelineStream: LocalTimelineStreamManager;
globalTimelineStream: GlobalTimelineStreamManager;
driveStream: DriveStreamManager;
- serverStream: ServerStreamManager;
+ serverStatsStream: ServerStatsStreamManager;
+ notesStatsStream: NotesStatsStreamManager;
messagingIndexStream: MessagingIndexStreamManager;
- othelloStream: OthelloStreamManager;
+ reversiStream: ReversiStreamManager;
} = {
localTimelineStream: null,
globalTimelineStream: null,
driveStream: null,
- serverStream: null,
+ serverStatsStream: null,
+ notesStatsStream: null,
messagingIndexStream: null,
- othelloStream: null
+ reversiStream: null
};
/**
@@ -225,15 +208,8 @@ export default class MiOS extends EventEmitter {
console.error.apply(null, args);
}
- public bakeMe() {
- // ローカルストレージにキャッシュ
- localStorage.setItem('me', JSON.stringify(this.i));
- }
-
public signout() {
- localStorage.removeItem('me');
- localStorage.removeItem('settings');
- document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ this.store.dispatch('logout');
location.href = '/';
}
@@ -245,18 +221,19 @@ export default class MiOS extends EventEmitter {
this.store = initStore(this);
//#region Init stream managers
- this.streams.serverStream = new ServerStreamManager(this);
+ 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.i);
+ this.stream = new HomeStreamManager(this, this.store.state.i);
// Init other stream manager
- this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.i);
- this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.i);
- this.streams.driveStream = new DriveStreamManager(this, this.i);
- this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.i);
- this.streams.othelloStream = new OthelloStreamManager(this, this.i);
+ this.streams.localTimelineStream = new LocalTimelineStreamManager(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
@@ -307,51 +284,29 @@ export default class MiOS extends EventEmitter {
};
// フェッチが完了したとき
- const fetched = me => {
- this.i = me;
-
- // ローカルストレージにキャッシュ
- this.bakeMe();
-
+ const fetched = () => {
this.emit('signedin');
// Finish init
callback();
- //#region Note
-
// Init service worker
if (this.shouldRegisterSw) this.registerSw();
-
- //#endregion
};
- // Get cached account data
- const cachedMe = JSON.parse(localStorage.getItem('me'));
-
- //#region キャッシュされた設定を復元
- const cachedSettings = JSON.parse(localStorage.getItem('settings'));
-
- if (cachedSettings) {
- this.store.dispatch('settings/merge', cachedSettings);
- }
- //#endregion
-
// キャッシュがあったとき
- if (cachedMe) {
- if (cachedMe.token == null) {
+ if (this.store.state.i != null) {
+ if (this.store.state.i.token == null) {
this.signout();
return;
}
// とりあえずキャッシュされたデータでお茶を濁して(?)おいて、
- fetched(cachedMe);
+ fetched();
// 後から新鮮なデータをフェッチ
- fetchme(cachedMe.token, freshData => {
- merge(cachedMe, freshData);
-
- this.store.dispatch('settings/merge', freshData.clientSettings);
+ fetchme(this.store.state.i.token, freshData => {
+ this.store.dispatch('mergeMe', freshData);
});
} else {
// Get token from cookie
@@ -359,9 +314,8 @@ export default class MiOS extends EventEmitter {
fetchme(i, me => {
if (me) {
- this.store.dispatch('settings/merge', me.clientSettings);
-
- fetched(me);
+ this.store.dispatch('login', me);
+ fetched();
} else {
// Finish init
callback();
@@ -382,7 +336,7 @@ export default class MiOS extends EventEmitter {
if (!isSwSupported) return;
// Reject when not signed in to Misskey
- if (!this.isSignedIn) return;
+ if (!this.store.getters.isSignedIn) return;
// When service worker activated
navigator.serviceWorker.ready.then(registration => {
@@ -435,12 +389,8 @@ export default class MiOS extends EventEmitter {
});
});
- // Whether use raw version script
- const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug)
- || process.env.NODE_ENV != 'production';
-
// The path of service worker script
- const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`;
+ const sw = `/sw.${version}.${lang}.js`;
// Register service worker
navigator.serviceWorker.register(sw).then(registration => {
@@ -471,8 +421,7 @@ export default class MiOS extends EventEmitter {
};
const promise = new Promise((resolve, reject) => {
- const viaStream = this.stream && this.stream.hasConnection &&
- (localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true);
+ const viaStream = this.stream && this.stream.hasConnection && this.store.state.device.apiViaStream;
if (viaStream) {
const stream = this.stream.borrow();
@@ -496,7 +445,7 @@ export default class MiOS extends EventEmitter {
});
} else {
// Append a credential
- if (this.isSignedIn) (data as any).i = this.i.token;
+ if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token;
const req = {
id: uuid(),
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 72919c6505..15b2f6b691 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,43 +1,24 @@
import PostForm from '../views/components/post-form.vue';
-//import RenoteForm from '../views/components/renote-form.vue';
-import getNoteSummary from '../../../../renderers/get-note-summary';
export default (os) => (opts) => {
const o = opts || {};
- if (o.renote) {
- /*const vm = new RenoteForm({
- propsData: {
- renote: o.renote
- }
- }).$mount();
- vm.$once('cancel', recover);
- vm.$once('note', recover);
- document.body.appendChild(vm.$el);*/
+ const app = document.getElementById('app');
+ app.style.display = 'none';
- const text = window.prompt(`「${getNoteSummary(o.renote)}」をRenote`);
- if (text == null) return;
- os.api('notes/create', {
- renoteId: o.renote.id,
- text: text == '' ? undefined : text
- });
- } else {
- const app = document.getElementById('app');
- app.style.display = 'none';
+ function recover() {
+ app.style.display = 'block';
+ }
- function recover() {
- app.style.display = 'block';
+ const vm = new PostForm({
+ parent: os.app,
+ propsData: {
+ reply: o.reply,
+ renote: o.renote
}
-
- const vm = new PostForm({
- parent: os.app,
- propsData: {
- reply: o.reply
- }
- }).$mount();
- vm.$once('cancel', recover);
- vm.$once('note', recover);
- document.body.appendChild(vm.$el);
- (vm as any).focus();
- }
+ }).$mount();
+ vm.$once('cancel', recover);
+ vm.$once('posted', recover);
+ document.body.appendChild(vm.$el);
+ (vm as any).focus();
};
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 2e9805e0d0..cc0a8331ba 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -23,15 +23,21 @@ import MkUser from './views/pages/user.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
import MkNotifications from './views/pages/notifications.vue';
+import MkWidgets from './views/pages/widgets.vue';
import MkMessaging from './views/pages/messaging.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
+import MkReceivedFollowRequests from './views/pages/received-follow-requests.vue';
import MkNote from './views/pages/note.vue';
import MkSearch from './views/pages/search.vue';
import MkFollowers from './views/pages/followers.vue';
import MkFollowing from './views/pages/following.vue';
+import MkFavorites from './views/pages/favorites.vue';
+import MkUserLists from './views/pages/user-lists.vue';
+import MkUserList from './views/pages/user-list.vue';
import MkSettings from './views/pages/settings.vue';
-import MkProfileSetting from './views/pages/profile-setting.vue';
-import MkOthello from './views/pages/othello.vue';
+import MkReversi from './views/pages/reversi.vue';
+import MkTag from './views/pages/tag.vue';
+import MkShare from './views/pages/share.vue';
/**
* init
@@ -53,9 +59,13 @@ init((launch) => {
routes: [
{ path: '/', name: 'index', component: MkIndex },
{ path: '/signup', name: 'signup', component: MkSignup },
- { path: '/i/settings', component: MkSettings },
- { path: '/i/settings/profile', component: MkProfileSetting },
+ { path: '/i/settings', name: 'settings', component: MkSettings },
{ path: '/i/notifications', name: 'notifications', component: MkNotifications },
+ { path: '/i/favorites', name: 'favorites', component: MkFavorites },
+ { path: '/i/lists', name: 'user-lists', component: MkUserLists },
+ { path: '/i/lists/:list', name: 'user-list', component: MkUserList },
+ { path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
+ { path: '/i/widgets', name: 'widgets', component: MkWidgets },
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', name: 'drive', component: MkDrive },
@@ -63,8 +73,10 @@ init((launch) => {
{ path: '/i/drive/file/:file', component: MkDrive },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
- { path: '/othello', name: 'othello', component: MkOthello },
- { path: '/othello/:game', component: MkOthello },
+ { path: '/tags/:tag', component: MkTag },
+ { path: '/share', component: MkShare },
+ { path: '/reversi', name: 'reversi', component: MkReversi },
+ { path: '/reversi/:game', component: MkReversi },
{ path: '/@:user', component: MkUser },
{ path: '/@:user/followers', component: MkFollowers },
{ path: '/@:user/following', component: MkFollowing },
diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl
index 847ae8eec5..df8f4a8fae 100644
--- a/src/client/app/mobile/style.styl
+++ b/src/client/app/mobile/style.styl
@@ -8,10 +8,10 @@
html
height 100%
- background #ececed
+ background #ececed !important
&[data-darkmode]
- background #191B22
+ background #191B22 !important
body
display flex
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 764822e98c..f6a22f95f0 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -34,15 +34,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>
+ <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>
</div>
</div>
<div class="exif" v-show="exif">
@@ -86,14 +81,14 @@ export default Vue.extend({
return this.file.type.split('/')[0];
},
style(): any {
- return this.file.properties.avgColor ? {
+ return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? {
'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
} : {};
}
},
methods: {
rename() {
- const name = window.prompt('名前を変更', this.file.name);
+ const name = window.prompt('%i18n:@rename%', this.file.name);
if (name == null || name == '' || name == this.file.name) return;
(this as any).api('drive/files/update', {
fileId: this.file.id,
@@ -112,6 +107,13 @@ export default Vue.extend({
});
});
},
+ del() {
+ (this as any).api('drive/files/delete', {
+ fileId: this.file.id
+ }).then(() => {
+ this.browser.cd(this.file.folderId, true);
+ });
+ },
showCreatedAt() {
alert(new Date(this.file.createdAt).toLocaleString());
},
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index 7d1957042b..94c8ae3535 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -42,7 +42,7 @@ export default Vue.extend({
},
thumbnail(): any {
return {
- 'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
+ 'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
'background-image': `url(${this.file.url}?thumbnail&size=128)`
};
}
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index ef3432a3ec..c313d225e4 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -32,7 +32,7 @@
<div class="files" v-if="files.length > 0">
<x-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%' }}
+ {{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:@load-more%' }}
</button>
</div>
<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
@@ -100,6 +100,7 @@ export default Vue.extend({
this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
+ this.connection.on('file_deleted', this.onStreamDriveFileDeleted);
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
@@ -118,6 +119,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);
@@ -136,6 +138,10 @@ export default Vue.extend({
}
},
+ onStreamDriveFileDeleted(fileId) {
+ this.removeFile(fileId);
+ },
+
onStreamDriveFolderCreated(folder) {
this.addFolder(folder, true);
},
@@ -372,7 +378,7 @@ export default Vue.extend({
},
openContextMenu() {
- const fn = window.prompt('何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>');
+ const fn = window.prompt('%i18n:@prompt%');
if (fn == null || fn == '') return;
switch (fn) {
case '1':
@@ -391,7 +397,7 @@ export default Vue.extend({
this.moveFolder();
break;
case '6':
- alert('ごめんなさい!フォルダの削除は未実装です...。');
+ alert('%i18n:@deletion-alert%');
break;
}
},
@@ -401,7 +407,7 @@ export default Vue.extend({
},
createFolder() {
- const name = window.prompt('フォルダー名');
+ const name = window.prompt('%i18n:@folder-name%');
if (name == null || name == '') return;
(this as any).api('drive/folders/create', {
name: name,
@@ -413,10 +419,10 @@ export default Vue.extend({
renameFolder() {
if (this.folder == null) {
- alert('現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。');
+ alert('%i18n:@root-rename-alert%');
return;
}
- const name = window.prompt('フォルダー名', this.folder.name);
+ const name = window.prompt('%i18n:@folder-name%', this.folder.name);
if (name == null || name == '') return;
(this as any).api('drive/folders/update', {
name: name,
@@ -428,7 +434,7 @@ export default Vue.extend({
moveFolder() {
if (this.folder == null) {
- alert('現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。');
+ alert('%i18n:@root-move-alert%');
return;
}
(this as any).apis.chooseDriveFolder().then(folder => {
@@ -442,13 +448,13 @@ export default Vue.extend({
},
urlUpload() {
- const url = window.prompt('アップロードしたいファイルのURL');
+ const url = window.prompt('%i18n:@url-prompt%');
if (url == null || url == '') return;
(this as any).api('drive/files/upload_from_url', {
url: url,
folderId: this.folder ? this.folder.id : undefined
});
- alert('アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。');
+ alert('%i18n:@uploading%');
},
onChangeLocalFile() {
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index 5d6b8ebf84..b6a52fe1ed 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -1,13 +1,16 @@
<template>
<button class="mk-follow-button"
- :class="{ wait: wait, follow: !user.isFollowing, unfollow: user.isFollowing }"
+ :class="{ wait: wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou }"
@click="onClick"
:disabled="wait"
>
- <template v-if="!wait && user.isFollowing">%fa:minus%</template>
- <template v-if="!wait && !user.isFollowing">%fa:plus%</template>
- <template v-if="wait">%fa:spinner .pulse .fw%</template>
- {{ user.isFollowing ? '%i18n:!@unfollow%' : '%i18n:!@follow%' }}
+ <template v-if="!wait">
+ <template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</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>
+ </template>
+ <template v-else>%fa:spinner .pulse .fw%</template>
</button>
</template>
@@ -22,6 +25,7 @@ export default Vue.extend({
},
data() {
return {
+ u: this.user,
wait: false,
connection: null,
connectionId: null
@@ -42,39 +46,44 @@ export default Vue.extend({
methods: {
onFollow(user) {
- if (user.id == this.user.id) {
- this.user.isFollowing = user.isFollowing;
+ if (user.id == this.u.id) {
+ this.u.isFollowing = user.isFollowing;
}
},
onUnfollow(user) {
- if (user.id == this.user.id) {
- this.user.isFollowing = user.isFollowing;
+ if (user.id == this.u.id) {
+ this.u.isFollowing = user.isFollowing;
}
},
- onClick() {
+ async onClick() {
this.wait = true;
- if (this.user.isFollowing) {
- (this as any).api('following/delete', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = false;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
- } else {
- (this as any).api('following/create', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = true;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
+
+ try {
+ if (this.u.isFollowing) {
+ this.u = await (this as any).api('following/delete', {
+ userId: this.u.id
+ });
+ } else {
+ if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ this.u = await (this as any).api('following/requests/cancel', {
+ userId: this.u.id
+ });
+ } else if (this.u.isLocked) {
+ this.u = await (this as any).api('following/create', {
+ userId: this.u.id
+ });
+ } else {
+ this.u = await (this as any).api('following/create', {
+ userId: this.user.id
+ });
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.wait = false;
}
}
}
@@ -90,34 +99,39 @@ export default Vue.extend({
cursor pointer
padding 0 16px
margin 0
- height inherit
- font-size 16px
+ min-width 150px
+ line-height 36px
+ font-size 14px
+ font-weight bold
+ color $theme-color
+ background transparent
outline none
border solid 1px $theme-color
- border-radius 4px
+ border-radius 36px
- *
- pointer-events none
+ &:hover
+ background rgba($theme-color, 0.1)
- &.follow
- color $theme-color
- background transparent
+ &:active
+ background rgba($theme-color, 0.2)
+
+ &.active
+ color $theme-color-foreground
+ background $theme-color
&:hover
- background rgba($theme-color, 0.1)
+ background lighten($theme-color, 10%)
+ border-color lighten($theme-color, 10%)
&:active
- background rgba($theme-color, 0.2)
-
- &.unfollow
- color $theme-color-foreground
- background $theme-color
+ background darken($theme-color, 10%)
+ border-color darken($theme-color, 10%)
&.wait
cursor wait !important
opacity 0.7
- > [data-fa]
- margin-right 4px
+ *
+ pointer-events none
</style>
diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
index ba4abe341f..e0461d2bc2 100644
--- a/src/client/app/mobile/views/components/friends-maker.vue
+++ b/src/client/app/mobile/views/components/friends-maker.vue
@@ -1,13 +1,13 @@
<template>
<div class="mk-friends-maker">
- <p class="title">気になるユーザーをフォロー:</p>
+ <p class="title">%i18n:@title%:</p>
<div class="users" v-if="!fetching && users.length > 0">
<mk-user-card v-for="user in users" :key="user.id" :user="user"/>
</div>
- <p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
- <a class="refresh" @click="refresh">もっと見る</a>
- <button class="close" @click="close" title="閉じる">%fa:times%</button>
+ <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="close" title="%i18n:@close%">%fa:times%</button>
</div>
</template>
diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
index 5ed8427b05..38c130ecbf 100644
--- a/src/client/app/mobile/views/components/index.ts
+++ b/src/client/app/mobile/views/components/index.ts
@@ -22,6 +22,7 @@ import userTimeline from './user-timeline.vue';
import userListTimeline from './user-list-timeline.vue';
import activity from './activity.vue';
import widgetContainer from './widget-container.vue';
+import postForm from './post-form.vue';
Vue.component('mk-ui', ui);
Vue.component('mk-note', note);
@@ -45,3 +46,4 @@ Vue.component('mk-user-timeline', userTimeline);
Vue.component('mk-user-list-timeline', userListTimeline);
Vue.component('mk-activity', activity);
Vue.component('mk-widget-container', widgetContainer);
+Vue.component('mk-post-form', postForm);
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index 92d1cdc6f5..c2f9c66e84 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -17,9 +17,17 @@ export default Vue.extend({
},
computed: {
style(): any {
+ let url = `url(${this.image.url}?thumbnail)`;
+
+ if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
+ url = null;
+ } else if (this.raw || this.$store.state.device.loadRawImages) {
+ url = `url(${this.image.url})`;
+ }
+
return {
- 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
- 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
+ 'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
+ 'background-image': url
};
}
}
diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue
deleted file mode 100644
index e515fda8a6..0000000000
--- a/src/client/app/mobile/views/components/note-detail.sub.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<template>
-<div class="root sub">
- <mk-avatar class="avatar" :user="note.user"/>
- <div class="main">
- <header>
- <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- <router-link class="time" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
- </header>
- <div class="body">
- <mk-sub-note-content class="text" :note="note"/>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- props: ['note']
-});
-</script>
-
-<style lang="stylus" scoped>
-root(isDark)
- padding 8px
- font-size 0.9em
- background isDark ? #21242d : #fdfdfd
-
- @media (min-width 500px)
- padding 12px
-
- @media (min-width 600px)
- padding 24px 32px
-
- &:after
- content ""
- display block
- clear both
-
- > .avatar
- display block
- float left
- margin 0 12px 0 0
- width 48px
- height 48px
- border-radius 8px
-
- > .main
- float left
- width calc(100% - 60px)
-
- > header
- display flex
- align-items baseline
- margin-bottom 4px
- white-space nowrap
-
- > .name
- display block
- margin 0 .5em 0 0
- padding 0
- overflow hidden
- color isDark ? #fff : #607073
- font-size 1em
- font-weight 700
- text-align left
- text-decoration none
- text-overflow ellipsis
-
- &:hover
- text-decoration underline
-
- > .username
- text-align left
- margin 0 .5em 0 0
- color isDark ? #606984 : #d1d8da
-
- > .time
- margin-left auto
- color isDark ? #606984 : #b2b8bb
-
- > .body
-
- > .text
- cursor default
- margin 0
- padding 0
- font-size 1.1em
- color isDark ? #959ba7 : #717171
-
-.root.sub[data-darkmode]
- root(true)
-
-.root.sub: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 5a7226faac..f3e77d7062 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -2,22 +2,28 @@
<div class="mk-note-detail">
<button
class="more"
- v-if="p.reply && p.reply.replyId && context.length == 0"
- @click="fetchContext"
- :disabled="fetchingContext"
+ v-if="p.reply && p.reply.replyId && conversation.length == 0"
+ @click="fetchConversation"
+ :disabled="conversationFetching"
>
- <template v-if="!contextFetching">%fa:ellipsis-v%</template>
- <template v-if="contextFetching">%fa:spinner .pulse%</template>
+ <template v-if="!conversationFetching">%fa:ellipsis-v%</template>
+ <template v-if="conversationFetching">%fa:spinner .pulse%</template>
</button>
- <div class="context">
- <x-sub v-for="note in context" :key="note.id" :note="note"/>
+ <div class="conversation">
+ <x-sub v-for="note in conversation" :key="note.id" :note="note"/>
</div>
<div class="reply-to" v-if="p.reply">
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
<p>
- <mk-avatar class="avatar" :user="note.user"/>%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
+ <mk-avatar class="avatar" :user="note.user"/>
+ %fa:retweet%
+ <router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+ <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+ <mk-time :time="note.createdAt"/>
</p>
</div>
<article>
@@ -25,23 +31,24 @@
<mk-avatar class="avatar" :user="p.user"/>
<div>
<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
- <span class="username">@{{ p.user | acct }}</span>
+ <span class="username"><mk-acct :user="p.user"/></span>
</div>
</header>
<div class="body">
<div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
+ <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
+ <mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
</div>
<div class="tags" v-if="p.tags && p.tags.length > 0">
- <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+ <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</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"/>
- <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+ <a class="location" v-if="p.geo" :href="`http://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"/>
@@ -80,7 +87,7 @@ import parse from '../../../../../text/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './note-detail.sub.vue';
+import XSub from './note.sub.vue';
export default Vue.extend({
components: {
@@ -99,8 +106,8 @@ export default Vue.extend({
data() {
return {
- context: [],
- contextFetching: false,
+ conversation: [],
+ conversationFetching: false,
replies: []
};
},
@@ -147,7 +154,7 @@ export default Vue.extend({
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
+ 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]);
@@ -165,15 +172,15 @@ export default Vue.extend({
},
methods: {
- fetchContext() {
- this.contextFetching = true;
+ fetchConversation() {
+ this.conversationFetching = true;
- // Fetch context
- (this as any).api('notes/context', {
+ // Fetch conversation
+ (this as any).api('notes/conversation', {
noteId: this.p.replyId
- }).then(context => {
- this.contextFetching = false;
- this.context = context.reverse();
+ }).then(conversation => {
+ this.conversationFetching = false;
+ this.conversation = conversation.reverse();
});
},
reply() {
@@ -209,8 +216,6 @@ export default Vue.extend({
root(isDark)
overflow hidden
- margin 0 auto
- padding 0
width 100%
text-align left
background isDark ? #282C37 : #fff
@@ -245,7 +250,7 @@ root(isDark)
&:disabled
color #ccc
- > .context
+ > .conversation
> *
border-bottom 1px solid isDark ? #1c2023 : #eef0f2
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index ec11f23315..5d56d2d326 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,14 +1,8 @@
<template>
-<div class="mk-note-preview">
- <mk-avatar class="avatar" :user="note.user"/>
+<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+ <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
- <header>
- <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- <router-link class="time" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
- </header>
+ <mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@@ -26,56 +20,48 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
+ display flex
margin 0
padding 0
- font-size 0.9em
+ font-size 10px
- &:after
- content ""
- display block
- clear both
+ @media (min-width 350px)
+ font-size 12px
+
+ @media (min-width 500px)
+ font-size 14px
+
+ &.smart
+ > .main
+ width 100%
+
+ > header
+ align-items center
> .avatar
+ flex-shrink 0
display block
- float left
- margin 0 12px 0 0
- width 48px
- height 48px
+ margin 0 10px 0 0
+ width 40px
+ height 40px
border-radius 8px
- > .main
- float left
- width calc(100% - 60px)
+ @media (min-width 350px)
+ margin 0 10px 0 0
+ width 44px
+ height 44px
- > header
- display flex
- align-items baseline
- margin-bottom 4px
- white-space nowrap
+ @media (min-width 500px)
+ margin 0 12px 0 0
+ width 48px
+ height 48px
- > .name
- display block
- margin 0 .5em 0 0
- padding 0
- overflow hidden
- color isDark ? #fff : #607073
- font-size 1em
- font-weight 700
- text-align left
- text-decoration none
- text-overflow ellipsis
-
- &:hover
- text-decoration underline
-
- > .username
- text-align left
- margin 0 .5em 0 0
- color isDark ? #606984 : #d1d8da
+ > .main
+ flex 1
+ min-width 0
- > .time
- margin-left auto
- color isDark ? #606984 : #b2b8bb
+ > .header
+ margin-bottom 2px
> .body
@@ -83,7 +69,6 @@ root(isDark)
cursor default
margin 0
padding 0
- font-size 1.1em
color isDark ? #959ba7 : #717171
.mk-note-preview[data-darkmode]
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 82025291da..a68aec40a1 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,23 +1,8 @@
<template>
-<div class="sub">
- <mk-avatar class="avatar" :user="note.user"/>
+<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+ <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
- <header>
- <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- <div class="info">
- <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
- <router-link class="created-at" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
- <span class="visibility" v-if="note.visibility != 'public'">
- <template v-if="note.visibility == 'home'">%fa:home%</template>
- <template v-if="note.visibility == 'followers'">%fa:unlock%</template>
- <template v-if="note.visibility == 'specified'">%fa:envelope%</template>
- <template v-if="note.visibility == 'private'">%fa:lock%</template>
- </span>
- </div>
- </header>
+ <mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
</div>
@@ -29,92 +14,73 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note']
+ props: {
+ note: {
+ type: Object,
+ required: true
+ },
+ // TODO
+ truncate: {
+ type: Boolean,
+ default: true
+ }
+ }
});
</script>
<style lang="stylus" scoped>
root(isDark)
+ display flex
padding 16px
- font-size 0.9em
+ font-size 10px
background isDark ? #21242d : #fcfcfc
+ @media (min-width 350px)
+ font-size 12px
+
+ @media (min-width 500px)
+ font-size 14px
+
@media (min-width 600px)
padding 24px 32px
- &:after
- content ""
- display block
- clear both
+ &.smart
+ > .main
+ width 100%
+
+ > header
+ align-items center
> .avatar
+ flex-shrink 0
display block
- float left
- margin 0 10px 0 0
- width 44px
- height 44px
+ margin 0 8px 0 0
+ width 38px
+ height 38px
border-radius 8px
+ @media (min-width 350px)
+ margin-right 10px
+ width 42px
+ height 42px
+
@media (min-width 500px)
- margin-right 16px
- width 52px
- height 52px
+ margin-right 14px
+ width 50px
+ height 50px
> .main
- float left
- width calc(100% - 54px)
-
- @media (min-width 500px)
- width calc(100% - 68px)
+ flex 1
+ min-width 0
- > header
- display flex
- align-items baseline
+ > .header
margin-bottom 2px
- white-space nowrap
-
- > .name
- display block
- margin 0 0.5em 0 0
- padding 0
- overflow hidden
- color isDark ? #fff : #607073
- font-size 1em
- font-weight 700
- text-align left
- text-decoration none
- text-overflow ellipsis
-
- &:hover
- text-decoration underline
-
- > .username
- text-align left
- margin 0
- color isDark ? #606984 : #d1d8da
-
- > .info
- margin-left auto
- font-size 0.9em
-
- > *
- color isDark ? #606984 : #b2b8bb
-
- > .mobile
- margin-right 6px
-
- > .visibility
- margin-left 6px
> .body
- max-height 128px
- overflow hidden
> .text
- cursor default
margin 0
padding 0
- font-size 1.1em
color isDark ? #959ba7 : #717171
pre
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index d66f5a1016..4498bb5633 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -1,47 +1,31 @@
<template>
-<div class="note" :class="{ renote: isRenote }">
- <div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
+<div class="note" :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }">
+ <div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/>
%fa:retweet%
- <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
- <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/>
</div>
<article>
- <mk-avatar class="avatar" :user="p.user"/>
+ <mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
- <header>
- <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
- <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
- <span class="username">@{{ p.user | acct }}</span>
- <div class="info">
- <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
- <router-link class="created-at" :to="p | notePage">
- <mk-time :time="p.createdAt"/>
- </router-link>
- <span class="visibility" v-if="p.visibility != 'public'">
- <template v-if="p.visibility == 'home'">%fa:home%</template>
- <template v-if="p.visibility == 'followers'">%fa:unlock%</template>
- <template v-if="p.visibility == 'specified'">%fa:envelope%</template>
- <template v-if="p.visibility == 'private'">%fa:lock%</template>
- </span>
- </div>
- </header>
+ <mk-note-header class="header" :note="p" :mini="true"/>
<div class="body">
- <p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
<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 ? '隠す' : 'もっと見る' }}</span>
+ <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
<a class="reply" v-if="p.reply">%fa:reply%</a>
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+ <mk-note-html v-if="p.text && !canHideText(p)" :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">
@@ -49,10 +33,10 @@
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
- <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+ <router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
- <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+ <a class="location" v-if="p.geo" :href="`http://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"/>
@@ -85,6 +69,7 @@
<script lang="ts">
import Vue from 'vue';
import parse from '../../../../../text/parse';
+import canHideText from '../../../common/scripts/can-hide-text';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
@@ -112,9 +97,11 @@ export default Vue.extend({
this.note.mediaIds.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)
@@ -122,6 +109,7 @@ export default Vue.extend({
.reduce((a, b) => a + b)
: 0;
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -135,7 +123,7 @@ export default Vue.extend({
},
created() {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
}
@@ -144,13 +132,13 @@ export default Vue.extend({
mounted() {
this.capture(true);
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.on('_connected_', this.onStreamConnected);
}
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
+ 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]);
@@ -170,15 +158,17 @@ export default Vue.extend({
beforeDestroy() {
this.decapture(true);
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.off('_connected_', this.onStreamConnected);
(this as any).os.stream.dispose(this.connectionId);
}
},
methods: {
+ canHideText,
+
capture(withHandler = false) {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.send({
type: 'capture',
id: this.p.id
@@ -186,8 +176,9 @@ export default Vue.extend({
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
}
},
+
decapture(withHandler = false) {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.send({
type: 'decapture',
id: this.p.id
@@ -195,9 +186,11 @@ export default Vue.extend({
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
}
},
+
onStreamConnected() {
this.capture();
},
+
onStreamNoteUpdated(data) {
const note = data.note;
if (note.id == this.note.id) {
@@ -206,16 +199,19 @@ export default Vue.extend({
this.note.renote = note;
}
},
+
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,
@@ -223,6 +219,7 @@ export default Vue.extend({
compact: true
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -250,11 +247,19 @@ root(isDark)
@media (min-width 500px)
font-size 16px
+ &.smart
+ > article
+ > .main
+ > header
+ align-items center
+ margin-bottom 4px
+
> .renote
display flex
align-items center
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%)
@@ -265,12 +270,17 @@ root(isDark)
padding 16px 32px
.avatar
+ flex-shrink 0
display inline-block
- width 28px
- height 28px
+ width 20px
+ height 20px
margin 0 8px 0 0
border-radius 6px
+ @media (min-width 500px)
+ width 28px
+ height 28px
+
[data-fa]
margin-right 4px
@@ -297,27 +307,28 @@ root(isDark)
padding-top 8px
> article
+ display flex
padding 16px 16px 9px
@media (min-width 600px)
padding 32px 32px 22px
- &:after
- content ""
- display block
- clear both
-
> .avatar
+ flex-shrink 0
display block
- float left
margin 0 10px 8px 0
- width 48px
- height 48px
+ width 42px
+ height 42px
border-radius 6px
//position -webkit-sticky
//position sticky
//top 62px
+ @media (min-width 350px)
+ width 48px
+ height 48px
+ border-radius 6px
+
@media (min-width 500px)
margin-right 16px
width 58px
@@ -325,62 +336,16 @@ root(isDark)
border-radius 8px
> .main
- float left
- width calc(100% - 58px)
-
- @media (min-width 500px)
- width calc(100% - 74px)
-
- > header
- display flex
- align-items baseline
- white-space nowrap
+ flex 1
+ min-width 0
+ > .header
@media (min-width 500px)
margin-bottom 2px
- > .name
- display block
- margin 0 0.5em 0 0
- padding 0
- overflow hidden
- color isDark ? #fff : #627079
- font-size 1em
- font-weight bold
- text-decoration none
- text-overflow ellipsis
-
- &:hover
- text-decoration underline
-
- > .is-bot
- margin 0 0.5em 0 0
- padding 1px 6px
- font-size 12px
- color isDark ? #758188 : #aaa
- border solid 1px isDark ? #57616f : #ddd
- border-radius 3px
-
- > .username
- margin 0 0.5em 0 0
- overflow hidden
- text-overflow ellipsis
- color isDark ? #606984 : #ccc
-
- > .info
- margin-left auto
- font-size 0.9em
-
- > *
- color isDark ? #606984 : #c0c0c0
-
- > .mobile
- margin-right 6px
-
- > .visibility
- margin-left 6px
-
> .body
+ @media (min-width 700px)
+ font-size 1.1em
> .cw
cursor default
@@ -388,7 +353,6 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- font-size 1.1em
color isDark ? #fff : #717171
> .text
@@ -414,7 +378,6 @@ root(isDark)
margin 0
padding 0
overflow-wrap break-word
- font-size 1.1em
color isDark ? #fff : #717171
>>> .title
@@ -456,9 +419,6 @@ root(isDark)
.mk-url-preview
margin-top 8px
- > .channel
- margin 0
-
> .tags
margin 4px 0 0 0
@@ -468,7 +428,7 @@ root(isDark)
padding 2px 8px 2px 16px
font-size 90%
color #8d969e
- background #edf0f3
+ background isDark ? #313543 : #edf0f3
border-radius 4px
&:before
@@ -481,7 +441,7 @@ root(isDark)
width 8px
height 8px
margin auto 0
- background #fff
+ background isDark ? #282c37 : #fff
border-radius 100%
> .media
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 53e232e521..7aaf0424c7 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -1,7 +1,5 @@
<template>
<div class="mk-notes">
- <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
-
<slot name="head"></slot>
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
@@ -11,8 +9,8 @@
</div>
<div v-if="!fetching && requestInitPromise != null">
- <p>読み込みに失敗しました。</p>
- <button @click="resolveInitPromise">リトライ</button>
+ <p>%i18n:@failed%</p>
+ <button @click="resolveInitPromise">%i18n:@retry%</button>
</div>
<transition-group name="mk-notes" class="transition">
@@ -71,9 +69,19 @@ export default Vue.extend({
}
},
+ watch: {
+ queue(x) {
+ if (x.length > 0) {
+ this.$store.commit('indicate', true);
+ } else {
+ this.$store.commit('indicate', false);
+ }
+ }
+ },
+
mounted() {
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
- window.addEventListener('scroll', this.onScroll);
+ window.addEventListener('scroll', this.onScroll, { passive: true });
},
beforeDestroy() {
@@ -113,24 +121,24 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
- const isMyNote = note.userId == (this as any).os.i.id;
+ const isMyNote = note.userId == this.$store.state.i.id;
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
- if ((this as any).clientSettings.showMyRenotes === false) {
+ if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
return;
}
}
- if ((this as any).clientSettings.showRenotedMyNotes === false) {
- if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
+ if (this.$store.state.settings.showRenotedMyNotes === false) {
+ if (isPureRenote && (note.renote.userId == this.$store.state.i.id)) {
return;
}
}
//#endregion
// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
- if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) {
+ if ((document.hidden || !this.isScrollTop()) && note.userId !== this.$store.state.i.id) {
this.unreadCount++;
document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
}
@@ -187,7 +195,7 @@ export default Vue.extend({
this.clearNotification();
}
- if ((this as any).clientSettings.fetchOnScroll !== false) {
+ if (this.$store.state.settings.fetchOnScroll !== false) {
// 親要素が display none だったら弾く
// https://github.com/syuilo/misskey/issues/1569
// http://d.hatena.ne.jp/favril/20091105/1257403319
@@ -238,13 +246,6 @@ root(isDark)
[data-fa]
margin-right 8px
- > .newer-indicator
- position -webkit-sticky
- position sticky
- z-index 100
- height 3px
- background $theme-color
-
> .init
padding 64px 0
text-align center
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index d39b2fbf9f..5e2306932b 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -1,7 +1,7 @@
<template>
<div class="mk-notification-preview" :class="notification.type">
<template v-if="notification.type == 'reaction'">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p>
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
@@ -9,7 +9,7 @@
</template>
<template v-if="notification.type == 'renote'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:retweet%{{ notification.note.user | userName }}</p>
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
@@ -17,7 +17,7 @@
</template>
<template v-if="notification.type == 'quote'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:quote-left%{{ notification.note.user | userName }}</p>
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
@@ -25,14 +25,21 @@
</template>
<template v-if="notification.type == 'follow'">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p>%fa:user-plus%{{ notification.user | userName }}</p>
</div>
</template>
+ <template v-if="notification.type == 'receiveFollowRequest'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div class="text">
+ <p>%fa:user-clock%{{ notification.user | userName }}</p>
+ </div>
+ </template>
+
<template v-if="notification.type == 'reply'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:reply%{{ notification.note.user | userName }}</p>
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
@@ -40,7 +47,7 @@
</template>
<template v-if="notification.type == 'mention'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:at%{{ notification.note.user | userName }}</p>
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
@@ -48,7 +55,7 @@
</template>
<template v-if="notification.type == 'poll_vote'">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p>%fa:chart-pie%{{ notification.user | userName }}</p>
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
@@ -83,16 +90,14 @@ export default Vue.extend({
display block
clear both
- img
+ > .avatar
display block
float left
- min-width 36px
- min-height 36px
- max-width 36px
- max-height 36px
+ width 36px
+ height 36px
border-radius 6px
- .text
+ > .text
float right
width calc(100% - 36px)
padding-left 8px
@@ -120,6 +125,10 @@ export default Vue.extend({
.text p i
color #53c7ce
+ &.receiveFollowRequest
+ .text p i
+ color #888
+
&.reply, &.mention
.text p i
color #fff
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index c1b37563ce..bbcae05f10 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -40,6 +40,17 @@
</div>
</div>
+ <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
+ %fa:user-clock%
+ <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
+ <mk-time :time="notification.createdAt"/>
+ </header>
+ </div>
+ </div>
+
<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div>
@@ -55,15 +66,15 @@
</div>
<template v-if="notification.type == 'quote'">
- <mk-note :note="notification.note"/>
+ <mk-note :note="notification.note" @update:note="onNoteUpdated"/>
</template>
<template v-if="notification.type == 'reply'">
- <mk-note :note="notification.note"/>
+ <mk-note :note="notification.note" @update:note="onNoteUpdated"/>
</template>
<template v-if="notification.type == 'mention'">
- <mk-note :note="notification.note"/>
+ <mk-note :note="notification.note" @update:note="onNoteUpdated"/>
</template>
</div>
</template>
@@ -78,6 +89,17 @@ export default Vue.extend({
return {
getNoteSummary
};
+ },
+ methods: {
+ onNoteUpdated(note) {
+ switch (this.notification.type) {
+ case 'quote':
+ case 'reply':
+ case 'mention':
+ Vue.set(this.notification, 'note', note);
+ break;
+ }
+ }
}
});
</script>
@@ -156,6 +178,10 @@ root(isDark)
> div > header i
color #53c7ce
+ &.receiveFollowRequest
+ > div > header i
+ color #888
+
.mk-notification[data-darkmode]
root(true)
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 8ab66940c4..6bb9e9bb2c 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -12,7 +12,7 @@
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
- {{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
+ {{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 6d80b3046b..62fa185085 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -5,17 +5,18 @@
<div>
<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
- <button class="submit" :disabled="posting" @click="post">{{ reply ? '返信' : '%i18n:!@submit%' }}</button>
+ <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"/>
<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">+ユーザーを追加</a>
+ <a @click="addVisibleUser">+%i18n:@add-visible-user%</a>
</div>
- <input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
- <textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
+ <input v-show="useCw" v-model="cw" placeholder="%i18n:@cw-placeholder%">
+ <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder"></textarea>
<div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }">
<div class="file" v-for="file in files" :key="file.id">
@@ -44,6 +45,8 @@ import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getKao from '../../../common/scripts/get-kao';
+import parse from '../../../../../text/parse';
+import { host } from '../../../config';
export default Vue.extend({
components: {
@@ -51,7 +54,25 @@ export default Vue.extend({
MkVisibilityChooser
},
- props: ['reply'],
+ props: {
+ reply: {
+ type: Object,
+ required: false
+ },
+ renote: {
+ type: Object,
+ required: false
+ },
+ initialText: {
+ type: String,
+ required: false
+ },
+ instant: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
data() {
return {
@@ -68,11 +89,72 @@ export default Vue.extend({
};
},
+ computed: {
+ draftId(): string {
+ return this.renote
+ ? 'renote:' + this.renote.id
+ : this.reply
+ ? 'reply:' + this.reply.id
+ : 'note';
+ },
+
+ placeholder(): string {
+ const xs = [
+ '%i18n:common.note-placeholders.a%',
+ '%i18n:common.note-placeholders.b%',
+ '%i18n:common.note-placeholders.c%',
+ '%i18n:common.note-placeholders.d%',
+ '%i18n:common.note-placeholders.e%',
+ '%i18n:common.note-placeholders.f%'
+ ];
+ const x = xs[Math.floor(Math.random() * xs.length)];
+
+ return this.renote
+ ? '%i18n:@quote-placeholder%'
+ : this.reply
+ ? '%i18n:@reply-placeholder%'
+ : x;
+ },
+
+ submitText(): string {
+ return this.renote
+ ? '%i18n:@renote%'
+ : this.reply
+ ? '%i18n:@reply%'
+ : '%i18n:@submit%';
+ },
+
+ canPost(): boolean {
+ return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote);
+ }
+ },
+
mounted() {
+ if (this.initialText) {
+ this.text = this.initialText;
+ }
+
if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
}
+ if (this.reply && this.reply.text != null) {
+ const ast = parse(this.reply.text);
+
+ ast.filter(t => t.type == 'mention').forEach(x => {
+ const mention = x.host ? `@${x.username}@${x.host}` : `@${x.username}`;
+
+ // 自分は除外
+ if (this.$store.state.i.username == x.username && x.host == null) return;
+ if (this.$store.state.i.username == x.username && x.host == host) return;
+
+ // 重複は除外
+ if (this.text.indexOf(`${mention} `) != -1) return;
+
+ this.text += `${mention} `;
+ });
+ }
+
this.$nextTick(() => {
this.focus();
});
@@ -119,14 +201,14 @@ export default Vue.extend({
setGeo() {
if (navigator.geolocation == null) {
- alert('お使いの端末は位置情報に対応していません');
+ alert('%i18n:@location-alert%');
return;
}
navigator.geolocation.getCurrentPosition(pos => {
this.geo = pos.coords;
}, err => {
- alert('エラー: ' + err.message);
+ alert('%i18n:@error%: ' + err.message);
}, {
enableHighAccuracy: true
});
@@ -149,7 +231,7 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
- title: 'ユーザー名を入力してください'
+ title: '%i18n:@username-prompt%'
}).then(username => {
(this as any).api('users/show', {
username
@@ -172,11 +254,12 @@ export default Vue.extend({
post() {
this.posting = true;
- const viaMobile = (this as any).clientSettings.disableViaMobile !== true;
+ 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,
replyId: this.reply ? this.reply.id : undefined,
+ renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? {
@@ -191,8 +274,10 @@ export default Vue.extend({
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
viaMobile: viaMobile
}).then(data => {
- this.$emit('note');
- this.$destroy();
+ this.$emit('posted');
+ this.$nextTick(() => {
+ this.$destroy();
+ });
}).catch(err => {
this.posting = false;
});
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 cc50977a58..4ad90b97df 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -1,13 +1,14 @@
<template>
<div class="mk-sub-note-content">
<div class="body">
- <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <span v-if="note.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
+ <span v-if="note.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
<a class="reply" v-if="note.replyId">%fa:reply%</a>
- <mk-note-html v-if="note.text" :text="note.text" :i="os.i"/>
+ <mk-note-html 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>({{ note.media.length }}個のメディア)</summary>
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
<mk-media-list :media-list="note.media"/>
</details>
<details v-if="note.poll">
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index 509463333d..c1ee70d105 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -3,16 +3,17 @@
<mk-special-message/>
<div class="main" ref="main">
<div class="backdrop"></div>
- <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p>
+ <p ref="welcomeback" v-if="$store.getters.isSignedIn">おかえりなさい、<b>{{ $store.state.i | userName }}</b>さん</p>
<div class="content" ref="mainContainer">
<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
- <template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template>
+ <template v-if="hasUnreadNotification || hasUnreadMessagingMessage || hasGameInvitation">%fa:circle%</template>
<h1>
<slot>Misskey</slot>
</h1>
<slot name="func"></slot>
</div>
</div>
+ <div class="indicator" v-show="$store.state.indicate"></div>
</div>
</template>
@@ -24,45 +25,33 @@ export default Vue.extend({
props: ['func'],
data() {
return {
- hasUnreadNotifications: false,
- hasUnreadMessagingMessages: false,
- hasGameInvitations: false,
+ hasGameInvitation: false,
connection: null,
connectionId: null
};
},
+ computed: {
+ hasUnreadNotification(): boolean {
+ return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
+ },
+ hasUnreadMessagingMessage(): boolean {
+ return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
+ }
+ },
mounted() {
this.$store.commit('setUiHeaderHeight', 48);
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
- this.connection.on('read_all_notifications', this.onReadAllNotifications);
- this.connection.on('unread_notification', this.onUnreadNotification);
- this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
- this.connection.on('othello_invited', this.onOthelloInvited);
- this.connection.on('othello_no_invites', this.onOthelloNoInvites);
+ this.connection.on('reversi_invited', this.onReversiInvited);
+ this.connection.on('reversi_no_invites', this.onReversiNoInvites);
- // Fetch count of unread notifications
- (this as any).api('notifications/get_unread_count').then(res => {
- if (res.count > 0) {
- this.hasUnreadNotifications = true;
- }
- });
-
- // Fetch count of unread messaging messages
- (this as any).api('messaging/unread').then(res => {
- if (res.count > 0) {
- this.hasUnreadMessagingMessages = true;
- }
- });
-
- const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000;
+ const ago = (new Date().getTime() - new Date(this.$store.state.i.lastUsedAt).getTime()) / 1000;
const isHisasiburi = ago >= 3600;
- (this as any).os.i.lastUsedAt = new Date();
- (this as any).os.bakeMe();
+ 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';
@@ -108,34 +97,18 @@ export default Vue.extend({
}
},
beforeDestroy() {
- if ((this as any).os.isSignedIn) {
- this.connection.off('read_all_notifications', this.onReadAllNotifications);
- this.connection.off('unread_notification', this.onUnreadNotification);
- this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
- this.connection.off('othello_invited', this.onOthelloInvited);
- this.connection.off('othello_no_invites', this.onOthelloNoInvites);
+ 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);
}
},
methods: {
- onReadAllNotifications() {
- this.hasUnreadNotifications = false;
+ onReversiInvited() {
+ this.hasGameInvitation = true;
},
- onUnreadNotification() {
- this.hasUnreadNotifications = true;
- },
- onReadAllMessagingMessages() {
- this.hasUnreadMessagingMessages = false;
- },
- onUnreadMessagingMessage() {
- this.hasUnreadMessagingMessages = true;
- },
- onOthelloInvited() {
- this.hasGameInvitations = true;
- },
- onOthelloNoInvites() {
- this.hasGameInvitations = false;
+ onReversiNoInvites() {
+ this.hasGameInvitation = false;
}
}
});
@@ -156,6 +129,10 @@ root(isDark)
&, *
user-select none
+ > .indicator
+ height 3px
+ background $theme-color
+
> .main
color rgba(#fff, 0.9)
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 5c65d52237..bb7a2f558c 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -9,26 +9,28 @@
</transition>
<transition name="nav">
<div class="body" v-if="isOpen">
- <router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`">
- <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
- <p class="name">{{ os.i | userName }}</p>
+ <router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`">
+ <img class="avatar" :src="`${$store.state.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
+ <p class="name">{{ $store.state.i | userName }}</p>
</router-link>
<div class="links">
<ul>
- <li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@home%%fa:angle-right%</router-link></li>
- <li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li>
- <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li>
- <li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@timeline%%fa:angle-right%</router-link></li>
+ <li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotification">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li v-if="$store.getters.isSignedIn && $store.state.i.isLocked"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'">%fa:R envelope%%i18n:@follow-requests%<template v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li><router-link to="/reversi" :data-active="$route.name == 'reversi'">%fa:gamepad%%i18n:@game%<template v-if="hasGameInvitation">%fa:circle%</template>%fa:angle-right%</router-link></li>
</ul>
<ul>
+ <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'">%fa:R calendar-alt%%i18n:@widgets%%fa:angle-right%</router-link></li>
+ <li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'">%fa:star%%i18n:@favorites%%fa:angle-right%</router-link></li>
+ <li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'">%fa:list%%i18n:@user-lists%%fa:angle-right%</router-link></li>
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li>
</ul>
<ul>
<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
- </ul>
- <ul>
- <li><router-link to="/i/settings">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
- <li @click="dark"><p><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li>
+ <li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
+ <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>
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
@@ -45,78 +47,53 @@ export default Vue.extend({
props: ['isOpen'],
data() {
return {
- hasUnreadNotifications: false,
- hasUnreadMessagingMessages: false,
- hasGameInvitations: false,
+ hasGameInvitation: false,
connection: null,
connectionId: null,
aboutUrl: `${docsUrl}/${lang}/about`
};
},
+ 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() {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
- this.connection.on('read_all_notifications', this.onReadAllNotifications);
- this.connection.on('unread_notification', this.onUnreadNotification);
- this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
- this.connection.on('othello_invited', this.onOthelloInvited);
- this.connection.on('othello_no_invites', this.onOthelloNoInvites);
-
- // Fetch count of unread notifications
- (this as any).api('notifications/get_unread_count').then(res => {
- if (res.count > 0) {
- this.hasUnreadNotifications = true;
- }
- });
-
- // Fetch count of unread messaging messages
- (this as any).api('messaging/unread').then(res => {
- if (res.count > 0) {
- this.hasUnreadMessagingMessages = true;
- }
- });
+ this.connection.on('reversi_invited', this.onReversiInvited);
+ this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
beforeDestroy() {
- if ((this as any).os.isSignedIn) {
- this.connection.off('read_all_notifications', this.onReadAllNotifications);
- this.connection.off('unread_notification', this.onUnreadNotification);
- this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
- this.connection.off('othello_invited', this.onOthelloInvited);
- this.connection.off('othello_no_invites', this.onOthelloNoInvites);
+ 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);
}
},
methods: {
search() {
- const query = window.prompt('%i18n:!@search%');
+ const query = window.prompt('%i18n:@search%');
if (query == null || query == '') return;
this.$router.push('/search?q=' + encodeURIComponent(query));
},
- onReadAllNotifications() {
- this.hasUnreadNotifications = false;
- },
- onUnreadNotification() {
- this.hasUnreadNotifications = true;
- },
- onReadAllMessagingMessages() {
- this.hasUnreadMessagingMessages = false;
- },
- onUnreadMessagingMessage() {
- this.hasUnreadMessagingMessages = true;
+ onReversiInvited() {
+ this.hasGameInvitation = true;
},
- onOthelloInvited() {
- this.hasGameInvitations = true;
- },
- onOthelloNoInvites() {
- this.hasGameInvitations = false;
+ onReversiNoInvites() {
+ this.hasGameInvitation = false;
},
dark() {
- (this as any)._updateDarkmode_(!(this as any)._darkmode_);
+ this.$store.commit('device/set', {
+ key: 'darkmode',
+ value: !this.$store.state.device.darkmode
+ });
}
}
});
@@ -182,7 +159,10 @@ root(isDark)
&:first-child
margin-top 0
- li
+ &:last-child
+ margin-bottom 0
+
+ > li
display block
font-size 1em
line-height 1em
@@ -205,6 +185,8 @@ root(isDark)
> [data-fa]:first-child
margin-right 0.5em
+ width 20px
+ text-align center
> [data-fa].circle
margin-left 6px
@@ -222,7 +204,7 @@ root(isDark)
opacity 0.5
.about
- margin 0
+ margin 0 0 8px 0
padding 1em 0
text-align center
font-size 0.8em
diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue
index 325ce9d40e..7e2d39f259 100644
--- a/src/client/app/mobile/views/components/ui.vue
+++ b/src/client/app/mobile/views/components/ui.vue
@@ -8,7 +8,7 @@
<div class="content">
<slot></slot>
</div>
- <mk-stream-indicator v-if="os.isSignedIn"/>
+ <mk-stream-indicator v-if="$store.getters.isSignedIn"/>
</div>
</template>
@@ -32,7 +32,7 @@ export default Vue.extend({
};
},
mounted() {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -40,7 +40,7 @@ export default Vue.extend({
}
},
beforeDestroy() {
- if ((this as any).os.isSignedIn) {
+ if (this.$store.getters.isSignedIn) {
this.connection.off('notification', this.onNotification);
(this as any).os.stream.dispose(this.connectionId);
}
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index 432560a54a..808ee72402 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -1,12 +1,10 @@
<template>
<div class="mk-user-card">
<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
- <a :href="user | userPage">
- <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
- </a>
+ <mk-avatar class="avatar" :user="user"/>
</header>
<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
- <p class="username">@{{ user | acct }}</p>
+ <p class="username"><mk-acct :user="user"/></p>
<mk-follow-button :user="user"/>
</div>
</template>
@@ -35,15 +33,14 @@ export default Vue.extend({
background-position center
border-radius 8px 8px 0 0
- > a
- > img
- position absolute
- top 20px
- left calc(50% - 40px)
- width 80px
- height 80px
- border solid 2px #fff
- border-radius 8px
+ > .avatar
+ position absolute
+ top 20px
+ left calc(50% - 40px)
+ width 80px
+ height 80px
+ border solid 2px #fff
+ border-radius 8px
> .name
display block
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 59d6abbbc1..2c1564b7ed 100644
--- a/src/client/app/mobile/views/components/user-list-timeline.vue
+++ b/src/client/app/mobile/views/components/user-list-timeline.vue
@@ -12,6 +12,7 @@ const fetchLimit = 10;
export default Vue.extend({
props: ['list'],
+
data() {
return {
fetching: true,
@@ -20,25 +21,36 @@ export default Vue.extend({
connection: null
};
},
+
+ computed: {
+ canFetchMore(): boolean {
+ return !this.moreFetching && !this.fetching && this.existMore;
+ }
+ },
+
watch: {
$route: 'init'
},
+
mounted() {
this.init();
},
+
beforeDestroy() {
this.connection.close();
},
+
methods: {
init() {
if (this.connection) this.connection.close();
- this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
+ this.connection = new UserListStream((this as any).os, this.$store.state.i, this.list.id);
this.connection.on('note', this.onNote);
this.connection.on('userAdded', this.onUserAdded);
this.connection.on('userRemoved', this.onUserRemoved);
this.fetch();
},
+
fetch() {
this.fetching = true;
@@ -46,8 +58,8 @@ export default Vue.extend({
(this as any).api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -59,16 +71,21 @@ export default Vue.extend({
}, rej);
}));
},
+
more() {
+ if (!this.canFetchMore) return;
+
this.moreFetching = true;
- (this as any).api('notes/user-list-timeline', {
+ const promise = (this as any).api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
- }).then(notes => {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ });
+
+ promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
@@ -77,14 +94,19 @@ export default Vue.extend({
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);
},
+
onUserAdded() {
this.fetch();
},
+
onUserRemoved() {
this.fetch();
}
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index d258360911..a165e66a9d 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -4,7 +4,7 @@
<div class="main">
<header>
<router-link class="name" :to="user | userPage">{{ user | userName }}</router-link>
- <span class="username">@{{ user | acct }}</span>
+ <span class="username"><mk-acct :user="user"/></span>
</header>
<div class="body">
<div class="description">{{ user.description }}</div>
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 3ceb876596..6be675c0a7 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -3,7 +3,7 @@
<mk-notes ref="timeline" :more="existMore ? more : null">
<div slot="empty">
%fa:R comments%
- {{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }}
+ {{ withMedia ? '%i18n:@no-notes-with-media%' : '%i18n:@no-notes%' }}
</div>
</mk-notes>
</div>
@@ -59,12 +59,15 @@ export default Vue.extend({
if (!this.canFetchMore) return;
this.moreFetching = true;
- (this as any).api('users/notes', {
+
+ const promise = (this as any).api('users/notes', {
userId: this.user.id,
withMedia: this.withMedia,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id
- }).then(notes => {
+ });
+
+ promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
@@ -73,6 +76,8 @@ export default Vue.extend({
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
+
+ return promise;
}
}
});
diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
index 6175067459..a57b821293 100644
--- a/src/client/app/mobile/views/components/users-list.vue
+++ b/src/client/app/mobile/views/components/users-list.vue
@@ -2,7 +2,7 @@
<div class="mk-users-list">
<nav>
<span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
- <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span>
+ <span v-if="$store.getters.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
<mk-user-preview v-for="u in users" :user="u" :key="u.id"/>
diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
index 1bdc875763..a713a10621 100644
--- a/src/client/app/mobile/views/components/widget-container.vue
+++ b/src/client/app/mobile/views/components/widget-container.vue
@@ -25,27 +25,27 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-widget-container
- background #eee
+root(isDark)
+ background isDark ? #21242f : #eee
border-radius 8px
- box-shadow 0 0 0 1px rgba(#000, 0.2)
+ box-shadow 0 4px 16px rgba(#000, 0.1)
overflow hidden
- &.hideHeader
- background #fff
-
&.naked
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 #465258
- background #fff
+ color isDark ? #b8c5cc : #465258
+ background isDark ? #282c37 : #fff
border-radius 8px 8px 0 0
> [data-fa]
@@ -65,4 +65,10 @@ export default Vue.extend({
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/favorites.vue b/src/client/app/mobile/views/pages/favorites.vue
new file mode 100644
index 0000000000..c4edd9d970
--- /dev/null
+++ b/src/client/app/mobile/views/pages/favorites.vue
@@ -0,0 +1,94 @@
+<template>
+<mk-ui>
+ <span slot="header">%fa:star%%i18n:@title%</span>
+
+ <main>
+ <template v-for="favorite in favorites">
+ <mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
+ </template>
+ <a v-if="existMore" @click="more">%i18n:@more%</a>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ favorites: [],
+ existMore: false,
+ moreFetching: false
+ };
+ },
+ created() {
+ this.fetch();
+ },
+ mounted() {
+ document.title = 'Misskey | %i18n:@notifications%';
+ },
+ methods: {
+ fetch() {
+ Progress.start();
+ this.fetching = true;
+
+ (this as any).api('i/favorites', {
+ limit: 11
+ }).then(favorites => {
+ if (favorites.length == 11) {
+ this.existMore = true;
+ favorites.pop();
+ }
+
+ this.favorites = favorites;
+ this.fetching = false;
+
+ Progress.done();
+ });
+ },
+ more() {
+ this.moreFetching = true;
+ (this as any).api('i/favorites', {
+ limit: 11,
+ maxId: this.favorites[this.favorites.length - 1].id
+ }).then(favorites => {
+ if (favorites.length == 11) {
+ this.existMore = true;
+ favorites.pop();
+ } else {
+ this.existMore = false;
+ }
+
+ this.favorites = this.favorites.concat(favorites);
+ this.moreFetching = false;
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+main
+ width 100%
+ max-width 680px
+ margin 0 auto
+ padding 8px
+
+ > .post
+ margin-bottom 8px
+
+ @media (min-width 500px)
+ padding 16px
+
+ > .post
+ margin-bottom 16px
+
+ @media (min-width 600px)
+ padding 32px
+
+</style>
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 33ade94e35..dfb9c62142 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -2,7 +2,7 @@
<mk-ui>
<template slot="header" v-if="!fetching">
<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
- {{ '%i18n:!@followers-of%'.replace('{}', name) }}
+ {{ '%i18n:@followers-of%'.replace('{}', name) }}
</template>
<mk-users-list
v-if="!fetching"
@@ -49,7 +49,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey';
+ document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey';
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index c6d6d44281..35461ea2fc 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -2,7 +2,7 @@
<mk-ui>
<template slot="header" v-if="!fetching">
<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
- {{ '%i18n:!@following-of%'.replace('{}', name) }}
+ {{ '%i18n:@following-of%'.replace('{}', name) }}
</template>
<mk-users-list
v-if="!fetching"
@@ -48,7 +48,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey';
+ document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey';
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 4c1c344db1..364367b940 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
computed: {
alone(): boolean {
- return (this as any).os.i.followingCount == 0;
+ return this.$store.state.i.followingCount == 0;
},
stream(): any {
@@ -92,8 +92,8 @@ export default Vue.extend({
(this as any).api(this.endpoint, {
limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -111,12 +111,14 @@ export default Vue.extend({
this.moreFetching = true;
- (this as any).api(this.endpoint, {
+ const promise = (this as any).api(this.endpoint, {
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: (this as any).clientSettings.showMyRenotes,
- includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
- }).then(notes => {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
+ });
+
+ promise.then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
@@ -125,6 +127,8 @@ export default Vue.extend({
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
+
+ return promise;
},
onNote(note) {
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index ad6d5ed408..c0c2ee8ab5 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -2,10 +2,10 @@
<mk-ui>
<span slot="header" @click="showNav = true">
<span>
- <span v-if="src == 'home'">%fa:home%ホーム</span>
- <span v-if="src == 'local'">%fa:R comments%ローカル</span>
- <span v-if="src == 'global'">%fa:globe%グローバル</span>
- <span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span>
+ <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 == 'global'">%fa:globe%%i18n:@global%</span>
+ <span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
</span>
<span style="margin-left:8px">
<template v-if="!showNav">%fa:angle-down%</template>
@@ -17,26 +17,26 @@
<button @click="fn">%fa:pencil-alt%</button>
</template>
- <main :data-darkmode="_darkmode_">
+ <main :data-darkmode="$store.state.device.darkmode">
<div class="nav" v-if="showNav">
<div class="bg" @click="showNav = false"></div>
<div class="body">
<div>
- <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
- <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
- <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span>
+ <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 == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
<template v-if="lists">
- <span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
+ <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>
</div>
</div>
<div class="tl">
- <x-tl v-if="src == 'home'" ref="tl" key="home" src="home" @loaded="onLoaded"/>
+ <x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/>
<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
- <mk-user-list-timeline v-if="src.startsWith('list:')" ref="tl" :key="list.id" :list="list"/>
+ <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div>
</main>
</mk-ui>
@@ -64,6 +64,12 @@ export default Vue.extend({
watch: {
src() {
this.showNav = false;
+ this.saveSrc();
+ },
+
+ list() {
+ this.showNav = false;
+ this.saveSrc();
},
showNav(v) {
@@ -76,7 +82,12 @@ export default Vue.extend({
},
created() {
- if ((this as any).os.i.followingCount == 0) {
+ 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.$store.state.i.followingCount == 0) {
this.src = 'local';
}
},
@@ -85,6 +96,10 @@ export default Vue.extend({
document.title = 'Misskey';
Progress.start();
+
+ (this.$refs.tl as any).$once('loaded', () => {
+ Progress.done();
+ });
},
methods: {
@@ -92,8 +107,11 @@ export default Vue.extend({
(this as any).apis.post();
},
- onLoaded() {
- Progress.done();
+ saveSrc() {
+ this.$store.commit('device/setTl', {
+ src: this.src,
+ arg: this.list
+ });
},
warp() {
diff --git a/src/client/app/mobile/views/pages/index.vue b/src/client/app/mobile/views/pages/index.vue
index 0ea47d913b..5d11fc5423 100644
--- a/src/client/app/mobile/views/pages/index.vue
+++ b/src/client/app/mobile/views/pages/index.vue
@@ -1,5 +1,5 @@
<template>
-<component :is="os.isSignedIn ? 'home' : 'welcome'"></component>
+<component :is="$store.getters.isSignedIn ? 'home' : 'welcome'"></component>
</template>
<script lang="ts">
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index c26a9b735e..8b82b03fb9 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -16,16 +16,30 @@ export default Vue.extend({
data() {
return {
fetching: true,
- user: null
+ user: null,
+ unwatchDarkmode: null
};
},
watch: {
$route: 'fetch'
},
created() {
- document.documentElement.style.background = '#fff';
+ const applyBg = v =>
+ document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
+
+ applyBg(this.$store.state.device.darkmode);
+
+ this.unwatchDarkmode = this.$store.watch(s => {
+ return s.device.darkmode;
+ }, applyBg);
+
this.fetch();
},
+ beforeDestroy() {
+ document.documentElement.style.removeProperty('background');
+ document.documentElement.style.removeProperty('background-color'); // for safari's bug
+ this.unwatchDarkmode();
+ },
methods: {
fetch() {
this.fetching = true;
@@ -39,4 +53,3 @@ export default Vue.extend({
}
});
</script>
-
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index cc328e5a1c..057470efd9 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -12,7 +12,6 @@ import getAcct from '../../../../../acct/render';
export default Vue.extend({
mounted() {
document.title = 'Misskey %i18n:@messaging%';
- document.documentElement.style.background = '#fff';
},
methods: {
navigate(user) {
diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
index d0c0fe9535..64cfa60da0 100644
--- a/src/client/app/mobile/views/pages/notifications.vue
+++ b/src/client/app/mobile/views/pages/notifications.vue
@@ -21,10 +21,10 @@ export default Vue.extend({
},
methods: {
fn() {
- const ok = window.confirm('%i18n:!@read-all%');
+ const ok = window.confirm('%i18n:@read-all%');
if (!ok) return;
- (this as any).api('notifications/markAsRead_all');
+ (this as any).api('notifications/mark_as_read_all');
},
onFetched() {
Progress.done();
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
deleted file mode 100644
index 7048cdef31..0000000000
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ /dev/null
@@ -1,225 +0,0 @@
-<template>
-<mk-ui>
- <span slot="header">%fa:user%%i18n:@title%</span>
- <div :class="$style.content">
- <p>%fa:info-circle%%i18n:@will-be-published%</p>
- <div :class="$style.form">
- <div :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner">
- <img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/>
- </div>
- <label>
- <p>%i18n:@name%</p>
- <input v-model="name" type="text"/>
- </label>
- <label>
- <p>%i18n:@location%</p>
- <input v-model="location" type="text"/>
- </label>
- <label>
- <p>%i18n:@description%</p>
- <textarea v-model="description"></textarea>
- </label>
- <label>
- <p>%i18n:@birthday%</p>
- <input v-model="birthday" type="date"/>
- </label>
- <label>
- <p>%i18n:@avatar%</p>
- <button @click="setAvatar" :disabled="avatarSaving">%i18n:@set-avatar%</button>
- </label>
- <label>
- <p>%i18n:@banner%</p>
- <button @click="setBanner" :disabled="bannerSaving">%i18n:@set-banner%</button>
- </label>
- </div>
- <button :class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:@save%</button>
- </div>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
- data() {
- return {
- name: null,
- location: null,
- description: null,
- birthday: null,
- avatarSaving: false,
- bannerSaving: false,
- saving: false
- };
- },
- created() {
- this.name = (this as any).os.i.name || '';
- this.location = (this as any).os.i.profile.location;
- this.description = (this as any).os.i.description;
- this.birthday = (this as any).os.i.profile.birthday;
- },
- mounted() {
- document.title = 'Misskey | %i18n:@title%';
- },
- methods: {
- setAvatar() {
- (this as any).apis.chooseDriveFile({
- multiple: false
- }).then(file => {
- this.avatarSaving = true;
-
- (this as any).api('i/update', {
- avatarId: file.id
- }).then(() => {
- this.avatarSaving = false;
- alert('%i18n:!@avatar-saved%');
- });
- });
- },
- setBanner() {
- (this as any).apis.chooseDriveFile({
- multiple: false
- }).then(file => {
- this.bannerSaving = true;
-
- (this as any).api('i/update', {
- bannerId: file.id
- }).then(() => {
- this.bannerSaving = false;
- alert('%i18n:!@banner-saved%');
- });
- });
- },
- save() {
- this.saving = true;
-
- (this as any).api('i/update', {
- name: this.name || null,
- location: this.location || null,
- description: this.description || null,
- birthday: this.birthday || null
- }).then(() => {
- this.saving = false;
- alert('%i18n:!@saved%');
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" module>
-@import '~const.styl'
-
-.content
- margin 8px auto
- max-width 500px
- width calc(100% - 16px)
-
- @media (min-width 500px)
- margin 16px auto
- width calc(100% - 32px)
-
- > p
- display block
- margin 0 0 8px 0
- padding 12px 16px
- font-size 14px
- color #79d4e6
- border solid 1px #71afbb
- //color #276f86
- //background #f8ffff
- //border solid 1px #a9d5de
- border-radius 8px
-
- > [data-fa]
- margin-right 6px
-
-.form
- position relative
- background #fff
- box-shadow 0 0 0 1px rgba(#000, 0.2)
- border-radius 8px
-
- &:before
- content ""
- display block
- position absolute
- bottom -20px
- left calc(50% - 10px)
- border-top solid 10px rgba(#000, 0.2)
- border-right solid 10px transparent
- border-bottom solid 10px transparent
- border-left solid 10px transparent
-
- &:after
- content ""
- display block
- position absolute
- bottom -16px
- left calc(50% - 8px)
- border-top solid 8px #fff
- border-right solid 8px transparent
- border-bottom solid 8px transparent
- border-left solid 8px transparent
-
- > div
- height 128px
- background-color #e4e4e4
- background-size cover
- background-position center
- border-radius 8px 8px 0 0
-
- > img
- position absolute
- top 25px
- left calc(50% - 40px)
- width 80px
- height 80px
- border solid 2px #fff
- border-radius 8px
-
- > label
- display block
- margin 0
- padding 16px
- border-bottom solid 1px #eee
-
- &:last-of-type
- border none
-
- > p:first-child
- display block
- margin 0
- padding 0 0 4px 0
- font-weight bold
- color #2f3c42
-
- > input[type="text"]
- > textarea
- display block
- width 100%
- padding 12px
- font-size 16px
- color #192427
- border solid 2px #ddd
- border-radius 4px
-
- > textarea
- min-height 80px
-
-.save
- display block
- margin 8px 0 0 0
- padding 16px
- width 100%
- font-size 16px
- color $theme-color-foreground
- background $theme-color
- border-radius 8px
-
- &:disabled
- opacity 0.7
-
- > [data-fa]
- margin-right 4px
-
-</style>
diff --git a/src/client/app/mobile/views/pages/received-follow-requests.vue b/src/client/app/mobile/views/pages/received-follow-requests.vue
new file mode 100644
index 0000000000..bf26a84ff9
--- /dev/null
+++ b/src/client/app/mobile/views/pages/received-follow-requests.vue
@@ -0,0 +1,78 @@
+<template>
+<mk-ui>
+ <span slot="header">%fa:envelope R%%i18n:@title%</span>
+
+ <main>
+ <div v-for="req in requests">
+ <router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
+ <span>
+ <a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
+ </span>
+ </div>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ requests: []
+ };
+ },
+ mounted() {
+ document.title = 'Misskey | %i18n:@title%';
+
+ Progress.start();
+
+ (this as any).api('following/requests/list').then(requests => {
+ this.fetching = false;
+ this.requests = requests;
+
+ Progress.done();
+ });
+ },
+ methods: {
+ accept(user) {
+ (this as any).api('following/requests/accept', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ },
+ reject(user) {
+ (this as any).api('following/requests/reject', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+main
+ width 100%
+ max-width 680px
+ margin 0 auto
+ padding 8px
+
+ @media (min-width 500px)
+ padding 16px
+
+ @media (min-width 600px)
+ padding 32px
+
+ > div
+ display flex
+ padding 16px
+ border solid 1px isDark ? #1c2023 : #eee
+ border-radius 4px
+
+ > span
+ margin 0 0 0 auto
+
+</style>
diff --git a/src/client/app/mobile/views/pages/othello.vue b/src/client/app/mobile/views/pages/reversi.vue
index e04e583c20..e2f0db6d87 100644
--- a/src/client/app/mobile/views/pages/othello.vue
+++ b/src/client/app/mobile/views/pages/reversi.vue
@@ -1,7 +1,7 @@
<template>
<mk-ui>
- <span slot="header">%fa:gamepad%オセロ</span>
- <mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/>
+ <span slot="header">%fa:gamepad%リバーシ</span>
+ <mk-reversi v-if="!fetching" :init-game="game" @gamed="onGamed"/>
</mk-ui>
</template>
@@ -23,7 +23,7 @@ export default Vue.extend({
this.fetch();
},
mounted() {
- document.title = 'Misskey オセロ';
+ document.title = 'Misskey リバーシ';
document.documentElement.style.background = '#fff';
},
methods: {
@@ -33,7 +33,7 @@ export default Vue.extend({
Progress.start();
this.fetching = true;
- (this as any).api('othello/games/show', {
+ (this as any).api('reversi/games/show', {
gameId: this.$route.params.game
}).then(game => {
this.game = game;
@@ -43,7 +43,7 @@ export default Vue.extend({
});
},
onGamed(game) {
- history.pushState(null, null, '/othello/' + game.id);
+ history.pushState(null, null, '/reversi/' + game.id);
}
}
});
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
index f038a6f81f..9850fbcfb4 100644
--- a/src/client/app/mobile/views/pages/search.vue
+++ b/src/client/app/mobile/views/pages/search.vue
@@ -3,7 +3,7 @@
<span slot="header">%fa:search% {{ q }}</span>
<main v-if="!fetching">
<mk-notes :class="$style.notes" :notes="notes">
- <span v-if="notes.length == 0">{{ '%i18n:!@empty%'.replace('{}', q) }}</span>
+ <span v-if="notes.length == 0">{{ '%i18n:@empty%'.replace('{}', q) }}</span>
<button v-if="existMore" @click="more" :disabled="fetching" slot="tail">
<span v-if="!fetching">%i18n:@load-more%</span>
<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index d730e4fcff..1a162b346c 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -25,7 +25,7 @@ export default Vue.extend({
}
},
mounted() {
- document.title = '%i18n:!@title%';
+ document.title = '%i18n:@title%';
},
methods: {
onSelected(file) {
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 0e9c5ea962..1c5a43ede4 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -1,106 +1,245 @@
<template>
<mk-ui>
<span slot="header">%fa:cog%%i18n:@settings%</span>
- <div :class="$style.content">
- <p v-html="'%i18n:!@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
- <ul>
- <li><router-link to="./settings/profile">%fa:user%%i18n:@profile%%fa:angle-right%</router-link></li>
- <li><router-link to="./settings/twitter">%fa:B twitter%%i18n:@twitter%%fa:angle-right%</router-link></li>
- <li><router-link to="./settings/signin-history">%fa:sign-in-alt%%i18n:@signin-history%%fa:angle-right%</router-link></li>
- </ul>
- <ul>
- <li><a @click="signout">%fa:power-off%%i18n:@signout%</a></li>
- </ul>
- <p><small>ver {{ version }} ({{ codename }})</small></p>
- </div>
+ <main :data-darkmode="$store.state.device.darkmode">
+ <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div>
+
+ <div>
+ <x-profile/>
+
+ <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>
+
+ <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>
+ </div>
+
+ <div>
+ <div>%i18n:@post-style%</div>
+ <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>
+ </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>
+ </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>
+
+ <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>
+ </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>
+ </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>
+ </ui-card>
+ </div>
+
+ <footer>
+ <small>ver {{ version }} ({{ codename }})</small>
+ </footer>
+ </main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
-import { version, codename } from '../../../config';
+import { apiUrl, version, codename, langs } from '../../../config';
+import checkForUpdate from '../../../common/scripts/check-for-update';
+
+import XProfile from './settings/settings.profile.vue';
export default Vue.extend({
+ components: {
+ XProfile
+ },
+
data() {
return {
+ apiUrl,
version,
- codename
+ codename,
+ langs,
+ latestVersion: undefined,
+ checkingForUpdate: false
};
},
+
computed: {
name(): string {
- return Vue.filter('userName')((this as any).os.i);
- }
+ return Vue.filter('userName')(this.$store.state.i);
+ },
+
+ darkmode: {
+ get() { return this.$store.state.device.darkmode; },
+ set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
+ },
+
+ postStyle: {
+ get() { return this.$store.state.device.postStyle; },
+ set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
+ },
+
+ lightmode: {
+ get() { return this.$store.state.device.lightmode; },
+ set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
+ },
+
+ loadRawImages: {
+ get() { return this.$store.state.device.loadRawImages; },
+ set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
+ },
+
+ lang: {
+ get() { return this.$store.state.device.lang; },
+ set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
+ },
},
+
mounted() {
document.title = 'Misskey | %i18n:@settings%';
},
+
methods: {
signout() {
(this as any).os.signout();
- }
- }
-});
-</script>
+ },
-<style lang="stylus" module>
-.content
+ onChangeFetchOnScroll(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'fetchOnScroll',
+ value: v
+ });
+ },
- > p
- display block
- margin 24px
- text-align center
- color #cad2da
+ onChangeDisableViaMobile(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'disableViaMobile',
+ value: v
+ });
+ },
- > ul
- $radius = 8px
+ onChangeLoadRemoteMedia(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'loadRemoteMedia',
+ value: v
+ });
+ },
- display block
- margin 16px auto
- padding 0
- max-width 500px
- width calc(100% - 32px)
- list-style none
- background #fff
- border solid 1px rgba(#000, 0.2)
- border-radius $radius
+ onChangeCircleIcons(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'circleIcons',
+ value: v
+ });
+ },
- > li
- display block
- border-bottom solid 1px #ddd
+ onChangeShowReplyTarget(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'showReplyTarget',
+ value: v
+ });
+ },
- &:hover
- background rgba(#000, 0.1)
+ onChangeShowMyRenotes(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'showMyRenotes',
+ value: v
+ });
+ },
- &:first-child
- border-top-left-radius $radius
- border-top-right-radius $radius
+ onChangeShowRenotedMyNotes(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'showRenotedMyNotes',
+ value: v
+ });
+ },
- &:last-child
- border-bottom-left-radius $radius
- border-bottom-right-radius $radius
- border-bottom none
+ checkForUpdate() {
+ this.checkingForUpdate = true;
+ checkForUpdate((this as any).os, true, true).then(newer => {
+ this.checkingForUpdate = false;
+ this.latestVersion = newer;
+ if (newer == null) {
+ (this as any).apis.dialog({
+ title: '%i18n:@no-updates%',
+ text: '%i18n:@no-updates-desc%'
+ });
+ } else {
+ (this as any).apis.dialog({
+ title: '%i18n:@update-available%',
+ text: '%i18n:@update-available-desc%'
+ });
+ }
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ margin 0 auto
+ max-width 500px
+ width 100%
- > a
- $height = 48px
+ > .signin-as
+ margin 16px
+ padding 16px
+ text-align center
+ color isDark ? #49ab63 : #2c662d
+ background isDark ? #273c34 : #fcfff5
+ 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)
- display block
- position relative
- padding 0 16px
- line-height $height
- color #4d635e
+ > footer
+ margin 16px
+ text-align center
+ color isDark ? #c9d2e0 : #888
- > [data-fa]:nth-of-type(1)
- margin-right 4px
+main[data-darkmode]
+ root(true)
- > [data-fa]:nth-of-type(2)
- display block
- position absolute
- top 0
- right 8px
- z-index 1
- padding 0 20px
- font-size 1.2em
- line-height $height
+main:not([data-darkmode])
+ root(false)
</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
new file mode 100644
index 0000000000..da97cbebd7
--- /dev/null
+++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue
@@ -0,0 +1,153 @@
+<template>
+<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>
+
+ <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="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-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-switch v-model="isCat">%i18n:@is-cat%</ui-switch>
+
+ <ui-button @click="save">%i18n:@save%</ui-button>
+ </ui-form>
+</ui-card>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { apiUrl, host } from '../../../../config';
+
+export default Vue.extend({
+ data() {
+ return {
+ host,
+ name: null,
+ username: null,
+ location: null,
+ description: null,
+ birthday: null,
+ avatarId: null,
+ bannerId: null,
+ isBot: false,
+ isCat: false,
+ saving: false,
+ avatarUploading: false,
+ bannerUploading: false
+ };
+ },
+
+ created() {
+ this.name = this.$store.state.i.name || '';
+ this.username = this.$store.state.i.username;
+ this.location = this.$store.state.i.profile.location;
+ this.description = this.$store.state.i.description;
+ this.birthday = this.$store.state.i.profile.birthday;
+ this.avatarId = this.$store.state.i.avatarId;
+ this.bannerId = this.$store.state.i.bannerId;
+ this.isBot = this.$store.state.i.isBot;
+ this.isCat = this.$store.state.i.isCat;
+ },
+
+ methods: {
+ onAvatarChange([file]) {
+ this.avatarUploading = true;
+
+ const data = new FormData();
+ data.append('file', file);
+ data.append('i', this.$store.state.i.token);
+
+ fetch(apiUrl + '/drive/files/create', {
+ method: 'POST',
+ body: data
+ })
+ .then(response => response.json())
+ .then(f => {
+ this.avatarId = f.id;
+ this.avatarUploading = false;
+ })
+ .catch(e => {
+ this.avatarUploading = false;
+ alert('%18n:!@upload-failed%');
+ });
+ },
+
+ onBannerChange([file]) {
+ this.bannerUploading = true;
+
+ const data = new FormData();
+ data.append('file', file);
+ data.append('i', this.$store.state.i.token);
+
+ fetch(apiUrl + '/drive/files/create', {
+ method: 'POST',
+ body: data
+ })
+ .then(response => response.json())
+ .then(f => {
+ this.bannerId = f.id;
+ this.bannerUploading = false;
+ })
+ .catch(e => {
+ this.bannerUploading = false;
+ alert('%18n:!@upload-failed%');
+ });
+ },
+
+ save() {
+ this.saving = true;
+
+ (this as any).api('i/update', {
+ name: this.name || null,
+ location: this.location || null,
+ description: this.description || null,
+ birthday: this.birthday || null,
+ avatarId: this.avatarId,
+ bannerId: this.bannerId,
+ isBot: this.isBot,
+ isCat: this.isCat
+ }).then(i => {
+ this.saving = false;
+ this.$store.state.i.avatarId = i.avatarId;
+ this.$store.state.i.avatarUrl = i.avatarUrl;
+ this.$store.state.i.bannerId = i.bannerId;
+ this.$store.state.i.bannerUrl = i.bannerUrl;
+
+ alert('%i18n:@saved%');
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/app/mobile/views/pages/share.vue b/src/client/app/mobile/views/pages/share.vue
new file mode 100644
index 0000000000..c69498007d
--- /dev/null
+++ b/src/client/app/mobile/views/pages/share.vue
@@ -0,0 +1,56 @@
+<template>
+<div class="azibmfpleajagva420swmu4c3r7ni7iw">
+ <h1>Misskeyで共有</h1>
+ <div>
+ <mk-signin v-if="!$store.getters.isSignedIn"/>
+ <mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
+ <p v-if="posted" class="posted">%fa:check%</p>
+ </div>
+ <ui-button class="close" v-if="posted" @click="close">閉じる</ui-button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ data() {
+ return {
+ posted: false,
+ text: new URLSearchParams(location.search).get('text')
+ };
+ },
+ methods: {
+ close() {
+ window.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.azibmfpleajagva420swmu4c3r7ni7iw
+ > h1
+ margin 8px 0
+ color #555
+ font-size 20px
+ text-align center
+
+ > div
+ max-width 500px
+ margin 0 auto
+
+ > .posted
+ display block
+ margin 0 auto
+ padding 64px
+ text-align center
+ background #fff
+ border-radius 6px
+ width calc(100% - 32px)
+
+ > .close
+ display block
+ margin 16px auto
+ width calc(100% - 32px)
+</style>
diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue
index b8245beb00..238d386efc 100644
--- a/src/client/app/mobile/views/pages/signup.vue
+++ b/src/client/app/mobile/views/pages/signup.vue
@@ -1,57 +1,26 @@
<template>
<div class="signup">
- <h1>Misskeyをはじめる</h1>
- <p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p>
- <div class="form">
- <p>新規登録</p>
- <div>
- <mk-signup/>
- </div>
- </div>
+ <h1>📦 始めましょう</h1>
+ <mk-signup/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
-export default Vue.extend({
- mounted() {
- document.documentElement.style.background = '#293946';
- }
-});
+export default Vue.extend({});
</script>
<style lang="stylus" scoped>
.signup
- padding 16px
+ padding 32px
margin 0 auto
max-width 500px
h1
margin 0
- padding 8px
+ padding 8px 0 0 0
font-size 1.5em
- font-weight normal
- color #c3c6ca
-
- & + p
- margin 0 0 16px 0
- padding 0 8px 0 8px
- color #949fa9
-
- .form
- background #fff
- border solid 1px rgba(#000, 0.2)
- border-radius 8px
- overflow hidden
-
- > p
- margin 0
- padding 12px 20px
- color #555
- background #f5f5f5
- border-bottom solid 1px #ddd
-
- > div
- padding 16px
+ font-weight bold
+ color #444
</style>
diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue
new file mode 100644
index 0000000000..b4c993e667
--- /dev/null
+++ b/src/client/app/mobile/views/pages/tag.vue
@@ -0,0 +1,81 @@
+<template>
+<mk-ui>
+ <span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
+
+ <main>
+ <p v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
+ <mk-notes ref="timeline" :more="existMore ? more : null"/>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+const limit = 20;
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ offset: 0,
+ empty: false
+ };
+ },
+ watch: {
+ $route: 'fetch'
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.fetch();
+ });
+ },
+ methods: {
+ fetch() {
+ this.fetching = true;
+ Progress.start();
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/search_by_tag', {
+ limit: limit + 1,
+ offset: this.offset,
+ tag: this.$route.params.tag
+ }).then(notes => {
+ if (notes.length == 0) this.empty = true;
+ if (notes.length == limit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ Progress.done();
+ }, rej);
+ }));
+ },
+ more() {
+ this.offset += limit;
+
+ const promise = (this as any).api('notes/search_by_tag', {
+ limit: limit + 1,
+ offset: this.offset,
+ tag: this.$route.params.tag
+ });
+
+ promise.then(notes => {
+ if (notes.length == limit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ }
+ }
+});
+</script>
diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue
new file mode 100644
index 0000000000..1c6a829cd5
--- /dev/null
+++ b/src/client/app/mobile/views/pages/user-list.vue
@@ -0,0 +1,70 @@
+<template>
+<mk-ui>
+ <span slot="header" v-if="!fetching">%fa:list%{{ list.title }}</span>
+
+ <main v-if="!fetching">
+ <ul>
+ <li v-for="user in users" :key="user.id"><router-link :to="user | userPage">{{ user | userName }}</router-link></li>
+ </ul>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ list: null,
+ users: null
+ };
+ },
+ watch: {
+ $route: 'fetch'
+ },
+ created() {
+ this.fetch();
+ },
+ methods: {
+ fetch() {
+ Progress.start();
+ this.fetching = true;
+
+ (this as any).api('users/lists/show', {
+ listId: this.$route.params.list
+ }).then(list => {
+ this.list = list;
+ this.fetching = false;
+
+ Progress.done();
+
+ (this as any).api('users/show', {
+ userIds: this.list.userIds
+ }).then(users => {
+ this.users = users;
+ });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+main
+ width 100%
+ max-width 680px
+ margin 0 auto
+ padding 8px
+
+ @media (min-width 500px)
+ padding 16px
+
+ @media (min-width 600px)
+ padding 32px
+
+</style>
diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue
new file mode 100644
index 0000000000..288295677e
--- /dev/null
+++ b/src/client/app/mobile/views/pages/user-lists.vue
@@ -0,0 +1,68 @@
+<template>
+<mk-ui>
+ <span slot="header">%fa:list%%i18n:@title%</span>
+ <template slot="func"><button @click="fn">%fa:plus%</button></template>
+
+ <main>
+ <ul>
+ <li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.title }}</router-link></li>
+ </ul>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ lists: []
+ };
+ },
+ mounted() {
+ document.title = 'Misskey | %i18n:@title%';
+
+ Progress.start();
+
+ (this as any).api('users/lists/list').then(lists => {
+ this.fetching = false;
+ this.lists = lists;
+
+ Progress.done();
+ });
+ },
+ methods: {
+ fn() {
+ (this as any).apis.input({
+ title: '%i18n:@enter-list-name%',
+ }).then(async title => {
+ const list = await (this as any).api('users/lists/create', {
+ title
+ });
+
+ this.$router.push('/i/lists/' + list.id);
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+main
+ width 100%
+ max-width 680px
+ margin 0 auto
+ padding 8px
+
+ @media (min-width 500px)
+ padding 16px
+
+ @media (min-width 600px)
+ padding 32px
+
+</style>
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 27482dc215..3d37015906 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}?thumbnail&size=64`" alt="">{{ user | userName }}</template>
- <main v-if="!fetching" :data-darkmode="_darkmode_">
+ <main v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
<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>
@@ -11,11 +11,11 @@
<a class="avatar">
<img :src="user.avatarUrl" alt="avatar"/>
</a>
- <mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
+ <mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
</div>
<div class="title">
<h1>{{ user | userName }}</h1>
- <span class="username">@{{ user | acct }}</span>
+ <span class="username"><mk-acct :user="user"/></span>
<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span>
</div>
<div class="description">{{ user.description }}</div>
@@ -84,7 +84,7 @@ export default Vue.extend({
style(): any {
if (this.user.bannerUrl == null) return {};
return {
- backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}
@@ -184,7 +184,6 @@ root(isDark)
> .mk-follow-button
float right
- height 40px
> .title
margin 8px 0
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
index d02daf5027..8b57276b17 100644
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ b/src/client/app/mobile/views/pages/user/home.vue
@@ -25,7 +25,7 @@
<x-friends :user="user"/>
</div>
</section>
- <section class="followers-you-know" v-if="os.isSignedIn && os.i.id !== user.id">
+ <section class="followers-you-know" v-if="$store.getters.isSignedIn && $store.state.i.id !== user.id">
<h2>%fa:users%%i18n:@followers-you-know%</h2>
<div>
<x-followers-you-know :user="user"/>
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 64cfa5a46c..cd8f5841e7 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -1,28 +1,22 @@
<template>
<div class="welcome">
<div>
- <h1><b>Misskey</b>へようこそ</h1>
- <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
- <div class="form">
- <p>%fa:lock% ログイン</p>
- <div>
- <form @submit.prevent="onSubmit">
- <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
- <input v-model="password" type="password" placeholder="パスワード" required/>
- <input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
- <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
- </form>
- <div>
- <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
- </div>
- </div>
+ <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey">
+ <p class="host">{{ host }}</p>
+ <div class="about">
+ <h2>{{ name || 'unidentified' }}</h2>
+ <p v-html="description || '%i18n:common.about%'"></p>
+ <router-link class="signup" to="/signup">新規登録</router-link>
+ </div>
+ <div class="login">
+ <mk-signin :with-avatar="false"/>
</div>
<div class="tl">
- <p>%fa:comments R% タイムラインを見てみる</p>
<mk-welcome-timeline/>
</div>
- <div class="users">
- <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
+ <div class="stats" v-if="stats">
+ <span>%fa:user% {{ stats.originalUsersCount | number }}</span>
+ <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
</div>
<footer>
<small>{{ copyright }}</small>
@@ -33,163 +27,115 @@
<script lang="ts">
import Vue from 'vue';
-import { apiUrl, copyright } from '../../../config';
+import { apiUrl, copyright, host, name, description } from '../../../config';
export default Vue.extend({
data() {
return {
- signing: false,
- user: null,
- username: '',
- password: '',
- token: '',
apiUrl,
copyright,
- users: []
+ stats: null,
+ host,
+ name,
+ description
};
},
- mounted() {
- (this as any).api('users', {
- sort: '+follower',
- limit: 20
- }).then(users => {
- this.users = users;
+ created() {
+ (this as any).api('stats').then(stats => {
+ this.stats = stats;
});
- },
- methods: {
- onUsernameChange() {
- (this as any).api('users/show', {
- username: this.username
- }).then(user => {
- this.user = user;
- });
- },
- onSubmit() {
- this.signing = true;
-
- (this as any).api('signin', {
- username: this.username,
- password: this.password,
- token: this.user && this.user.twoFactorEnabled ? this.token : undefined
- }).then(() => {
- location.reload();
- }).catch(() => {
- alert('something happened');
- this.signing = false;
- });
- }
}
});
</script>
<style lang="stylus" scoped>
.welcome
- background linear-gradient(to bottom, #1e1d65, #bd6659)
+ text-align center
+ //background #fff
> div
- padding 16px
+ padding 32px
margin 0 auto
max-width 500px
- h1
- margin 0
- padding 8px
- font-size 1.5em
- font-weight normal
- color #cacac3
+ > img
+ display block
+ max-width 200px
+ margin 0 auto
- & + p
- margin 0 0 16px 0
- padding 0 8px 0 8px
- color #949fa9
+ > .host
+ display block
+ text-align center
+ padding 6px 12px
+ line-height 32px
+ font-weight bold
+ color #333
+ background rgba(#000, 0.035)
+ border-radius 6px
- .form
- margin-bottom 16px
+ > .about
+ margin-top 16px
+ padding 16px
+ color #555
background #fff
- border solid 1px rgba(#000, 0.2)
- border-radius 8px
- overflow hidden
+ border-radius 6px
- > p
+ > h2
margin 0
- padding 12px 20px
- color #555
- background #f5f5f5
- border-bottom solid 1px #ddd
-
- > div
- > form
- padding 16px
- border-bottom solid 1px #ddd
+ > p
+ margin 8px
- input
- display block
- padding 12px
- margin 0 0 16px 0
- width 100%
- font-size 1em
- color rgba(#000, 0.7)
- background #fff
- outline none
- border solid 1px #ddd
- border-radius 4px
+ > .signup
+ font-weight bold
- button
- display block
- width 100%
- padding 10px
- margin 0
- color #333
- font-size 1em
- 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 4px
+ > .login
+ margin 16px 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)
+ > form
- > div
- padding 16px
+ button
+ display block
+ width 100%
+ padding 10px
+ margin 0
+ color #333
+ font-size 1em
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 4px
- > .tl
- background #fff
- border solid 1px rgba(#000, 0.2)
- border-radius 8px
- overflow hidden
-
- > p
- margin 0
- padding 12px 20px
- color #555
- background #f5f5f5
- border-bottom solid 1px #ddd
+ &: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)
- > .mk-welcome-timeline
+ > .tl
+ > *
max-height 300px
+ border-radius 6px
overflow auto
+ -webkit-overflow-scrolling touch
- > .users
- margin 12px 0 0 0
+ > .stats
+ margin 16px 0
+ padding 8px
+ font-size 14px
+ color #444
+ background rgba(#000, 0.1)
+ border-radius 6px
> *
- display inline-block
- margin 4px
- width 38px
- height 38px
- border-radius 6px
+ margin 0 8px
> footer
text-align center
- color #fff
+ color #444
> small
display block
diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/widgets.vue
index a5ca6cb4a2..294c80e7a0 100644
--- a/src/client/app/mobile/views/pages/dashboard.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -8,18 +8,21 @@
<template v-if="customizing">
<header>
<select v-model="widgetAdderSelected">
- <option value="profile">プロフィール</option>
- <option value="calendar">カレンダー</option>
- <option value="activity">アクティビティ</option>
- <option value="rss">RSSリーダー</option>
- <option value="photo-stream">フォトストリーム</option>
- <option value="slideshow">スライドショー</option>
- <option value="version">バージョン</option>
- <option value="access-log">アクセスログ</option>
- <option value="server">サーバー情報</option>
- <option value="donation">寄付のお願い</option>
- <option value="nav">ナビゲーション</option>
- <option value="tips">ヒント</option>
+ <option value="profile">%i18n:common.widgets.profile%</option>
+ <option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
+ <option value="calendar">%i18n:common.widgets.calendar%</option>
+ <option value="activity">%i18n:common.widgets.activity%</option>
+ <option value="rss">%i18n:common.widgets.rss%</option>
+ <option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
+ <option value="slideshow">%i18n:common.widgets.slideshow%</option>
+ <option value="hashtags">%i18n:common.widgets.hashtags%</option>
+ <option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
+ <option value="version">%i18n:common.widgets.version%</option>
+ <option value="server">%i18n:common.widgets.server%</option>
+ <option value="memo">%i18n:common.widgets.memo%</option>
+ <option value="donation">%i18n:common.widgets.donation%</option>
+ <option value="nav">%i18n:common.widgets.nav%</option>
+ <option value="tips">%i18n:common.widgets.tips%</option>
</select>
<button @click="addWidget">追加</button>
<p><a @click="hint">カスタマイズのヒント</a></p>
@@ -34,13 +37,13 @@
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
</header>
<div @click="widgetFunc(widget.id)">
- <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/>
+ <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="mobile"/>
</div>
</div>
</x-draggable>
</template>
<template v-else>
- <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
+ <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="mobile"/>
</template>
</main>
</mk-ui>
@@ -55,17 +58,24 @@ export default Vue.extend({
components: {
XDraggable
},
+
data() {
return {
showNav: false,
- widgets: [],
customizing: false,
widgetAdderSelected: null
};
},
+
+ computed: {
+ widgets(): any[] {
+ return this.$store.state.settings.mobileHome;
+ }
+ },
+
created() {
- if ((this as any).clientSettings.mobileHome == null) {
- Vue.set((this as any).clientSettings, 'mobileHome', [{
+ if (this.widgets.length == 0) {
+ this.widgets = [{
name: 'calendar',
id: 'a', data: {}
}, {
@@ -86,18 +96,9 @@ export default Vue.extend({
}, {
name: 'version',
id: 'g', data: {}
- }]);
- this.widgets = (this as any).clientSettings.mobileHome;
+ }];
this.saveHome();
- } else {
- this.widgets = (this as any).clientSettings.mobileHome;
}
-
- this.$watch('clientSettings', i => {
- this.widgets = (this as any).clientSettings.mobileHome;
- }, {
- deep: true
- });
},
mounted() {
@@ -105,46 +106,33 @@ export default Vue.extend({
},
methods: {
- onHomeUpdated(data) {
- if (data.home) {
- (this as any).clientSettings.mobileHome = data.home;
- this.widgets = data.home;
- } else {
- const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id);
- if (w != null) {
- w.data = data.data;
- this.$refs[w.id][0].preventSave = true;
- this.$refs[w.id][0].props = w.data;
- this.widgets = (this as any).clientSettings.mobileHome;
- }
- }
- },
hint() {
alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
},
+
widgetFunc(id) {
const w = this.$refs[id][0];
if (w.func) w.func();
},
+
onWidgetSort() {
this.saveHome();
},
+
addWidget() {
- const widget = {
+ this.$store.dispatch('settings/addMobileHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
data: {}
- };
-
- this.widgets.unshift(widget);
- this.saveHome();
+ });
},
+
removeWidget(widget) {
- this.widgets = this.widgets.filter(w => w.id != widget.id);
- this.saveHome();
+ this.$store.dispatch('settings/removeMobileHomeWidget', widget);
},
+
saveHome() {
- (this as any).clientSettings.mobileHome = this.widgets;
+ this.$store.commit('settings/setMobileHome', this.widgets);
(this as any).api('i/update_mobile_home', {
home: this.widgets
});
@@ -156,17 +144,25 @@ export default Vue.extend({
<style lang="stylus" scoped>
main
margin 0 auto
+ padding 8px
max-width 500px
+ width 100%
@media (min-width 500px)
- padding 8px
+ padding 16px 8px
+
+ @media (min-width 600px)
+ padding 32px 8px
> header
padding 8px
background #fff
.widget
- margin 8px
+ margin-bottom 8px
+
+ @media (min-width 600px)
+ margin-bottom 16px
.customize-container
margin 8px
diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue
index 7763be41f5..85f925ceda 100644
--- a/src/client/app/mobile/views/widgets/activity.vue
+++ b/src/client/app/mobile/views/widgets/activity.vue
@@ -3,7 +3,7 @@
<mk-widget-container :show-header="!props.compact">
<template slot="header">%fa:chart-bar%アクティビティ</template>
<div :class="$style.body">
- <mk-activity :user="os.i"/>
+ <mk-activity :user="$store.state.i"/>
</div>
</mk-widget-container>
</div>
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
index 59c1ec7c0e..a94f7e94b8 100644
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ b/src/client/app/mobile/views/widgets/profile.vue
@@ -2,13 +2,13 @@
<div class="mkw-profile">
<mk-widget-container>
<div :class="$style.banner"
- :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
+ :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl}?thumbnail&size=256)` : ''"
></div>
<img :class="$style.avatar"
- :src="`${os.i.avatarUrl}?thumbnail&size=96`"
+ :src="`${$store.state.i.avatarUrl}?thumbnail&size=96`"
alt="avatar"
/>
- <router-link :class="$style.name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
+ <router-link :class="$style.name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
</mk-widget-container>
</div>
</template>
diff --git a/src/client/app/reset.styl b/src/client/app/reset.styl
index 10bd3113a2..c0a88f27b0 100644
--- a/src/client/app/reset.styl
+++ b/src/client/app/reset.styl
@@ -1,3 +1,6 @@
+input
+ min-width 0px
+
input:not([type])
input[type='text']
input[type='password']
diff --git a/src/client/app/safe.js b/src/client/app/safe.js
index 2fd5361725..3d73fa1a9c 100644
--- a/src/client/app/safe.js
+++ b/src/client/app/safe.js
@@ -5,10 +5,10 @@
// Detect an old browser
if (!('fetch' in window)) {
alert(
- 'お使いのブラウザが古いためMisskeyを動作させることができません。' +
+ 'お使いのブラウザ(またはOS)が古いためMisskeyを動作させることができません。' +
'バージョンを最新のものに更新するか、別のブラウザをお試しください。' +
'\n\n' +
- 'Your browser seems outdated. ' +
+ 'Your browser (or your OS) seems outdated. ' +
'To run Misskey, please update your browser to latest version or try other browsers.');
}
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 0bdfdef6a0..267c804fbd 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -1,8 +1,13 @@
import Vuex from 'vuex';
+import createPersistedState from 'vuex-persistedstate';
+
import MiOS from './mios';
+import { hostname } from './config';
const defaultSettings = {
- home: [],
+ home: null,
+ mobileHome: [],
+ deck: null,
fetchOnScroll: true,
showMaps: true,
showPostFormOnTopOfTl: false,
@@ -10,59 +15,276 @@ const defaultSettings = {
gradientWindowHeader: false,
showReplyTarget: true,
showMyRenotes: true,
- showRenotedMyNotes: true
+ showRenotedMyNotes: true,
+ loadRemoteMedia: true,
+ disableViaMobile: false,
+ memo: null
+};
+
+const defaultDeviceSettings = {
+ apiViaStream: true,
+ autoPopout: false,
+ darkmode: false,
+ enableSounds: true,
+ soundVolume: 0.5,
+ lang: null,
+ preventUpdate: false,
+ debug: false,
+ lightmode: false,
+ loadRawImages: false,
+ postStyle: 'standard'
};
export default (os: MiOS) => new Vuex.Store({
- plugins: [store => {
- store.subscribe((mutation, state) => {
- if (mutation.type.startsWith('settings/')) {
- localStorage.setItem('settings', JSON.stringify(state.settings.data));
- }
- });
- }],
+ plugins: [createPersistedState({
+ paths: ['i', 'device', 'settings']
+ })],
state: {
+ i: null,
+ indicate: false,
uiHeaderHeight: 0
},
+ getters: {
+ isSignedIn: state => state.i != null
+ },
+
mutations: {
+ updateI(state, x) {
+ state.i = x;
+ },
+
+ updateIKeyValue(state, x) {
+ state.i[x.key] = x.value;
+ },
+
+ indicate(state, x) {
+ state.indicate = x;
+ },
+
setUiHeaderHeight(state, height) {
state.uiHeaderHeight = height;
}
},
+ actions: {
+ login(ctx, i) {
+ ctx.commit('updateI', i);
+ ctx.dispatch('settings/merge', i.clientSettings);
+ },
+
+ logout(ctx) {
+ ctx.commit('updateI', null);
+ document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ },
+
+ mergeMe(ctx, me) {
+ Object.entries(me).forEach(([key, value]) => {
+ ctx.commit('updateIKeyValue', { key, value });
+ });
+
+ if (me.clientSettings) {
+ ctx.dispatch('settings/merge', me.clientSettings);
+ }
+ },
+ },
+
modules: {
+ device: {
+ namespaced: true,
+
+ state: defaultDeviceSettings,
+
+ mutations: {
+ set(state, x: { key: string; value: any }) {
+ state[x.key] = x.value;
+ },
+
+ setTl(state, x) {
+ state.tl = {
+ src: x.src,
+ arg: x.arg
+ };
+ }
+ }
+ },
+
settings: {
namespaced: true,
- state: {
- data: defaultSettings
- },
+ state: defaultSettings,
mutations: {
set(state, x: { key: string; value: any }) {
- state.data[x.key] = x.value;
+ state[x.key] = x.value;
},
setHome(state, data) {
- state.data.home = data;
+ state.home = data;
},
- setHomeWidget(state, x) {
- const w = state.data.home.find(w => w.id == x.id);
- if (w) {
- w.data = x.data;
+ addHomeWidget(state, widget) {
+ state.home.unshift(widget);
+ },
+
+ setMobileHome(state, data) {
+ state.mobileHome = data;
+ },
+
+ setWidget(state, x) {
+ let w;
+
+ //#region Decktop home
+ if (state.home) {
+ w = state.home.find(w => w.id == x.id);
+ if (w) {
+ w.data = x.data;
+ }
}
+ //#endregion
+
+ //#region Mobile home
+ if (state.mobileHome) {
+ w = state.mobileHome.find(w => w.id == x.id);
+ if (w) {
+ w.data = x.data;
+ }
+ }
+ //#endregion
+
+ //#region Deck
+ if (state.deck && state.deck.columns) {
+ state.deck.columns.filter(c => c.type == 'widgets').forEach(c => {
+ c.widgets.forEach(w => {
+ if (w.id == x.id) w.data = x.data;
+ });
+ });
+ }
+ //#endregion
},
- addHomeWidget(state, widget) {
- state.data.home.unshift(widget);
+ addMobileHomeWidget(state, widget) {
+ state.mobileHome.unshift(widget);
+ },
+
+ removeMobileHomeWidget(state, widget) {
+ state.mobileHome = state.mobileHome.filter(w => w.id != widget.id);
+ },
+
+ addDeckColumn(state, column) {
+ state.deck.columns.push(column);
+ state.deck.layout.push([column.id]);
+ },
+
+ 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.filter(ids => ids.length > 0);
+ },
+
+ swapDeckColumn(state, x) {
+ const a = x.a;
+ const b = x.b;
+ const aX = state.deck.layout.findIndex(ids => ids.indexOf(a) != -1);
+ const aY = state.deck.layout[aX].findIndex(id => id == a);
+ const bX = state.deck.layout.findIndex(ids => ids.indexOf(b) != -1);
+ const bY = state.deck.layout[bX].findIndex(id => id == b);
+ state.deck.layout[aX][aY] = b;
+ state.deck.layout[bX][bY] = a;
+ },
+
+ swapLeftDeckColumn(state, id) {
+ state.deck.layout.some((ids, i) => {
+ if (ids.indexOf(id) != -1) {
+ const left = state.deck.layout[i - 1];
+ if (left) {
+ state.deck.layout[i - 1] = state.deck.layout[i];
+ state.deck.layout[i] = left;
+ }
+ return true;
+ }
+ });
+ },
+
+ swapRightDeckColumn(state, id) {
+ state.deck.layout.some((ids, i) => {
+ if (ids.indexOf(id) != -1) {
+ const right = state.deck.layout[i + 1];
+ if (right) {
+ state.deck.layout[i + 1] = state.deck.layout[i];
+ state.deck.layout[i] = right;
+ }
+ return true;
+ }
+ });
+ },
+
+ swapUpDeckColumn(state, id) {
+ const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
+ ids.some((x, i) => {
+ if (x == id) {
+ const up = ids[i - 1];
+ if (up) {
+ ids[i - 1] = id;
+ ids[i] = up;
+ }
+ return true;
+ }
+ });
+ },
+
+ swapDownDeckColumn(state, id) {
+ const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
+ ids.some((x, i) => {
+ if (x == id) {
+ const down = ids[i + 1];
+ if (down) {
+ ids[i + 1] = id;
+ ids[i] = down;
+ }
+ return true;
+ }
+ });
+ },
+
+ 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));
+ 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);
+ },
+
+ 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.splice(i + 1, 0, [id]);
+ state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
+ },
+
+ addDeckWidget(state, x) {
+ const column = state.deck.columns.find(c => c.id == x.id);
+ if (column == null) return;
+ column.widgets.unshift(x.widget);
+ },
+
+ removeDeckWidget(state, x) {
+ const column = state.deck.columns.find(c => c.id == x.id);
+ if (column == null) return;
+ column.widgets = column.widgets.filter(w => w.id != x.widget.id);
+ },
+
+ renameDeckColumn(state, x) {
+ const column = state.deck.columns.find(c => c.id == x.id);
+ if (column == null) return;
+ column.name = x.name;
}
},
actions: {
merge(ctx, settings) {
+ if (settings == null) return;
Object.entries(settings).forEach(([key, value]) => {
ctx.commit('set', { key, value });
});
@@ -71,7 +293,7 @@ export default (os: MiOS) => new Vuex.Store({
set(ctx, x) {
ctx.commit('set', x);
- if (os.isSignedIn) {
+ if (ctx.rootGetters.isSignedIn) {
os.api('i/update_client_setting', {
name: x.key,
value: x.value
@@ -79,11 +301,94 @@ export default (os: MiOS) => new Vuex.Store({
}
},
+ saveDeck(ctx) {
+ os.api('i/update_client_setting', {
+ name: 'deck',
+ value: ctx.state.deck
+ });
+ },
+
+ addDeckColumn(ctx, column) {
+ ctx.commit('addDeckColumn', column);
+ ctx.dispatch('saveDeck');
+ },
+
+ removeDeckColumn(ctx, id) {
+ ctx.commit('removeDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ swapDeckColumn(ctx, id) {
+ ctx.commit('swapDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ swapLeftDeckColumn(ctx, id) {
+ ctx.commit('swapLeftDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ swapRightDeckColumn(ctx, id) {
+ ctx.commit('swapRightDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ swapUpDeckColumn(ctx, id) {
+ ctx.commit('swapUpDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ swapDownDeckColumn(ctx, id) {
+ ctx.commit('swapDownDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ stackLeftDeckColumn(ctx, id) {
+ ctx.commit('stackLeftDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ popRightDeckColumn(ctx, id) {
+ ctx.commit('popRightDeckColumn', id);
+ ctx.dispatch('saveDeck');
+ },
+
+ addDeckWidget(ctx, x) {
+ ctx.commit('addDeckWidget', x);
+ ctx.dispatch('saveDeck');
+ },
+
+ removeDeckWidget(ctx, x) {
+ ctx.commit('removeDeckWidget', x);
+ ctx.dispatch('saveDeck');
+ },
+
+ renameDeckColumn(ctx, x) {
+ ctx.commit('renameDeckColumn', x);
+ ctx.dispatch('saveDeck');
+ },
+
addHomeWidget(ctx, widget) {
ctx.commit('addHomeWidget', widget);
os.api('i/update_home', {
- home: ctx.state.data.home
+ home: ctx.state.home
+ });
+ },
+
+ addMobileHomeWidget(ctx, widget) {
+ ctx.commit('addMobileHomeWidget', widget);
+
+ os.api('i/update_mobile_home', {
+ home: ctx.state.mobileHome
+ });
+ },
+
+ removeMobileHomeWidget(ctx, widget) {
+ ctx.commit('removeMobileHomeWidget', widget);
+
+ os.api('i/update_mobile_home', {
+ home: ctx.state.mobileHome.filter(w => w.id != widget.id)
});
}
}
diff --git a/src/client/assets/manifest.json b/src/client/assets/manifest.json
index a0f6745b01..25be82fdc6 100644
--- a/src/client/assets/manifest.json
+++ b/src/client/assets/manifest.json
@@ -4,11 +4,39 @@
"start_url": "/",
"display": "standalone",
"background_color": "#313a42",
- "icons": {
- "16": "/assets/favicon/16.png",
- "32": "/assets/favicon/32.png",
- "64": "/assets/favicon/64.png",
- "128": "/assets/favicon/128.png",
- "256": "/assets/favicon/256.png"
+ "icons": [
+ {
+ "src": "/assets/favicon/16.png",
+ "size": "16x16",
+ "type": "image/png"
+ },
+ {
+ "src": "/assets/favicon/32.png",
+ "size": "32x32",
+ "type": "image/png"
+ },
+ {
+ "src": "/assets/favicon/64.png",
+ "size": "64x64",
+ "type": "image/png"
+ },
+ {
+ "src": "/assets/favicon/128.png",
+ "size": "128x128",
+ "type": "image/png"
+ },
+ {
+ "src": "/assets/favicon/192.png",
+ "size": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/assets/favicon/256.png",
+ "size": "256x256",
+ "type": "image/png"
+ }
+ ],
+ "share_target": {
+ "url_template": "share?text={title}%20-%20{text}%20-%20{url}"
}
}
diff --git a/src/client/assets/pointer.png b/src/client/assets/pointer.png
new file mode 100644
index 0000000000..c8bd07a3ae
--- /dev/null
+++ b/src/client/assets/pointer.png
Binary files differ
diff --git a/src/client/assets/othello-put-me.mp3 b/src/client/assets/reversi-put-me.mp3
index 4e0e72091c..4e0e72091c 100644
--- a/src/client/assets/othello-put-me.mp3
+++ b/src/client/assets/reversi-put-me.mp3
Binary files differ
diff --git a/src/client/assets/othello-put-you.mp3 b/src/client/assets/reversi-put-you.mp3
index 9244189c2d..9244189c2d 100644
--- a/src/client/assets/othello-put-you.mp3
+++ b/src/client/assets/reversi-put-you.mp3
Binary files differ
diff --git a/src/client/assets/title.dark.svg b/src/client/assets/title.dark.svg
new file mode 100644
index 0000000000..10139024ad
--- /dev/null
+++ b/src/client/assets/title.dark.svg
@@ -0,0 +1,140 @@
+<?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:#fff;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"
+ 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"
+ 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"
+ 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"
+ 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"
+ 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"
+ 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"
+ 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.light.svg b/src/client/assets/title.light.svg
new file mode 100644
index 0000000000..95ad11c399
--- /dev/null
+++ b/src/client/assets/title.light.svg
@@ -0,0 +1,140 @@
+<?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.svg b/src/client/assets/title.svg
deleted file mode 100644
index 747fcd38b1..0000000000
--- a/src/client/assets/title.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle fill="#2B2F2D" cx="128" cy="153.6" r="19.201"/>
-<circle fill="#2B2F2D" cx="51.2" cy="153.6" r="19.2"/>
-<circle fill="#2B2F2D" cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#2B2F2D" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6
- 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle fill="#2B2F2D" cx="89.6" cy="102.4" r="19.2"/>
-<circle fill="#2B2F2D" cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/client/assets/version.html b/src/client/assets/version.html
index d8a98279a6..177d37db8f 100644
--- a/src/client/assets/version.html
+++ b/src/client/assets/version.html
@@ -10,11 +10,6 @@
localStorage.setItem('v', v);
}
- const lang = window.prompt('Enter language (optional):');
- if (lang && lang.length > 0) {
- localStorage.setItem('lang', lang);
- }
-
setTimeout(() => {
location.href = '/';
}, 500);
diff --git a/src/client/assets/welcome-bg.dark.svg b/src/client/assets/welcome-bg.dark.svg
new file mode 100644
index 0000000000..1866170327
--- /dev/null
+++ b/src/client/assets/welcome-bg.dark.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="900"><path d="M667,476L630,367L567,460Z" fill="#121931" stroke="#121931" stroke-width="1.51"/><path d="M630,367L576,311L567,460Z" fill="#121c31" stroke="#121c31" stroke-width="1.51"/><path d="M567,460L631,579L667,476Z" fill="#121731" stroke="#121731" stroke-width="1.51"/><path d="M567,460L556,584L631,579Z" fill="#131830" stroke="#131830" stroke-width="1.51"/><path d="M437,475L556,584L567,460Z" fill="#121a31" stroke="#121a31" stroke-width="1.51"/><path d="M667,476L773,349L630,367Z" fill="#121831" stroke="#121831" stroke-width="1.51"/><path d="M630,367L580,255L576,311Z" fill="#122031" stroke="#122031" stroke-width="1.51"/><path d="M813,438L773,349L667,476Z" fill="#121530" stroke="#121530" stroke-width="1.51"/><path d="M576,311L437,475L567,460Z" fill="#111d31" stroke="#111d31" stroke-width="1.51"/><path d="M642,193L580,255L630,367Z" fill="#112131" stroke="#112131" stroke-width="1.51"/><path d="M800,574L813,438L667,476Z" fill="#121330" stroke="#121330" stroke-width="1.51"/><path d="M400,299L437,475L576,311Z" fill="#122231" stroke="#122231" stroke-width="1.51"/><path d="M556,584L583,647L631,579Z" fill="#14162f" stroke="#14162f" stroke-width="1.51"/><path d="M631,579L800,574L667,476Z" fill="#121430" stroke="#121430" stroke-width="1.51"/><path d="M423,693L583,647L556,584Z" fill="#14192f" stroke="#14192f" stroke-width="1.51"/><path d="M653,680L800,574L631,579Z" fill="#13132f" stroke="#13132f" stroke-width="1.51"/><path d="M773,349L642,193L630,367Z" fill="#111d31" stroke="#111d31" stroke-width="1.51"/><path d="M813,438L874,423L773,349Z" fill="#131630" stroke="#131630" stroke-width="1.51"/><path d="M773,349L789,203L642,193Z" fill="#121f31" stroke="#121f31" stroke-width="1.51"/><path d="M906,568L874,423L813,438Z" fill="#13122f" stroke="#13122f" stroke-width="1.51"/><path d="M583,647L653,680L631,579Z" fill="#14152e" stroke="#14152e" stroke-width="1.51"/><path d="M449,219L400,299L576,311Z" fill="#112631" stroke="#112631" stroke-width="1.51"/><path d="M449,219L576,311L580,255Z" fill="#112431" stroke="#112431" stroke-width="1.51"/><path d="M437,475L411,599L556,584Z" fill="#131c30" stroke="#131c30" stroke-width="1.51"/><path d="M556,765L666,768L653,680Z" fill="#15142d" stroke="#15142d" stroke-width="1.51"/><path d="M327,557L411,599L437,475Z" fill="#131f30" stroke="#131f30" stroke-width="1.51"/><path d="M572,116L449,219L580,255Z" fill="#102731" stroke="#102731" stroke-width="1.51"/><path d="M920,229L789,203L773,349Z" fill="#131d30" stroke="#131d30" stroke-width="1.51"/><path d="M874,423L909,355L773,349Z" fill="#131730" stroke="#131730" stroke-width="1.51"/><path d="M1018,299L909,355L1040,413Z" fill="#14172e" stroke="#14172e" stroke-width="1.51"/><path d="M642,193L572,116L580,255Z" fill="#112631" stroke="#112631" stroke-width="1.51"/><path d="M802,140L639,121L642,193Z" fill="#112531" stroke="#112531" stroke-width="1.51"/><path d="M800,574L906,568L813,438Z" fill="#13122f" stroke="#13122f" stroke-width="1.51"/><path d="M860,653L906,568L800,574Z" fill="#14112e" stroke="#14112e" stroke-width="1.51"/><path d="M653,680L792,696L800,574Z" fill="#14112e" stroke="#14112e" stroke-width="1.51"/><path d="M639,121L572,116L642,193Z" fill="#102731" stroke="#102731" stroke-width="1.51"/><path d="M280,464L327,557L437,475Z" fill="#122131" stroke="#122131" stroke-width="1.51"/><path d="M792,696L860,653L800,574Z" fill="#15102d" stroke="#15102d" stroke-width="1.51"/><path d="M315,686L423,693L411,599Z" fill="#141d2f" stroke="#141d2f" stroke-width="1.51"/><path d="M411,599L423,693L556,584Z" fill="#141b2f" stroke="#141b2f" stroke-width="1.51"/><path d="M653,680L666,768L792,696Z" fill="#14112d" stroke="#14112d" stroke-width="1.51"/><path d="M400,299L298,362L437,475Z" fill="#112431" stroke="#112431" stroke-width="1.51"/><path d="M289,189L298,362L400,299Z" fill="#0f2a31" stroke="#0f2a31" stroke-width="1.51"/><path d="M556,765L653,680L583,647Z" fill="#15152e" stroke="#15152e" stroke-width="1.51"/><path d="M789,203L802,140L642,193Z" fill="#112231" stroke="#112231" stroke-width="1.51"/><path d="M920,229L802,140L789,203Z" fill="#132130" stroke="#132130" stroke-width="1.51"/><path d="M423,693L556,765L583,647Z" fill="#15182e" stroke="#15182e" stroke-width="1.51"/><path d="M298,362L280,464L437,475Z" fill="#112331" stroke="#112331" stroke-width="1.51"/><path d="M909,355L920,229L773,349Z" fill="#131a30" stroke="#131a30" stroke-width="1.51"/><path d="M1018,299L920,229L909,355Z" fill="#141a2f" stroke="#141a2f" stroke-width="1.51"/><path d="M675,877L748,809L666,768Z" fill="#17112d" stroke="#17112d" stroke-width="1.51"/><path d="M423,693L443,781L556,765Z" fill="#16192e" stroke="#16192e" stroke-width="1.51"/><path d="M336,786L443,781L423,693Z" fill="#161c2d" stroke="#161c2d" stroke-width="1.51"/><path d="M792,696L924,797L860,653Z" fill="#150e2c" stroke="#150e2c" stroke-width="1.51"/><path d="M666,768L748,809L792,696Z" fill="#150f2d" stroke="#150f2d" stroke-width="1.51"/><path d="M675,877L666,768L556,765Z" fill="#17132d" stroke="#17132d" stroke-width="1.51"/><path d="M327,557L315,686L411,599Z" fill="#141f2f" stroke="#141f2f" stroke-width="1.51"/><path d="M224,597L315,686L327,557Z" fill="#14222f" stroke="#14222f" stroke-width="1.51"/><path d="M566,23L421,77L572,116Z" fill="#132a2e" stroke="#132a2e" stroke-width="1.51"/><path d="M572,116L421,77L449,219Z" fill="#0e2d31" stroke="#0e2d31" stroke-width="1.51"/><path d="M449,219L289,189L400,299Z" fill="#0f2b31" stroke="#0f2b31" stroke-width="1.51"/><path d="M566,23L572,116L639,121Z" fill="#13282f" stroke="#13282f" stroke-width="1.51"/><path d="M644,-20L566,23L639,121Z" fill="#16272d" stroke="#16272d" stroke-width="1.51"/><path d="M328,133L289,189L449,219Z" fill="#0d2f31" stroke="#0d2f31" stroke-width="1.51"/><path d="M171,486L224,597L280,464Z" fill="#13232f" stroke="#13232f" stroke-width="1.51"/><path d="M1040,413L909,355L874,423Z" fill="#14152f" stroke="#14152f" stroke-width="1.51"/><path d="M920,229L902,89L802,140Z" fill="#132230" stroke="#132230" stroke-width="1.51"/><path d="M1020,585L906,568L1018,660Z" fill="#150f2c" stroke="#150f2c" stroke-width="1.51"/><path d="M1020,585L1040,413L906,568Z" fill="#14102d" stroke="#14102d" stroke-width="1.51"/><path d="M906,568L1040,413L874,423Z" fill="#13122f" stroke="#13122f" stroke-width="1.51"/><path d="M421,77L328,133L449,219Z" fill="#0d2f31" stroke="#0d2f31" stroke-width="1.51"/><path d="M1018,299L987,217L920,229Z" fill="#141d2f" stroke="#141d2f" stroke-width="1.51"/><path d="M755,-6L644,-20L639,121Z" fill="#16242d" stroke="#16242d" stroke-width="1.51"/><path d="M1018,660L906,568L860,653Z" fill="#15102c" stroke="#15102c" stroke-width="1.51"/><path d="M190,365L280,464L298,362Z" fill="#102731" stroke="#102731" stroke-width="1.51"/><path d="M280,464L224,597L327,557Z" fill="#122330" stroke="#122330" stroke-width="1.51"/><path d="M225,235L190,365L298,362Z" fill="#102b31" stroke="#102b31" stroke-width="1.51"/><path d="M1009,122L902,89L920,229Z" fill="#14222f" stroke="#14222f" stroke-width="1.51"/><path d="M289,189L225,235L298,362Z" fill="#0d2e31" stroke="#0d2e31" stroke-width="1.51"/><path d="M185,70L225,235L289,189Z" fill="#0c3330" stroke="#0c3330" stroke-width="1.51"/><path d="M527,877L675,877L556,765Z" fill="#19152d" stroke="#19152d" stroke-width="1.51"/><path d="M976,767L1018,660L860,653Z" fill="#160e2c" stroke="#160e2c" stroke-width="1.51"/><path d="M566,23L439,-17L421,77Z" fill="#152c2c" stroke="#152c2c" stroke-width="1.51"/><path d="M755,-6L639,121L802,140Z" fill="#14242f" stroke="#14242f" stroke-width="1.51"/><path d="M190,365L171,486L280,464Z" fill="#132430" stroke="#132430" stroke-width="1.51"/><path d="M902,89L755,-6L802,140Z" fill="#15232e" stroke="#15232e" stroke-width="1.51"/><path d="M924,797L792,696L748,809Z" fill="#160e2c" stroke="#160e2c" stroke-width="1.51"/><path d="M1020,585L1106,477L1040,413Z" fill="#14102d" stroke="#14102d" stroke-width="1.51"/><path d="M443,781L527,877L556,765Z" fill="#17182d" stroke="#17182d" stroke-width="1.51"/><path d="M427,924L527,877L443,781Z" fill="#1b192d" stroke="#1b192d" stroke-width="1.51"/><path d="M315,686L336,786L423,693Z" fill="#151e2e" stroke="#151e2e" stroke-width="1.51"/><path d="M201,784L336,786L315,686Z" fill="#16202d" stroke="#16202d" stroke-width="1.51"/><path d="M779,911L924,797L748,809Z" fill="#190e2c" stroke="#190e2c" stroke-width="1.51"/><path d="M421,77L296,-6L328,133Z" fill="#10312e" stroke="#10312e" stroke-width="1.51"/><path d="M328,133L185,70L289,189Z" fill="#093431" stroke="#093431" stroke-width="1.51"/><path d="M545,-110L439,-17L566,23Z" fill="#18292a" stroke="#18292a" stroke-width="1.51"/><path d="M1098,562L1106,477L1020,585Z" fill="#150f2d" stroke="#150f2d" stroke-width="1.51"/><path d="M1040,413L1149,365L1018,299Z" fill="#15162e" stroke="#15162e" stroke-width="1.51"/><path d="M1135,188L1009,122L987,217Z" fill="#14202e" stroke="#14202e" stroke-width="1.51"/><path d="M224,597L189,651L315,686Z" fill="#15222e" stroke="#15222e" stroke-width="1.51"/><path d="M122,584L189,651L224,597Z" fill="#16212d" stroke="#16212d" stroke-width="1.51"/><path d="M1095,646L1098,562L1020,585Z" fill="#160e2c" stroke="#160e2c" stroke-width="1.51"/><path d="M924,797L976,767L860,653Z" fill="#160d2b" stroke="#160d2b" stroke-width="1.51"/><path d="M1095,646L1020,585L1018,660Z" fill="#160e2b" stroke="#160e2b" stroke-width="1.51"/><path d="M902,89L855,-47L755,-6Z" fill="#18222b" stroke="#18222b" stroke-width="1.51"/><path d="M987,217L1009,122L920,229Z" fill="#14202f" stroke="#14202f" stroke-width="1.51"/><path d="M1135,188L987,217L1018,299Z" fill="#141c2e" stroke="#141c2e" stroke-width="1.51"/><path d="M675,877L779,911L748,809Z" fill="#1a102d" stroke="#1a102d" stroke-width="1.51"/><path d="M924,797L1032,900L976,767Z" fill="#190d2a" stroke="#190d2a" stroke-width="1.51"/><path d="M758,1032L779,911L675,877Z" fill="#1d102d" stroke="#1d102d" stroke-width="1.51"/><path d="M1153,782L1095,646L1018,660Z" fill="#170c2b" stroke="#170c2b" stroke-width="1.51"/><path d="M1262,481L1149,365L1106,477Z" fill="#15102d" stroke="#15102d" stroke-width="1.51"/><path d="M171,486L122,584L224,597Z" fill="#16222d" stroke="#16222d" stroke-width="1.51"/><path d="M70,425L122,584L171,486Z" fill="#17232c" stroke="#17232c" stroke-width="1.51"/><path d="M70,425L171,486L190,365Z" fill="#15242e" stroke="#15242e" stroke-width="1.51"/><path d="M527,877L566,1030L675,877Z" fill="#1d152d" stroke="#1d152d" stroke-width="1.51"/><path d="M336,786L350,882L443,781Z" fill="#181d2d" stroke="#181d2d" stroke-width="1.51"/><path d="M201,784L350,882L336,786Z" fill="#18202d" stroke="#18202d" stroke-width="1.51"/><path d="M201,784L315,686L189,651Z" fill="#15212d" stroke="#15212d" stroke-width="1.51"/><path d="M1106,477L1149,365L1040,413Z" fill="#14122d" stroke="#14122d" stroke-width="1.51"/><path d="M1262,481L1106,477L1098,562Z" fill="#150e2c" stroke="#150e2c" stroke-width="1.51"/><path d="M689,-114L545,-110L644,-20Z" fill="#1b2427" stroke="#1b2427" stroke-width="1.51"/><path d="M644,-20L545,-110L566,23Z" fill="#19272a" stroke="#19272a" stroke-width="1.51"/><path d="M1028,16L855,-47L902,89Z" fill="#19212a" stroke="#19212a" stroke-width="1.51"/><path d="M350,882L427,924L443,781Z" fill="#1b1c2d" stroke="#1b1c2d" stroke-width="1.51"/><path d="M439,-17L296,-6L421,77Z" fill="#142e2c" stroke="#142e2c" stroke-width="1.51"/><path d="M225,235L80,349L190,365Z" fill="#132a2e" stroke="#132a2e" stroke-width="1.51"/><path d="M689,-114L644,-20L755,-6Z" fill="#192229" stroke="#192229" stroke-width="1.51"/><path d="M439,-17L324,-108L296,-6Z" fill="#172e29" stroke="#172e29" stroke-width="1.51"/><path d="M753,-160L689,-114L755,-6Z" fill="#1c2127" stroke="#1c2127" stroke-width="1.51"/><path d="M90,203L80,349L225,235Z" fill="#132d2d" stroke="#132d2d" stroke-width="1.51"/><path d="M102,767L201,784L189,651Z" fill="#18202b" stroke="#18202b" stroke-width="1.51"/><path d="M80,349L70,425L190,365Z" fill="#16262d" stroke="#16262d" stroke-width="1.51"/><path d="M1009,122L1028,16L902,89Z" fill="#16212c" stroke="#16212c" stroke-width="1.51"/><path d="M1149,365L1135,188L1018,299Z" fill="#15192e" stroke="#15192e" stroke-width="1.51"/><path d="M296,-6L185,70L328,133Z" fill="#0f332e" stroke="#0f332e" stroke-width="1.51"/><path d="M779,911L893,937L924,797Z" fill="#1b0e2b" stroke="#1b0e2b" stroke-width="1.51"/><path d="M976,767L1153,782L1018,660Z" fill="#170c2a" stroke="#170c2a" stroke-width="1.51"/><path d="M758,1032L893,937L779,911Z" fill="#1f0f2c" stroke="#1f0f2c" stroke-width="1.51"/><path d="M1131,95L1028,16L1009,122Z" fill="#17212c" stroke="#17212c" stroke-width="1.51"/><path d="M855,-47L753,-160L755,-6Z" fill="#1b2128" stroke="#1b2128" stroke-width="1.51"/><path d="M123,103L90,203L225,235Z" fill="#11312e" stroke="#11312e" stroke-width="1.51"/><path d="M80,349L-14,363L70,425Z" fill="#18252b" stroke="#18252b" stroke-width="1.51"/><path d="M1028,16L881,-96L855,-47Z" fill="#1b2028" stroke="#1b2028" stroke-width="1.51"/><path d="M185,70L123,103L225,235Z" fill="#0e342e" stroke="#0e342e" stroke-width="1.51"/><path d="M118,-15L123,103L185,70Z" fill="#15322a" stroke="#15322a" stroke-width="1.51"/><path d="M296,-6L182,-12L185,70Z" fill="#13322a" stroke="#13322a" stroke-width="1.51"/><path d="M545,-110L463,-146L439,-17Z" fill="#1a2927" stroke="#1a2927" stroke-width="1.51"/><path d="M753,-160L463,-146L545,-110Z" fill="#1d2525" stroke="#1d2525" stroke-width="1.51"/><path d="M753,-160L545,-110L689,-114Z" fill="#1d2226" stroke="#1d2226" stroke-width="1.51"/><path d="M122,584L64,646L189,651Z" fill="#18212b" stroke="#18212b" stroke-width="1.51"/><path d="M-55,555L64,646L122,584Z" fill="#1a2029" stroke="#1a2029" stroke-width="1.51"/><path d="M465,1018L566,1030L527,877Z" fill="#20182d" stroke="#20182d" stroke-width="1.51"/><path d="M465,1018L527,877L427,924Z" fill="#1f1a2d" stroke="#1f1a2d" stroke-width="1.51"/><path d="M465,1018L427,924L298,1048Z" fill="#211c2d" stroke="#211c2d" stroke-width="1.51"/><path d="M881,-96L753,-160L855,-47Z" fill="#1c2026" stroke="#1c2026" stroke-width="1.51"/><path d="M1248,72L1131,95L1135,188Z" fill="#16212d" stroke="#16212d" stroke-width="1.51"/><path d="M1135,188L1131,95L1009,122Z" fill="#15222e" stroke="#15222e" stroke-width="1.51"/><path d="M298,1048L427,924L350,882Z" fill="#1f1e2d" stroke="#1f1e2d" stroke-width="1.51"/><path d="M463,-146L324,-108L439,-17Z" fill="#1a2c27" stroke="#1a2c27" stroke-width="1.51"/><path d="M696,1054L758,1032L675,877Z" fill="#20102d" stroke="#20102d" stroke-width="1.51"/><path d="M201,784L170,884L350,882Z" fill="#1b212d" stroke="#1b212d" stroke-width="1.51"/><path d="M64,646L102,767L189,651Z" fill="#18202a" stroke="#18202a" stroke-width="1.51"/><path d="M913,1005L1032,900L893,937Z" fill="#1f0d2b" stroke="#1f0d2b" stroke-width="1.51"/><path d="M893,937L1032,900L924,797Z" fill="#1c0d2b" stroke="#1c0d2b" stroke-width="1.51"/><path d="M207,-137L182,-12L296,-6Z" fill="#173127" stroke="#173127" stroke-width="1.51"/><path d="M566,1030L696,1054L675,877Z" fill="#20132d" stroke="#20132d" stroke-width="1.51"/><path d="M298,1048L696,1054L566,1030Z" fill="#23182d" stroke="#23182d" stroke-width="1.51"/><path d="M1089,882L1153,782L976,767Z" fill="#1a0b2a" stroke="#1a0b2a" stroke-width="1.51"/><path d="M1255,588L1262,481L1098,562Z" fill="#160d2c" stroke="#160d2c" stroke-width="1.51"/><path d="M70,425L-21,485L122,584Z" fill="#19222b" stroke="#19222b" stroke-width="1.51"/><path d="M-5,194L-14,363L80,349Z" fill="#18292a" stroke="#18292a" stroke-width="1.51"/><path d="M-5,194L80,349L90,203Z" fill="#162d2b" stroke="#162d2b" stroke-width="1.51"/><path d="M-5,194L90,203L123,103Z" fill="#13312b" stroke="#13312b" stroke-width="1.51"/><path d="M1255,588L1098,562L1095,646Z" fill="#170d2b" stroke="#170d2b" stroke-width="1.51"/><path d="M1149,365L1263,231L1135,188Z" fill="#161a2d" stroke="#161a2d" stroke-width="1.51"/><path d="M102,767L170,884L201,784Z" fill="#1b202a" stroke="#1b202a" stroke-width="1.51"/><path d="M758,1032L913,1005L893,937Z" fill="#200e2c" stroke="#200e2c" stroke-width="1.51"/><path d="M990,1060L913,1005L758,1032Z" fill="#220e2b" stroke="#220e2b" stroke-width="1.51"/><path d="M64,646L-24,765L102,767Z" fill="#1b1f27" stroke="#1b1f27" stroke-width="1.51"/><path d="M-14,363L-21,485L70,425Z" fill="#192329" stroke="#192329" stroke-width="1.51"/><path d="M1094,1029L1089,882L1032,900Z" fill="#1f0b2a" stroke="#1f0b2a" stroke-width="1.51"/><path d="M1032,900L1089,882L976,767Z" fill="#1b0c2a" stroke="#1b0c2a" stroke-width="1.51"/><path d="M1248,648L1255,588L1095,646Z" fill="#170c2a" stroke="#170c2a" stroke-width="1.51"/><path d="M1318,235L1271,354L1377,345Z" fill="#1b182d" stroke="#1b182d" stroke-width="1.51"/><path d="M1262,481L1271,354L1149,365Z" fill="#17122d" stroke="#17122d" stroke-width="1.51"/><path d="M1253,799L1248,648L1153,782Z" fill="#190a29" stroke="#190a29" stroke-width="1.51"/><path d="M1153,782L1248,648L1095,646Z" fill="#180b2a" stroke="#180b2a" stroke-width="1.51"/><path d="M1131,95L1110,-28L1028,16Z" fill="#192029" stroke="#192029" stroke-width="1.51"/><path d="M1028,16L971,-156L881,-96Z" fill="#1c1f26" stroke="#1c1f26" stroke-width="1.51"/><path d="M881,-96L971,-156L753,-160Z" fill="#1e1f24" stroke="#1e1f24" stroke-width="1.51"/><path d="M1248,72L1110,-28L1131,95Z" fill="#191f29" stroke="#191f29" stroke-width="1.51"/><path d="M1271,354L1263,231L1149,365Z" fill="#17172d" stroke="#17172d" stroke-width="1.51"/><path d="M-121,448L-55,555L-21,485Z" fill="#1c2027" stroke="#1c2027" stroke-width="1.51"/><path d="M-38,73L-5,194L123,103Z" fill="#153229" stroke="#153229" stroke-width="1.51"/><path d="M182,-12L118,-15L185,70Z" fill="#163228" stroke="#163228" stroke-width="1.51"/><path d="M207,-137L118,-15L182,-12Z" fill="#193125" stroke="#193125" stroke-width="1.51"/><path d="M1110,-28L971,-156L1028,16Z" fill="#1c1f26" stroke="#1c1f26" stroke-width="1.51"/><path d="M465,1018L298,1048L566,1030Z" fill="#231b2d" stroke="#231b2d" stroke-width="1.51"/><path d="M170,884L228,990L350,882Z" fill="#1e212d" stroke="#1e212d" stroke-width="1.51"/><path d="M93,915L228,990L170,884Z" fill="#20202a" stroke="#20202a" stroke-width="1.51"/><path d="M-21,485L-55,555L122,584Z" fill="#1a2129" stroke="#1a2129" stroke-width="1.51"/><path d="M102,767L93,915L170,884Z" fill="#1e2029" stroke="#1e2029" stroke-width="1.51"/><path d="M-121,448L-21,485L-14,363Z" fill="#1b2228" stroke="#1b2228" stroke-width="1.51"/><path d="M228,990L298,1048L350,882Z" fill="#21212d" stroke="#21212d" stroke-width="1.51"/><path d="M-55,555L-35,672L64,646Z" fill="#1b1f27" stroke="#1b1f27" stroke-width="1.51"/><path d="M324,-108L207,-137L296,-6Z" fill="#183127" stroke="#183127" stroke-width="1.51"/><path d="M463,-146L207,-137L324,-108Z" fill="#1a2e25" stroke="#1a2e25" stroke-width="1.51"/><path d="M73,-154L207,-137L463,-146Z" fill="#1a3024" stroke="#1a3024" stroke-width="1.51"/><path d="M-24,765L93,915L102,767Z" fill="#1d1f27" stroke="#1d1f27" stroke-width="1.51"/><path d="M228,990L72,1047L298,1048Z" fill="#25212b" stroke="#25212b" stroke-width="1.51"/><path d="M1110,-28L1136,-111L971,-156Z" fill="#1e1e24" stroke="#1e1e24" stroke-width="1.51"/><path d="M1263,231L1248,72L1135,188Z" fill="#171f2d" stroke="#171f2d" stroke-width="1.51"/><path d="M1263,231L1348,116L1248,72Z" fill="#1a212d" stroke="#1a212d" stroke-width="1.51"/><path d="M1271,354L1318,235L1263,231Z" fill="#19192d" stroke="#19192d" stroke-width="1.51"/><path d="M1377,345L1271,354L1386,431Z" fill="#1b142d" stroke="#1b142d" stroke-width="1.51"/><path d="M1355,543L1262,481L1255,588Z" fill="#190e2c" stroke="#190e2c" stroke-width="1.51"/><path d="M-132,680L-24,765L-35,672Z" fill="#1e1e25" stroke="#1e1e25" stroke-width="1.51"/><path d="M-35,672L-24,765L64,646Z" fill="#1c1e27" stroke="#1c1e27" stroke-width="1.51"/><path d="M913,1005L990,1060L1032,900Z" fill="#200d2a" stroke="#200d2a" stroke-width="1.51"/><path d="M1218,912L1253,799L1153,782Z" fill="#1c0a29" stroke="#1c0a29" stroke-width="1.51"/><path d="M696,1054L990,1060L758,1032Z" fill="#220f2c" stroke="#220f2c" stroke-width="1.51"/><path d="M-136,1054L990,1060L696,1054Z" fill="#24182d" stroke="#24182d" stroke-width="1.51"/><path d="M1218,912L1153,782L1089,882Z" fill="#1c0a29" stroke="#1c0a29" stroke-width="1.51"/><path d="M1248,648L1355,543L1255,588Z" fill="#190d2b" stroke="#190d2b" stroke-width="1.51"/><path d="M1367,710L1355,543L1248,648Z" fill="#1b0c2a" stroke="#1b0c2a" stroke-width="1.51"/><path d="M-55,555L-132,680L-35,672Z" fill="#1d1e25" stroke="#1d1e25" stroke-width="1.51"/><path d="M-131,315L-121,448L-14,363Z" fill="#1c2426" stroke="#1c2426" stroke-width="1.51"/><path d="M1246,-36L1136,-111L1110,-28Z" fill="#1d1e25" stroke="#1d1e25" stroke-width="1.51"/><path d="M118,-15L-38,73L123,103Z" fill="#183128" stroke="#183128" stroke-width="1.51"/><path d="M-5,194L-131,315L-14,363Z" fill="#1a2928" stroke="#1a2928" stroke-width="1.51"/><path d="M-39,7L-38,73L118,-15Z" fill="#1a3025" stroke="#1a3025" stroke-width="1.51"/><path d="M1324,-3L1246,-36L1248,72Z" fill="#1e1f28" stroke="#1e1f28" stroke-width="1.51"/><path d="M1386,431L1271,354L1262,481Z" fill="#19112d" stroke="#19112d" stroke-width="1.51"/><path d="M990,1060L1094,1029L1032,900Z" fill="#210c2a" stroke="#210c2a" stroke-width="1.51"/><path d="M-124,219L-131,315L-5,194Z" fill="#1a2b26" stroke="#1a2b26" stroke-width="1.51"/><path d="M1355,543L1386,431L1262,481Z" fill="#1b0f2c" stroke="#1b0f2c" stroke-width="1.51"/><path d="M1094,1029L1218,912L1089,882Z" fill="#1f0a29" stroke="#1f0a29" stroke-width="1.51"/><path d="M1253,799L1367,710L1248,648Z" fill="#1b0b29" stroke="#1b0b29" stroke-width="1.51"/><path d="M-38,73L-124,219L-5,194Z" fill="#183027" stroke="#183027" stroke-width="1.51"/><path d="M1248,72L1246,-36L1110,-28Z" fill="#1b1f27" stroke="#1b1f27" stroke-width="1.51"/><path d="M1348,116L1263,231L1318,235Z" fill="#1a1e2d" stroke="#1a1e2d" stroke-width="1.51"/><path d="M-38,73L-167,117L-124,219Z" fill="#193125" stroke="#193125" stroke-width="1.51"/><path d="M-7,-152L-39,7L118,-15Z" fill="#1d3021" stroke="#1d3021" stroke-width="1.51"/><path d="M1465,118L1348,116L1318,235Z" fill="#1d202d" stroke="#1d202d" stroke-width="1.51"/><path d="M-121,448L-165,529L-55,555Z" fill="#1d1f25" stroke="#1d1f25" stroke-width="1.51"/><path d="M-131,315L-165,529L-121,448Z" fill="#1d2125" stroke="#1d2125" stroke-width="1.51"/><path d="M-167,117L-165,529L-131,315Z" fill="#1d2724" stroke="#1d2724" stroke-width="1.51"/><path d="M1476,551L1473,419L1386,431Z" fill="#1e102d" stroke="#1e102d" stroke-width="1.51"/><path d="M1335,825L1367,710L1253,799Z" fill="#1d0a29" stroke="#1d0a29" stroke-width="1.51"/><path d="M-165,529L-132,680L-55,555Z" fill="#1e1f24" stroke="#1e1f24" stroke-width="1.51"/><path d="M-24,765L-15,943L93,915Z" fill="#211e26" stroke="#211e26" stroke-width="1.51"/><path d="M753,-160L73,-154L463,-146Z" fill="#1c2a24" stroke="#1c2a24" stroke-width="1.51"/><path d="M207,-137L73,-154L118,-15Z" fill="#1b3023" stroke="#1b3023" stroke-width="1.51"/><path d="M1218,912L1335,825L1253,799Z" fill="#1e0a29" stroke="#1e0a29" stroke-width="1.51"/><path d="M1344,904L1335,825L1218,912Z" fill="#200a29" stroke="#200a29" stroke-width="1.51"/><path d="M-175,829L-15,943L-24,765Z" fill="#211d23" stroke="#211d23" stroke-width="1.51"/><path d="M93,915L72,1047L228,990Z" fill="#242029" stroke="#242029" stroke-width="1.51"/><path d="M1094,1029L1223,992L1218,912Z" fill="#210a29" stroke="#210a29" stroke-width="1.51"/><path d="M1339,1016L1223,992L1094,1029Z" fill="#230a29" stroke="#230a29" stroke-width="1.51"/><path d="M1348,116L1324,-3L1248,72Z" fill="#1d202a" stroke="#1d202a" stroke-width="1.51"/><path d="M1246,-36L1241,-144L1136,-111Z" fill="#1f1d23" stroke="#1f1d23" stroke-width="1.51"/><path d="M-15,943L72,1047L93,915Z" fill="#241f27" stroke="#241f27" stroke-width="1.51"/><path d="M298,1048L-136,1054L696,1054Z" fill="#24212d" stroke="#24212d" stroke-width="1.51"/><path d="M1384,-153L1241,-144L1246,-36Z" fill="#211d23" stroke="#211d23" stroke-width="1.51"/><path d="M1136,-111L1241,-144L971,-156Z" fill="#1f1c22" stroke="#1f1c22" stroke-width="1.51"/><path d="M-124,219L-167,117L-131,315Z" fill="#1b2c25" stroke="#1b2c25" stroke-width="1.51"/><path d="M-117,3L-167,117L-38,73Z" fill="#1c3022" stroke="#1c3022" stroke-width="1.51"/><path d="M-117,3L-38,73L-39,7Z" fill="#1c3022" stroke="#1c3022" stroke-width="1.51"/><path d="M1501,228L1465,118L1318,235Z" fill="#1f1e2d" stroke="#1f1e2d" stroke-width="1.51"/><path d="M1348,116L1465,118L1324,-3Z" fill="#1f202b" stroke="#1f202b" stroke-width="1.51"/><path d="M1386,431L1476,362L1377,345Z" fill="#1d142d" stroke="#1d142d" stroke-width="1.51"/><path d="M1476,551L1386,431L1355,543Z" fill="#1d0e2c" stroke="#1d0e2c" stroke-width="1.51"/><path d="M1476,551L1355,543L1459,701Z" fill="#1e0d2b" stroke="#1e0d2b" stroke-width="1.51"/><path d="M-110,-95L-117,3L-39,7Z" fill="#1f2e1e" stroke="#1f2e1e" stroke-width="1.51"/><path d="M1473,419L1476,362L1386,431Z" fill="#1e132d" stroke="#1e132d" stroke-width="1.51"/><path d="M73,-154L-7,-152L118,-15Z" fill="#1e2f20" stroke="#1e2f20" stroke-width="1.51"/><path d="M1459,701L1355,543L1367,710Z" fill="#1d0c2a" stroke="#1d0c2a" stroke-width="1.51"/><path d="M1579,184L1501,228L1612,342Z" fill="#231c2d" stroke="#231c2d" stroke-width="1.51"/><path d="M1467,827L1459,701L1367,710Z" fill="#1f0a29" stroke="#1f0a29" stroke-width="1.51"/><path d="M1223,992L1344,904L1218,912Z" fill="#220a29" stroke="#220a29" stroke-width="1.51"/><path d="M-15,943L-43,1000L72,1047Z" fill="#261e25" stroke="#261e25" stroke-width="1.51"/><path d="M-145,939L-43,1000L-15,943Z" fill="#261e23" stroke="#261e23" stroke-width="1.51"/><path d="M1467,827L1367,710L1335,825Z" fill="#1f0a29" stroke="#1f0a29" stroke-width="1.51"/><path d="M-167,117L-175,829L-165,529Z" fill="#1e1f24" stroke="#1e1f24" stroke-width="1.51"/><path d="M-165,529L-175,829L-132,680Z" fill="#1f1d22" stroke="#1f1d22" stroke-width="1.51"/><path d="M-132,680L-175,829L-24,765Z" fill="#1e1c22" stroke="#1e1c22" stroke-width="1.51"/><path d="M1501,228L1318,235L1377,345Z" fill="#1d1a2d" stroke="#1d1a2d" stroke-width="1.51"/><path d="M1324,-3L1384,-153L1246,-36Z" fill="#211e25" stroke="#211e25" stroke-width="1.51"/><path d="M1476,362L1501,228L1377,345Z" fill="#1f182d" stroke="#1f182d" stroke-width="1.51"/><path d="M1567,465L1476,362L1473,419Z" fill="#20122d" stroke="#20122d" stroke-width="1.51"/><path d="M-7,-152L-110,-95L-39,7Z" fill="#1f2e1e" stroke="#1f2e1e" stroke-width="1.51"/><path d="M-117,3L-110,-95L-167,117Z" fill="#1e2f1f" stroke="#1e2f1f" stroke-width="1.51"/><path d="M-175,829L-145,939L-15,943Z" fill="#241d22" stroke="#241d22" stroke-width="1.51"/><path d="M1344,904L1467,827L1335,825Z" fill="#210a29" stroke="#210a29" stroke-width="1.51"/><path d="M1223,992L1339,1016L1344,904Z" fill="#240a29" stroke="#240a29" stroke-width="1.51"/><path d="M990,1060L1339,1016L1094,1029Z" fill="#220b29" stroke="#220b29" stroke-width="1.51"/><path d="M1484,941L1467,827L1344,904Z" fill="#240a29" stroke="#240a29" stroke-width="1.51"/><path d="M1582,529L1567,465L1476,551Z" fill="#210e2c" stroke="#210e2c" stroke-width="1.51"/><path d="M1476,551L1567,465L1473,419Z" fill="#200f2c" stroke="#200f2c" stroke-width="1.51"/><path d="M1582,529L1476,551L1577,710Z" fill="#220d2b" stroke="#220d2b" stroke-width="1.51"/><path d="M1481,-21L1324,-3L1465,118Z" fill="#222029" stroke="#222029" stroke-width="1.51"/><path d="M1481,-21L1384,-153L1324,-3Z" fill="#231e25" stroke="#231e25" stroke-width="1.51"/><path d="M1241,-144L1384,-153L971,-156Z" fill="#201c21" stroke="#201c21" stroke-width="1.51"/><path d="M971,-156L1384,-153L753,-160Z" fill="#1f1d22" stroke="#1f1d22" stroke-width="1.51"/><path d="M1577,710L1476,551L1459,701Z" fill="#210d2a" stroke="#210d2a" stroke-width="1.51"/><path d="M1501,228L1476,362L1612,342Z" fill="#21182d" stroke="#21182d" stroke-width="1.51"/><path d="M1579,184L1465,118L1501,228Z" fill="#211f2d" stroke="#211f2d" stroke-width="1.51"/><path d="M1568,-35L1481,-21L1465,118Z" fill="#252028" stroke="#252028" stroke-width="1.51"/><path d="M-175,829L-136,1054L-145,939Z" fill="#271d21" stroke="#271d21" stroke-width="1.51"/><path d="M-145,939L-136,1054L-43,1000Z" fill="#281d22" stroke="#281d22" stroke-width="1.51"/><path d="M-43,1000L-136,1054L72,1047Z" fill="#291f24" stroke="#291f24" stroke-width="1.51"/><path d="M72,1047L-136,1054L298,1048Z" fill="#272028" stroke="#272028" stroke-width="1.51"/><path d="M1612,342L1476,362L1567,465Z" fill="#21132d" stroke="#21132d" stroke-width="1.51"/><path d="M1582,529L1612,342L1567,465Z" fill="#22102d" stroke="#22102d" stroke-width="1.51"/><path d="M1582,529L1577,710L1612,342Z" fill="#220f2c" stroke="#220f2c" stroke-width="1.51"/><path d="M1565,807L1577,710L1459,701Z" fill="#210a29" stroke="#210a29" stroke-width="1.51"/><path d="M1565,807L1459,701L1467,827Z" fill="#210a29" stroke="#210a29" stroke-width="1.51"/><path d="M1456,1026L1484,941L1344,904Z" fill="#270a29" stroke="#270a29" stroke-width="1.51"/><path d="M1577,908L1565,807L1467,827Z" fill="#250a29" stroke="#250a29" stroke-width="1.51"/><path d="M1484,941L1577,908L1467,827Z" fill="#260a29" stroke="#260a29" stroke-width="1.51"/><path d="M1339,1016L1456,1026L1344,904Z" fill="#260a29" stroke="#260a29" stroke-width="1.51"/><path d="M990,1060L1456,1026L1339,1016Z" fill="#250a29" stroke="#250a29" stroke-width="1.51"/><path d="M1481,-21L1493,-148L1384,-153Z" fill="#261d23" stroke="#261d23" stroke-width="1.51"/><path d="M1568,-35L1493,-148L1481,-21Z" fill="#271e25" stroke="#271e25" stroke-width="1.51"/><path d="M1579,184L1618,111L1465,118Z" fill="#23222d" stroke="#23222d" stroke-width="1.51"/><path d="M1612,342L1618,111L1579,184Z" fill="#241e2d" stroke="#241e2d" stroke-width="1.51"/><path d="M1618,111L1568,-35L1465,118Z" fill="#25212a" stroke="#25212a" stroke-width="1.51"/><path d="M1568,1003L1577,908L1484,941Z" fill="#2a0a29" stroke="#2a0a29" stroke-width="1.51"/><path d="M1565,807L1577,908L1577,710Z" fill="#240a29" stroke="#240a29" stroke-width="1.51"/><path d="M1577,710L1577,908L1612,342Z" fill="#230d2a" stroke="#230d2a" stroke-width="1.51"/><path d="M1456,1026L1568,1003L1484,941Z" fill="#2a0a29" stroke="#2a0a29" stroke-width="1.51"/><path d="M1568,-35L1582,-127L1493,-148Z" fill="#281e23" stroke="#281e23" stroke-width="1.51"/><path d="M1618,111L1582,-127L1568,-35Z" fill="#281f26" stroke="#281f26" stroke-width="1.51"/></svg> \ No newline at end of file
diff --git a/src/client/assets/welcome-bg.light.svg b/src/client/assets/welcome-bg.light.svg
new file mode 100644
index 0000000000..ebccb648ea
--- /dev/null
+++ b/src/client/assets/welcome-bg.light.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="900"><path d="M741,416L678,396L681,478Z" fill="#f4f4f3" stroke="#f4f4f3" stroke-width="1.51"/><path d="M681,478L777,499L741,416Z" fill="#f2f3f2" stroke="#f2f3f2" stroke-width="1.51"/><path d="M678,396L605,487L681,478Z" fill="#f3f3f2" stroke="#f3f3f2" stroke-width="1.51"/><path d="M681,478L683,560L777,499Z" fill="#edf1ee" stroke="#edf1ee" stroke-width="1.51"/><path d="M760,327L659,322L678,396Z" fill="#f1f1ef" stroke="#f1f1ef" stroke-width="1.51"/><path d="M678,396L571,389L605,487Z" fill="#f2f2f0" stroke="#f2f2f0" stroke-width="1.51"/><path d="M741,416L760,327L678,396Z" fill="#f3f2f1" stroke="#f3f2f1" stroke-width="1.51"/><path d="M854,398L760,327L741,416Z" fill="#eef0ec" stroke="#eef0ec" stroke-width="1.51"/><path d="M605,487L683,560L681,478Z" fill="#edf0ec" stroke="#edf0ec" stroke-width="1.51"/><path d="M659,322L571,389L678,396Z" fill="#f1f1ee" stroke="#f1f1ee" stroke-width="1.51"/><path d="M683,560L783,568L777,499Z" fill="#e7eee9" stroke="#e7eee9" stroke-width="1.51"/><path d="M777,499L854,398L741,416Z" fill="#eef2ef" stroke="#eef2ef" stroke-width="1.51"/><path d="M844,505L854,398L777,499Z" fill="#eaefeb" stroke="#eaefeb" stroke-width="1.51"/><path d="M783,568L844,505L777,499Z" fill="#e6ece7" stroke="#e6ece7" stroke-width="1.51"/><path d="M659,322L596,308L571,389Z" fill="#f0f0eb" stroke="#f0f0eb" stroke-width="1.51"/><path d="M597,246L596,308L659,322Z" fill="#efeee9" stroke="#efeee9" stroke-width="1.51"/><path d="M605,487L579,600L683,560Z" fill="#e7ede7" stroke="#e7ede7" stroke-width="1.51"/><path d="M571,389L512,495L605,487Z" fill="#f1f1ee" stroke="#f1f1ee" stroke-width="1.51"/><path d="M492,390L512,495L571,389Z" fill="#f0f0ec" stroke="#f0f0ec" stroke-width="1.51"/><path d="M760,327L745,238L659,322Z" fill="#f1f0ed" stroke="#f1f0ed" stroke-width="1.51"/><path d="M852,301L745,238L760,327Z" fill="#ebede7" stroke="#ebede7" stroke-width="1.51"/><path d="M512,495L579,600L605,487Z" fill="#e9ede7" stroke="#e9ede7" stroke-width="1.51"/><path d="M764,652L848,599L783,568Z" fill="#dce7de" stroke="#dce7de" stroke-width="1.51"/><path d="M854,398L852,301L760,327Z" fill="#eaeee8" stroke="#eaeee8" stroke-width="1.51"/><path d="M513,332L492,390L571,389Z" fill="#efeeea" stroke="#efeeea" stroke-width="1.51"/><path d="M596,308L513,332L571,389Z" fill="#efefe9" stroke="#efefe9" stroke-width="1.51"/><path d="M521,240L513,332L596,308Z" fill="#edede6" stroke="#edede6" stroke-width="1.51"/><path d="M655,216L597,246L659,322Z" fill="#eeeee8" stroke="#eeeee8" stroke-width="1.51"/><path d="M783,568L848,599L844,505Z" fill="#e0e9e1" stroke="#e0e9e1" stroke-width="1.51"/><path d="M844,505L926,479L854,398Z" fill="#e7ede8" stroke="#e7ede8" stroke-width="1.51"/><path d="M764,652L783,568L683,560Z" fill="#e2eae3" stroke="#e2eae3" stroke-width="1.51"/><path d="M745,238L655,216L659,322Z" fill="#efefea" stroke="#efefea" stroke-width="1.51"/><path d="M691,673L764,652L683,560Z" fill="#e0e9e1" stroke="#e0e9e1" stroke-width="1.51"/><path d="M930,580L926,479L844,505Z" fill="#dee8e0" stroke="#dee8e0" stroke-width="1.51"/><path d="M854,398L924,327L852,301Z" fill="#e5ebe3" stroke="#e5ebe3" stroke-width="1.51"/><path d="M926,479L932,405L854,398Z" fill="#e5ece6" stroke="#e5ece6" stroke-width="1.51"/><path d="M579,600L691,673L683,560Z" fill="#e1e9e1" stroke="#e1e9e1" stroke-width="1.51"/><path d="M852,301L854,250L745,238Z" fill="#e8ebe4" stroke="#e8ebe4" stroke-width="1.51"/><path d="M745,238L695,139L655,216Z" fill="#eeede7" stroke="#eeede7" stroke-width="1.51"/><path d="M932,252L854,250L852,301Z" fill="#e3e9df" stroke="#e3e9df" stroke-width="1.51"/><path d="M932,405L924,327L854,398Z" fill="#e4ebe3" stroke="#e4ebe3" stroke-width="1.51"/><path d="M512,495L484,563L579,600Z" fill="#e5ebe3" stroke="#e5ebe3" stroke-width="1.51"/><path d="M579,600L598,675L691,673Z" fill="#dce6dc" stroke="#dce6dc" stroke-width="1.51"/><path d="M424,483L484,563L512,495Z" fill="#e9ece5" stroke="#e9ece5" stroke-width="1.51"/><path d="M523,666L598,675L579,600Z" fill="#dbe6da" stroke="#dbe6da" stroke-width="1.51"/><path d="M597,246L521,240L596,308Z" fill="#edede6" stroke="#edede6" stroke-width="1.51"/><path d="M403,323L433,428L492,390Z" fill="#eeede7" stroke="#eeede7" stroke-width="1.51"/><path d="M509,138L521,240L597,246Z" fill="#ebeae2" stroke="#ebeae2" stroke-width="1.51"/><path d="M492,390L433,428L512,495Z" fill="#f0efec" stroke="#f0efec" stroke-width="1.51"/><path d="M403,323L492,390L513,332Z" fill="#edede7" stroke="#edede7" stroke-width="1.51"/><path d="M848,599L930,580L844,505Z" fill="#dce7dd" stroke="#dce7dd" stroke-width="1.51"/><path d="M926,479L1021,423L932,405Z" fill="#e1e9e2" stroke="#e1e9e2" stroke-width="1.51"/><path d="M914,673L930,580L848,599Z" fill="#d3e2d5" stroke="#d3e2d5" stroke-width="1.51"/><path d="M433,428L424,483L512,495Z" fill="#eeefe9" stroke="#eeefe9" stroke-width="1.51"/><path d="M439,658L523,666L484,563Z" fill="#dce5d9" stroke="#dce5d9" stroke-width="1.51"/><path d="M484,563L523,666L579,600Z" fill="#dee7dd" stroke="#dee7dd" stroke-width="1.51"/><path d="M764,652L859,684L848,599Z" fill="#d6e4d8" stroke="#d6e4d8" stroke-width="1.51"/><path d="M841,752L859,684L764,652Z" fill="#d0e0d3" stroke="#d0e0d3" stroke-width="1.51"/><path d="M771,770L764,652L691,673Z" fill="#d6e4d9" stroke="#d6e4d9" stroke-width="1.51"/><path d="M781,131L695,139L745,238Z" fill="#ecece5" stroke="#ecece5" stroke-width="1.51"/><path d="M924,327L932,252L852,301Z" fill="#e2e8df" stroke="#e2e8df" stroke-width="1.51"/><path d="M1000,247L932,252L924,327Z" fill="#dde6da" stroke="#dde6da" stroke-width="1.51"/><path d="M781,131L745,238L854,250Z" fill="#e8eae3" stroke="#e8eae3" stroke-width="1.51"/><path d="M655,216L610,125L597,246Z" fill="#ecece4" stroke="#ecece4" stroke-width="1.51"/><path d="M869,153L781,131L854,250Z" fill="#e4e8de" stroke="#e4e8de" stroke-width="1.51"/><path d="M521,240L403,323L513,332Z" fill="#ecebe4" stroke="#ecebe4" stroke-width="1.51"/><path d="M342,409L347,500L424,483Z" fill="#edede7" stroke="#edede7" stroke-width="1.51"/><path d="M930,580L1009,511L926,479Z" fill="#d9e6db" stroke="#d9e6db" stroke-width="1.51"/><path d="M932,405L1014,332L924,327Z" fill="#dfe8de" stroke="#dfe8de" stroke-width="1.51"/><path d="M1044,592L1009,511L930,580Z" fill="#d1e1d4" stroke="#d1e1d4" stroke-width="1.51"/><path d="M859,684L914,673L848,599Z" fill="#d1e1d4" stroke="#d1e1d4" stroke-width="1.51"/><path d="M1009,511L1021,423L926,479Z" fill="#dde7de" stroke="#dde7de" stroke-width="1.51"/><path d="M424,483L415,588L484,563Z" fill="#e5eae1" stroke="#e5eae1" stroke-width="1.51"/><path d="M347,500L415,588L424,483Z" fill="#e6eae1" stroke="#e6eae1" stroke-width="1.51"/><path d="M695,139L610,125L655,216Z" fill="#ecebe3" stroke="#ecebe3" stroke-width="1.51"/><path d="M521,240L403,213L403,323Z" fill="#eaeae1" stroke="#eaeae1" stroke-width="1.51"/><path d="M659,40L610,125L695,139Z" fill="#ebe7df" stroke="#ebe7df" stroke-width="1.51"/><path d="M598,675L697,764L691,673Z" fill="#d6e3d8" stroke="#d6e3d8" stroke-width="1.51"/><path d="M859,684L935,739L914,673Z" fill="#c9ddcd" stroke="#c9ddcd" stroke-width="1.51"/><path d="M523,666L582,742L598,675Z" fill="#d5e2d5" stroke="#d5e2d5" stroke-width="1.51"/><path d="M504,768L582,742L523,666Z" fill="#d1e0d1" stroke="#d1e0d1" stroke-width="1.51"/><path d="M582,742L697,764L598,675Z" fill="#d3e1d4" stroke="#d3e1d4" stroke-width="1.51"/><path d="M932,252L869,153L854,250Z" fill="#e1e7dd" stroke="#e1e7dd" stroke-width="1.51"/><path d="M1021,423L1014,332L932,405Z" fill="#dde7dd" stroke="#dde7dd" stroke-width="1.51"/><path d="M932,252L934,126L869,153Z" fill="#dee5d9" stroke="#dee5d9" stroke-width="1.51"/><path d="M697,764L771,770L691,673Z" fill="#d4e2d6" stroke="#d4e2d6" stroke-width="1.51"/><path d="M415,588L439,658L484,563Z" fill="#dee6da" stroke="#dee6da" stroke-width="1.51"/><path d="M771,770L841,752L764,652Z" fill="#cfe0d2" stroke="#cfe0d2" stroke-width="1.51"/><path d="M610,125L509,138L597,246Z" fill="#eaeae1" stroke="#eaeae1" stroke-width="1.51"/><path d="M1014,332L1000,247L924,327Z" fill="#dce6da" stroke="#dce6da" stroke-width="1.51"/><path d="M342,409L424,483L433,428Z" fill="#eeeee9" stroke="#eeeee9" stroke-width="1.51"/><path d="M415,588L317,676L439,658Z" fill="#d9e2d4" stroke="#d9e2d4" stroke-width="1.51"/><path d="M403,323L342,409L433,428Z" fill="#edece5" stroke="#edece5" stroke-width="1.51"/><path d="M318,300L342,409L403,323Z" fill="#ebebe2" stroke="#ebebe2" stroke-width="1.51"/><path d="M438,164L403,213L521,240Z" fill="#e9e9df" stroke="#e9e9df" stroke-width="1.51"/><path d="M1009,511L1102,487L1021,423Z" fill="#d7e5da" stroke="#d7e5da" stroke-width="1.51"/><path d="M1021,423L1106,419L1014,332Z" fill="#d9e4d9" stroke="#d9e4d9" stroke-width="1.51"/><path d="M1039,662L1044,592L930,580Z" fill="#cbddce" stroke="#cbddce" stroke-width="1.51"/><path d="M1039,662L930,580L914,673Z" fill="#ccdecf" stroke="#ccdecf" stroke-width="1.51"/><path d="M509,138L438,164L521,240Z" fill="#e9e9de" stroke="#e9e9de" stroke-width="1.51"/><path d="M841,752L935,739L859,684Z" fill="#c8dccc" stroke="#c8dccc" stroke-width="1.51"/><path d="M848,838L935,739L841,752Z" fill="#c3d9c8" stroke="#c3d9c8" stroke-width="1.51"/><path d="M439,658L504,768L523,666Z" fill="#d4e1d2" stroke="#d4e1d2" stroke-width="1.51"/><path d="M582,742L595,833L697,764Z" fill="#ceded0" stroke="#ceded0" stroke-width="1.51"/><path d="M697,764L752,857L771,770Z" fill="#cdded2" stroke="#cdded2" stroke-width="1.51"/><path d="M1000,247L934,126L932,252Z" fill="#dbe4d7" stroke="#dbe4d7" stroke-width="1.51"/><path d="M869,153L847,70L781,131Z" fill="#e4e5da" stroke="#e4e5da" stroke-width="1.51"/><path d="M754,40L659,40L695,139Z" fill="#ece5de" stroke="#ece5de" stroke-width="1.51"/><path d="M925,70L847,70L869,153Z" fill="#dfe1d5" stroke="#dfe1d5" stroke-width="1.51"/><path d="M610,125L596,39L509,138Z" fill="#e9e5dc" stroke="#e9e5dc" stroke-width="1.51"/><path d="M754,40L695,139L781,131Z" fill="#eae7df" stroke="#eae7df" stroke-width="1.51"/><path d="M509,138L481,39L438,164Z" fill="#e7e4d9" stroke="#e7e4d9" stroke-width="1.51"/><path d="M847,70L754,40L781,131Z" fill="#e6e3da" stroke="#e6e3da" stroke-width="1.51"/><path d="M439,658L431,757L504,768Z" fill="#d0dece" stroke="#d0dece" stroke-width="1.51"/><path d="M347,500L320,574L415,588Z" fill="#e2e8dd" stroke="#e2e8dd" stroke-width="1.51"/><path d="M252,484L320,574L347,500Z" fill="#e5e8de" stroke="#e5e8de" stroke-width="1.51"/><path d="M1044,592L1102,487L1009,511Z" fill="#d0e1d4" stroke="#d0e1d4" stroke-width="1.51"/><path d="M1014,332L1093,313L1000,247Z" fill="#d7e2d5" stroke="#d7e2d5" stroke-width="1.51"/><path d="M403,213L318,300L403,323Z" fill="#e9e9df" stroke="#e9e9df" stroke-width="1.51"/><path d="M342,409L252,484L347,500Z" fill="#ecece5" stroke="#ecece5" stroke-width="1.51"/><path d="M336,215L318,300L403,213Z" fill="#e8e8dd" stroke="#e8e8dd" stroke-width="1.51"/><path d="M1102,487L1106,419L1021,423Z" fill="#d8e5da" stroke="#d8e5da" stroke-width="1.51"/><path d="M1000,247L1035,167L934,126Z" fill="#d8e2d3" stroke="#d8e2d3" stroke-width="1.51"/><path d="M935,739L1039,662L914,673Z" fill="#c5dbc9" stroke="#c5dbc9" stroke-width="1.51"/><path d="M1044,592L1121,583L1102,487Z" fill="#cbdece" stroke="#cbdece" stroke-width="1.51"/><path d="M516,826L595,833L582,742Z" fill="#cbdcce" stroke="#cbdcce" stroke-width="1.51"/><path d="M771,770L848,838L841,752Z" fill="#c7dbcc" stroke="#c7dbcc" stroke-width="1.51"/><path d="M659,40L596,39L610,125Z" fill="#eae3db" stroke="#eae3db" stroke-width="1.51"/><path d="M661,-27L596,39L659,40Z" fill="#eadfd7" stroke="#eadfd7" stroke-width="1.51"/><path d="M1106,419L1093,313L1014,332Z" fill="#d6e3d6" stroke="#d6e3d6" stroke-width="1.51"/><path d="M504,768L516,826L582,742Z" fill="#ccddcd" stroke="#ccddcd" stroke-width="1.51"/><path d="M317,676L431,757L439,658Z" fill="#d2dfcf" stroke="#d2dfcf" stroke-width="1.51"/><path d="M595,833L691,858L697,764Z" fill="#cbddd1" stroke="#cbddd1" stroke-width="1.51"/><path d="M691,858L752,857L697,764Z" fill="#ccddd2" stroke="#ccddd2" stroke-width="1.51"/><path d="M353,127L403,213L438,164Z" fill="#e7e6db" stroke="#e7e6db" stroke-width="1.51"/><path d="M353,127L336,215L403,213Z" fill="#e6e6da" stroke="#e6e6da" stroke-width="1.51"/><path d="M752,857L848,838L771,770Z" fill="#c7dacd" stroke="#c7dacd" stroke-width="1.51"/><path d="M935,739L1021,771L1039,662Z" fill="#bfd7c3" stroke="#bfd7c3" stroke-width="1.51"/><path d="M934,126L925,70L869,153Z" fill="#dde1d5" stroke="#dde1d5" stroke-width="1.51"/><path d="M1121,236L1035,167L1000,247Z" fill="#d4e0d0" stroke="#d4e0d0" stroke-width="1.51"/><path d="M857,-32L751,-25L754,40Z" fill="#e6dcd3" stroke="#e6dcd3" stroke-width="1.51"/><path d="M1020,81L925,70L934,126Z" fill="#d9ddce" stroke="#d9ddce" stroke-width="1.51"/><path d="M426,848L516,826L504,768Z" fill="#c8d9ca" stroke="#c8d9ca" stroke-width="1.51"/><path d="M595,833L598,906L691,858Z" fill="#c7d9cd" stroke="#c7d9cd" stroke-width="1.51"/><path d="M1114,656L1121,583L1044,592Z" fill="#c4d9c8" stroke="#c4d9c8" stroke-width="1.51"/><path d="M1102,487L1185,403L1106,419Z" fill="#d3e2d6" stroke="#d3e2d6" stroke-width="1.51"/><path d="M239,415L342,409L232,341Z" fill="#ebeae1" stroke="#ebeae1" stroke-width="1.51"/><path d="M239,415L252,484L342,409Z" fill="#ecebe4" stroke="#ecebe4" stroke-width="1.51"/><path d="M320,574L317,676L415,588Z" fill="#dbe3d6" stroke="#dbe3d6" stroke-width="1.51"/><path d="M265,661L317,676L320,574Z" fill="#d8e1d2" stroke="#d8e1d2" stroke-width="1.51"/><path d="M481,907L598,906L516,826Z" fill="#c4d7ca" stroke="#c4d7ca" stroke-width="1.51"/><path d="M596,39L481,39L509,138Z" fill="#e8e2d8" stroke="#e8e2d8" stroke-width="1.51"/><path d="M232,341L342,409L318,300Z" fill="#eae9e1" stroke="#eae9e1" stroke-width="1.51"/><path d="M1039,662L1114,656L1044,592Z" fill="#c3d9c7" stroke="#c3d9c7" stroke-width="1.51"/><path d="M1003,828L1021,771L935,739Z" fill="#bad4c0" stroke="#bad4c0" stroke-width="1.51"/><path d="M754,40L751,-25L659,40Z" fill="#ebe1da" stroke="#ebe1da" stroke-width="1.51"/><path d="M596,39L496,-7L481,39Z" fill="#e8ddd4" stroke="#e8ddd4" stroke-width="1.51"/><path d="M857,-32L754,40L847,70Z" fill="#e4ddd3" stroke="#e4ddd3" stroke-width="1.51"/><path d="M425,40L353,127L438,164Z" fill="#e6e3d7" stroke="#e6e3d7" stroke-width="1.51"/><path d="M247,249L232,341L318,300Z" fill="#e8e6dc" stroke="#e8e6dc" stroke-width="1.51"/><path d="M751,-25L661,-27L659,40Z" fill="#ebded7" stroke="#ebded7" stroke-width="1.51"/><path d="M1093,313L1121,236L1000,247Z" fill="#d3e0d1" stroke="#d3e0d1" stroke-width="1.51"/><path d="M1035,167L1020,81L934,126Z" fill="#d6decf" stroke="#d6decf" stroke-width="1.51"/><path d="M1213,315L1121,236L1093,313Z" fill="#cedecd" stroke="#cedecd" stroke-width="1.51"/><path d="M1185,403L1093,313L1106,419Z" fill="#d2e1d3" stroke="#d2e1d3" stroke-width="1.51"/><path d="M1093,741L1114,656L1039,662Z" fill="#bcd5c1" stroke="#bcd5c1" stroke-width="1.51"/><path d="M247,249L318,300L336,215Z" fill="#e7e7dc" stroke="#e7e7dc" stroke-width="1.51"/><path d="M1114,139L1020,81L1035,167Z" fill="#d1dccb" stroke="#d1dccb" stroke-width="1.51"/><path d="M925,70L857,-32L847,70Z" fill="#dfdcd0" stroke="#dfdcd0" stroke-width="1.51"/><path d="M661,-27L577,-27L596,39Z" fill="#e9ddd4" stroke="#e9ddd4" stroke-width="1.51"/><path d="M426,848L504,768L431,757Z" fill="#cadaca" stroke="#cadaca" stroke-width="1.51"/><path d="M516,826L598,906L595,833Z" fill="#c6d9cc" stroke="#c6d9cc" stroke-width="1.51"/><path d="M691,858L757,921L752,857Z" fill="#c7dacf" stroke="#c7dacf" stroke-width="1.51"/><path d="M840,910L941,859L848,838Z" fill="#bcd4c5" stroke="#bcd4c5" stroke-width="1.51"/><path d="M496,-7L425,40L481,39Z" fill="#e7dcd2" stroke="#e7dcd2" stroke-width="1.51"/><path d="M481,39L425,40L438,164Z" fill="#e7e1d6" stroke="#e7e1d6" stroke-width="1.51"/><path d="M147,423L252,484L239,415Z" fill="#ece7e0" stroke="#ece7e0" stroke-width="1.51"/><path d="M164,575L241,587L252,484Z" fill="#e1e1d7" stroke="#e1e1d7" stroke-width="1.51"/><path d="M252,484L241,587L320,574Z" fill="#e1e5da" stroke="#e1e5da" stroke-width="1.51"/><path d="M265,661L319,736L317,676Z" fill="#d1ddcc" stroke="#d1ddcc" stroke-width="1.51"/><path d="M317,676L319,736L431,757Z" fill="#cfddcb" stroke="#cfddcb" stroke-width="1.51"/><path d="M1187,514L1102,487L1121,583Z" fill="#caddcd" stroke="#caddcd" stroke-width="1.51"/><path d="M1187,514L1185,403L1102,487Z" fill="#cfe0d3" stroke="#cfe0d3" stroke-width="1.51"/><path d="M848,838L941,859L935,739Z" fill="#bdd5c5" stroke="#bdd5c5" stroke-width="1.51"/><path d="M840,910L848,838L752,857Z" fill="#c1d7ca" stroke="#c1d7ca" stroke-width="1.51"/><path d="M577,-27L496,-7L596,39Z" fill="#e8dcd3" stroke="#e8dcd3" stroke-width="1.51"/><path d="M687,939L757,921L691,858Z" fill="#c5d9cf" stroke="#c5d9cf" stroke-width="1.51"/><path d="M241,587L265,661L320,574Z" fill="#dae2d3" stroke="#dae2d3" stroke-width="1.51"/><path d="M225,125L336,215L353,127Z" fill="#e5e4d7" stroke="#e5e4d7" stroke-width="1.51"/><path d="M225,125L247,249L336,215Z" fill="#e6e4d7" stroke="#e6e4d7" stroke-width="1.51"/><path d="M1184,577L1187,514L1121,583Z" fill="#c5dac9" stroke="#c5dac9" stroke-width="1.51"/><path d="M953,-21L857,-32L925,70Z" fill="#dcd8cb" stroke="#dcd8cb" stroke-width="1.51"/><path d="M751,-25L666,-95L661,-27Z" fill="#ebdad4" stroke="#ebdad4" stroke-width="1.51"/><path d="M661,-27L591,-101L577,-27Z" fill="#e9d9d0" stroke="#e9d9d0" stroke-width="1.51"/><path d="M757,921L840,910L752,857Z" fill="#c1d6cb" stroke="#c1d6cb" stroke-width="1.51"/><path d="M319,736L426,848L431,757Z" fill="#c9dac8" stroke="#c9dac8" stroke-width="1.51"/><path d="M1021,771L1093,741L1039,662Z" fill="#bad5bf" stroke="#bad5bf" stroke-width="1.51"/><path d="M941,859L1003,828L935,739Z" fill="#bad3c1" stroke="#bad3c1" stroke-width="1.51"/><path d="M1043,903L1003,828L941,859Z" fill="#b2cfbd" stroke="#b2cfbd" stroke-width="1.51"/><path d="M1110,818L1093,741L1021,771Z" fill="#b3d0b9" stroke="#b3d0b9" stroke-width="1.51"/><path d="M1114,656L1184,577L1121,583Z" fill="#c1d8c5" stroke="#c1d8c5" stroke-width="1.51"/><path d="M870,1015L917,930L840,910Z" fill="#b5cfc3" stroke="#b5cfc3" stroke-width="1.51"/><path d="M598,906L687,939L691,858Z" fill="#c5d8cd" stroke="#c5d8cd" stroke-width="1.51"/><path d="M683,991L687,939L598,906Z" fill="#c1d5cb" stroke="#c1d5cb" stroke-width="1.51"/><path d="M1184,688L1184,577L1114,656Z" fill="#bbd4c0" stroke="#bbd4c0" stroke-width="1.51"/><path d="M1016,-5L953,-21L925,70Z" fill="#d8d6c7" stroke="#d8d6c7" stroke-width="1.51"/><path d="M1121,236L1114,139L1035,167Z" fill="#d0ddcb" stroke="#d0ddcb" stroke-width="1.51"/><path d="M1190,163L1114,139L1121,236Z" fill="#ccdbc7" stroke="#ccdbc7" stroke-width="1.51"/><path d="M425,40L336,72L353,127Z" fill="#e5dfd3" stroke="#e5dfd3" stroke-width="1.51"/><path d="M343,-19L336,72L425,40Z" fill="#e5dbcf" stroke="#e5dbcf" stroke-width="1.51"/><path d="M426,848L481,907L516,826Z" fill="#c4d6c8" stroke="#c4d6c8" stroke-width="1.51"/><path d="M400,933L481,907L426,848Z" fill="#c1d4c6" stroke="#c1d4c6" stroke-width="1.51"/><path d="M1016,-5L925,70L1020,81Z" fill="#d6d8c9" stroke="#d6d8c9" stroke-width="1.51"/><path d="M1185,403L1213,315L1093,313Z" fill="#cedecf" stroke="#cedecf" stroke-width="1.51"/><path d="M1273,400L1213,315L1185,403Z" fill="#ccdccf" stroke="#ccdccf" stroke-width="1.51"/><path d="M1273,400L1185,403L1293,477Z" fill="#ccddd1" stroke="#ccddd1" stroke-width="1.51"/><path d="M577,-27L492,-108L496,-7Z" fill="#e7d7ce" stroke="#e7d7ce" stroke-width="1.51"/><path d="M496,-7L420,-42L425,40Z" fill="#e6dace" stroke="#e6dace" stroke-width="1.51"/><path d="M772,-110L666,-95L751,-25Z" fill="#ead8d2" stroke="#ead8d2" stroke-width="1.51"/><path d="M772,-110L751,-25L857,-32Z" fill="#e5d7ce" stroke="#e5d7ce" stroke-width="1.51"/><path d="M840,910L917,930L941,859Z" fill="#b7d1c2" stroke="#b7d1c2" stroke-width="1.51"/><path d="M1195,761L1184,688L1093,741Z" fill="#b1d0b7" stroke="#b1d0b7" stroke-width="1.51"/><path d="M754,1005L840,910L757,921Z" fill="#bdd4c9" stroke="#bdd4c9" stroke-width="1.51"/><path d="M1090,57L1016,-5L1020,81Z" fill="#d2d5c4" stroke="#d2d5c4" stroke-width="1.51"/><path d="M864,-125L772,-110L857,-32Z" fill="#e1d2c9" stroke="#e1d2c9" stroke-width="1.51"/><path d="M1114,139L1090,57L1020,81Z" fill="#cfd8c6" stroke="#cfd8c6" stroke-width="1.51"/><path d="M1184,73L1090,57L1114,139Z" fill="#cbd5c2" stroke="#cbd5c2" stroke-width="1.51"/><path d="M1293,477L1185,403L1187,514Z" fill="#ccded2" stroke="#ccded2" stroke-width="1.51"/><path d="M1093,741L1184,688L1114,656Z" fill="#b7d3bc" stroke="#b7d3bc" stroke-width="1.51"/><path d="M1110,818L1021,771L1003,828Z" fill="#b3d0bb" stroke="#b3d0bb" stroke-width="1.51"/><path d="M666,-95L591,-101L661,-27Z" fill="#e9d7cf" stroke="#e9d7cf" stroke-width="1.51"/><path d="M864,-125L857,-32L932,-133Z" fill="#ddcfc4" stroke="#ddcfc4" stroke-width="1.51"/><path d="M666,-95L772,-110L591,-101Z" fill="#e9d5ce" stroke="#e9d5ce" stroke-width="1.51"/><path d="M232,341L147,423L239,415Z" fill="#ebe6dd" stroke="#ebe6dd" stroke-width="1.51"/><path d="M241,587L157,669L265,661Z" fill="#d6ddcd" stroke="#d6ddcd" stroke-width="1.51"/><path d="M141,302L147,423L232,341Z" fill="#eae3d9" stroke="#eae3d9" stroke-width="1.51"/><path d="M165,251L232,341L247,249Z" fill="#e7e3d8" stroke="#e7e3d8" stroke-width="1.51"/><path d="M136,501L164,575L252,484Z" fill="#e4e1d7" stroke="#e4e1d7" stroke-width="1.51"/><path d="M265,661L243,762L319,736Z" fill="#cedac8" stroke="#cedac8" stroke-width="1.51"/><path d="M319,736L311,858L426,848Z" fill="#c6d7c6" stroke="#c6d7c6" stroke-width="1.51"/><path d="M492,-108L420,-42L496,-7Z" fill="#e6d6cb" stroke="#e6d6cb" stroke-width="1.51"/><path d="M1213,315L1208,214L1121,236Z" fill="#cbdbca" stroke="#cbdbca" stroke-width="1.51"/><path d="M687,939L754,1005L757,921Z" fill="#c0d6cd" stroke="#c0d6cd" stroke-width="1.51"/><path d="M606,1030L683,991L598,906Z" fill="#bed3c9" stroke="#bed3c9" stroke-width="1.51"/><path d="M1043,903L1110,818L1003,828Z" fill="#afcdb9" stroke="#afcdb9" stroke-width="1.51"/><path d="M170,746L243,762L265,661Z" fill="#cdd7c5" stroke="#cdd7c5" stroke-width="1.51"/><path d="M1208,214L1190,163L1121,236Z" fill="#cadac8" stroke="#cadac8" stroke-width="1.51"/><path d="M225,125L353,127L336,72Z" fill="#e4e1d3" stroke="#e4e1d3" stroke-width="1.51"/><path d="M225,125L165,251L247,249Z" fill="#e5e1d4" stroke="#e5e1d4" stroke-width="1.51"/><path d="M86,589L136,501L62,472Z" fill="#e3dcd2" stroke="#e3dcd2" stroke-width="1.51"/><path d="M147,423L136,501L252,484Z" fill="#eae4dd" stroke="#eae4dd" stroke-width="1.51"/><path d="M255,61L225,125L336,72Z" fill="#e4ddcf" stroke="#e4ddcf" stroke-width="1.51"/><path d="M683,991L754,1005L687,939Z" fill="#c0d5cc" stroke="#c0d5cc" stroke-width="1.51"/><path d="M516,1028L606,1030L598,906Z" fill="#bcd1c7" stroke="#bcd1c7" stroke-width="1.51"/><path d="M243,762L311,858L319,736Z" fill="#c7d7c4" stroke="#c7d7c4" stroke-width="1.51"/><path d="M1293,477L1187,514L1292,592Z" fill="#c3d8ca" stroke="#c3d8ca" stroke-width="1.51"/><path d="M1213,315L1302,245L1208,214Z" fill="#c7d9c9" stroke="#c7d9c9" stroke-width="1.51"/><path d="M165,251L141,302L232,341Z" fill="#e8e1d7" stroke="#e8e1d7" stroke-width="1.51"/><path d="M420,-42L343,-19L425,40Z" fill="#e5d8cd" stroke="#e5d8cd" stroke-width="1.51"/><path d="M412,-112L343,-19L420,-42Z" fill="#e4d4c8" stroke="#e4d4c8" stroke-width="1.51"/><path d="M1014,1027L1043,903L917,930Z" fill="#accaba" stroke="#accaba" stroke-width="1.51"/><path d="M917,930L1043,903L941,859Z" fill="#b2cebd" stroke="#b2cebd" stroke-width="1.51"/><path d="M311,858L400,933L426,848Z" fill="#c1d4c4" stroke="#c1d4c4" stroke-width="1.51"/><path d="M343,-19L255,61L336,72Z" fill="#e4dbcd" stroke="#e4dbcd" stroke-width="1.51"/><path d="M225,125L146,150L165,251Z" fill="#e5ded1" stroke="#e5ded1" stroke-width="1.51"/><path d="M591,-101L492,-108L577,-27Z" fill="#e7d5cc" stroke="#e7d5cc" stroke-width="1.51"/><path d="M772,-110L492,-108L591,-101Z" fill="#e8d4cc" stroke="#e8d4cc" stroke-width="1.51"/><path d="M932,-133L857,-32L953,-21Z" fill="#dbd1c5" stroke="#dbd1c5" stroke-width="1.51"/><path d="M772,-110L864,-125L492,-108Z" fill="#ead5ce" stroke="#ead5ce" stroke-width="1.51"/><path d="M243,762L256,859L311,858Z" fill="#c4d4c2" stroke="#c4d4c2" stroke-width="1.51"/><path d="M164,575L157,669L241,587Z" fill="#dadcce" stroke="#dadcce" stroke-width="1.51"/><path d="M86,589L157,669L164,575Z" fill="#d9d9cb" stroke="#d9d9cb" stroke-width="1.51"/><path d="M1200,836L1195,761L1110,818Z" fill="#aacab3" stroke="#aacab3" stroke-width="1.51"/><path d="M1110,818L1195,761L1093,741Z" fill="#afceb6" stroke="#afceb6" stroke-width="1.51"/><path d="M1292,592L1187,514L1184,577Z" fill="#c1d8c7" stroke="#c1d8c7" stroke-width="1.51"/><path d="M1292,592L1184,577L1287,654Z" fill="#b9d3c1" stroke="#b9d3c1" stroke-width="1.51"/><path d="M516,1028L598,906L481,907Z" fill="#bfd3c7" stroke="#bfd3c7" stroke-width="1.51"/><path d="M683,991L606,1030L754,1005Z" fill="#bcd2ca" stroke="#bcd2ca" stroke-width="1.51"/><path d="M754,1005L870,1015L840,910Z" fill="#b7d0c6" stroke="#b7d0c6" stroke-width="1.51"/><path d="M606,1030L870,1015L754,1005Z" fill="#bad2ca" stroke="#bad2ca" stroke-width="1.51"/><path d="M1022,-108L932,-133L953,-21Z" fill="#d7cdbf" stroke="#d7cdbf" stroke-width="1.51"/><path d="M1090,57L1087,-29L1016,-5Z" fill="#d0d1c0" stroke="#d0d1c0" stroke-width="1.51"/><path d="M1190,163L1184,73L1114,139Z" fill="#c9d7c3" stroke="#c9d7c3" stroke-width="1.51"/><path d="M1259,129L1184,73L1190,163Z" fill="#c6d4c2" stroke="#c6d4c2" stroke-width="1.51"/><path d="M1259,129L1190,163L1208,214Z" fill="#c6d7c5" stroke="#c6d7c5" stroke-width="1.51"/><path d="M1191,-40L1087,-29L1090,57Z" fill="#cbcdba" stroke="#cbcdba" stroke-width="1.51"/><path d="M1273,400L1301,336L1213,315Z" fill="#c9dacd" stroke="#c9dacd" stroke-width="1.51"/><path d="M1367,403L1301,336L1273,400Z" fill="#c7d9cd" stroke="#c7d9cd" stroke-width="1.51"/><path d="M1287,654L1184,577L1184,688Z" fill="#b8d3bf" stroke="#b8d3bf" stroke-width="1.51"/><path d="M1293,477L1367,403L1273,400Z" fill="#c8dbd0" stroke="#c8dbd0" stroke-width="1.51"/><path d="M178,821L256,859L243,762Z" fill="#c4d2bf" stroke="#c4d2bf" stroke-width="1.51"/><path d="M311,858L334,941L400,933Z" fill="#bed1c2" stroke="#bed1c2" stroke-width="1.51"/><path d="M157,669L170,746L265,661Z" fill="#d0d8c6" stroke="#d0d8c6" stroke-width="1.51"/><path d="M348,-127L412,-112L492,-108Z" fill="#e4cfc4" stroke="#e4cfc4" stroke-width="1.51"/><path d="M1022,-108L953,-21L1016,-5Z" fill="#d5cfc0" stroke="#d5cfc0" stroke-width="1.51"/><path d="M238,-42L163,72L255,61Z" fill="#e3d5c8" stroke="#e3d5c8" stroke-width="1.51"/><path d="M492,-108L412,-112L420,-42Z" fill="#e5d2c7" stroke="#e5d2c7" stroke-width="1.51"/><path d="M348,-127L492,-108L864,-125Z" fill="#e7d2c9" stroke="#e7d2c9" stroke-width="1.51"/><path d="M418,1011L481,907L400,933Z" fill="#bcd1c4" stroke="#bcd1c4" stroke-width="1.51"/><path d="M418,1011L516,1028L481,907Z" fill="#bad0c4" stroke="#bad0c4" stroke-width="1.51"/><path d="M260,932L334,941L311,858Z" fill="#bed1c1" stroke="#bed1c1" stroke-width="1.51"/><path d="M163,72L146,150L225,125Z" fill="#e4dacc" stroke="#e4dacc" stroke-width="1.51"/><path d="M63,247L75,313L141,302Z" fill="#e7dcd1" stroke="#e7dcd1" stroke-width="1.51"/><path d="M163,72L225,125L255,61Z" fill="#e3dacc" stroke="#e3dacc" stroke-width="1.51"/><path d="M1283,769L1287,654L1184,688Z" fill="#afcdb7" stroke="#afcdb7" stroke-width="1.51"/><path d="M1301,336L1302,245L1213,315Z" fill="#c7d9ca" stroke="#c7d9ca" stroke-width="1.51"/><path d="M1119,-104L1022,-108L1087,-29Z" fill="#cec9b7" stroke="#cec9b7" stroke-width="1.51"/><path d="M1087,-29L1022,-108L1016,-5Z" fill="#d1cdbd" stroke="#d1cdbd" stroke-width="1.51"/><path d="M136,501L86,589L164,575Z" fill="#e0dbd0" stroke="#e0dbd0" stroke-width="1.51"/><path d="M157,669L65,684L170,746Z" fill="#cfd3c1" stroke="#cfd3c1" stroke-width="1.51"/><path d="M62,472L136,501L147,423Z" fill="#eae1d9" stroke="#eae1d9" stroke-width="1.51"/><path d="M75,313L147,423L141,302Z" fill="#e9dfd6" stroke="#e9dfd6" stroke-width="1.51"/><path d="M1195,761L1283,769L1184,688Z" fill="#acccb5" stroke="#acccb5" stroke-width="1.51"/><path d="M1043,903L1124,911L1110,818Z" fill="#aac9b5" stroke="#aac9b5" stroke-width="1.51"/><path d="M1124,989L1124,911L1043,903Z" fill="#a5c6b3" stroke="#a5c6b3" stroke-width="1.51"/><path d="M63,247L141,302L165,251Z" fill="#e7ddd2" stroke="#e7ddd2" stroke-width="1.51"/><path d="M1191,-40L1119,-104L1087,-29Z" fill="#cac8b5" stroke="#cac8b5" stroke-width="1.51"/><path d="M1302,245L1259,129L1208,214Z" fill="#c5d7c5" stroke="#c5d7c5" stroke-width="1.51"/><path d="M60,406L62,472L147,423Z" fill="#ebe0d8" stroke="#ebe0d8" stroke-width="1.51"/><path d="M334,941L418,1011L400,933Z" fill="#bbcfc2" stroke="#bbcfc2" stroke-width="1.51"/><path d="M1124,911L1200,836L1110,818Z" fill="#a7c9b2" stroke="#a7c9b2" stroke-width="1.51"/><path d="M870,1015L950,1026L917,930Z" fill="#afccbe" stroke="#afccbe" stroke-width="1.51"/><path d="M606,1030L950,1026L870,1015Z" fill="#b5cec5" stroke="#b5cec5" stroke-width="1.51"/><path d="M75,313L60,406L147,423Z" fill="#e9ded6" stroke="#e9ded6" stroke-width="1.51"/><path d="M53,774L178,821L170,746Z" fill="#c7cebb" stroke="#c7cebb" stroke-width="1.51"/><path d="M170,746L178,821L243,762Z" fill="#c7d2bf" stroke="#c7d2bf" stroke-width="1.51"/><path d="M256,859L260,932L311,858Z" fill="#bfd1c0" stroke="#bfd1c0" stroke-width="1.51"/><path d="M334,941L312,1020L418,1011Z" fill="#b8cdc0" stroke="#b8cdc0" stroke-width="1.51"/><path d="M146,150L63,247L165,251Z" fill="#e5dccf" stroke="#e5dccf" stroke-width="1.51"/><path d="M238,-42L255,61L343,-19Z" fill="#e3d6c9" stroke="#e3d6c9" stroke-width="1.51"/><path d="M147,938L260,932L256,859Z" fill="#bdcdbc" stroke="#bdcdbc" stroke-width="1.51"/><path d="M412,-112L348,-127L343,-19Z" fill="#e3d1c5" stroke="#e3d1c5" stroke-width="1.51"/><path d="M932,-133L348,-127L864,-125Z" fill="#ead4cd" stroke="#ead4cd" stroke-width="1.51"/><path d="M-12,302L-6,427L60,406Z" fill="#e9d9d1" stroke="#e9d9d1" stroke-width="1.51"/><path d="M76,150L63,247L146,150Z" fill="#e4d9cc" stroke="#e4d9cc" stroke-width="1.51"/><path d="M249,-116L238,-42L343,-19Z" fill="#e2d1c4" stroke="#e2d1c4" stroke-width="1.51"/><path d="M1345,669L1350,584L1292,592Z" fill="#b5d0bf" stroke="#b5d0bf" stroke-width="1.51"/><path d="M1292,592L1350,584L1293,477Z" fill="#bdd5c6" stroke="#bdd5c6" stroke-width="1.51"/><path d="M1301,336L1367,325L1302,245Z" fill="#c4d7c9" stroke="#c4d7c9" stroke-width="1.51"/><path d="M1345,669L1292,592L1287,654Z" fill="#b3cfbd" stroke="#b3cfbd" stroke-width="1.51"/><path d="M1382,498L1367,403L1293,477Z" fill="#c6d9cf" stroke="#c6d9cf" stroke-width="1.51"/><path d="M1389,78L1261,67L1259,129Z" fill="#c1cebd" stroke="#c1cebd" stroke-width="1.51"/><path d="M950,1026L1014,1027L917,930Z" fill="#abc9bb" stroke="#abc9bb" stroke-width="1.51"/><path d="M1280,824L1195,761L1200,836Z" fill="#a7c8b1" stroke="#a7c8b1" stroke-width="1.51"/><path d="M606,1030L1014,1027L950,1026Z" fill="#b0ccc1" stroke="#b0ccc1" stroke-width="1.51"/><path d="M1280,824L1283,769L1195,761Z" fill="#a7c8b1" stroke="#a7c8b1" stroke-width="1.51"/><path d="M1187,933L1200,836L1124,911Z" fill="#a3c6b0" stroke="#a3c6b0" stroke-width="1.51"/><path d="M1259,129L1261,67L1184,73Z" fill="#c5d1be" stroke="#c5d1be" stroke-width="1.51"/><path d="M1354,168L1259,129L1302,245Z" fill="#c2d4c3" stroke="#c2d4c3" stroke-width="1.51"/><path d="M1367,403L1367,325L1301,336Z" fill="#c4d7cb" stroke="#c4d7cb" stroke-width="1.51"/><path d="M265,1033L312,1020L260,932Z" fill="#b6cbbd" stroke="#b6cbbd" stroke-width="1.51"/><path d="M86,589L65,684L157,669Z" fill="#d5d5c5" stroke="#d5d5c5" stroke-width="1.51"/><path d="M4,663L65,684L86,589Z" fill="#d5d2c2" stroke="#d5d2c2" stroke-width="1.51"/><path d="M-23,562L86,589L62,472Z" fill="#e1d7cd" stroke="#e1d7cd" stroke-width="1.51"/><path d="M1191,-40L1090,57L1184,73Z" fill="#c9cfbb" stroke="#c9cfbb" stroke-width="1.51"/><path d="M1022,-108L1119,-104L932,-133Z" fill="#d2c8b9" stroke="#d2c8b9" stroke-width="1.51"/><path d="M1261,67L1191,-40L1184,73Z" fill="#c6cdba" stroke="#c6cdba" stroke-width="1.51"/><path d="M1367,403L1456,317L1367,325Z" fill="#c1d5ca" stroke="#c1d5ca" stroke-width="1.51"/><path d="M1350,584L1382,498L1293,477Z" fill="#bfd5c9" stroke="#bfd5c9" stroke-width="1.51"/><path d="M1433,478L1382,498L1450,600Z" fill="#bad2c6" stroke="#bad2c6" stroke-width="1.51"/><path d="M1283,769L1345,669L1287,654Z" fill="#adcbb7" stroke="#adcbb7" stroke-width="1.51"/><path d="M163,72L76,150L146,150Z" fill="#e3d8ca" stroke="#e3d8ca" stroke-width="1.51"/><path d="M81,44L76,150L163,72Z" fill="#e3d4c6" stroke="#e3d4c6" stroke-width="1.51"/><path d="M1367,325L1367,247L1302,245Z" fill="#c2d5c7" stroke="#c2d5c7" stroke-width="1.51"/><path d="M1456,317L1367,247L1367,325Z" fill="#bfd3c7" stroke="#bfd3c7" stroke-width="1.51"/><path d="M1283,769L1382,751L1345,669Z" fill="#a7c8b3" stroke="#a7c8b3" stroke-width="1.51"/><path d="M1124,989L1187,933L1124,911Z" fill="#a0c4af" stroke="#a0c4af" stroke-width="1.51"/><path d="M1014,1027L1124,989L1043,903Z" fill="#a5c6b4" stroke="#a5c6b4" stroke-width="1.51"/><path d="M1299,904L1280,824L1200,836Z" fill="#a1c4af" stroke="#a1c4af" stroke-width="1.51"/><path d="M260,932L312,1020L334,941Z" fill="#b9cebf" stroke="#b9cebf" stroke-width="1.51"/><path d="M418,1011L312,1020L516,1028Z" fill="#b7cdc1" stroke="#b7cdc1" stroke-width="1.51"/><path d="M265,1033L260,932L177,999Z" fill="#b6c9ba" stroke="#b6c9ba" stroke-width="1.51"/><path d="M60,406L-6,427L62,472Z" fill="#eadcd5" stroke="#eadcd5" stroke-width="1.51"/><path d="M-12,302L60,406L75,313Z" fill="#e8dad1" stroke="#e8dad1" stroke-width="1.51"/><path d="M-12,302L75,313L63,247Z" fill="#e6d9ce" stroke="#e6d9ce" stroke-width="1.51"/><path d="M1367,247L1354,168L1302,245Z" fill="#c1d3c4" stroke="#c1d3c4" stroke-width="1.51"/><path d="M1261,67L1273,-26L1191,-40Z" fill="#c4c9b7" stroke="#c4c9b7" stroke-width="1.51"/><path d="M238,-42L175,-47L163,72Z" fill="#e2d0c3" stroke="#e2d0c3" stroke-width="1.51"/><path d="M348,-127L249,-116L343,-19Z" fill="#e2d0c3" stroke="#e2d0c3" stroke-width="1.51"/><path d="M249,-116L175,-47L238,-42Z" fill="#e1cdbf" stroke="#e1cdbf" stroke-width="1.51"/><path d="M-6,427L-23,477L62,472Z" fill="#eadbd4" stroke="#eadbd4" stroke-width="1.51"/><path d="M-94,493L-23,477L-6,427Z" fill="#e9d7d0" stroke="#e9d7d0" stroke-width="1.51"/><path d="M-18,229L-12,302L63,247Z" fill="#e5d6cb" stroke="#e5d6cb" stroke-width="1.51"/><path d="M-18,229L63,247L76,150Z" fill="#e4d6ca" stroke="#e4d6ca" stroke-width="1.51"/><path d="M65,684L53,774L170,746Z" fill="#cbcebc" stroke="#cbcebc" stroke-width="1.51"/><path d="M7,751L53,774L65,684Z" fill="#cacbb8" stroke="#cacbb8" stroke-width="1.51"/><path d="M-23,562L4,663L86,589Z" fill="#d9d2c5" stroke="#d9d2c5" stroke-width="1.51"/><path d="M175,-47L81,44L163,72Z" fill="#e2d0c2" stroke="#e2d0c2" stroke-width="1.51"/><path d="M-23,477L-23,562L62,472Z" fill="#e5d7ce" stroke="#e5d7ce" stroke-width="1.51"/><path d="M1382,498L1433,478L1367,403Z" fill="#c3d7ce" stroke="#c3d7ce" stroke-width="1.51"/><path d="M1367,247L1469,163L1354,168Z" fill="#bdd1c2" stroke="#bdd1c2" stroke-width="1.51"/><path d="M1450,600L1382,498L1350,584Z" fill="#b7d1c3" stroke="#b7d1c3" stroke-width="1.51"/><path d="M1450,600L1350,584L1345,669Z" fill="#b1cdbd" stroke="#b1cdbd" stroke-width="1.51"/><path d="M1362,-22L1273,-26L1261,67Z" fill="#c0c7b6" stroke="#c0c7b6" stroke-width="1.51"/><path d="M1191,-40L1204,-112L1119,-104Z" fill="#c7c4b1" stroke="#c7c4b1" stroke-width="1.51"/><path d="M312,1020L265,1033L516,1028Z" fill="#b5cbbf" stroke="#b5cbbf" stroke-width="1.51"/><path d="M516,1028L265,1033L606,1030Z" fill="#b7cdc2" stroke="#b7cdc2" stroke-width="1.51"/><path d="M606,1030L1286,1032L1014,1027Z" fill="#a8c7b9" stroke="#a8c7b9" stroke-width="1.51"/><path d="M147,938L256,859L178,821Z" fill="#c0cdbc" stroke="#c0cdbc" stroke-width="1.51"/><path d="M1259,-90L1204,-112L1191,-40Z" fill="#c5c3b0" stroke="#c5c3b0" stroke-width="1.51"/><path d="M1119,-104L1204,-112L932,-133Z" fill="#cdc5b4" stroke="#cdc5b4" stroke-width="1.51"/><path d="M53,774L86,858L178,821Z" fill="#c4cbb8" stroke="#c4cbb8" stroke-width="1.51"/><path d="M64,907L86,858L-13,828Z" fill="#c0c4b3" stroke="#c0c4b3" stroke-width="1.51"/><path d="M86,858L147,938L178,821Z" fill="#c0c9b8" stroke="#c0c9b8" stroke-width="1.51"/><path d="M-12,302L-87,319L-6,427Z" fill="#e8d5cd" stroke="#e8d5cd" stroke-width="1.51"/><path d="M-31,141L-18,229L76,150Z" fill="#e3d3c7" stroke="#e3d3c7" stroke-width="1.51"/><path d="M1014,1027L1181,1013L1124,989Z" fill="#9ec2b0" stroke="#9ec2b0" stroke-width="1.51"/><path d="M1124,989L1181,1013L1187,933Z" fill="#9dc1ad" stroke="#9dc1ad" stroke-width="1.51"/><path d="M1187,933L1299,904L1200,836Z" fill="#a0c3ae" stroke="#a0c3ae" stroke-width="1.51"/><path d="M1353,843L1283,769L1280,824Z" fill="#a2c4af" stroke="#a2c4af" stroke-width="1.51"/><path d="M1353,843L1382,751L1283,769Z" fill="#a2c5af" stroke="#a2c5af" stroke-width="1.51"/><path d="M1286,1032L1299,904L1187,933Z" fill="#99bfab" stroke="#99bfab" stroke-width="1.51"/><path d="M-13,828L7,751L-96,769Z" fill="#c6c4b1" stroke="#c6c4b1" stroke-width="1.51"/><path d="M4,663L7,751L65,684Z" fill="#cecdbb" stroke="#cecdbb" stroke-width="1.51"/><path d="M1362,-22L1259,-90L1273,-26Z" fill="#c1c3b1" stroke="#c1c3b1" stroke-width="1.51"/><path d="M1476,404L1456,317L1367,403Z" fill="#c0d4ca" stroke="#c0d4ca" stroke-width="1.51"/><path d="M1461,680L1450,600L1345,669Z" fill="#accab9" stroke="#accab9" stroke-width="1.51"/><path d="M1433,478L1476,404L1367,403Z" fill="#c2d6cd" stroke="#c2d6cd" stroke-width="1.51"/><path d="M1273,-26L1259,-90L1191,-40Z" fill="#c4c4b2" stroke="#c4c4b2" stroke-width="1.51"/><path d="M1389,78L1259,129L1354,168Z" fill="#bfd0bf" stroke="#bfd0bf" stroke-width="1.51"/><path d="M147,938L177,999L260,932Z" fill="#b9c8b9" stroke="#b9c8b9" stroke-width="1.51"/><path d="M71,1004L177,999L147,938Z" fill="#b7c3b4" stroke="#b7c3b4" stroke-width="1.51"/><path d="M64,907L147,938L86,858Z" fill="#bdc6b5" stroke="#bdc6b5" stroke-width="1.51"/><path d="M1299,904L1353,843L1280,824Z" fill="#9ec2ae" stroke="#9ec2ae" stroke-width="1.51"/><path d="M1382,751L1461,680L1345,669Z" fill="#a7c8b5" stroke="#a7c8b5" stroke-width="1.51"/><path d="M1469,163L1389,78L1354,168Z" fill="#bccfbe" stroke="#bccfbe" stroke-width="1.51"/><path d="M1532,489L1476,404L1433,478Z" fill="#bed4cc" stroke="#bed4cc" stroke-width="1.51"/><path d="M-10,82L-31,141L76,150Z" fill="#e2d1c3" stroke="#e2d1c3" stroke-width="1.51"/><path d="M-18,229L-87,319L-12,302Z" fill="#e6d4c9" stroke="#e6d4c9" stroke-width="1.51"/><path d="M-23,477L-94,493L-23,562Z" fill="#e3d4cb" stroke="#e3d4cb" stroke-width="1.51"/><path d="M-10,82L76,150L81,44Z" fill="#e2d0c2" stroke="#e2d0c2" stroke-width="1.51"/><path d="M-10,82L81,44L-6,-8Z" fill="#e1cabc" stroke="#e1cabc" stroke-width="1.51"/><path d="M1456,317L1469,252L1367,247Z" fill="#bdd2c5" stroke="#bdd2c5" stroke-width="1.51"/><path d="M1534,335L1469,252L1456,317Z" fill="#bbd0c5" stroke="#bbd0c5" stroke-width="1.51"/><path d="M-13,828L86,858L53,774Z" fill="#c3c7b5" stroke="#c3c7b5" stroke-width="1.51"/><path d="M58,-47L81,44L175,-47Z" fill="#e1cabd" stroke="#e1cabd" stroke-width="1.51"/><path d="M-93,222L-87,319L-18,229Z" fill="#e5d1c6" stroke="#e5d1c6" stroke-width="1.51"/><path d="M-23,562L-120,600L4,663Z" fill="#d8cec0" stroke="#d8cec0" stroke-width="1.51"/><path d="M249,-116L139,-128L175,-47Z" fill="#e1c8bb" stroke="#e1c8bb" stroke-width="1.51"/><path d="M348,-127L139,-128L249,-116Z" fill="#e1c9bc" stroke="#e1c9bc" stroke-width="1.51"/><path d="M932,-133L139,-128L348,-127Z" fill="#e5cfc5" stroke="#e5cfc5" stroke-width="1.51"/><path d="M-114,385L-94,493L-6,427Z" fill="#ead6ce" stroke="#ead6ce" stroke-width="1.51"/><path d="M7,751L-13,828L53,774Z" fill="#c6c7b4" stroke="#c6c7b4" stroke-width="1.51"/><path d="M1450,600L1532,489L1433,478Z" fill="#b7d0c5" stroke="#b7d0c5" stroke-width="1.51"/><path d="M1469,733L1461,680L1382,751Z" fill="#a3c5b1" stroke="#a3c5b1" stroke-width="1.51"/><path d="M94,-100L58,-47L175,-47Z" fill="#e1c6b9" stroke="#e1c6b9" stroke-width="1.51"/><path d="M1450,46L1362,-22L1389,78Z" fill="#bbc6b6" stroke="#bbc6b6" stroke-width="1.51"/><path d="M1389,78L1362,-22L1261,67Z" fill="#bfc9b8" stroke="#bfc9b8" stroke-width="1.51"/><path d="M139,-128L94,-100L175,-47Z" fill="#e0c5b8" stroke="#e0c5b8" stroke-width="1.51"/><path d="M-102,683L7,751L4,663Z" fill="#cec9b8" stroke="#cec9b8" stroke-width="1.51"/><path d="M-87,319L-114,385L-6,427Z" fill="#e8d4cc" stroke="#e8d4cc" stroke-width="1.51"/><path d="M-93,222L-114,385L-87,319Z" fill="#e6d1c7" stroke="#e6d1c7" stroke-width="1.51"/><path d="M1469,252L1469,163L1367,247Z" fill="#bcd0c2" stroke="#bcd0c2" stroke-width="1.51"/><path d="M1540,251L1469,163L1469,252Z" fill="#b8cec1" stroke="#b8cec1" stroke-width="1.51"/><path d="M-89,147L-31,141L-115,69Z" fill="#e1cbbe" stroke="#e1cbbe" stroke-width="1.51"/><path d="M-89,147L-93,222L-31,141Z" fill="#e2cec1" stroke="#e2cec1" stroke-width="1.51"/><path d="M-31,141L-93,222L-18,229Z" fill="#e3d1c4" stroke="#e3d1c4" stroke-width="1.51"/><path d="M1547,584L1532,489L1450,600Z" fill="#b1cdc1" stroke="#b1cdc1" stroke-width="1.51"/><path d="M1467,836L1469,733L1382,751Z" fill="#9ec2ae" stroke="#9ec2ae" stroke-width="1.51"/><path d="M1467,836L1382,751L1353,843Z" fill="#9dc1ad" stroke="#9dc1ad" stroke-width="1.51"/><path d="M1476,404L1534,335L1456,317Z" fill="#bcd1c7" stroke="#bcd1c7" stroke-width="1.51"/><path d="M1547,584L1450,600L1543,644Z" fill="#abc9bb" stroke="#abc9bb" stroke-width="1.51"/><path d="M58,-47L-6,-8L81,44Z" fill="#e1c7ba" stroke="#e1c7ba" stroke-width="1.51"/><path d="M0,-128L-6,-8L58,-47Z" fill="#e0c1b4" stroke="#e0c1b4" stroke-width="1.51"/><path d="M1299,904L1375,939L1353,843Z" fill="#99beac" stroke="#99beac" stroke-width="1.51"/><path d="M1181,1013L1286,1032L1187,933Z" fill="#98beab" stroke="#98beab" stroke-width="1.51"/><path d="M1014,1027L1286,1032L1181,1013Z" fill="#99beac" stroke="#99beac" stroke-width="1.51"/><path d="M265,1033L1286,1032L606,1030Z" fill="#bbd2cb" stroke="#bbd2cb" stroke-width="1.51"/><path d="M-94,493L-120,600L-23,562Z" fill="#dfcfc5" stroke="#dfcfc5" stroke-width="1.51"/><path d="M-114,385L-120,600L-94,493Z" fill="#e5d1c9" stroke="#e5d1c9" stroke-width="1.51"/><path d="M-120,600L-102,683L4,663Z" fill="#d4cabb" stroke="#d4cabb" stroke-width="1.51"/><path d="M1550,388L1534,335L1476,404Z" fill="#bbd1c8" stroke="#bbd1c8" stroke-width="1.51"/><path d="M-115,69L-31,141L-10,82Z" fill="#e1cabd" stroke="#e1cabd" stroke-width="1.51"/><path d="M-93,222L-115,69L-114,385Z" fill="#e3cdc2" stroke="#e3cdc2" stroke-width="1.51"/><path d="M1354,990L1375,939L1299,904Z" fill="#95bcaa" stroke="#95bcaa" stroke-width="1.51"/><path d="M1469,163L1450,46L1389,78Z" fill="#b9caba" stroke="#b9caba" stroke-width="1.51"/><path d="M1472,-5L1450,46L1521,65Z" fill="#b7c3b4" stroke="#b7c3b4" stroke-width="1.51"/><path d="M-19,934L64,907L-13,828Z" fill="#bec1b0" stroke="#bec1b0" stroke-width="1.51"/><path d="M-19,934L71,1004L64,907Z" fill="#b9bfb0" stroke="#b9bfb0" stroke-width="1.51"/><path d="M64,907L71,1004L147,938Z" fill="#b9c2b3" stroke="#b9c2b3" stroke-width="1.51"/><path d="M177,999L71,1004L265,1033Z" fill="#b5c4b6" stroke="#b5c4b6" stroke-width="1.51"/><path d="M1534,335L1540,251L1469,252Z" fill="#b8cec3" stroke="#b8cec3" stroke-width="1.51"/><path d="M1532,489L1550,388L1476,404Z" fill="#bcd3cb" stroke="#bcd3cb" stroke-width="1.51"/><path d="M1286,1032L1354,990L1299,904Z" fill="#94bcaa" stroke="#94bcaa" stroke-width="1.51"/><path d="M1543,644L1461,680L1532,755Z" fill="#a2c4b2" stroke="#a2c4b2" stroke-width="1.51"/><path d="M1543,644L1450,600L1461,680Z" fill="#a9c8b9" stroke="#a9c8b9" stroke-width="1.51"/><path d="M1532,489L1547,584L1550,388Z" fill="#b7d0c7" stroke="#b7d0c7" stroke-width="1.51"/><path d="M1467,836L1353,843L1457,919Z" fill="#97bdaa" stroke="#97bdaa" stroke-width="1.51"/><path d="M1550,388L1540,251L1534,335Z" fill="#b8cfc5" stroke="#b8cfc5" stroke-width="1.51"/><path d="M-102,683L-96,769L7,751Z" fill="#cac5b2" stroke="#cac5b2" stroke-width="1.51"/><path d="M-120,600L-96,769L-102,683Z" fill="#d0c6b5" stroke="#d0c6b5" stroke-width="1.51"/><path d="M1457,919L1353,843L1375,939Z" fill="#96bcab" stroke="#96bcab" stroke-width="1.51"/><path d="M1532,755L1461,680L1469,733Z" fill="#a0c3b0" stroke="#a0c3b0" stroke-width="1.51"/><path d="M1362,-22L1371,-117L1259,-90Z" fill="#bfbfae" stroke="#bfbfae" stroke-width="1.51"/><path d="M1259,-90L1371,-117L1204,-112Z" fill="#c1bead" stroke="#c1bead" stroke-width="1.51"/><path d="M1204,-112L1371,-117L932,-133Z" fill="#c7c1ae" stroke="#c7c1ae" stroke-width="1.51"/><path d="M1440,-114L1371,-117L1362,-22Z" fill="#bbbcac" stroke="#bbbcac" stroke-width="1.51"/><path d="M-88,822L-19,934L-13,828Z" fill="#c0bfae" stroke="#c0bfae" stroke-width="1.51"/><path d="M-96,769L-88,822L-13,828Z" fill="#c4c1ae" stroke="#c4c1ae" stroke-width="1.51"/><path d="M1521,65L1450,46L1469,163Z" fill="#b7c8b9" stroke="#b7c8b9" stroke-width="1.51"/><path d="M1450,46L1472,-5L1362,-22Z" fill="#bac2b3" stroke="#bac2b3" stroke-width="1.51"/><path d="M1467,836L1532,755L1469,733Z" fill="#9ac0ac" stroke="#9ac0ac" stroke-width="1.51"/><path d="M1530,858L1532,755L1467,836Z" fill="#96bdaa" stroke="#96bdaa" stroke-width="1.51"/><path d="M-85,-17L-115,69L-10,82Z" fill="#e0c5b8" stroke="#e0c5b8" stroke-width="1.51"/><path d="M-89,147L-115,69L-93,222Z" fill="#e1ccbf" stroke="#e1ccbf" stroke-width="1.51"/><path d="M-114,385L-115,69L-120,600Z" fill="#e7d0c8" stroke="#e7d0c8" stroke-width="1.51"/><path d="M-85,-17L-10,82L-6,-8Z" fill="#e0c4b8" stroke="#e0c4b8" stroke-width="1.51"/><path d="M1466,1010L1457,919L1375,939Z" fill="#8fb8a8" stroke="#8fb8a8" stroke-width="1.51"/><path d="M94,-100L0,-128L58,-47Z" fill="#dfc0b3" stroke="#dfc0b3" stroke-width="1.51"/><path d="M139,-128L0,-128L94,-100Z" fill="#dfbfb3" stroke="#dfbfb3" stroke-width="1.51"/><path d="M932,-133L0,-128L139,-128Z" fill="#e3cdc1" stroke="#e3cdc1" stroke-width="1.51"/><path d="M1556,125L1521,65L1469,163Z" fill="#b5c8ba" stroke="#b5c8ba" stroke-width="1.51"/><path d="M-80,-93L-85,-17L-6,-8Z" fill="#dfbeb1" stroke="#dfbeb1" stroke-width="1.51"/><path d="M1540,251L1556,125L1469,163Z" fill="#b6ccbe" stroke="#b6ccbe" stroke-width="1.51"/><path d="M1550,388L1556,125L1540,251Z" fill="#b6cdc2" stroke="#b6cdc2" stroke-width="1.51"/><path d="M1547,584L1556,125L1550,388Z" fill="#b9cfc7" stroke="#b9cfc7" stroke-width="1.51"/><path d="M-107,936L-24,996L-19,934Z" fill="#b8baab" stroke="#b8baab" stroke-width="1.51"/><path d="M-19,934L-24,996L71,1004Z" fill="#b7bcad" stroke="#b7bcad" stroke-width="1.51"/><path d="M71,1004L-106,993L265,1033Z" fill="#b4beb0" stroke="#b4beb0" stroke-width="1.51"/><path d="M1472,-5L1440,-114L1362,-22Z" fill="#babeae" stroke="#babeae" stroke-width="1.51"/><path d="M1536,-22L1440,-114L1472,-5Z" fill="#b7bcad" stroke="#b7bcad" stroke-width="1.51"/><path d="M1536,-22L1472,-5L1521,65Z" fill="#b5c0b1" stroke="#b5c0b1" stroke-width="1.51"/><path d="M1457,919L1530,858L1467,836Z" fill="#93bba9" stroke="#93bba9" stroke-width="1.51"/><path d="M1532,755L1530,858L1543,644Z" fill="#9abfac" stroke="#9abfac" stroke-width="1.51"/><path d="M1543,644L1531,1032L1547,584Z" fill="#99bfac" stroke="#99bfac" stroke-width="1.51"/><path d="M0,-128L-80,-93L-6,-8Z" fill="#dfbdb0" stroke="#dfbdb0" stroke-width="1.51"/><path d="M-85,-17L-80,-93L-115,69Z" fill="#dfbeb1" stroke="#dfbeb1" stroke-width="1.51"/><path d="M-88,822L-107,936L-19,934Z" fill="#bdbcab" stroke="#bdbcab" stroke-width="1.51"/><path d="M-96,769L-107,936L-88,822Z" fill="#c1bdab" stroke="#c1bdab" stroke-width="1.51"/><path d="M-120,600L-107,936L-96,769Z" fill="#c6c0ad" stroke="#c6c0ad" stroke-width="1.51"/><path d="M1556,125L1536,-22L1521,65Z" fill="#b3c2b4" stroke="#b3c2b4" stroke-width="1.51"/><path d="M1457,919L1530,924L1530,858Z" fill="#8fb8a8" stroke="#8fb8a8" stroke-width="1.51"/><path d="M1354,990L1466,1010L1375,939Z" fill="#8fb8a8" stroke="#8fb8a8" stroke-width="1.51"/><path d="M1286,1032L1466,1010L1354,990Z" fill="#8eb7a7" stroke="#8eb7a7" stroke-width="1.51"/><path d="M1466,1010L1530,924L1457,919Z" fill="#8cb6a7" stroke="#8cb6a7" stroke-width="1.51"/><path d="M1530,858L1530,924L1543,644Z" fill="#95bcaa" stroke="#95bcaa" stroke-width="1.51"/><path d="M-107,936L-106,993L-24,996Z" fill="#b7b7a8" stroke="#b7b7a8" stroke-width="1.51"/><path d="M-24,996L-106,993L71,1004Z" fill="#b5b9ab" stroke="#b5b9ab" stroke-width="1.51"/><path d="M-120,600L-106,993L-107,936Z" fill="#c1bcab" stroke="#c1bcab" stroke-width="1.51"/><path d="M1466,1010L1531,1032L1530,924Z" fill="#88b3a4" stroke="#88b3a4" stroke-width="1.51"/><path d="M1530,924L1531,1032L1543,644Z" fill="#91b9a8" stroke="#91b9a8" stroke-width="1.51"/><path d="M1286,1032L1531,1032L1466,1010Z" fill="#8ab4a5" stroke="#8ab4a5" stroke-width="1.51"/><path d="M265,1033L1531,1032L1286,1032Z" fill="#a3c4b4" stroke="#a3c4b4" stroke-width="1.51"/><path d="M1536,-22L1554,-129L1440,-114Z" fill="#b5b8a9" stroke="#b5b8a9" stroke-width="1.51"/><path d="M1440,-114L1554,-129L1371,-117Z" fill="#b7b7a8" stroke="#b7b7a8" stroke-width="1.51"/><path d="M1371,-117L1554,-129L932,-133Z" fill="#c1bdab" stroke="#c1bdab" stroke-width="1.51"/><path d="M1556,125L1554,-129L1536,-22Z" fill="#b3bdaf" stroke="#b3bdaf" stroke-width="1.51"/></svg> \ No newline at end of file
diff --git a/src/client/assets/welcome-bg.svg b/src/client/assets/welcome-bg.svg
deleted file mode 100644
index ba8cd8dc0a..0000000000
--- a/src/client/assets/welcome-bg.svg
+++ /dev/null
@@ -1,579 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
- 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:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="1920"
- height="1080"
- viewBox="0 0 507.99999 285.75001"
- version="1.1"
- id="svg8"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="welcome-bg.svg">
- <defs
- id="defs2">
- <pattern
- inkscape:collect="always"
- xlink:href="#Checkerboard"
- id="pattern7194"
- patternTransform="scale(1.3152942)" />
- <linearGradient
- id="linearGradient7169"
- inkscape:collect="always">
- <stop
- id="stop7165"
- offset="0"
- style="stop-color:#eaeaea;stop-opacity:1" />
- <stop
- id="stop7167"
- offset="1"
- style="stop-color:#000000;stop-opacity:1" />
- </linearGradient>
- <linearGradient
- inkscape:collect="always"
- id="linearGradient7044">
- <stop
- style="stop-color:#000000;stop-opacity:1;"
- offset="0"
- id="stop7040" />
- <stop
- style="stop-color:#ffffff;stop-opacity:1"
- offset="1"
- id="stop7042" />
- </linearGradient>
- <pattern
- inkscape:collect="always"
- xlink:href="#Checkerboard"
- id="pattern7010"
- patternTransform="matrix(1.673813,0,0,1.673813,-177.6001,-146.38611)" />
- <pattern
- inkscape:stockid="Checkerboard"
- id="Checkerboard"
- patternTransform="translate(0,0) scale(10,10)"
- height="2"
- width="2"
- patternUnits="userSpaceOnUse"
- inkscape:collect="always"
- inkscape:isstock="true">
- <rect
- id="rect6201"
- height="1"
- width="1"
- y="0"
- x="0"
- style="fill:black;stroke:none" />
- <rect
- id="rect6203"
- height="1"
- width="1"
- y="1"
- x="1"
- style="fill:black;stroke:none" />
- </pattern>
- <linearGradient
- id="linearGradient5406"
- osb:paint="solid">
- <stop
- style="stop-color:#000000;stop-opacity:1;"
- offset="0"
- id="stop5404" />
- </linearGradient>
- <pattern
- patternUnits="userSpaceOnUse"
- width="15.999999"
- height="16.000025"
- patternTransform="matrix(0.26458333,0,0,0.26458333,-16.933332,263.1333)"
- id="pattern6465">
- <path
- d="m 8.0000542,8.0000126 h 7.9998878 c 3e-5,0 5.7e-5,3.78e-5 5.7e-5,3.78e-5 V 15.99995 c 0,3.7e-5 -2.7e-5,7.5e-5 -5.7e-5,7.5e-5 H 8.0000542 c -3.03e-5,0 -5.67e-5,-3.8e-5 -5.67e-5,-7.5e-5 V 8.0000504 c 0,0 2.64e-5,-3.78e-5 5.67e-5,-3.78e-5 z M 5.6692913e-5,0 H 7.9999408 c 3.02e-5,0 5.67e-5,3.7795275e-5 5.67e-5,7.5590551e-5 V 7.999937 c 0,3.78e-5 -2.65e-5,7.56e-5 -5.67e-5,7.56e-5 H 5.6692913e-5 C 2.2677165e-5,8.0000126 0,7.9999748 0,7.999937 V 7.5590551e-5 C 0,3.7795276e-5 2.2677165e-5,0 5.6692913e-5,0 Z"
- style="opacity:1;fill:#db1545;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.99999905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect6445-2"
- inkscape:connector-curvature="0" />
- </pattern>
- <linearGradient
- inkscape:collect="always"
- xlink:href="#linearGradient7044"
- id="linearGradient6476"
- gradientUnits="userSpaceOnUse"
- gradientTransform="matrix(3.223659,0,0,2.5556636,-579.27357,808.39)"
- x1="86.490868"
- y1="-216.62756"
- x2="176.77992"
- y2="-216.62756" />
- <mask
- maskUnits="userSpaceOnUse"
- id="mask6472">
- <rect
- transform="rotate(-90)"
- y="-0.91986513"
- x="-300.45657"
- height="511.36566"
- width="291.06116"
- id="rect6474"
- style="opacity:1;fill:url(#linearGradient6476);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.92238116;stroke-miterlimit:4;stroke-dasharray:none" />
- </mask>
- <pattern
- patternUnits="userSpaceOnUse"
- width="2340.7208"
- height="2340.7236"
- patternTransform="matrix(0.26458333,0,0,0.26458333,-63.499801,-58.601683)"
- id="pattern7142">
- <path
- d="m 1170.3684,1170.3628 h 1170.3448 c 0,0 0.01,0 0.01,0 v 1170.3457 c 0,0 0,0.011 -0.01,0.011 H 1170.3684 c 0,0 -0.01,0 -0.01,-0.011 v -1170.344 c 0,0 0,0 0.01,0 z M 0.00869291,1.1338583e-5 H 1170.352 c 0,0 0.01,0.0052913414 0.01,0.01096063142 V 1170.3511 c 0,0 0,0.011 -0.01,0.011 H 0.00869291 C 0.00340157,1170.3625 0,1170.3549 0,1170.3511 V 0.01096063 C 0,0.00566929 0.00312945,0 0.00869291,0 Z"
- style="opacity:1;fill:#763971;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2340.72119141;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path7135"
- inkscape:connector-curvature="0" />
- </pattern>
- <linearGradient
- inkscape:collect="always"
- xlink:href="#linearGradient7169"
- id="linearGradient7157"
- x1="-3.631536"
- y1="155.11069"
- x2="511.52777"
- y2="155.11069"
- gradientUnits="userSpaceOnUse"
- gradientTransform="matrix(2.184742,0,0,6.5696504,-17.948376,-1979.8074)" />
- <linearGradient
- inkscape:collect="always"
- xlink:href="#linearGradient7169"
- id="linearGradient7200"
- gradientUnits="userSpaceOnUse"
- gradientTransform="matrix(0.57804632,0,0,1.73822,6.5011419,-523.82404)"
- x1="-3.631536"
- y1="155.11069"
- x2="511.52777"
- y2="155.11069" />
- <mask
- maskUnits="userSpaceOnUse"
- id="mask7196">
- <rect
- transform="rotate(90)"
- y="-512.56537"
- x="4.4019437"
- height="516.7157"
- width="297.78595"
- id="rect7198"
- style="opacity:1;fill:url(#linearGradient7200);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.1217103;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
- </mask>
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#1e1d65"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0.84705882"
- inkscape:pageshadow="2"
- inkscape:zoom="0.79170474"
- inkscape:cx="1093.7227"
- inkscape:cy="695.27372"
- inkscape:document-units="mm"
- inkscape:current-layer="layer5"
- showgrid="true"
- units="px"
- inkscape:pagecheckerboard="false"
- inkscape:window-width="1920"
- inkscape:window-height="1017"
- inkscape:window-x="-8"
- inkscape:window-y="1072"
- inkscape:window-maximized="1"
- objecttolerance="1"
- guidetolerance="10000"
- gridtolerance="10000"
- inkscape:snap-bbox="true"
- inkscape:bbox-paths="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false"
- inkscape:lockguides="true">
- <inkscape:grid
- type="xygrid"
- id="grid6443"
- spacingx="2.1166667"
- spacingy="2.1166667"
- empspacing="4"
- color="#3f3fff"
- opacity="0.1254902"
- enabled="false" />
- <sodipodi:guide
- position="-69.219003,3.872392"
- orientation="1,0"
- id="guide6508"
- inkscape:locked="true" />
- </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(0,-11.249983)"
- style="display:inline"
- sodipodi:insensitive="true">
- <rect
- style="display:inline;opacity:0.2;fill:url(#pattern7194);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect7178"
- width="568.07599"
- height="367.82269"
- x="-37.871731"
- y="-52.665051"
- mask="url(#mask7196)" />
- </g>
- <g
- inkscape:groupmode="layer"
- id="layer2"
- inkscape:label="レイヤー 2"
- style="display:inline">
- <rect
- transform="translate(0,-11.249983)"
- style="opacity:0.5;fill:none;stroke:none;stroke-width:140.99996948"
- width="596.8999"
- height="596.90082"
- x="-63.49987"
- y="-58.600021"
- id="rect6468"
- mask="url(#mask6472)" />
- <path
- transform="translate(0,-11.249983)"
- sodipodi:type="star"
- style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6921"
- sodipodi:sides="4"
- sodipodi:cx="117.63232"
- sodipodi:cy="102.13793"
- sodipodi:r1="5.7652407"
- sodipodi:r2="2.8826203"
- sodipodi:arg1="1.4464413"
- sodipodi:arg2="2.2318395"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 118.34741,107.85865 -2.48485,-3.44532 -3.95096,-1.56031 3.44531,-2.48485 1.56032,-3.950959 2.48484,3.445318 3.95097,1.560311 -3.44532,2.48485 z"
- inkscape:transform-center-x="1.481982e-006"
- inkscape:transform-center-y="-1.1450451e-006" />
- <path
- transform="translate(0,-11.249983)"
- sodipodi:type="star"
- style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6923"
- sodipodi:sides="4"
- sodipodi:cx="317.5"
- sodipodi:cy="75.679596"
- sodipodi:r1="3.949214"
- sodipodi:r2="1.974607"
- sodipodi:arg1="1.6614562"
- sodipodi:arg2="2.4468544"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 317.14246,79.612591 -1.1594,-2.668882 -2.41606,-1.621658 2.66889,-1.15939 1.62165,-2.41606 1.1594,2.668882 2.41606,1.621658 -2.66889,1.15939 z"
- inkscape:transform-center-x="4.0000001e-006" />
- <path
- transform="translate(0,-11.249983)"
- sodipodi:type="star"
- style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6925"
- sodipodi:sides="4"
- sodipodi:cx="230.97409"
- sodipodi:cy="57.802349"
- sodipodi:r1="2.2613134"
- sodipodi:r2="1.1306567"
- sodipodi:arg1="1.2490458"
- sodipodi:arg2="2.0344439"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 231.68918,59.947619 -1.22073,-1.13398 -1.63963,-0.2962 1.13398,-1.220735 0.2962,-1.639625 1.22074,1.13398 1.63962,0.2962 -1.13398,1.220735 z"
- inkscape:transform-center-x="2.9099099e-006" />
- <path
- transform="translate(0,-11.249983)"
- sodipodi:type="star"
- style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6927"
- sodipodi:sides="4"
- sodipodi:cx="260.65033"
- sodipodi:cy="106.42847"
- sodipodi:r1="1.59899"
- sodipodi:r2="0.79949504"
- sodipodi:arg1="2.0344439"
- sodipodi:arg2="2.8198421"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 259.93524,107.85865 -0.0434,-1.17736 -0.67171,-0.96791 1.17736,-0.0434 0.96791,-0.67171 0.0434,1.17735 0.67171,0.96792 -1.17736,0.0434 z"
- inkscape:transform-center-x="3.2837838e-006"
- inkscape:transform-center-y="-1.1990991e-006" />
- <path
- sodipodi:type="star"
- style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6925-2"
- sodipodi:sides="4"
- sodipodi:cx="87.956078"
- sodipodi:cy="127.16609"
- sodipodi:r1="2.2613134"
- sodipodi:r2="1.1306567"
- sodipodi:arg1="1.2490458"
- sodipodi:arg2="2.0344439"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 88.671168,129.31136 -1.220735,-1.13398 -1.639626,-0.2962 1.13398,-1.22073 0.296201,-1.63963 1.220735,1.13398 1.639625,0.2962 -1.13398,1.22074 z"
- inkscape:transform-center-x="2.4830149e-006"
- transform="matrix(0.91666666,0,0,1,7.1509006,-11.249983)" />
- <ellipse
- style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06383465"
- id="path5313-3-7"
- cx="178.44102"
- cy="110.95996"
- rx="21.691566"
- ry="5.0825601"
- transform="rotate(-1.570553,-410.38805,-5.6250559)" />
- <ellipse
- style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.08063243"
- id="path5313-3-7-5"
- cx="200.1326"
- cy="116.80371"
- rx="27.399597"
- ry="6.4200115"
- transform="rotate(-1.570553,-410.38805,-5.6250559)" />
- <ellipse
- style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06734787"
- id="path5313-3-7-2"
- cx="-429.23041"
- cy="90.631134"
- rx="24.144913"
- ry="5.0825605"
- transform="matrix(-0.99537478,-0.09606802,-0.09606802,0.99537478,0,-11.249983)" />
- <ellipse
- style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.08507013"
- id="path5313-3-7-5-9"
- cx="-405.08548"
- cy="96.474884"
- rx="30.498529"
- ry="6.4200115"
- transform="matrix(-0.99537478,-0.09606802,-0.09606802,0.99537478,0,-11.249983)" />
- <ellipse
- style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.05208009"
- id="path5313-3-7-2-9"
- cx="-46.428764"
- cy="163.90004"
- rx="18.893074"
- ry="3.884198"
- transform="matrix(-0.99073724,0.13579293,0.14607844,0.98927301,0,-11.249983)" />
- <ellipse
- style="opacity:0.68000034;fill:#6e76a3;fill-opacity:1;stroke:none;stroke-width:0.06578472"
- id="path5313-3-7-5-9-1"
- cx="-27.535677"
- cy="168.36595"
- rx="23.864695"
- ry="4.9063048"
- transform="matrix(-0.99073724,0.13579293,0.14607844,0.98927301,0,-11.249983)" />
- <path
- transform="translate(0,-11.249983)"
- sodipodi:type="star"
- style="fill:#000000;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6923-9"
- sodipodi:sides="4"
- sodipodi:cx="459.82239"
- sodipodi:cy="139.8455"
- sodipodi:r1="3.949214"
- sodipodi:r2="1.9746071"
- sodipodi:arg1="1.6614562"
- sodipodi:arg2="2.4468544"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 459.46484,143.7785 -1.15939,-2.66888 -2.41606,-1.62166 2.66889,-1.15939 1.62165,-2.41606 1.15939,2.66888 2.41606,1.62166 -2.66888,1.15939 z"
- inkscape:transform-center-x="4.0000001e-006" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.81509405;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path5229"
- cx="192.18326"
- cy="74.677902"
- r="2.7216933" />
- <path
- sodipodi:type="star"
- style="fill:#ffffff;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6923-8"
- sodipodi:sides="4"
- sodipodi:cx="53.989292"
- sodipodi:cy="88.908768"
- sodipodi:r1="3.949214"
- sodipodi:r2="1.9746071"
- sodipodi:arg1="1.6614562"
- sodipodi:arg2="2.4468544"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 53.631747,92.841763 -1.15939,-2.668883 -2.41606,-1.621657 2.668883,-1.159391 1.621657,-2.41606 1.15939,2.668883 2.416061,1.621658 -2.668883,1.15939 z"
- inkscape:transform-center-x="2.0634674e-006"
- transform="matrix(0.61390676,-0.48689202,0.48689202,0.61390676,-23.159158,48.648961)"
- inkscape:transform-center-y="1.4320049e-006" />
- <path
- sodipodi:type="star"
- style="fill:#ffffff;fill-opacity:0.09661835;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- id="path6923-8-3"
- sodipodi:sides="4"
- sodipodi:cx="53.989292"
- sodipodi:cy="88.908768"
- sodipodi:r1="3.949214"
- sodipodi:r2="1.9746071"
- sodipodi:arg1="1.6614562"
- sodipodi:arg2="2.4468544"
- inkscape:flatsided="false"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 53.631747,92.841763 -1.15939,-2.668883 -2.41606,-1.621657 2.668883,-1.159391 1.621657,-2.41606 1.15939,2.668883 2.416061,1.621658 -2.668883,1.15939 z"
- inkscape:transform-center-x="3.0260172e-006"
- transform="matrix(0.58032639,0.43093706,-0.43093706,0.58032639,446.58431,23.35553)"
- inkscape:transform-center-y="-1.3594204e-006" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28035584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path5229-6"
- cx="347.17841"
- cy="36.709366"
- r="0.9361406" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28035584;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path5229-6-5"
- cx="116.0927"
- cy="42.136036"
- r="0.9361406" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.15;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.55002564;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path5229-0"
- cx="456.28247"
- cy="47.488548"
- r="1.8365992" />
- </g>
- <g
- inkscape:groupmode="layer"
- id="layer5"
- inkscape:label="レイヤー 4"
- style="display:none">
- <path
- transform="translate(0,-11.249983)"
- style="display:inline;fill:#ffff7c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-miterlimit:4;stroke-dasharray:none"
- d="m 377.25876,69.781182 a 18.234796,18.234796 0 0 1 8.1747,15.19442 18.234796,18.234796 0 0 1 -18.23455,18.235058 18.234796,18.234796 0 0 1 -10.14098,-3.08921 20.380066,20.380066 0 0 0 17.64905,10.2402 20.380066,20.380066 0 0 0 20.38015,-20.380152 20.380066,20.380066 0 0 0 -17.82837,-20.200316 z"
- id="path6914"
- inkscape:connector-curvature="0" />
- </g>
- <g
- inkscape:groupmode="layer"
- id="layer4"
- inkscape:label="レイヤー 3"
- style="display:none">
- <circle
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.36438358;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path5306"
- cx="168.31279"
- cy="2.1908164"
- r="36.253109" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.39123487px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 201.1259,19.428383 2.66976,2.617062 1.21734,-1.978474 -0.34264,5.194221 -4.15215,2.110811 1.0283,-1.928856 -2.76172,-2.210044 z"
- id="path5168"
- inkscape:connector-curvature="0" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.89719725px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 196.25421,26.631949 6.0286,8.817373 -3.70059,3.384671 -1.84127,-4.638447 -2.48924,2.916491 -2.23471,-6.507119 z"
- id="path5174"
- inkscape:connector-curvature="0" />
- <path
- style="display:inline;opacity:0.1;fill:#000500;fill-opacity:1;stroke:none;stroke-width:0.05121958px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 187.00695,34.050482 1.26268,2.214392 1.44195,-0.54357 1.31981,0.86123 0.21375,1.739039 -1.36828,1.61618 -1.80409,0.265403 -1.1589,-1.059687 -0.23516,-1.721875 1.11047,-0.916698 -0.43413,-0.680502 -0.4102,0.997264 0.74387,1.070883 -0.49255,1.027197 -1.26776,0.228606 -0.5501,-0.871237 0.15467,-0.82956 0.93559,-0.424446 0.58058,-1.450625 -0.75664,-1.131455 z"
- id="path6985"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccc" />
- <path
- style="display:inline;opacity:0.1;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.04695854px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 203.23593,14.367789 4.43345,3.766934 0.87976,-0.995725 0.46812,0.475437 -0.80488,0.995031 0.83731,0.705238 0.86731,-0.962102 0.50998,0.516259 -0.87206,0.921255 0.99505,0.941692 -0.44277,0.42746 -0.91483,-0.900095 -0.8367,0.879711 -0.43031,-0.474867 0.78065,-0.831436 -0.86665,-0.779727 -0.81136,0.912638 -0.55866,-0.483362 0.8179,-0.927279 -4.48211,-3.638676 z"
- id="path6891-8"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccc" />
- <path
- style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.58045781px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 204.43932,-5.3971152 6.34563,7.5781721 -3.73895,4.9604312 0.33681,4.6546149 -5.20345,5.793617 c 2.83273,-8.049795 3.31033,-11.8140092 3.09986,-18.9271334 z"
- id="path5208"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccc" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.11183073px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 205.60259,0.56695919 1.24493,0.127049 0.0916,-0.59592195 0.28719,0.07174803 -0.065,0.56786179 0.62071,0.0788993 -0.0423,0.36840374 -0.62423,-0.048236 -0.0804,0.8381885 0.52004,0.075191 -0.0192,0.3709729 -0.5764,-0.058257 -0.10087,0.8125312 0.54747,0.039404 -0.04,0.4153104 -0.5593,-0.071919 -0.0636,0.6224815 -0.3736,0.00386 0.0816,-0.6437327 -1.20305,-0.1533942 0.0499,-0.3674909 1.2006,0.1064631 0.11092,-0.7647515 -1.19622,-0.1448386 0.027,-0.3701253 1.23042,0.1176518 0.12327,-0.8721654 -1.26199,-0.1134749 z"
- id="path7229"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccccccccccc" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.16325578px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 204.68821,9.1424652 1.78173,-0.049987 -1.44996,0.7563273 1.12166,0.7127945 -1.34099,0.0029 0.93885,1.309289 -1.59949,-0.942185 z"
- id="path7212-4-6"
- inkscape:connector-curvature="0" />
- <path
- style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.71902335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 180.87434,36.932251 -8.12162,8.095249 -6.61262,-3.934427 -5.68596,1.043018 -7.6496,-6.371879 c 10.33078,4.527622 19.43137,4.062311 28.0698,1.168039 z"
- id="path5208-6"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccc" />
- <path
- style="display:inline;opacity:0.1;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.04569969px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 156.79314,37.138611 -0.83209,5.600235 1.27513,0.214749 -0.15211,0.631281 -1.23602,-0.153244 -0.15211,1.0545 1.24093,0.221743 -0.16427,0.686859 -1.20964,-0.246683 -0.26626,1.306416 -0.58089,-0.145968 0.27316,-1.218758 -1.15712,-0.238846 0.17092,-0.599741 1.08842,0.21735 0.19853,-1.117028 -1.17126,-0.200972 0.11204,-0.710141 1.18676,0.198837 0.70106,-5.574493 z"
- id="path6891-8-9"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccc" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.84177661px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 143.96364,29.933272 -4.59686,9.216397 3.65156,2.834687 1.22043,-4.692866 2.51661,2.524357 1.39851,-6.542721 z"
- id="path5174-1"
- inkscape:connector-curvature="0" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56489706px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 142.60658,28.70585 -2.96842,6.930652 -3.79379,-3.925042 4.56394,-5.124749 z"
- id="path5285"
- inkscape:connector-curvature="0" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.35393918px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 137.9306,23.319484 -3.42616,1.224261 1.2143,1.906916 -4.40128,-2.508612 -0.0822,-4.53226 1.25123,1.720316 3.10894,-1.477793 z"
- id="path5168-0"
- inkscape:connector-curvature="0" />
- <path
- style="display:inline;opacity:0.1;fill:#000500;fill-opacity:1;stroke:none;stroke-width:0.0498465px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 132.55595,11.444656 -2.31852,0.882408 0.30663,1.468015 -1.02588,1.140069 -1.70428,-0.05499 -1.34908,-1.557886 0.015,-1.774566 1.1926,-0.955614 1.69096,0.03182 0.7151,1.205156 0.71942,-0.315492 -0.89748,-0.543864 -1.14121,0.554849 -0.91394,-0.627513 -0.0299,-1.2533405 0.92017,-0.3984462 0.77453,0.2730438 0.26797,0.9632459 1.30792,0.775623 1.20137,-0.558052 z"
- id="path6985-7"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccc" />
- <path
- style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15882961px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 131.32384,2.4817954 -1.6313,-0.4305236 1.16551,1.0474206 -1.19547,0.453907 1.23564,0.290212 -1.16202,1.0740836 1.68796,-0.5749329 z"
- id="path7212-4-6-8"
- inkscape:connector-curvature="0" />
- <path
- style="display:inline;opacity:0.05;fill:#000016;fill-opacity:1;stroke:none;stroke-width:0.55575538px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 137.04207,-21.420699 -7.13207,5.035868 1.31743,5.70794 -2.10914,4.1341529 2.26645,6.93249012 c 0.67636,-8.23493742 2.69888,-15.39599902 5.65733,-21.81045102 z"
- id="path5208-4"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccc" />
- </g>
-</svg>
diff --git a/src/client/assets/welcome-fg.svg b/src/client/assets/welcome-fg.svg
deleted file mode 100644
index 5c795c3027..0000000000
--- a/src/client/assets/welcome-fg.svg
+++ /dev/null
@@ -1,380 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
- 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:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="1920"
- height="1080"
- viewBox="0 0 507.99999 285.75001"
- version="1.1"
- id="svg8"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="welcome-fg.svg">
- <defs
- id="defs2">
- <linearGradient
- inkscape:collect="always"
- id="linearGradient7044">
- <stop
- style="stop-color:#000000;stop-opacity:1;"
- offset="0"
- id="stop7040" />
- <stop
- style="stop-color:#ffffff;stop-opacity:1"
- offset="1"
- id="stop7042" />
- </linearGradient>
- <pattern
- inkscape:collect="always"
- xlink:href="#Checkerboard"
- id="pattern7010"
- patternTransform="matrix(1.673813,0,0,1.673813,-177.6001,-146.38611)" />
- <pattern
- inkscape:stockid="Checkerboard"
- id="Checkerboard"
- patternTransform="translate(0,0) scale(10,10)"
- height="2"
- width="2"
- patternUnits="userSpaceOnUse"
- inkscape:collect="always">
- <rect
- id="rect6201"
- height="1"
- width="1"
- y="0"
- x="0"
- style="fill:black;stroke:none" />
- <rect
- id="rect6203"
- height="1"
- width="1"
- y="1"
- x="1"
- style="fill:black;stroke:none" />
- </pattern>
- <linearGradient
- id="linearGradient5406"
- osb:paint="solid">
- <stop
- style="stop-color:#000000;stop-opacity:1;"
- offset="0"
- id="stop5404" />
- </linearGradient>
- <pattern
- patternUnits="userSpaceOnUse"
- width="15.999999"
- height="16.000025"
- patternTransform="matrix(0.26458333,0,0,0.26458333,-16.933332,263.1333)"
- id="pattern6465">
- <path
- d="m 8.0000542,8.0000126 h 7.9998878 c 3e-5,0 5.7e-5,3.78e-5 5.7e-5,3.78e-5 V 15.99995 c 0,3.7e-5 -2.7e-5,7.5e-5 -5.7e-5,7.5e-5 H 8.0000542 c -3.03e-5,0 -5.67e-5,-3.8e-5 -5.67e-5,-7.5e-5 V 8.0000504 c 0,0 2.64e-5,-3.78e-5 5.67e-5,-3.78e-5 z M 5.6692913e-5,0 H 7.9999408 c 3.02e-5,0 5.67e-5,3.7795275e-5 5.67e-5,7.5590551e-5 V 7.999937 c 0,3.78e-5 -2.65e-5,7.56e-5 -5.67e-5,7.56e-5 H 5.6692913e-5 C 2.2677165e-5,8.0000126 0,7.9999748 0,7.999937 V 7.5590551e-5 C 0,3.7795276e-5 2.2677165e-5,0 5.6692913e-5,0 Z"
- style="opacity:1;fill:#db1545;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.99999905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect6445-2"
- inkscape:connector-curvature="0" />
- </pattern>
- <linearGradient
- inkscape:collect="always"
- xlink:href="#linearGradient7044"
- id="linearGradient6476"
- gradientUnits="userSpaceOnUse"
- gradientTransform="matrix(3.223659,0,0,2.5556636,-579.27357,808.39)"
- x1="86.490868"
- y1="-216.62756"
- x2="176.77992"
- y2="-216.62756" />
- <mask
- maskUnits="userSpaceOnUse"
- id="mask6472">
- <rect
- transform="rotate(-90)"
- y="-0.91986513"
- x="-300.45657"
- height="511.36566"
- width="291.06116"
- id="rect6474"
- style="opacity:1;fill:url(#linearGradient6476);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.92238116;stroke-miterlimit:4;stroke-dasharray:none" />
- </mask>
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#1e1d65"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0.84705882"
- inkscape:pageshadow="2"
- inkscape:zoom="0.6363961"
- inkscape:cx="720.54406"
- inkscape:cy="371.58659"
- inkscape:document-units="mm"
- inkscape:current-layer="layer1"
- showgrid="true"
- units="px"
- inkscape:pagecheckerboard="true"
- inkscape:window-width="1920"
- inkscape:window-height="1057"
- inkscape:window-x="1912"
- inkscape:window-y="1143"
- inkscape:window-maximized="1"
- objecttolerance="1"
- guidetolerance="10000"
- gridtolerance="10000"
- inkscape:snap-bbox="true"
- inkscape:bbox-paths="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false">
- <inkscape:grid
- type="xygrid"
- id="grid6443"
- spacingx="2.1166667"
- spacingy="2.1166667"
- empspacing="4"
- color="#3f3fff"
- opacity="0.1254902"
- enabled="false" />
- <sodipodi:guide
- position="-69.219003,3.872392"
- orientation="1,0"
- id="guide6508"
- inkscape:locked="false" />
- </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:groupmode="layer"
- id="layer2"
- inkscape:label="Back"
- style="display:inline">
- <path
- style="fill:#253276;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 500.58203,825.29688 -54.2207,18.9121 18.91406,56.74219 -45.39258,10.08594 -11.34765,-39.08789 -46.6543,12.60937 13.87109,34.04493 -55.48047,15.13086 -12.60937,-44.13086 -47.91406,13.86914 13.86914,44.13086 -32.78321,11.3496 17.65235,35.30469 278.66211,-63.04492 z m -11.0957,26.45312 0.44726,11.5918 -12.03711,2.67382 -3.5664,-9.80664 z m 4.90429,24.51953 0.89258,9.80859 -9.36328,2.67383 -4.45703,-9.36133 z m -201.5,32.09766 v 11.14453 l -8.4707,1.7832 -4.9043,-8.91601 z"
- id="path4522"
- inkscape:connector-curvature="0"
- transform="scale(0.26458333)" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#253276;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 381.65643,238.28361 -47.37344,16.34717 116.09827,29.02457 -14.01186,-23.68672 -31.02626,-0.33362 z"
- id="path4520"
- inkscape:connector-curvature="0" />
- </g>
- <g
- inkscape:label="Ground"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(0,-11.249983)"
- style="display:inline">
- <circle
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:1.99730551"
- id="path5392"
- cx="253.06117"
- cy="887.61829"
- r="642.68146" />
- </g>
- <g
- inkscape:groupmode="layer"
- id="layer3"
- inkscape:label="Front">
- <path
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:1.00157475;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- d="m 565.38867,666.80078 -115.20508,24.36914 70.24414,231.09766 121.20118,-18.97656 8.61523,-148.01368 -76.28906,21.625 z m -30.15234,38.82813 3.09765,47.0625 -11.44531,2.49414 -9.14062,-46.10743 z m -26.41211,5.20898 10.30664,46.03906 -9.47852,2.06641 -17.14257,-44.88672 z m 41.45508,65.93945 2.80078,44.04493 -12.50391,3.40234 L 532.1543,781.75 Z m -25.15039,6.90039 9.4414,42.18165 -9.54297,2.59765 -13.99804,-40.91015 z m 85.48242,50.83789 1,42.35938 -22.15235,4.89648 -4.53906,-41.66406 z m -54.21485,10.16797 4.54102,41.66211 -7.67188,1.89649 -8.07421,-40.73047 z m -16.66992,4.20899 9.05469,40.45703 -8.88477,2.19727 -12.02734,-39.66016 z"
- id="path5398"
- transform="scale(0.26458333)"
- inkscape:connector-curvature="0" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 329.51477,199.15082 -32.04286,18.26817 12.8142,1.28619 -6.02656,28.18505 32.94792,3.49531 0.51681,-27.76301 11.91226,1.00737 z m -14.10711,25.93826 6.27123,0.90288 -1.15019,5.4805 -6.00929,-0.898 z m 13.58524,2.09643 0.42171,5.50053 -6.35262,-0.44337 1.22618,-5.67857 z m -15.04127,5.73678 6.21844,0.90138 -1.87301,4.94347 -5.07899,-0.81761 z m 8.80707,1.53673 6.3403,1.10313 0.43128,4.98637 -7.83808,-1.19409 z"
- id="path6874"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccccccccccccccccccccccc" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 366.28967,254.78298 7.49431,-30.40441 -7.41388,-2.66046 1.18763,-3.36104 7.21205,2.27141 1.38362,-5.73044 -7.20912,-2.66047 1.28561,-3.65794 7.01313,2.7643 2.17341,-7.01022 3.35519,1.48161 -2.1734,6.51147 6.70747,2.66046 -1.28564,3.16213 -6.31255,-2.46154 -1.68638,6.02735 6.80837,2.46447 -0.9887,3.84808 -6.90052,-2.47031 -6.71038,30.41026 z"
- id="path6891"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccc" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 74.047433,217.56203 -1.20251,0.65577 2.314585,6.84299 -4.564578,1.31517 13.625009,41.10395 21.186821,-5.50251 -7.183542,-43.56323 -22.044649,6.35259 z m 16.734379,10.06088 1.478463,10.23607 -8.339026,1.96939 -3.82509,-9.42992 z m 3.780131,14.55519 0.781863,9.82627 -7.001121,1.81797 -3.593063,-9.29297 z"
- id="path6944"
- inkscape:connector-curvature="0" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.24600939px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 43.603475,280.06036 -10.564819,-28.58824 -6.574764,2.28618 -0.916385,-3.37337 6.23111,-2.47535 -2.011396,-5.37101 -6.431418,2.16468 -1.002197,-3.66725 6.348194,-1.96596 -2.123972,-6.85578 3.11982,-0.81419 1.86458,6.45975 6.080155,-1.86705 0.744318,3.27357 -5.700174,1.79072 1.953823,5.78639 6.048884,-2.08256 1.308957,3.64208 -6.116434,2.13257 11.116753,28.12778 z"
- id="path6891-8"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccc" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 411.98753,264.70523 3.91734,-12.57157 -7.13355,-3.53259 -1.396,-8.02014 5.81668,-6.93436 10.92618,-0.52461 7.35863,5.88054 0.0806,8.11138 -5.67524,6.95564 -7.37536,-0.96565 -1.04168,4.03744 5.21293,-1.96321 1.42492,-6.58308 5.61592,-1.7579 5.33002,3.98422 -1.35343,5.14755 -3.67857,2.33882 -4.89966,-2.03926 -7.52592,2.91667 -1.60892,6.84465 z"
- id="path6985"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccc" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.27861062px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 438.77767,272.41521 -0.009,-2.99656 1.24656,2.44908 1.28337,-1.87551 -0.0534,2.25473 2.30831,-1.55949 -1.70125,2.67579 z"
- id="path7212"
- inkscape:connector-curvature="0" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.29395995px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 387.1467,259.13862 -0.3913,-3.17093 1.60741,2.46066 1.09423,-2.12083 0.23196,2.39229 2.19942,-1.8946 -1.42637,3.01207 z"
- id="path7212-4"
- inkscape:connector-curvature="0" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 457.96894,278.42384 1.02302,-2.77836 -1.31183,-0.56021 0.33336,-0.616 1.26318,0.48291 0.54568,-1.37607 0.81934,0.31324 -0.47741,1.4022 1.87364,0.67714 0.47795,-1.14765 0.83893,0.26207 -0.47245,1.28672 1.80283,0.70884 0.41215,-1.23149 0.92825,0.33529 -0.49337,1.23952 1.38917,0.51162 -0.21081,0.85845 -1.42731,-0.56527 -1.05878,2.6669 -0.81279,-0.33034 0.94975,-2.68892 -1.68742,-0.7038 -1.03512,2.65627 -0.83236,-0.27915 0.99293,-2.75061 -1.92628,-0.79522 -1.00194,2.82543 z"
- id="path7229"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccccccccccccccccccccccc" />
- <path
- transform="translate(0,-11.249983)"
- id="path7233"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.3185696px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 73.482785,265.42476 4.944364,-1.72314 -0.207904,-0.52164 -2.012479,0.86151 -0.0213,-0.63037 -0.837931,0.3339 0.324488,0.46118 -2.371778,0.68852 z m 0.497305,0.21764 4.223597,-1.35549 0.556753,4.37406 -2.879727,0.92419 z"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccccccccc" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 156.55184,206.61884 0.47605,-0.20403 1.0201,8.90891 -0.47605,0.20402 z"
- id="path7236"
- inkscape:connector-curvature="0" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 160.97229,209.47512 0.20402,4.96451 0.47605,-0.068 0.068,-5.03251 z"
- id="path7238"
- inkscape:connector-curvature="0" />
- <path
- transform="translate(0,-11.249983)"
- style="fill:#172062;fill-opacity:1;stroke:none;stroke-width:0.34364724px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- d="m 23.838748,287.33572 -2.186787,-3.04882 3.027872,1.63785 -0.07842,-2.79635 1.585239,2.33549 1.177306,-3.18042 0.241718,3.90016 z"
- id="path7212-4-6"
- inkscape:connector-curvature="0" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535"
- cx="120.03474"
- cy="193.66763"
- r="2.5126758" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-2"
- cx="97.333473"
- cy="218.84901"
- r="2.5126758" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.5;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-24"
- cx="70.128021"
- cy="226.19046"
- r="2.5126758" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.25;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.41842699;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-25"
- cx="118.05532"
- cy="234.83446"
- r="1.6838019" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-9"
- cx="110.59546"
- cy="252.2408"
- r="1.5653913" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-9-7"
- cx="122.43651"
- cy="242.53113"
- r="1.5653913" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.5;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.3186785;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-9-2"
- cx="64.415337"
- cy="265.26596"
- r="1.5653913" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-24-4"
- cx="69.61615"
- cy="226.18503"
- r="7.648705" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-24-4-4"
- cx="97.333473"
- cy="218.84901"
- r="7.648705" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.1;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-24-4-2"
- cx="119.52941"
- cy="193.50121"
- r="7.648705" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-9-2-6"
- cx="64.415337"
- cy="265.26596"
- r="4.9115925" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-9-2-6-7"
- cx="110.59546"
- cy="252.2408"
- r="4.9115925" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.02999998;fill:#ffbe16;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.13750315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-9-2-6-3"
- cx="122.43651"
- cy="242.53113"
- r="4.9115925" />
- <circle
- transform="translate(0,-11.249983)"
- style="opacity:0.05;fill:#ff0016;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.44323444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="path4535-24-4-4-8"
- cx="117.52492"
- cy="234.88242"
- r="7.648705" />
- </g>
-</svg>
diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts
index 31027c0be3..9980ede231 100644
--- a/src/client/docs/api/gulpfile.ts
+++ b/src/client/docs/api/gulpfile.ts
@@ -127,7 +127,7 @@ gulp.task('doc:api:endpoints', async () => {
return;
}
const i18n = new I18nReplacer(lang);
- html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+ html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {
@@ -171,7 +171,7 @@ gulp.task('doc:api:entities', async () => {
return;
}
const i18n = new I18nReplacer(lang);
- html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+ html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {
diff --git a/src/client/docs/gulpfile.ts b/src/client/docs/gulpfile.ts
index 5e81d6d3b5..56bf6188c8 100644
--- a/src/client/docs/gulpfile.ts
+++ b/src/client/docs/gulpfile.ts
@@ -53,7 +53,7 @@ gulp.task('doc:docs', async () => {
return;
}
const i18n = new I18nReplacer(lang);
- html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+ html = html.replace(i18n.pattern, i18n.replacement);
html = fa(html);
const htmlPath = `./built/client/docs/${lang}/${name}.html`;
mkdirp(path.dirname(htmlPath), (mkdirErr) => {