mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-30 15:58:16 +09:00
Migrate to Vue3 (#6587)
* Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
This commit is contained in:
parent
a40f38b2b5
commit
7199e6f4e0
11
gulpfile.ts
11
gulpfile.ts
@ -7,9 +7,6 @@ import * as gulp from 'gulp';
|
||||
import * as ts from 'gulp-typescript';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as rename from 'gulp-rename';
|
||||
const cleanCSS = require('gulp-clean-css');
|
||||
const sass = require('gulp-dart-sass');
|
||||
const fiber = require('fibers');
|
||||
|
||||
const locales: { [x: string]: any } = require('./locales');
|
||||
const meta = require('./package.json');
|
||||
@ -61,13 +58,6 @@ gulp.task('cleanall', gulp.parallel('clean', cb =>
|
||||
rimraf('./node_modules', cb)
|
||||
));
|
||||
|
||||
gulp.task('build:client:styles', () =>
|
||||
gulp.src('./src/client/style.scss')
|
||||
.pipe(sass({ fiber }))
|
||||
.pipe(cleanCSS())
|
||||
.pipe(gulp.dest('./built/client/assets/'))
|
||||
);
|
||||
|
||||
gulp.task('copy:client', () =>
|
||||
gulp.src([
|
||||
'./assets/**/*',
|
||||
@ -87,7 +77,6 @@ gulp.task('copy:docs', () =>
|
||||
);
|
||||
|
||||
gulp.task('build:client', gulp.parallel(
|
||||
'build:client:styles',
|
||||
'copy:client',
|
||||
'copy:docs'
|
||||
));
|
||||
|
@ -16,6 +16,9 @@ noNotes: "ノートはありません"
|
||||
noNotifications: "通知はありません"
|
||||
instance: "インスタンス"
|
||||
settings: "設定"
|
||||
basicSettings: "基本設定"
|
||||
otherSettings: "その他の設定"
|
||||
openInWindow: "ウィンドウで開く"
|
||||
profile: "プロフィール"
|
||||
timeline: "タイムライン"
|
||||
noAccountDescription: "自己紹介はありません"
|
||||
@ -40,6 +43,7 @@ deleteAndEditConfirm: "このノートを削除してもう一度編集します
|
||||
addToList: "リストに追加"
|
||||
sendMessage: "メッセージを送信"
|
||||
copyUsername: "ユーザー名をコピー"
|
||||
searchUser: "ユーザーを検索"
|
||||
reply: "返信"
|
||||
loadMore: "もっと見る"
|
||||
youGotNewFollower: "フォローされました"
|
||||
@ -66,8 +70,11 @@ followers: "フォロワー"
|
||||
followsYou: "フォローされています"
|
||||
createList: "リスト作成"
|
||||
manageLists: "リストの管理"
|
||||
error: "問題が発生しました"
|
||||
error: "エラー"
|
||||
somethingHappened: "問題が発生しました"
|
||||
retry: "再試行"
|
||||
pageLoadError: "ページの読み込みに失敗しました。"
|
||||
pageLoadErrorDescription: "これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。"
|
||||
enterListName: "リスト名を入力"
|
||||
privacy: "プライバシー"
|
||||
makeFollowManuallyApprove: "フォローを承認制にする"
|
||||
@ -106,6 +113,8 @@ unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
selectAntenna: "アンテナを選択"
|
||||
selectWidget: "ウィジェットを選択"
|
||||
editWidgets: "ウィジェットを編集"
|
||||
editWidgetsExit: "編集を終了"
|
||||
customEmojis: "カスタム絵文字"
|
||||
emoji: "絵文字"
|
||||
emojiName: "絵文字名"
|
||||
@ -177,7 +186,6 @@ processing: "処理中"
|
||||
preview: "プレビュー"
|
||||
default: "デフォルト"
|
||||
noCustomEmojis: "絵文字はありません"
|
||||
customEmojisOfRemote: "リモートの絵文字"
|
||||
noJobs: "ジョブはありません"
|
||||
federating: "連合中"
|
||||
blocked: "ブロック中"
|
||||
@ -445,7 +453,7 @@ total: "合計"
|
||||
weekOverWeekChanges: "前週比"
|
||||
dayOverDayChanges: "前日比"
|
||||
appearance: "アピアランス"
|
||||
clinetSettings: "クライアント設定"
|
||||
clientSettings: "クライアント設定"
|
||||
accountSettings: "アカウント設定"
|
||||
promotion: "プロモーション"
|
||||
promote: "プロモート"
|
||||
@ -476,6 +484,8 @@ newNoteRecived: "新しいノートがあります"
|
||||
sounds: "サウンド"
|
||||
listen: "聴く"
|
||||
none: "なし"
|
||||
showInPage: "ページで表示"
|
||||
popout: "ポップアウト"
|
||||
volume: "音量"
|
||||
details: "詳細"
|
||||
chooseEmoji: "絵文字を選択"
|
||||
@ -518,7 +528,6 @@ enableInfiniteScroll: "自動でもっと見る"
|
||||
visibility: "公開範囲"
|
||||
poll: "アンケート"
|
||||
useCw: "内容を隠す"
|
||||
fixedWidgetsPosition: "ウィジェットの位置を固定する"
|
||||
enablePlayer: "プレイヤーを開く"
|
||||
disablePlayer: "プレイヤーを閉じる"
|
||||
expandTweet: "ツイートを展開する"
|
||||
@ -570,6 +579,12 @@ notificationSetting: "通知設定"
|
||||
notificationSettingDesc: "表示する通知の種別を選択してください。"
|
||||
useGlobalSetting: "グローバル設定を使う"
|
||||
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
|
||||
other: "その他"
|
||||
regenerateLoginToken: "ログイントークンを再生成"
|
||||
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
|
||||
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
|
||||
fileIdOrUrl: "ファイルIDまたはURL"
|
||||
chatOpenBehavior: "チャットを開くときの動作"
|
||||
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "自動でリロード"
|
||||
@ -802,6 +817,7 @@ _widgets:
|
||||
photos: "フォト"
|
||||
digitalClock: "デジタル時計"
|
||||
federation: "連合"
|
||||
postForm: "投稿フォーム"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
106
package.json
106
package.json
@ -37,11 +37,11 @@
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-runtime": "7.11.0",
|
||||
"@elastic/elasticsearch": "7.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.30",
|
||||
"@fortawesome/free-brands-svg-icons": "5.14.0",
|
||||
"@fortawesome/free-regular-svg-icons": "5.14.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.14.0",
|
||||
"@fortawesome/vue-fontawesome": "0.1.10",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.32",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.1",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.1",
|
||||
"@fortawesome/vue-fontawesome": "3.0.0-2",
|
||||
"@koa/cors": "3.1.0",
|
||||
"@koa/multer": "3.0.0",
|
||||
"@koa/router": "9.0.1",
|
||||
@ -97,19 +97,20 @@
|
||||
"@types/speakeasy": "2.0.5",
|
||||
"@types/tinycolor2": "1.4.2",
|
||||
"@types/tmp": "0.2.0",
|
||||
"@types/uuid": "8.0.0",
|
||||
"@types/uuid": "8.3.0",
|
||||
"@types/web-push": "3.3.0",
|
||||
"@types/webpack": "4.41.18",
|
||||
"@types/webpack": "4.41.22",
|
||||
"@types/webpack-stream": "3.2.11",
|
||||
"@types/websocket": "1.0.1",
|
||||
"@types/ws": "7.2.6",
|
||||
"@typescript-eslint/parser": "3.6.0",
|
||||
"@types/ws": "7.2.7",
|
||||
"@typescript-eslint/parser": "4.4.0",
|
||||
"@vue/compiler-sfc": "3.0.0",
|
||||
"abort-controller": "3.0.0",
|
||||
"apexcharts": "3.20.0",
|
||||
"apexcharts": "3.22.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.724.0",
|
||||
"aws-sdk": "2.770.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.3",
|
||||
"bull": "3.18.0",
|
||||
@ -122,35 +123,33 @@
|
||||
"content-disposition": "0.5.3",
|
||||
"core-js": "3.6.5",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "4.2.1",
|
||||
"css-loader": "4.3.0",
|
||||
"cssnano": "4.1.10",
|
||||
"dateformat": "3.0.3",
|
||||
"deep-entries": "3.1.0",
|
||||
"diskusage": "1.1.3",
|
||||
"double-ended-queue": "2.1.0-0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "7.4.0",
|
||||
"eslint-plugin-vue": "6.2.2",
|
||||
"eventemitter3": "4.0.4",
|
||||
"eslint": "7.10.0",
|
||||
"eslint-plugin-vue": "7.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.1",
|
||||
"fibers": "5.0.0",
|
||||
"file-type": "14.7.1",
|
||||
"file-type": "15.0.1",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"glob": "7.1.6",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-clean-css": "4.3.0",
|
||||
"gulp-dart-sass": "1.0.2",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.0.0",
|
||||
"gulp-sourcemaps": "2.6.5",
|
||||
"gulp-terser": "1.3.2",
|
||||
"gulp-terser": "1.4.0",
|
||||
"gulp-tslint": "8.1.4",
|
||||
"gulp-typescript": "6.0.0-alpha.1",
|
||||
"hard-source-webpack-plugin": "0.13.1",
|
||||
"hcaptcha": "0.0.2",
|
||||
"html-minifier": "4.0.0",
|
||||
"http-proxy-agent": "4.0.1",
|
||||
"http-signature": "1.3.4",
|
||||
"http-signature": "1.3.5",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"idb-keyval": "3.2.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
@ -171,27 +170,27 @@
|
||||
"koa-mount": "4.0.0",
|
||||
"koa-send": "5.0.1",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "6.3.0",
|
||||
"koa-views": "6.3.1",
|
||||
"langmap": "0.0.16",
|
||||
"lookup-dns-cache": "2.1.0",
|
||||
"markdown-it": "11.0.0",
|
||||
"markdown-it-anchor": "5.3.0",
|
||||
"mocha": "8.1.1",
|
||||
"markdown-it": "11.0.1",
|
||||
"markdown-it-anchor": "6.0.0",
|
||||
"mocha": "8.1.3",
|
||||
"moji": "0.5.1",
|
||||
"ms": "2.1.2",
|
||||
"multer": "1.4.2",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "2.6.0",
|
||||
"nodemailer": "6.4.11",
|
||||
"nprogress": "0.2.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"nodemailer": "6.4.13",
|
||||
"object-assign-deep": "0.4.0",
|
||||
"os-utils": "0.0.14",
|
||||
"p-cancelable": "2.0.0",
|
||||
"parse5": "6.0.1",
|
||||
"parsimmon": "1.15.0",
|
||||
"pg": "8.3.2",
|
||||
"portal-vue": "2.1.7",
|
||||
"parsimmon": "1.16.0",
|
||||
"pg": "8.4.1",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss": "8.1.1",
|
||||
"postcss-loader": "4.0.3",
|
||||
"prismjs": "1.21.0",
|
||||
"probe-image-size": "5.0.0",
|
||||
"promise-limit": "2.7.0",
|
||||
@ -202,7 +201,7 @@
|
||||
"qrcode": "1.4.4",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.15.4",
|
||||
"re2": "1.15.5",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "4.4.0",
|
||||
"redis": "3.0.2",
|
||||
@ -215,54 +214,49 @@
|
||||
"rimraf": "3.0.2",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.26.10",
|
||||
"sass-loader": "9.0.3",
|
||||
"sass": "1.27.0",
|
||||
"sass-loader": "10.0.2",
|
||||
"seedrandom": "3.0.5",
|
||||
"sharp": "0.25.4",
|
||||
"sharp": "0.26.1",
|
||||
"speakeasy": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "1.2.1",
|
||||
"style-loader": "1.3.0",
|
||||
"summaly": "2.4.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "4.26.12",
|
||||
"systeminformation": "4.27.8",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.117.1",
|
||||
"tinycolor2": "1.4.1",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "8.0.2",
|
||||
"ts-loader": "8.0.4",
|
||||
"ts-node": "9.0.0",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-sonarts": "1.9.0",
|
||||
"typeorm": "0.2.25",
|
||||
"typescript": "4.0.2",
|
||||
"typeorm": "0.2.28",
|
||||
"typescript": "4.0.3",
|
||||
"ulid": "2.3.0",
|
||||
"url-loader": "4.1.0",
|
||||
"uuid": "8.3.0",
|
||||
"v-animate-css": "0.0.3",
|
||||
"uuid": "8.3.1",
|
||||
"v-debounce": "0.1.2",
|
||||
"vue": "2.6.12",
|
||||
"vue": "3.0.1",
|
||||
"vue-color": "2.7.1",
|
||||
"vue-content-loading": "1.6.0",
|
||||
"vue-cropperjs": "4.1.0",
|
||||
"vue-i18n": "8.21.0",
|
||||
"vue-json-pretty": "1.6.7",
|
||||
"vue-loader": "15.9.3",
|
||||
"vue-marquee-text-component": "1.1.1",
|
||||
"vue-meta": "2.4.0",
|
||||
"vue-draggable-next": "1.0.8",
|
||||
"vue-i18n": "9.0.0-beta.4",
|
||||
"vue-json-pretty": "1.7.0",
|
||||
"vue-loader": "16.0.0-beta.7",
|
||||
"vue-prism-component": "1.2.0",
|
||||
"vue-prism-editor": "1.2.2",
|
||||
"vue-router": "3.4.3",
|
||||
"vue-router": "4.0.0-beta.13",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-svg-inline-loader-corejs3": "1.5.0",
|
||||
"vue-template-compiler": "2.6.12",
|
||||
"vuedraggable": "2.24.1",
|
||||
"vuex": "3.5.1",
|
||||
"vuex": "4.0.0-beta.4",
|
||||
"vuex-persistedstate": "3.1.0",
|
||||
"web-push": "3.4.4",
|
||||
"webpack": "5.0.0-beta.28",
|
||||
"webpack": "5.1.3",
|
||||
"webpack-cli": "3.3.12",
|
||||
"websocket": "1.0.31",
|
||||
"websocket": "1.0.32",
|
||||
"ws": "7.3.1",
|
||||
"xev": "2.0.1"
|
||||
},
|
||||
|
12
src/client/.eslintrc
Normal file
12
src/client/.eslintrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"globals": {
|
||||
"_DEV_": false,
|
||||
"_LANGS_": false,
|
||||
"_VERSION_": false,
|
||||
"_ENV_": false,
|
||||
"_PERF_PREFIX_": false,
|
||||
"_DATA_TRANSFER_DRIVE_FILE_": false,
|
||||
"_DATA_TRANSFER_DRIVE_FOLDER_": false,
|
||||
"_DATA_TRANSFER_DECK_COLUMN_": false
|
||||
}
|
||||
}
|
8
src/client/@types/global.d.ts
vendored
Normal file
8
src/client/@types/global.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
declare const _LANGS_: string[];
|
||||
declare const _VERSION_: string;
|
||||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
11
src/client/@types/vuex-shim.d.ts
vendored
Normal file
11
src/client/@types/vuex-shim.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import { ComponentCustomProperties } from 'vue';
|
||||
import { Store } from 'vuex';
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface State {
|
||||
}
|
||||
|
||||
interface ComponentCustomProperties {
|
||||
$store: Store<State>
|
||||
}
|
||||
}
|
@ -1,788 +0,0 @@
|
||||
<template>
|
||||
<div class="mk-app" v-hotkey.global="keymap">
|
||||
<header class="header" ref="header">
|
||||
<div class="title" ref="title">
|
||||
<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
|
||||
<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button>
|
||||
</transition>
|
||||
<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
|
||||
<div class="body" :key="pageKey">
|
||||
<div class="default">
|
||||
<portal-target name="avatar" slim/>
|
||||
<h1 class="title"><portal-target name="icon" slim/><portal-target name="title" slim/></h1>
|
||||
</div>
|
||||
<div class="custom">
|
||||
<portal-target name="header" slim/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<template v-if="$store.getters.isSignedIn">
|
||||
<button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button>
|
||||
<button v-else class="_button edit" @click="widgetsEditMode = true"><fa :icon="faGripVertical"/></button>
|
||||
</template>
|
||||
<div class="search">
|
||||
<fa :icon="faSearch"/>
|
||||
<input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
|
||||
</div>
|
||||
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
|
||||
<x-clock v-if="isDesktop" class="clock"/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<x-sidebar ref="nav" @change-view-mode="calcHeaderWidth"/>
|
||||
|
||||
<div class="contents" ref="contents" :class="{ wallpaper, full: $store.state.fullView }">
|
||||
<main ref="main">
|
||||
<div class="content">
|
||||
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
|
||||
<keep-alive :include="['index']">
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="powerd-by" :class="{ visible: !$store.getters.isSignedIn }">
|
||||
<b><router-link to="/">{{ host }}</router-link></b>
|
||||
<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<template v-if="isDesktop">
|
||||
<div v-for="place in ['left', 'right']" ref="widgets" class="widgets" :class="{ edit: widgetsEditMode, fixed: $store.state.device.fixedWidgetsPosition, empty: widgets[place].length === 0 && !widgetsEditMode }" :key="place">
|
||||
<div class="spacer"></div>
|
||||
<div class="container" v-if="widgetsEditMode">
|
||||
<mk-button primary @click="addWidget(place)" class="add"><fa :icon="faPlus"/></mk-button>
|
||||
<x-draggable
|
||||
:list="widgets[place]"
|
||||
handle=".handle"
|
||||
animation="150"
|
||||
class="sortable"
|
||||
@sort="onWidgetSort"
|
||||
>
|
||||
<div v-for="widget in widgets[place]" class="customize-container _panel" :key="widget.id">
|
||||
<header>
|
||||
<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
|
||||
</header>
|
||||
<div @click="widgetFunc(widget.id)">
|
||||
<component class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</x-draggable>
|
||||
</div>
|
||||
<div class="container" v-else>
|
||||
<component v-for="widget in widgets[place]" class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="buttons" :class="{ navHidden }">
|
||||
<button class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
|
||||
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="$router.push('/my/notifications')"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
|
||||
</div>
|
||||
|
||||
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" :class="{ navHidden }" @click="post()"><fa :icon="faPencilAlt"/></button>
|
||||
|
||||
<stream-indicator v-if="$store.getters.isSignedIn"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { host } from './config';
|
||||
import { search } from './scripts/search';
|
||||
import { StickySidebar } from './scripts/sticky-sidebar';
|
||||
import { widgets } from './widgets';
|
||||
import XSidebar from './components/sidebar.vue';
|
||||
|
||||
const DESKTOP_THRESHOLD = 1100;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XSidebar,
|
||||
XClock: () => import('./components/header-clock.vue').then(m => m.default),
|
||||
MkButton: () => import('./components/ui/button.vue').then(m => m.default),
|
||||
XDraggable: () => import('vuedraggable'),
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
host: host,
|
||||
pageKey: 0,
|
||||
searching: false,
|
||||
connection: null,
|
||||
searchQuery: '',
|
||||
searchWait: false,
|
||||
widgetsEditMode: false,
|
||||
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
|
||||
canBack: false,
|
||||
menuDef: this.$store.getters.nav({}),
|
||||
navHidden: false,
|
||||
wallpaper: localStorage.getItem('wallpaper') != null,
|
||||
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'd': () => {
|
||||
if (this.$store.state.device.syncDeviceDarkMode) return;
|
||||
this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
|
||||
},
|
||||
'p': this.post,
|
||||
'n': this.post,
|
||||
's': this.search,
|
||||
'h|/': this.help
|
||||
};
|
||||
},
|
||||
|
||||
widgets(): any {
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
const widgets = this.$store.state.deviceUser.widgets;
|
||||
return {
|
||||
left: widgets.filter(x => x.place === 'left'),
|
||||
right: widgets.filter(x => x.place == null || x.place === 'right'),
|
||||
mobile: widgets.filter(x => x.place === 'mobile'),
|
||||
};
|
||||
} else {
|
||||
const right = [{
|
||||
name: 'calendar',
|
||||
id: 'b', place: 'right', data: {}
|
||||
}, {
|
||||
name: 'trends',
|
||||
id: 'c', place: 'right', data: {}
|
||||
}];
|
||||
|
||||
if (this.$route.name !== 'index') {
|
||||
right.unshift({
|
||||
name: 'welcome',
|
||||
id: 'a', place: 'right', data: {}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
left: [],
|
||||
right,
|
||||
mobile: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
menu(): string[] {
|
||||
return this.$store.state.deviceUser.menu;
|
||||
},
|
||||
|
||||
navIndicated(): boolean {
|
||||
if (!this.$store.getters.isSignedIn) return false;
|
||||
for (const def in this.menuDef) {
|
||||
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
|
||||
if (this.menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.pageKey++;
|
||||
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
|
||||
},
|
||||
|
||||
isDesktop() {
|
||||
this.$nextTick(() => {
|
||||
this.attachSticky();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
document.documentElement.style.overflowY = 'scroll';
|
||||
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection.on('notification', this.onNotification);
|
||||
|
||||
if (this.$store.state.deviceUser.widgets.length === 0) {
|
||||
this.$store.commit('deviceUser/setWidgets', [{
|
||||
name: 'calendar',
|
||||
id: 'a', place: 'right', data: {}
|
||||
}, {
|
||||
name: 'notifications',
|
||||
id: 'b', place: 'right', data: {}
|
||||
}, {
|
||||
name: 'trends',
|
||||
id: 'c', place: 'right', data: {}
|
||||
}]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.adjustTitlePosition();
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
this.adjustTitlePosition();
|
||||
});
|
||||
|
||||
ro.observe(this.$refs.contents);
|
||||
|
||||
window.addEventListener('resize', this.adjustTitlePosition, { passive: true });
|
||||
|
||||
if (!this.isDesktop) {
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
// widget follow
|
||||
this.attachSticky();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.calcHeaderWidth();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
adjustTitlePosition() {
|
||||
const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.$el.offsetWidth;
|
||||
if (left >= 0) {
|
||||
this.$refs.title.style.left = left + 'px';
|
||||
}
|
||||
},
|
||||
|
||||
calcHeaderWidth() {
|
||||
const navWidth = this.$refs.nav.$el.offsetWidth;
|
||||
this.navHidden = navWidth === 0;
|
||||
this.$refs.header.style.width = `calc(100% - ${navWidth}px)`;
|
||||
this.adjustTitlePosition();
|
||||
},
|
||||
|
||||
showNav() {
|
||||
this.$refs.nav.show();
|
||||
},
|
||||
|
||||
attachSticky() {
|
||||
if (!this.isDesktop) return;
|
||||
if (this.$store.state.device.fixedWidgetsPosition) return;
|
||||
|
||||
const stickyWidgetColumns = this.$refs.widgets.map(w => new StickySidebar(w.children[1], w.children[0], w.offsetTop));
|
||||
window.addEventListener('scroll', () => {
|
||||
for (const stickyWidgetColumn of stickyWidgetColumns) {
|
||||
stickyWidgetColumn.calc(window.scrollY);
|
||||
}
|
||||
}, { passive: true });
|
||||
},
|
||||
|
||||
top() {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
|
||||
help() {
|
||||
this.$router.push('/docs/keyboard-shortcut');
|
||||
},
|
||||
|
||||
back() {
|
||||
if (this.canBack) window.history.back();
|
||||
},
|
||||
|
||||
onTransition() {
|
||||
if (window._scroll) window._scroll();
|
||||
},
|
||||
|
||||
post() {
|
||||
this.$root.post();
|
||||
},
|
||||
|
||||
search() {
|
||||
if (this.searching) return;
|
||||
|
||||
this.$root.dialog({
|
||||
title: this.$t('search'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: query }) => {
|
||||
if (canceled || query == null || query === '') return;
|
||||
|
||||
this.searching = true;
|
||||
search(this, query).finally(() => {
|
||||
this.searching = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
searchKeypress(e) {
|
||||
if (e.keyCode === 13) {
|
||||
this.searchWait = true;
|
||||
search(this, this.searchQuery).finally(() => {
|
||||
this.searchWait = false;
|
||||
this.searchQuery = '';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async onNotification(notification) {
|
||||
if (this.$store.state.i.mutingNotificationTypes.includes(notification.type)) {
|
||||
return;
|
||||
}
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.$root.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.$root.new(await import('./components/toast.vue').then(m => m.default), {
|
||||
notification
|
||||
});
|
||||
}
|
||||
|
||||
this.$root.sound('notification');
|
||||
},
|
||||
|
||||
widgetFunc(id) {
|
||||
this.$refs[id][0].setting();
|
||||
},
|
||||
|
||||
onWidgetSort() {
|
||||
this.saveHome();
|
||||
},
|
||||
|
||||
async addWidget(place) {
|
||||
const { canceled, result: widget } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('chooseWidget'),
|
||||
select: {
|
||||
items: widgets.map(widget => ({
|
||||
value: widget,
|
||||
text: this.$t('_widgets.' + widget),
|
||||
}))
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.$store.commit('deviceUser/addWidget', {
|
||||
name: widget,
|
||||
id: uuid(),
|
||||
place: place,
|
||||
data: {}
|
||||
});
|
||||
},
|
||||
|
||||
removeWidget(widget) {
|
||||
this.$store.commit('deviceUser/removeWidget', widget);
|
||||
},
|
||||
|
||||
saveHome() {
|
||||
this.$store.commit('deviceUser/setWidgets', [...this.widgets.left, ...this.widgets.right, ...this.widgets.mobile]);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-app {
|
||||
$header-height: 60px;
|
||||
$main-width: 670px;
|
||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
||||
$header-sub-hide-threshold: 1090px;
|
||||
$left-widgets-hide-threshold: 1600px;
|
||||
$right-widgets-hide-threshold: 1090px;
|
||||
|
||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
min-height: calc(var(--vh, 1vh) * 100);
|
||||
box-sizing: border-box;
|
||||
padding-top: $header-height;
|
||||
|
||||
&, > .header > .body {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
> .header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: $header-height;
|
||||
width: 100%;
|
||||
//background-color: var(--panel);
|
||||
-webkit-backdrop-filter: blur(32px);
|
||||
backdrop-filter: blur(32px);
|
||||
background-color: var(--header);
|
||||
border-bottom: solid 1px var(--divider);
|
||||
|
||||
> .title {
|
||||
position: relative;
|
||||
line-height: $header-height;
|
||||
height: $header-height;
|
||||
max-width: $main-width;
|
||||
text-align: center;
|
||||
|
||||
> .back {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: $header-height;
|
||||
width: $header-height;
|
||||
}
|
||||
|
||||
> .body {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: $header-height;
|
||||
|
||||
> .default {
|
||||
padding: 0 $header-height;
|
||||
|
||||
> .avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: bottom;
|
||||
margin: (($header-height - $size) / 2) 8px (($header-height - $size) / 2) 0;
|
||||
}
|
||||
|
||||
> .title {
|
||||
display: inline-block;
|
||||
font-size: $ui-font-size;
|
||||
margin: 0;
|
||||
line-height: $header-height;
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .custom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .sub {
|
||||
$post-button-size: 42px;
|
||||
$post-button-margin: (($header-height - $post-button-size) / 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 16px;
|
||||
height: $header-height;
|
||||
|
||||
@media (max-width: $header-sub-hide-threshold) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .edit {
|
||||
padding: 16px;
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .search {
|
||||
position: relative;
|
||||
|
||||
> input {
|
||||
width: 220px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 8px;
|
||||
padding: 0 12px 0 42px;
|
||||
font-size: 1rem;
|
||||
line-height: 38px;
|
||||
border: none;
|
||||
border-radius: 38px;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
-webkit-appearance: textfield;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
> [data-icon] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 16px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .post {
|
||||
width: $post-button-size;
|
||||
height: $post-button-size;
|
||||
margin-left: $post-button-margin;
|
||||
border-radius: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
> .clock {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .contents {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
min-width: 0;
|
||||
|
||||
&.wallpaper {
|
||||
background: var(--wallpaperOverlay);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
|
||||
> main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .widgets {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
width: $main-width;
|
||||
min-width: 0;
|
||||
|
||||
> .content {
|
||||
> * {
|
||||
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
|
||||
box-sizing: border-box;
|
||||
padding: var(--margin);
|
||||
|
||||
&.full {
|
||||
padding: 0 var(--margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .powerd-by {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin: 32px 0;
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:not(.visible) {
|
||||
@media (min-width: 850px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
> small {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
opacity: 0.5;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .widgets {
|
||||
padding: 0 var(--margin);
|
||||
box-shadow: 1px 0 0 0 var(--divider), -1px 0 0 0 var(--divider);
|
||||
|
||||
&.fixed {
|
||||
position: sticky;
|
||||
overflow: auto;
|
||||
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
height: calc((var(--vh, 1vh) * 100) - #{$header-height});
|
||||
top: $header-height;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
order: -1;
|
||||
|
||||
@media (max-width: $left-widgets-hide-threshold) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: $right-widgets-hide-threshold) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .container {
|
||||
position: sticky;
|
||||
height: min-content;
|
||||
// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
|
||||
padding: var(--margin) 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
> * {
|
||||
margin: var(--margin) 0;
|
||||
width: 300px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .add {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.customize-container {
|
||||
margin: 8px 0;
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
line-height: 32px;
|
||||
|
||||
> .handle {
|
||||
padding: 0 8px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
> .remove {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0 8px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
padding: 8px;
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .post {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
bottom: 32px;
|
||||
right: 32px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
|
||||
font-size: 22px;
|
||||
|
||||
&.navHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: ($header-sub-hide-threshold + 1px)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
bottom: 0;
|
||||
padding: 0 32px 32px 32px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(0deg, var(--bg), var(--X1));
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
&:not(.navHidden) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .button {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
|
||||
> * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.post) {
|
||||
background: var(--panel);
|
||||
color: var(--fg);
|
||||
|
||||
&:hover {
|
||||
background: var(--X2);
|
||||
}
|
||||
|
||||
> i {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: var(--indicator);
|
||||
font-size: 16px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -6,11 +6,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { toUnicode } from 'punycode';
|
||||
import { host } from '../config';
|
||||
import { host } from '@/config';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: ['user', 'detail'],
|
||||
data() {
|
||||
return {
|
||||
|
@ -34,10 +34,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
now: new Date(),
|
||||
@ -127,7 +128,7 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.enabled = false;
|
||||
},
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="swhvrteh" @contextmenu.prevent="() => {}">
|
||||
<div class="swhvrteh _popup _shadow" @contextmenu.prevent="() => {}">
|
||||
<ol class="users" ref="suggests" v-if="type === 'user'">
|
||||
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user">
|
||||
<img class="avatar" :src="user.avatarUrl"/>
|
||||
<span class="name">
|
||||
<mk-user-name :user="user" :key="user.id"/>
|
||||
<MkUserName :user="user" :key="user.id"/>
|
||||
</span>
|
||||
<span class="username">@{{ user | acct }}</span>
|
||||
<span class="username">@{{ acct(user) }}</span>
|
||||
</li>
|
||||
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $t('selectUser') }}</li>
|
||||
</ol>
|
||||
@ -28,12 +28,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { emojilist } from '../../misc/emojilist';
|
||||
import contains from '../scripts/contains';
|
||||
import contains from '@/scripts/contains';
|
||||
import { twemojiSvgBase } from '../../misc/twemoji-base';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
import MkUserSelect from './user-select.vue';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
@ -74,7 +75,7 @@ for (const x of lib) {
|
||||
|
||||
emjdb.sort((a, b) => a.name.length - b.name.length);
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
@ -91,11 +92,6 @@ export default Vue.extend({
|
||||
required: true,
|
||||
},
|
||||
|
||||
complete: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
close: {
|
||||
type: Function,
|
||||
required: true,
|
||||
@ -110,7 +106,14 @@ export default Vue.extend({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
|
||||
showing: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
@ -135,6 +138,14 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
showing() {
|
||||
if (!this.showing) {
|
||||
this.$emit('closed');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.setPosition();
|
||||
},
|
||||
@ -189,7 +200,7 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.textarea.removeEventListener('keydown', this.onKeydown);
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
@ -198,6 +209,11 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
complete(type, value) {
|
||||
this.$emit('done', { type, value });
|
||||
this.$emit('closed');
|
||||
},
|
||||
|
||||
setPosition() {
|
||||
if (this.x + this.$el.offsetWidth > window.innerWidth) {
|
||||
this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
|
||||
@ -236,8 +252,8 @@ export default Vue.extend({
|
||||
this.users = users;
|
||||
this.fetching = false;
|
||||
} else {
|
||||
this.$root.api('users/search', {
|
||||
query: this.q,
|
||||
os.api('users/search-by-username-and-host', {
|
||||
username: this.q,
|
||||
limit: 10,
|
||||
detail: false
|
||||
}).then(users => {
|
||||
@ -260,7 +276,7 @@ export default Vue.extend({
|
||||
this.hashtags = hashtags;
|
||||
this.fetching = false;
|
||||
} else {
|
||||
this.$root.api('hashtags/search', {
|
||||
os.api('hashtags/search', {
|
||||
query: this.q,
|
||||
limit: 30
|
||||
}).then(hashtags => {
|
||||
@ -374,14 +390,13 @@ export default Vue.extend({
|
||||
|
||||
chooseUser() {
|
||||
this.close();
|
||||
const vm = this.$root.new(MkUserSelect, {});
|
||||
vm.$once('selected', user => {
|
||||
os.selectUser().then(user => {
|
||||
this.complete('user', user);
|
||||
});
|
||||
vm.$once('closed', () => {
|
||||
this.textarea.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
acct
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -393,9 +408,6 @@ export default Vue.extend({
|
||||
max-width: 100%;
|
||||
margin-top: calc(1em + 8px);
|
||||
overflow: hidden;
|
||||
background: var(--panel);
|
||||
border: solid 1px rgba(#000, 0.1);
|
||||
border-radius: 4px;
|
||||
transition: top 0.1s ease, left 0.1s ease;
|
||||
|
||||
> ol {
|
||||
|
@ -1,17 +1,19 @@
|
||||
<template>
|
||||
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
|
||||
<span class="eiwwqkts" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
|
||||
<img class="inner" :src="url"/>
|
||||
</span>
|
||||
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
|
||||
<router-link class="eiwwqkts" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
|
||||
<img class="inner" :src="url"/>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
import { defineComponent } from 'vue';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||
import { acct, userPage } from '../filters/user';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
@ -30,6 +32,7 @@ export default Vue.extend({
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
computed: {
|
||||
cat(): boolean {
|
||||
return this.user.isCat;
|
||||
@ -42,25 +45,19 @@ export default Vue.extend({
|
||||
},
|
||||
watch: {
|
||||
'user.avatarBlurhash'() {
|
||||
this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
|
||||
if (this.$el == null) return;
|
||||
this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
|
||||
this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
|
||||
},
|
||||
methods: {
|
||||
getBlurhashAvgColor(s) {
|
||||
return typeof s == 'string'
|
||||
? '#' + [...s.slice(2, 6)]
|
||||
.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
|
||||
.reduce((a, c) => a * 83 + c, 0)
|
||||
.toString(16)
|
||||
.padStart(6, '0')
|
||||
: undefined;
|
||||
},
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
}
|
||||
},
|
||||
acct,
|
||||
userPage
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
||||
<mk-avatar :user="user" style="width:32px;height:32px;"/>
|
||||
<MkAvatar :user="user" style="width:32px;height:32px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
userIds: {
|
||||
required: true
|
||||
@ -21,7 +22,7 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.us = await this.$root.api('users/show', {
|
||||
this.us = await os.api('users/show', {
|
||||
userIds: this.userIds
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="!available">{{ $t('waiting') }}<mk-ellipsis/></span>
|
||||
<span v-if="!available">{{ $t('waiting') }}<MkEllipsis/></span>
|
||||
<div ref="captcha"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
type Captcha = {
|
||||
render(container: string | Node, options: {
|
||||
@ -28,8 +28,9 @@ declare global {
|
||||
interface Window extends CaptchaContainer {
|
||||
}
|
||||
}
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
provider: {
|
||||
type: String,
|
||||
@ -88,7 +89,7 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.reset();
|
||||
},
|
||||
|
||||
@ -110,7 +111,7 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
callback(response?: string) {
|
||||
this.$emit('input', typeof response == 'string' ? response : null);
|
||||
this.$emit('update:value', typeof response == 'string' ? response : null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -6,23 +6,24 @@
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="isFollowing">
|
||||
<span v-if="full">{{ $t('unfollow') }}</span><fa :icon="faMinus"/>
|
||||
<span v-if="full">{{ $t('unfollow') }}</span><Fa :icon="faMinus"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ $t('follow') }}</span><fa :icon="faPlus"/>
|
||||
<span v-if="full">{{ $t('follow') }}</span><Fa :icon="faPlus"/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse fixed-width/>
|
||||
<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse fixed-width/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faSpinner, faPlus, faMinus, } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
channel: {
|
||||
type: Object,
|
||||
@ -49,12 +50,12 @@ export default Vue.extend({
|
||||
|
||||
try {
|
||||
if (this.isFollowing) {
|
||||
await this.$root.api('channels/unfollow', {
|
||||
await os.api('channels/unfollow', {
|
||||
channelId: this.channel.id
|
||||
});
|
||||
this.isFollowing = false;
|
||||
} else {
|
||||
await this.$root.api('channels/follow', {
|
||||
await os.api('channels/follow', {
|
||||
channelId: this.channel.id
|
||||
});
|
||||
this.isFollowing = true;
|
||||
|
@ -2,28 +2,42 @@
|
||||
<router-link :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
||||
<div class="banner" v-if="channel.bannerUrl" :style="`background-image: url('${channel.bannerUrl}')`">
|
||||
<div class="fade"></div>
|
||||
<div class="name"><fa :icon="faSatelliteDish"/> {{ channel.name }}</div>
|
||||
<div class="name"><Fa :icon="faSatelliteDish"/> {{ channel.name }}</div>
|
||||
<div class="status">
|
||||
<div><fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.usersCount }}</b></i18n></div>
|
||||
<div><fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.notesCount }}</b></i18n></div>
|
||||
<div>
|
||||
<Fa :icon="faUsers" fixed-width/>
|
||||
<i18n-t keypath="_channel.usersCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.usersCount }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div>
|
||||
<Fa :icon="faPencilAlt" fixed-width/>
|
||||
<i18n-t keypath="_channel.notesCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.notesCount }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<article v-if="channel.description">
|
||||
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
|
||||
</article>
|
||||
<footer>
|
||||
<span>
|
||||
{{ $t('updatedAt') }}: <mk-time :time="channel.lastNotedAt"/>
|
||||
<span v-if="channel.lastNotedAt">
|
||||
{{ $t('updatedAt') }}: <MkTime :time="channel.lastNotedAt"/>
|
||||
</span>
|
||||
</footer>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faSatelliteDish, faUsers, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
channel: {
|
||||
type: Object,
|
||||
@ -44,7 +58,6 @@ export default Vue.extend({
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border: 1px solid var(--divider);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<x-prism :inline="inline" :language="prismLang">{{ code }}</x-prism>
|
||||
<XPrism :inline="inline" :language="prismLang">{{ code }}</XPrism>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import 'prismjs';
|
||||
import 'prismjs/themes/prism-okaidia.css';
|
||||
import XPrism from 'vue-prism-component';
|
||||
export default Vue.extend({
|
||||
import XPrism from 'vue-prism-component';import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPrism
|
||||
},
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<x-code :code="code" :lang="lang" :inline="inline"/>
|
||||
<XCode :code="code" :lang="lang" :inline="inline"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XCode: () => import('./code-core.vue').then(m => m.default)
|
||||
XCode: defineAsyncComponent(() => import('./code-core.vue'))
|
||||
},
|
||||
props: {
|
||||
code: {
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh _button" @click="toggle">
|
||||
<b>{{ value ? this.$t('_cw.hide') : this.$t('_cw.show') }}</b>
|
||||
<span v-if="!value">{{ this.label }}</span>
|
||||
<button class="nrvgflfu _button" @click="toggle">
|
||||
<b>{{ value ? $t('_cw.hide') : $t('_cw.show') }}</b>
|
||||
<span v-if="!value">{{ label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { length } from 'stringz';
|
||||
import { concat } from '../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
@ -36,14 +36,14 @@ export default Vue.extend({
|
||||
length,
|
||||
|
||||
toggle() {
|
||||
this.$emit('input', !this.value);
|
||||
this.$emit('update:value', !this.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nrvgflfuaxwgkxoynpnumyookecqrrvh {
|
||||
.nrvgflfu {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.7em;
|
||||
|
@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv _list_" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
|
||||
<transition-group class="sqadhkmv _list_" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
|
||||
<template v-for="(item, i) in items">
|
||||
<slot :item="item"></slot>
|
||||
<div class="separator" v-if="showDate(i, item)" :key="item.id + '_date'">
|
||||
<p class="date">
|
||||
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
|
||||
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
|
||||
<span><Fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
|
||||
<span>{{ getDateText(items[i + 1].createdAt) }}<Fa class="icon" :icon="faAngleDown"/></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@ -82,14 +82,14 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
&[data-direction="up"] {
|
||||
> .list-enter {
|
||||
> .list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(64px);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-direction="down"] {
|
||||
> .list-enter {
|
||||
> .list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-64px);
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
<template>
|
||||
<x-column :menu="menu" :column="column" :is-stacked="isStacked">
|
||||
<XColumn :menu="menu" :column="column" :is-stacked="isStacked">
|
||||
<template #header>
|
||||
<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
<Fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<x-timeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
|
||||
</x-column>
|
||||
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XTimeline from '../timeline.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XTimeline,
|
||||
@ -59,8 +60,8 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
async setAntenna() {
|
||||
const antennas = await this.$root.api('antennas/list');
|
||||
const { canceled, result: antenna } = await this.$root.dialog({
|
||||
const antennas = await os.api('antennas/list');
|
||||
const { canceled, result: antenna } = await os.dialog({
|
||||
title: this.$t('selectAntenna'),
|
||||
type: null,
|
||||
select: {
|
||||
@ -72,7 +73,7 @@ export default Vue.extend({
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
Vue.set(this.column, 'antennaId', antenna.id);
|
||||
this.column.antennaId = antenna.id;
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
},
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<!-- TODO: リファクタの余地がありそう -->
|
||||
<x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -->
|
||||
<x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<XWidgetsColumn v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<!-- TODO: <XTlColumn v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> -->
|
||||
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import XTlColumn from './tl-column.vue';
|
||||
import XAntennaColumn from './antenna-column.vue';
|
||||
import XListColumn from './list-column.vue';
|
||||
@ -20,7 +20,7 @@ import XWidgetsColumn from './widgets-column.vue';
|
||||
import XMentionsColumn from './mentions-column.vue';
|
||||
import XDirectColumn from './direct-column.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XTlColumn,
|
||||
XAntennaColumn,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||
<section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }"
|
||||
<section class="dnpfarvg _panel _narrow_" :class="{ paged: isMainColumn, naked, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@ -15,15 +15,14 @@
|
||||
@contextmenu.prevent.stop="onContextmenu"
|
||||
>
|
||||
<button class="toggleActive _button" @click="toggleActive" v-if="isStacked">
|
||||
<template v-if="active"><fa :icon="faAngleUp"/></template>
|
||||
<template v-else><fa :icon="faAngleDown"/></template>
|
||||
<template v-if="active"><Fa :icon="faAngleUp"/></template>
|
||||
<template v-else><Fa :icon="faAngleDown"/></template>
|
||||
</button>
|
||||
<div class="action">
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
<span class="header"><slot name="header"></slot></span>
|
||||
<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button>
|
||||
<button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button>
|
||||
<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><Fa :icon="faCaretDown"/></button>
|
||||
</header>
|
||||
<div ref="body" v-show="active">
|
||||
<slot></slot>
|
||||
@ -32,11 +31,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
column: {
|
||||
type: Object,
|
||||
@ -71,7 +71,7 @@ export default Vue.extend({
|
||||
dragging: false,
|
||||
draghover: false,
|
||||
dropready: false,
|
||||
faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes,
|
||||
faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown,
|
||||
};
|
||||
},
|
||||
|
||||
@ -86,10 +86,10 @@ export default Vue.extend({
|
||||
|
||||
keymap(): any {
|
||||
return {
|
||||
'shift+up': () => this.$parent.$emit('parentFocus', 'up'),
|
||||
'shift+down': () => this.$parent.$emit('parentFocus', 'down'),
|
||||
'shift+left': () => this.$parent.$emit('parentFocus', 'left'),
|
||||
'shift+right': () => this.$parent.$emit('parentFocus', 'right'),
|
||||
'shift+up': () => this.$parent.$emit('parent-focus', 'up'),
|
||||
'shift+down': () => this.$parent.$emit('parent-focus', 'down'),
|
||||
'shift+left': () => this.$parent.$emit('parent-focus', 'left'),
|
||||
'shift+right': () => this.$parent.$emit('parent-focus', 'right'),
|
||||
};
|
||||
}
|
||||
},
|
||||
@ -100,21 +100,21 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
dragging(v) {
|
||||
this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd');
|
||||
os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd');
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.isMainColumn) {
|
||||
this.$root.$on('deck.column.dragStart', this.onOtherDragStart);
|
||||
this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd);
|
||||
os.deckGlobalEvents.on('column.dragStart', this.onOtherDragStart);
|
||||
os.deckGlobalEvents.on('column.dragEnd', this.onOtherDragEnd);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (!this.isMainColumn) {
|
||||
this.$root.$off('deck.column.dragStart', this.onOtherDragStart);
|
||||
this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd);
|
||||
os.deckGlobalEvents.off('column.dragStart', this.onOtherDragStart);
|
||||
os.deckGlobalEvents.off('column.dragEnd', this.onOtherDragEnd);
|
||||
}
|
||||
},
|
||||
|
||||
@ -137,7 +137,7 @@ export default Vue.extend({
|
||||
icon: faPencilAlt,
|
||||
text: this.$t('rename'),
|
||||
action: () => {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('rename'),
|
||||
input: {
|
||||
default: this.column.name,
|
||||
@ -207,14 +207,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
showMenu() {
|
||||
this.$root.menu({
|
||||
items: this.getMenu(),
|
||||
source: this.$refs.menu,
|
||||
});
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$router.push('/');
|
||||
os.modalMenu(this.getMenu(), this.$refs.menu);
|
||||
},
|
||||
|
||||
goTop() {
|
||||
@ -232,7 +225,7 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('mk-deck-column', this.column.id);
|
||||
e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, this.column.id);
|
||||
this.dragging = true;
|
||||
},
|
||||
|
||||
@ -254,7 +247,7 @@ export default Vue.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column';
|
||||
const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
|
||||
|
||||
e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||
|
||||
@ -267,9 +260,9 @@ export default Vue.extend({
|
||||
|
||||
onDrop(e) {
|
||||
this.draghover = false;
|
||||
this.$root.$emit('deck.column.dragEnd');
|
||||
os.deckGlobalEvents.emit('column.dragEnd');
|
||||
|
||||
const id = e.dataTransfer.getData('mk-deck-column');
|
||||
const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
|
||||
if (id != null && id != '') {
|
||||
this.$store.commit('deviceUser/swapDeckColumn', {
|
||||
a: this.column.id,
|
||||
@ -285,9 +278,11 @@ export default Vue.extend({
|
||||
.dnpfarvg {
|
||||
$header-height: 42px;
|
||||
|
||||
--section-padding: 10px;
|
||||
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px var(--deckColumnBorder);
|
||||
contain: content;
|
||||
|
||||
&.draghover {
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
@ -341,7 +336,6 @@ export default Vue.extend({
|
||||
&.paged {
|
||||
> div {
|
||||
background: var(--bg);
|
||||
padding: var(--margin);
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,8 +373,7 @@ export default Vue.extend({
|
||||
|
||||
> .toggleActive,
|
||||
> .action > *,
|
||||
> .menu,
|
||||
> .close {
|
||||
> .menu {
|
||||
z-index: 1;
|
||||
width: $header-height;
|
||||
line-height: $header-height;
|
||||
@ -408,8 +401,7 @@ export default Vue.extend({
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .menu,
|
||||
> .close {
|
||||
> .menu {
|
||||
margin-left: auto;
|
||||
margin-right: -16px;
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
<XColumn :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><Fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-notes :pagination="pagination" @before="before()" @after="after()"/>
|
||||
</x-column>
|
||||
<XNotes :pagination="pagination" @before="before()" @after="after()"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
||||
import Progress from '../../scripts/loading';
|
||||
import Progress from '@/scripts/loading';
|
||||
import XColumn from './column.vue';
|
||||
import XNotes from '../notes.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XNotes
|
||||
|
@ -1,20 +1,21 @@
|
||||
<template>
|
||||
<x-column :menu="menu" :column="column" :is-stacked="isStacked">
|
||||
<XColumn :menu="menu" :column="column" :is-stacked="isStacked">
|
||||
<template #header>
|
||||
<fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
<Fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
|
||||
</x-column>
|
||||
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XTimeline from '../timeline.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XTimeline,
|
||||
@ -59,8 +60,8 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
async setList() {
|
||||
const lists = await this.$root.api('users/lists/list');
|
||||
const { canceled, result: list } = await this.$root.dialog({
|
||||
const lists = await os.api('users/lists/list');
|
||||
const { canceled, result: list } = await os.dialog({
|
||||
title: this.$t('selectList'),
|
||||
type: null,
|
||||
select: {
|
||||
@ -72,7 +73,7 @@ export default Vue.extend({
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
Vue.set(this.column, 'listId', list.id);
|
||||
this.column.listId = list.id;
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
},
|
||||
|
||||
|
@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
<XColumn :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><Fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-notes :pagination="pagination" @before="before()" @after="after()"/>
|
||||
</x-column>
|
||||
<XNotes :pagination="pagination" @before="before()" @after="after()"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faAt } from '@fortawesome/free-solid-svg-icons';
|
||||
import Progress from '../../scripts/loading';
|
||||
import Progress from '@/scripts/loading';
|
||||
import XColumn from './column.vue';
|
||||
import XNotes from '../notes.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XNotes
|
||||
|
@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
<XColumn :column="column" :is-stacked="isStacked" :menu="menu">
|
||||
<template #header><Fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<x-notifications :include-types="column.includingTypes"/>
|
||||
</x-column>
|
||||
<XNotifications :include-types="column.includingTypes"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XNotifications from '../notifications.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XNotifications
|
||||
@ -42,12 +43,17 @@ export default Vue.extend({
|
||||
icon: faCog,
|
||||
text: this.$t('notificationSetting'),
|
||||
action: async () => {
|
||||
this.$root.new(await import('../notification-setting-window.vue').then(m => m.default), {
|
||||
os.popup(await import('@/components/notification-setting-window.vue'), {
|
||||
includingTypes: this.column.includingTypes,
|
||||
}).$on('ok', async ({ includingTypes }) => {
|
||||
this.$set(this.column, 'includingTypes', includingTypes);
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
}, {
|
||||
done: async (res) => {
|
||||
const { includingTypes } = res;
|
||||
this.$store.commit('deviceUser/updateDeckColumn', {
|
||||
...this.column,
|
||||
includingTypes: includingTypes
|
||||
});
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
}];
|
||||
},
|
||||
|
@ -1,31 +1,32 @@
|
||||
<template>
|
||||
<x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
|
||||
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
|
||||
<template #header>
|
||||
<fa v-if="column.tl === 'home'" :icon="faHome"/>
|
||||
<fa v-else-if="column.tl === 'local'" :icon="faComments"/>
|
||||
<fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/>
|
||||
<fa v-else-if="column.tl === 'global'" :icon="faGlobe"/>
|
||||
<Fa v-if="column.tl === 'home'" :icon="faHome"/>
|
||||
<Fa v-else-if="column.tl === 'local'" :icon="faComments"/>
|
||||
<Fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/>
|
||||
<Fa v-else-if="column.tl === 'global'" :icon="faGlobe"/>
|
||||
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<div class="iwaalbte" v-if="disabled">
|
||||
<p>
|
||||
<fa :icon="faMinusCircle"/>
|
||||
<Fa :icon="faMinusCircle"/>
|
||||
{{ $t('disabled-timeline.title') }}
|
||||
</p>
|
||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||
</div>
|
||||
<x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
|
||||
</x-column>
|
||||
<XTimeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XColumn from './column.vue';
|
||||
import XTimeline from '../timeline.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XTimeline,
|
||||
@ -78,7 +79,7 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
async setType() {
|
||||
const { canceled, result: src } = await this.$root.dialog({
|
||||
const { canceled, result: src } = await os.dialog({
|
||||
title: this.$t('timeline'),
|
||||
type: null,
|
||||
select: {
|
||||
@ -99,7 +100,7 @@ export default Vue.extend({
|
||||
}
|
||||
return;
|
||||
}
|
||||
Vue.set(this.column, 'tl', src);
|
||||
this.column.tl = src;
|
||||
this.$store.commit('deviceUser/updateDeckColumn', this.column);
|
||||
},
|
||||
|
||||
|
@ -1,47 +1,46 @@
|
||||
<template>
|
||||
<x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked">
|
||||
<template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked">
|
||||
<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
|
||||
|
||||
<div class="wtdtxvec">
|
||||
<template v-if="edit">
|
||||
<header>
|
||||
<mk-select v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
|
||||
<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
|
||||
<template #label>{{ $t('selectWidget') }}</template>
|
||||
<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
|
||||
</mk-select>
|
||||
<mk-button inline @click="addWidget" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
|
||||
<mk-button inline @click="edit = false">{{ $t('close') }}</mk-button>
|
||||
</MkSelect>
|
||||
<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
|
||||
<MkButton inline @click="edit = false">{{ $t('close') }}</MkButton>
|
||||
</header>
|
||||
<x-draggable
|
||||
<XDraggable
|
||||
:list="column.widgets"
|
||||
animation="150"
|
||||
@sort="onWidgetSort"
|
||||
>
|
||||
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
|
||||
<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><fa :icon="faTimes"/></button>
|
||||
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
|
||||
<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><Fa :icon="faTimes"/></button>
|
||||
<component :is="`mkw-${widget.name}`" :widget="widget" :setting-callback="setting => settings[widget.id] = setting" :column="column"/>
|
||||
</div>
|
||||
</x-draggable>
|
||||
</XDraggable>
|
||||
</template>
|
||||
<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/>
|
||||
<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column"/>
|
||||
</div>
|
||||
</x-column>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as XDraggable from 'vuedraggable';
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSelect from '@/components/ui/select.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XColumn from './column.vue';
|
||||
import { widgets } from '../../widgets';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XColumn,
|
||||
XDraggable,
|
||||
XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)),
|
||||
MkSelect,
|
||||
MkButton,
|
||||
},
|
||||
@ -63,6 +62,7 @@ export default Vue.extend({
|
||||
menu: null,
|
||||
widgetAdderSelected: null,
|
||||
widgets,
|
||||
settings: {},
|
||||
faWindowMaximize, faTimes, faPlus
|
||||
};
|
||||
},
|
||||
@ -79,7 +79,7 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
widgetFunc(id) {
|
||||
this.$refs[id][0].setting();
|
||||
this.settings[id]();
|
||||
},
|
||||
|
||||
onWidgetSort() {
|
||||
|
@ -1,31 +1,23 @@
|
||||
<template>
|
||||
<div class="mk-dialog" :class="{ iconOnly }">
|
||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
||||
<div class="bg _modalBg" ref="bg" @click="onBgClick" v-if="show"></div>
|
||||
</transition>
|
||||
<transition :name="$store.state.device.animation ? 'dialog' : ''" appear @after-leave="() => { destroyDom(); }">
|
||||
<div class="main" ref="main" v-if="show">
|
||||
<template v-if="type == 'signin'">
|
||||
<mk-signin/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
|
||||
<div class="mk-dialog">
|
||||
<div class="icon" v-if="icon">
|
||||
<fa :icon="icon"/>
|
||||
<Fa :icon="icon"/>
|
||||
</div>
|
||||
<div class="icon" v-else-if="!input && !select && !user" :class="type">
|
||||
<fa :icon="faCheck" v-if="type === 'success'"/>
|
||||
<fa :icon="faTimesCircle" v-if="type === 'error'"/>
|
||||
<fa :icon="faExclamationTriangle" v-if="type === 'warning'"/>
|
||||
<fa :icon="faInfoCircle" v-if="type === 'info'"/>
|
||||
<fa :icon="faQuestionCircle" v-if="type === 'question'"/>
|
||||
<fa :icon="faSpinner" pulse v-if="type === 'waiting'"/>
|
||||
<Fa :icon="faCheck" v-if="type === 'success'"/>
|
||||
<Fa :icon="faTimesCircle" v-if="type === 'error'"/>
|
||||
<Fa :icon="faExclamationTriangle" v-if="type === 'warning'"/>
|
||||
<Fa :icon="faInfoCircle" v-if="type === 'info'"/>
|
||||
<Fa :icon="faQuestionCircle" v-if="type === 'question'"/>
|
||||
<Fa :icon="faSpinner" pulse v-if="type === 'waiting'"/>
|
||||
</div>
|
||||
<header v-if="title" v-html="title"></header>
|
||||
<header v-if="title == null && user">{{ $t('enterUsername') }}</header>
|
||||
<div class="body" v-if="text" v-html="text"></div>
|
||||
<mk-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></mk-input>
|
||||
<mk-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></mk-input>
|
||||
<mk-select v-if="select" v-model="selectedValue" autofocus>
|
||||
<MkInput v-if="input" v-model:value="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput>
|
||||
<MkInput v-if="user" v-model:value="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></MkInput>
|
||||
<MkSelect v-if="select" v-model:value="selectedValue" autofocus>
|
||||
<template v-if="select.items">
|
||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||
</template>
|
||||
@ -34,36 +26,35 @@
|
||||
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
</mk-select>
|
||||
<div class="buttons" v-if="!iconOnly && (showOkButton || showCancelButton) && !actions">
|
||||
<mk-button inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</mk-button>
|
||||
<mk-button inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</mk-button>
|
||||
</MkSelect>
|
||||
<div class="buttons" v-if="(showOkButton || showCancelButton) && !actions">
|
||||
<MkButton inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</MkButton>
|
||||
<MkButton inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</MkButton>
|
||||
</div>
|
||||
<div class="buttons" v-if="actions">
|
||||
<mk-button v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</mk-button>
|
||||
<MkButton v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faSpinner, faInfoCircle, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkButton from './ui/button.vue';
|
||||
import MkInput from './ui/input.vue';
|
||||
import MkSelect from './ui/select.vue';
|
||||
import MkSignin from './signin.vue';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/ui/input.vue';
|
||||
import MkSelect from '@/components/ui/select.vue';
|
||||
import parseAcct from '../../misc/acct/parse';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModal,
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkSelect,
|
||||
MkSignin,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -107,19 +98,12 @@ export default Vue.extend({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
iconOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
inputValue: this.input && this.input.default ? this.input.default : null,
|
||||
userInputValue: null,
|
||||
selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
|
||||
@ -131,63 +115,51 @@ export default Vue.extend({
|
||||
watch: {
|
||||
userInputValue() {
|
||||
if (this.user) {
|
||||
this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => {
|
||||
os.api('users/show', parseAcct(this.userInputValue)).then(u => {
|
||||
this.canOk = u != null;
|
||||
}).catch(() => {
|
||||
this.canOk = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.user) this.canOk = false;
|
||||
|
||||
if (this.autoClose) {
|
||||
setTimeout(() => {
|
||||
this.close();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', this.onKeydown);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeydown);
|
||||
},
|
||||
|
||||
methods: {
|
||||
done(canceled, result?) {
|
||||
this.$emit('done', { canceled, result });
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
|
||||
async ok() {
|
||||
if (!this.canOk) return;
|
||||
if (!this.showOkButton) return;
|
||||
|
||||
if (this.user) {
|
||||
const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
|
||||
const user = await os.api('users/show', parseAcct(this.userInputValue));
|
||||
if (user) {
|
||||
this.$emit('ok', user);
|
||||
this.close();
|
||||
this.done(false, user);
|
||||
}
|
||||
} else {
|
||||
const result =
|
||||
this.input ? this.inputValue :
|
||||
this.select ? this.selectedValue :
|
||||
true;
|
||||
this.$emit('ok', result);
|
||||
this.close();
|
||||
this.done(false, result);
|
||||
}
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.$emit('cancel');
|
||||
this.close();
|
||||
},
|
||||
|
||||
close() {
|
||||
if (!this.show) return;
|
||||
this.show = false;
|
||||
this.$el.style.pointerEvents = 'none';
|
||||
(this.$refs.bg as any).style.pointerEvents = 'none';
|
||||
(this.$refs.main as any).style.pointerEvents = 'none';
|
||||
this.done(true);
|
||||
},
|
||||
|
||||
onBgClick() {
|
||||
@ -214,46 +186,12 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-enter-active, .dialog-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s !important;
|
||||
}
|
||||
.dialog-enter, .dialog-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.bg-fade-enter-active, .bg-fade-leave-active {
|
||||
transition: opacity 0.3s !important;
|
||||
}
|
||||
.bg-fade-enter, .bg-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mk-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
z-index: 30000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.iconOnly > .main {
|
||||
min-width: 0;
|
||||
width: initial;
|
||||
}
|
||||
|
||||
> .main {
|
||||
display: block;
|
||||
position: fixed;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 32px);
|
||||
text-align: center;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
@ -305,5 +243,4 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="zdjebgpv" ref="thumbnail">
|
||||
<img-with-blurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
|
||||
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
|
||||
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
|
||||
<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
|
||||
<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
|
||||
<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
|
||||
<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
|
||||
<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
|
||||
<fa :icon="faFile" class="icon" v-else/>
|
||||
<fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
|
||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
|
||||
<Fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
|
||||
<Fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
|
||||
<Fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
|
||||
<Fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
|
||||
<Fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
|
||||
<Fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
|
||||
<Fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
|
||||
<Fa :icon="faFile" class="icon" v-else/>
|
||||
<Fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
faFile,
|
||||
faFileAlt,
|
||||
@ -28,7 +28,7 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import ImgWithBlurhash from './img-with-blurhash.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ImgWithBlurhash
|
||||
},
|
||||
|
@ -1,24 +1,34 @@
|
||||
<template>
|
||||
<x-window ref="window" :width="800" :height="500" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="(type === 'file') && (selected.length === 0)" @ok="ok()">
|
||||
<XModalWindow ref="dialog"
|
||||
:width="800"
|
||||
:height="500"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="(type === 'file') && (selected.length === 0)"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ multiple ? ((type === 'file') ? $t('selectFiles') : $t('selectFolders')) : ((type === 'file') ? $t('selectFile') : $t('selectFolder')) }}
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length | number }})</span>
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
||||
</template>
|
||||
<div>
|
||||
<x-drive :multiple="multiple" @change-selection="onChangeSelection" :select="type"/>
|
||||
<XDrive :multiple="multiple" @changeSelection="onChangeSelection" @selected="ok()" :select="type"/>
|
||||
</div>
|
||||
</x-window>
|
||||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import XDrive from './drive.vue';
|
||||
import XWindow from './window.vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import number from '@/filters/number';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XDrive,
|
||||
XWindow,
|
||||
XModalWindow,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -33,6 +43,8 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
selected: []
|
||||
@ -41,13 +53,20 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.$emit('selected', this.selected);
|
||||
this.$refs.window.close();
|
||||
this.$emit('done', this.selected);
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.$emit('done');
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
|
||||
onChangeSelection(xs) {
|
||||
this.selected = xs;
|
||||
}
|
||||
},
|
||||
|
||||
number
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="ncvczrfv"
|
||||
:data-is-selected="isSelected"
|
||||
:class="{ isSelected }"
|
||||
@click="onClick"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
draggable="true"
|
||||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
@ -20,7 +21,7 @@
|
||||
<p>{{ $t('nsfw') }}</p>
|
||||
</div>
|
||||
|
||||
<x-file-thumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
|
||||
<p class="name">
|
||||
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
||||
@ -30,17 +31,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import copyToClipboard from '../scripts/copy-to-clipboard';
|
||||
//import updateAvatar from '../api/update-avatar';
|
||||
//import updateBanner from '../api/update-banner';
|
||||
import XFileThumbnail from './drive-file-thumbnail.vue';
|
||||
import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
|
||||
import bytes from '../filters/bytes';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XFileThumbnail
|
||||
MkDriveFileThumbnail
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -60,6 +61,8 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['chosen'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isDragging: false
|
||||
@ -72,17 +75,13 @@ export default Vue.extend({
|
||||
return this.$parent;
|
||||
},
|
||||
title(): string {
|
||||
return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`;
|
||||
return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(ev) {
|
||||
if (this.selectMode) {
|
||||
this.$emit('chosen', this.file);
|
||||
} else {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
getMenu() {
|
||||
return [{
|
||||
text: this.$t('rename'),
|
||||
icon: faICursor,
|
||||
action: this.rename
|
||||
@ -104,16 +103,26 @@ export default Vue.extend({
|
||||
}, null, {
|
||||
text: this.$t('delete'),
|
||||
icon: faTrashAlt,
|
||||
danger: true,
|
||||
action: this.deleteFile
|
||||
}],
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
}];
|
||||
},
|
||||
|
||||
onClick(ev) {
|
||||
if (this.selectMode) {
|
||||
this.$emit('chosen', this.file);
|
||||
} else {
|
||||
os.modalMenu(this.getMenu(), ev.currentTarget || ev.target);
|
||||
}
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
os.contextMenu(this.getMenu(), e);
|
||||
},
|
||||
|
||||
onDragstart(e) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file));
|
||||
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
|
||||
this.isDragging = true;
|
||||
|
||||
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
||||
@ -127,7 +136,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
rename() {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('renameFile'),
|
||||
input: {
|
||||
placeholder: this.$t('inputNewFileName'),
|
||||
@ -136,7 +145,7 @@ export default Vue.extend({
|
||||
}
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('drive/files/update', {
|
||||
os.api('drive/files/update', {
|
||||
fileId: this.file.id,
|
||||
name: name
|
||||
});
|
||||
@ -144,7 +153,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
toggleSensitive() {
|
||||
this.$root.api('drive/files/update', {
|
||||
os.api('drive/files/update', {
|
||||
fileId: this.file.id,
|
||||
isSensitive: !this.file.isSensitive
|
||||
});
|
||||
@ -152,18 +161,15 @@ export default Vue.extend({
|
||||
|
||||
copyUrl() {
|
||||
copyToClipboard(this.file.url);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
os.success();
|
||||
},
|
||||
|
||||
setAsAvatar() {
|
||||
updateAvatar(this.$root)(this.file);
|
||||
os.updateAvatar(this.file);
|
||||
},
|
||||
|
||||
setAsBanner() {
|
||||
updateBanner(this.$root)(this.file);
|
||||
os.updateBanner(this.file);
|
||||
},
|
||||
|
||||
addApp() {
|
||||
@ -171,17 +177,19 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
async deleteFile() {
|
||||
const { canceled } = await this.$root.dialog({
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('drive/files/delete', {
|
||||
os.api('drive/files/delete', {
|
||||
fileId: this.file.id
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
bytes
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -197,6 +205,10 @@ export default Vue.extend({
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(#000, 0.05);
|
||||
|
||||
@ -233,7 +245,7 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-selected] {
|
||||
&.isSelected {
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="rghtznwe"
|
||||
:data-draghover="draghover"
|
||||
:class="{ draghover }"
|
||||
@click="onClick"
|
||||
@mouseover="onMouseover"
|
||||
@mouseout="onMouseout"
|
||||
@ -14,8 +14,8 @@
|
||||
:title="title"
|
||||
>
|
||||
<p class="name">
|
||||
<template v-if="hover"><fa :icon="faFolderOpen" fixed-width/></template>
|
||||
<template v-if="!hover"><fa :icon="faFolder" fixed-width/></template>
|
||||
<template v-if="hover"><Fa :icon="faFolderOpen" fixed-width/></template>
|
||||
<template v-if="!hover"><Fa :icon="faFolder" fixed-width/></template>
|
||||
{{ folder.name }}
|
||||
</p>
|
||||
<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id">
|
||||
@ -26,10 +26,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faFolder, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
folder: {
|
||||
type: Object,
|
||||
@ -47,6 +48,8 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['chosen'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
hover: false,
|
||||
@ -91,8 +94,8 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
const isFile = e.dataTransfer.items[0].kind == 'file';
|
||||
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
|
||||
const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
|
||||
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
||||
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||
|
||||
if (isFile || isDriveFile || isDriveFolder) {
|
||||
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
||||
@ -121,11 +124,11 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = e.dataTransfer.getData('mk_drive_file');
|
||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile != '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
this.browser.removeFile(file.id);
|
||||
this.$root.api('drive/files/update', {
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: this.folder.id
|
||||
});
|
||||
@ -133,7 +136,7 @@ export default Vue.extend({
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = e.dataTransfer.getData('mk_drive_folder');
|
||||
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder != '') {
|
||||
const folder = JSON.parse(driveFolder);
|
||||
|
||||
@ -141,7 +144,7 @@ export default Vue.extend({
|
||||
if (folder.id == this.folder.id) return;
|
||||
|
||||
this.browser.removeFolder(folder.id);
|
||||
this.$root.api('drive/folders/update', {
|
||||
os.api('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: this.folder.id
|
||||
}).then(() => {
|
||||
@ -149,15 +152,15 @@ export default Vue.extend({
|
||||
}).catch(err => {
|
||||
switch (err) {
|
||||
case 'detected-circular-definition':
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('unableToProcess'),
|
||||
text: this.$t('circularReferenceFolder')
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('error')
|
||||
text: this.$t('somethingHappened')
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -167,7 +170,7 @@ export default Vue.extend({
|
||||
|
||||
onDragstart(e) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('mk_drive_folder', JSON.stringify(this.folder));
|
||||
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
|
||||
this.isDragging = true;
|
||||
|
||||
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
|
||||
@ -189,7 +192,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
rename() {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('renameFolder'),
|
||||
input: {
|
||||
placeholder: this.$t('inputNewFolderName'),
|
||||
@ -197,7 +200,7 @@ export default Vue.extend({
|
||||
}
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('drive/folders/update', {
|
||||
os.api('drive/folders/update', {
|
||||
folderId: this.folder.id,
|
||||
name: name
|
||||
});
|
||||
@ -205,7 +208,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
deleteFolder() {
|
||||
this.$root.api('drive/folders/delete', {
|
||||
os.api('drive/folders/delete', {
|
||||
folderId: this.folder.id
|
||||
}).then(() => {
|
||||
if (this.$store.state.settings.uploadFolder === this.folder.id) {
|
||||
@ -217,14 +220,14 @@ export default Vue.extend({
|
||||
}).catch(err => {
|
||||
switch(err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
title: this.$t('unableToDelete'),
|
||||
text: this.$t('hasChildFilesOrFolders')
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('unableToDelete')
|
||||
});
|
||||
@ -272,7 +275,7 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
|
||||
&[data-draghover] {
|
||||
&.draghover {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
|
@ -1,22 +1,23 @@
|
||||
<template>
|
||||
<div class="drylbebk"
|
||||
:data-draghover="draghover"
|
||||
:class="{ draghover }"
|
||||
@click="onClick"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.stop="onDrop"
|
||||
>
|
||||
<i v-if="folder == null"><fa :icon="faCloud"/></i>
|
||||
<i v-if="folder == null"><Fa :icon="faCloud"/></i>
|
||||
<span>{{ folder == null ? $t('drive') : folder.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faCloud } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
folder: {
|
||||
type: Object,
|
||||
@ -58,8 +59,8 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
const isFile = e.dataTransfer.items[0].kind == 'file';
|
||||
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
|
||||
const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
|
||||
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
||||
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||
|
||||
if (isFile || isDriveFile || isDriveFolder) {
|
||||
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
||||
@ -90,11 +91,11 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = e.dataTransfer.getData('mk_drive_file');
|
||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile != '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
this.browser.removeFile(file.id);
|
||||
this.$root.api('drive/files/update', {
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: this.folder ? this.folder.id : null
|
||||
});
|
||||
@ -102,13 +103,13 @@ export default Vue.extend({
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = e.dataTransfer.getData('mk_drive_folder');
|
||||
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder != '') {
|
||||
const folder = JSON.parse(driveFolder);
|
||||
// 移動先が自分自身ならreject
|
||||
if (this.folder && folder.id == this.folder.id) return;
|
||||
this.browser.removeFolder(folder.id);
|
||||
this.$root.api('drive/folders/update', {
|
||||
os.api('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: this.folder ? this.folder.id : null
|
||||
});
|
||||
@ -125,7 +126,7 @@ export default Vue.extend({
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&[data-draghover] {
|
||||
&.draghover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
|
@ -2,34 +2,35 @@
|
||||
<div class="yfudmmck">
|
||||
<nav>
|
||||
<div class="path" @contextmenu.prevent.stop="() => {}">
|
||||
<x-nav-folder :class="{ current: folder == null }"/>
|
||||
<XNavFolder :class="{ current: folder == null }"/>
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span class="separator" :key="f.id + ':separator'"><fa :icon="faAngleRight"/></span>
|
||||
<x-nav-folder :folder="f" :key="f.id"/>
|
||||
<span class="separator"><Fa :icon="faAngleRight"/></span>
|
||||
<XNavFolder :folder="f"/>
|
||||
</template>
|
||||
<span class="separator" v-if="folder != null"><fa :icon="faAngleRight"/></span>
|
||||
<span class="separator" v-if="folder != null"><Fa :icon="faAngleRight"/></span>
|
||||
<span class="folder current" v-if="folder != null">{{ folder.name }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
|
||||
<div class="main _section" :class="{ uploading: uploadings.length > 0, fetching }"
|
||||
ref="main"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu="onContextmenu"
|
||||
>
|
||||
<div class="contents" ref="contents">
|
||||
<div class="folders" ref="foldersContainer" v-show="folders.length > 0">
|
||||
<x-folder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
|
||||
<XFolder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
|
||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
|
||||
<mk-button ref="moreFolders" v-if="moreFolders">{{ $t('loadMore') }}</mk-button>
|
||||
<MkButton ref="moreFolders" v-if="moreFolders">{{ $t('loadMore') }}</MkButton>
|
||||
</div>
|
||||
<div class="files" ref="filesContainer" v-show="files.length > 0">
|
||||
<x-file v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
|
||||
<XFile v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
|
||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
|
||||
<mk-button ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $t('loadMore') }}</mk-button>
|
||||
<MkButton ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $t('loadMore') }}</MkButton>
|
||||
</div>
|
||||
<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
|
||||
<p v-if="draghover">{{ $t('empty-draghover') }}</p>
|
||||
@ -37,29 +38,28 @@
|
||||
<p v-if="!draghover && folder != null">{{ $t('emptyFolder') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<mk-loading v-if="fetching"/>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
<div class="dropzone" v-if="draghover"></div>
|
||||
<x-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/>
|
||||
<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faAngleRight, faFolderPlus, faICursor, faLink, faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||
import XNavFolder from './drive.nav-folder.vue';
|
||||
import XFolder from './drive.folder.vue';
|
||||
import XFile from './drive.file.vue';
|
||||
import XUploader from './uploader.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNavFolder,
|
||||
XFolder,
|
||||
XFile,
|
||||
XUploader,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
@ -85,6 +85,8 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
/**
|
||||
@ -100,7 +102,7 @@ export default Vue.extend({
|
||||
hierarchyFolders: [],
|
||||
selectedFiles: [],
|
||||
selectedFolders: [],
|
||||
uploadings: [],
|
||||
uploadings: os.uploads,
|
||||
connection: null,
|
||||
|
||||
/**
|
||||
@ -140,7 +142,7 @@ export default Vue.extend({
|
||||
});
|
||||
}
|
||||
|
||||
this.connection = this.$root.stream.useSharedConnection('drive');
|
||||
this.connection = os.stream.useSharedConnection('drive');
|
||||
|
||||
this.connection.on('fileCreated', this.onStreamDriveFileCreated);
|
||||
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
|
||||
@ -164,7 +166,7 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.connection.dispose();
|
||||
this.ilFilesObserver.disconnect();
|
||||
},
|
||||
@ -204,14 +206,6 @@ export default Vue.extend({
|
||||
this.removeFolder(folderId);
|
||||
},
|
||||
|
||||
onChangeUploaderUploads(uploads) {
|
||||
this.uploadings = uploads;
|
||||
},
|
||||
|
||||
onUploaderUploaded(file) {
|
||||
this.addFile(file, true);
|
||||
},
|
||||
|
||||
onDragover(e): any {
|
||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||
if (this.isDragSource) {
|
||||
@ -221,8 +215,8 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
const isFile = e.dataTransfer.items[0].kind == 'file';
|
||||
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
|
||||
const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
|
||||
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
||||
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
|
||||
|
||||
if (isFile || isDriveFile || isDriveFolder) {
|
||||
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
||||
@ -253,12 +247,12 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = e.dataTransfer.getData('mk_drive_file');
|
||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile != '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
if (this.files.some(f => f.id == file.id)) return;
|
||||
this.removeFile(file.id);
|
||||
this.$root.api('drive/files/update', {
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: this.folder ? this.folder.id : null
|
||||
});
|
||||
@ -266,7 +260,7 @@ export default Vue.extend({
|
||||
//#endregion
|
||||
|
||||
//#region ドライブのフォルダ
|
||||
const driveFolder = e.dataTransfer.getData('mk_drive_folder');
|
||||
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
|
||||
if (driveFolder != null && driveFolder != '') {
|
||||
const folder = JSON.parse(driveFolder);
|
||||
|
||||
@ -274,7 +268,7 @@ export default Vue.extend({
|
||||
if (this.folder && folder.id == this.folder.id) return false;
|
||||
if (this.folders.some(f => f.id == folder.id)) return false;
|
||||
this.removeFolder(folder.id);
|
||||
this.$root.api('drive/folders/update', {
|
||||
os.api('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: this.folder ? this.folder.id : null
|
||||
}).then(() => {
|
||||
@ -282,15 +276,15 @@ export default Vue.extend({
|
||||
}).catch(err => {
|
||||
switch (err) {
|
||||
case 'detected-circular-definition':
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('unableToProcess'),
|
||||
text: this.$t('circularReferenceFolder')
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('error')
|
||||
text: this.$t('somethingHappened')
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -303,19 +297,19 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
urlUpload() {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('uploadFromUrl'),
|
||||
input: {
|
||||
placeholder: this.$t('uploadFromUrlDescription')
|
||||
}
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('drive/files/upload_from_url', {
|
||||
os.api('drive/files/upload_from_url', {
|
||||
url: url,
|
||||
folderId: this.folder ? this.folder.id : undefined
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('uploadFromUrlRequested'),
|
||||
text: this.$t('uploadFromUrlMayTakeTime')
|
||||
});
|
||||
@ -323,14 +317,14 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
createFolder() {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('createFolder'),
|
||||
input: {
|
||||
placeholder: this.$t('folderName')
|
||||
}
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('drive/folders/create', {
|
||||
os.api('drive/folders/create', {
|
||||
name: name,
|
||||
parentId: this.folder ? this.folder.id : undefined
|
||||
}).then(folder => {
|
||||
@ -340,7 +334,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
renameFolder(folder) {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
title: this.$t('renameFolder'),
|
||||
input: {
|
||||
placeholder: this.$t('inputNewFolderName'),
|
||||
@ -348,7 +342,7 @@ export default Vue.extend({
|
||||
}
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('drive/folders/update', {
|
||||
os.api('drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
name: name
|
||||
}).then(folder => {
|
||||
@ -359,7 +353,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
deleteFolder(folder) {
|
||||
this.$root.api('drive/folders/delete', {
|
||||
os.api('drive/folders/delete', {
|
||||
folderId: folder.id
|
||||
}).then(() => {
|
||||
// 削除時に親フォルダに移動
|
||||
@ -367,14 +361,14 @@ export default Vue.extend({
|
||||
}).catch(err => {
|
||||
switch(err.id) {
|
||||
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
title: this.$t('unableToDelete'),
|
||||
text: this.$t('hasChildFilesOrFolders')
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('unableToDelete')
|
||||
});
|
||||
@ -390,7 +384,9 @@ export default Vue.extend({
|
||||
|
||||
upload(file, folder) {
|
||||
if (folder && typeof folder == 'object') folder = folder.id;
|
||||
(this.$refs.uploader as any).upload(file, folder);
|
||||
os.upload(file, folder).then(res => {
|
||||
this.addFile(res, true);
|
||||
});
|
||||
},
|
||||
|
||||
chooseFile(file) {
|
||||
@ -441,7 +437,7 @@ export default Vue.extend({
|
||||
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.api('drive/folders/show', {
|
||||
os.api('drive/folders/show', {
|
||||
folderId: target
|
||||
}).then(folder => {
|
||||
this.folder = folder;
|
||||
@ -465,7 +461,7 @@ export default Vue.extend({
|
||||
|
||||
if (this.folders.some(f => f.id == folder.id)) {
|
||||
const exist = this.folders.map(f => f.id).indexOf(folder.id);
|
||||
Vue.set(this.folders, exist, folder);
|
||||
this.folders[exist] = folder;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -482,7 +478,7 @@ export default Vue.extend({
|
||||
|
||||
if (this.files.some(f => f.id == file.id)) {
|
||||
const exist = this.files.map(f => f.id).indexOf(file.id);
|
||||
Vue.set(this.files, exist, file);
|
||||
this.files[exist] = file;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -543,7 +539,7 @@ export default Vue.extend({
|
||||
const filesMax = 30;
|
||||
|
||||
// フォルダ一覧取得
|
||||
this.$root.api('drive/folders', {
|
||||
os.api('drive/folders', {
|
||||
folderId: this.folder ? this.folder.id : null,
|
||||
limit: foldersMax + 1
|
||||
}).then(folders => {
|
||||
@ -556,7 +552,7 @@ export default Vue.extend({
|
||||
});
|
||||
|
||||
// ファイル一覧取得
|
||||
this.$root.api('drive/files', {
|
||||
os.api('drive/files', {
|
||||
folderId: this.folder ? this.folder.id : null,
|
||||
type: this.type,
|
||||
limit: filesMax + 1
|
||||
@ -587,7 +583,7 @@ export default Vue.extend({
|
||||
const max = 30;
|
||||
|
||||
// ファイル一覧取得
|
||||
this.$root.api('drive/files', {
|
||||
os.api('drive/files', {
|
||||
folderId: this.folder ? this.folder.id : null,
|
||||
type: this.type,
|
||||
untilId: this.files[this.files.length - 1].id,
|
||||
@ -602,7 +598,41 @@ export default Vue.extend({
|
||||
for (const x of files) this.appendFile(x);
|
||||
this.fetching = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getMenu() {
|
||||
return [{
|
||||
text: this.$t('addFile'),
|
||||
type: 'label'
|
||||
}, {
|
||||
text: this.$t('upload'),
|
||||
icon: faUpload,
|
||||
action: () => { this.selectLocalFile(); }
|
||||
}, {
|
||||
text: this.$t('fromUrl'),
|
||||
icon: faLink,
|
||||
action: () => { this.urlUpload(); }
|
||||
}, null, {
|
||||
text: this.folder ? this.folder.name : this.$t('drive'),
|
||||
type: 'label'
|
||||
}, this.folder ? {
|
||||
text: this.$t('renameFolder'),
|
||||
icon: faICursor,
|
||||
action: () => { this.renameFolder(this.folder); }
|
||||
} : undefined, this.folder ? {
|
||||
text: this.$t('deleteFolder'),
|
||||
icon: faTrashAlt,
|
||||
action: () => { this.deleteFolder(this.folder); }
|
||||
} : undefined, {
|
||||
text: this.$t('createFolder'),
|
||||
icon: faFolderPlus,
|
||||
action: () => { this.createFolder(); }
|
||||
}];
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
os.contextMenu(this.getMenu(), e);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -613,6 +643,8 @@ export default Vue.extend({
|
||||
display: block;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 1px 0 var(--divider);
|
||||
@ -666,7 +698,6 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
> .main {
|
||||
padding: 8px 0;
|
||||
overflow: auto;
|
||||
|
||||
&, * {
|
||||
@ -734,11 +765,6 @@ export default Vue.extend({
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .mk-uploader {
|
||||
height: 100px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
> input {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
|
||||
<div class="omfetrab">
|
||||
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<div class="omfetrab _popup">
|
||||
<header>
|
||||
<button v-for="(category, i) in categories"
|
||||
class="_button"
|
||||
@ -8,26 +8,26 @@
|
||||
:class="{ active: category.isActive }"
|
||||
:key="i"
|
||||
>
|
||||
<fa :icon="category.icon" fixed-width/>
|
||||
<Fa :icon="category.icon" fixed-width/>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="emojis">
|
||||
<template v-if="categories[0].isActive">
|
||||
<header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header>
|
||||
<header class="category"><Fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header>
|
||||
<div class="list">
|
||||
<button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])"
|
||||
<button v-for="emoji in ($store.state.device.recentEmojis || [])"
|
||||
class="_button"
|
||||
:title="emoji.name"
|
||||
@click="chosen(emoji)"
|
||||
:key="i"
|
||||
:key="emoji"
|
||||
>
|
||||
<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/>
|
||||
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
|
||||
<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header class="category"><fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header>
|
||||
<header class="category"><Fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header>
|
||||
</template>
|
||||
|
||||
<template v-if="categories.find(x => x.isActive).name">
|
||||
@ -38,7 +38,7 @@
|
||||
@click="chosen(emoji)"
|
||||
:key="emoji.name"
|
||||
>
|
||||
<mk-emoji :emoji="emoji.char"/>
|
||||
<MkEmoji :emoji="emoji.char"/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@ -59,29 +59,31 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-popup>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { emojilist } from '../../misc/emojilist';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
|
||||
import { groupByX } from '../../prelude/array';
|
||||
import XPopup from './popup.vue';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPopup,
|
||||
MkModal,
|
||||
},
|
||||
|
||||
props: {
|
||||
source: {
|
||||
required: true
|
||||
src: {
|
||||
required: false
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
emojilist,
|
||||
@ -162,12 +164,9 @@ export default Vue.extend({
|
||||
recents = recents.filter((e: any) => getKey(e) !== getKey(emoji));
|
||||
recents.unshift(emoji)
|
||||
this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) });
|
||||
this.$emit('chosen', getKey(emoji));
|
||||
this.$emit('done', getKey(emoji));
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$refs.popup.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -6,11 +6,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
import { defineComponent } from 'vue';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { twemojiSvgBase } from '../../misc/twemoji-base';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
|
@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<transition :name="$store.state.device.animation ? 'zoom' : ''" appear>
|
||||
<div class="mjndxjcg _panel">
|
||||
<div class="mjndxjcg">
|
||||
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
|
||||
<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
|
||||
<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
|
||||
<p><Fa :icon="faExclamationTriangle"/> {{ $t('somethingHappened') }}</p>
|
||||
<MkButton @click="() => $emit('retry')" class="button">{{ $t('retry') }}</MkButton>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkButton from './ui/button.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<span class="mk-file-type-icon">
|
||||
<template v-if="kind == 'image'"><fa :icon="faFileImage"/></template>
|
||||
<template v-if="kind == 'image'"><Fa :icon="faFileImage"/></template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faFileImage } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
@ -7,32 +7,33 @@
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||
<span v-if="full">{{ $t('followRequestPending') }}</span><fa :icon="faHourglassHalf"/>
|
||||
<span v-if="full">{{ $t('followRequestPending') }}</span><Fa :icon="faHourglassHalf"/>
|
||||
</template>
|
||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
|
||||
<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse/>
|
||||
<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse/>
|
||||
</template>
|
||||
<template v-else-if="isFollowing">
|
||||
<span v-if="full">{{ $t('unfollow') }}</span><fa :icon="faMinus"/>
|
||||
<span v-if="full">{{ $t('unfollow') }}</span><Fa :icon="faMinus"/>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && user.isLocked">
|
||||
<span v-if="full">{{ $t('followRequest') }}</span><fa :icon="faPlus"/>
|
||||
<span v-if="full">{{ $t('followRequest') }}</span><Fa :icon="faPlus"/>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && !user.isLocked">
|
||||
<span v-if="full">{{ $t('follow') }}</span><fa :icon="faPlus"/>
|
||||
<span v-if="full">{{ $t('follow') }}</span><Fa :icon="faPlus"/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse fixed-width/>
|
||||
<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse fixed-width/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faSpinner, faPlus, faMinus, faHourglassHalf } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
@ -58,7 +59,7 @@ export default Vue.extend({
|
||||
created() {
|
||||
// 渡されたユーザー情報が不完全な場合
|
||||
if (this.user.isFollowing == null) {
|
||||
this.$root.api('users/show', {
|
||||
os.api('users/show', {
|
||||
userId: this.user.id
|
||||
}).then(u => {
|
||||
this.isFollowing = u.isFollowing;
|
||||
@ -68,13 +69,13 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
|
||||
this.connection.on('follow', this.onFollowChange);
|
||||
this.connection.on('unfollow', this.onFollowChange);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
@ -91,7 +92,7 @@ export default Vue.extend({
|
||||
|
||||
try {
|
||||
if (this.isFollowing) {
|
||||
const { canceled } = await this.$root.dialog({
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
|
||||
showCancelButton: true
|
||||
@ -99,21 +100,21 @@ export default Vue.extend({
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
await this.$root.api('following/delete', {
|
||||
await os.api('following/delete', {
|
||||
userId: this.user.id
|
||||
});
|
||||
} else {
|
||||
if (this.hasPendingFollowRequestFromYou) {
|
||||
await this.$root.api('following/requests/cancel', {
|
||||
await os.api('following/requests/cancel', {
|
||||
userId: this.user.id
|
||||
});
|
||||
} else if (this.user.isLocked) {
|
||||
await this.$root.api('following/create', {
|
||||
await os.api('following/create', {
|
||||
userId: this.user.id
|
||||
});
|
||||
this.hasPendingFollowRequestFromYou = true;
|
||||
} else {
|
||||
await this.$root.api('following/create', {
|
||||
await os.api('following/create', {
|
||||
userId: this.user.id
|
||||
});
|
||||
this.hasPendingFollowRequestFromYou = true;
|
||||
|
@ -1,41 +1,50 @@
|
||||
<template>
|
||||
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false">
|
||||
<XModalWindow ref="dialog"
|
||||
:width="400"
|
||||
:can-close="false"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
@click="cancel()"
|
||||
@ok="ok()"
|
||||
@close="cancel()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ title }}
|
||||
</template>
|
||||
<div class="xkpnjxcv">
|
||||
<div class="xkpnjxcv _section">
|
||||
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
|
||||
<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||
<MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</mk-input>
|
||||
<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text">
|
||||
</MkInput>
|
||||
<MkInput v-else-if="form[item].type === 'string' && !item.multiline" v-model:value="values[item]" type="text">
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</mk-input>
|
||||
<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]">
|
||||
</MkInput>
|
||||
<MkTextarea v-else-if="form[item].type === 'string' && item.multiline" v-model:value="values[item]">
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</mk-textarea>
|
||||
<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
||||
</MkTextarea>
|
||||
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</mk-switch>
|
||||
</MkSwitch>
|
||||
</label>
|
||||
</div>
|
||||
</x-window>
|
||||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XWindow from './window.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import MkInput from './ui/input.vue';
|
||||
import MkTextarea from './ui/textarea.vue';
|
||||
import MkSwitch from './ui/switch.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XWindow,
|
||||
XModalWindow,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
@ -52,6 +61,8 @@ export default Vue.extend({
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['done'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
values: {}
|
||||
@ -60,15 +71,24 @@ export default Vue.extend({
|
||||
|
||||
created() {
|
||||
for (const item in this.form) {
|
||||
Vue.set(this.values, item, this.form[item].hasOwnProperty('default') ? this.form[item].default : null);
|
||||
this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.$emit('ok', this.values);
|
||||
this.$refs.window.close();
|
||||
this.$emit('done', {
|
||||
result: this.values
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.$emit('done', {
|
||||
canceled: true
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -77,7 +97,10 @@ export default Vue.extend({
|
||||
.xkpnjxcv {
|
||||
> label {
|
||||
display: block;
|
||||
padding: 16px 24px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -5,9 +5,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as katex from 'katex';
|
||||
export default Vue.extend({
|
||||
import { defineComponent } from 'vue';
|
||||
import * as katex from 'katex';import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
formula: {
|
||||
type: String,
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<x-formula :formula="formula" :block="block" />
|
||||
<XFormula :formula="formula" :block="block" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XFormula: () => import('./formula-core.vue').then(m => m.default)
|
||||
XFormula: defineAsyncComponent(() => import('./formula-core.vue'))
|
||||
},
|
||||
props: {
|
||||
formula: {
|
||||
|
@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div class="mk-google">
|
||||
<input type="search" v-model="query" :placeholder="q">
|
||||
<button @click="search"><fa :icon="faSearch"/> {{ $t('search') }}</button>
|
||||
<button @click="search"><Fa :icon="faSearch"/> {{ $t('search') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: ['q'],
|
||||
data() {
|
||||
return {
|
||||
@ -23,7 +24,7 @@ export default Vue.extend({
|
||||
methods: {
|
||||
search() {
|
||||
const engine = this.$store.state.settings.webSearchEngine ||
|
||||
'https://www.google.com/?#q={{query}}';
|
||||
'https://www.google.com/search?q={{query}}';
|
||||
const url = engine.replace('{{query}}', this.query)
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
@ -8,16 +8,17 @@
|
||||
</time>
|
||||
</div>
|
||||
<div class="content _panel _ghost">
|
||||
<mk-clock/>
|
||||
<MkClock/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkClock from './analog-clock.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkClock
|
||||
},
|
||||
@ -48,7 +49,7 @@ export default Vue.extend({
|
||||
this.tick();
|
||||
this.clock = setInterval(this.tick, 1000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
|
73
src/client/components/icon-dialog.vue
Normal file
73
src/client/components/icon-dialog.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<MkModal ref="modal" @click="type === 'success' ? done() : () => {}" @closed="$emit('closed')">
|
||||
<div class="iuyakobc" :class="type">
|
||||
<Fa class="icon" v-if="type === 'success'" :icon="faCheck"/>
|
||||
<Fa class="icon" v-else-if="type === 'waiting'" :icon="faSpinner" pulse/>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faCheck, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModal,
|
||||
},
|
||||
|
||||
props: {
|
||||
type: {
|
||||
required: true
|
||||
},
|
||||
showing: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
faCheck, faSpinner,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
showing() {
|
||||
if (!this.showing) this.done();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
done() {
|
||||
this.$emit('done');
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iuyakobc {
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
width: initial;
|
||||
font-size: 32px;
|
||||
|
||||
&.success {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
> .icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,16 +1,26 @@
|
||||
<template>
|
||||
<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
|
||||
<img class="xubzgfga" ref="img" :src="image.url" :alt="image.name" :title="image.name" @click="close" tabindex="-1"/>
|
||||
</x-modal>
|
||||
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<div class="xubzgfga">
|
||||
<header>{{ image.name }}</header>
|
||||
<img :src="image.url" :alt="image.name" :title="image.name" @click="$refs.modal.close()"/>
|
||||
<footer>
|
||||
<span>{{ image.type }}</span>
|
||||
<span>{{ bytes(image.size) }}</span>
|
||||
<span v-if="image.properties?.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
|
||||
</footer>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XModal from './modal.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import number from '@/filters/number';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XModal,
|
||||
MkModal,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -20,32 +30,50 @@ export default Vue.extend({
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.img.focus();
|
||||
});
|
||||
},
|
||||
emits: ['closed'],
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
bytes,
|
||||
number,
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xubzgfga {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
max-width: 1024px;
|
||||
|
||||
> header,
|
||||
> footer {
|
||||
display: inline-block;
|
||||
padding: 6px 9px;
|
||||
font-size: 90%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> header {
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
cursor: zoom-out;
|
||||
image-orientation: from-image;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 8px;
|
||||
opacity: 0.8;
|
||||
|
||||
> span + span {
|
||||
margin-left: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
border-left: solid 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="xubzgfgb" :title="title">
|
||||
<div class="xubzgfgb" :class="{ cover }" :title="title">
|
||||
<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/>
|
||||
<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
@ -35,6 +35,11 @@ export default Vue.extend({
|
||||
required: false,
|
||||
default: 64
|
||||
},
|
||||
cover: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -49,6 +54,7 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
draw() {
|
||||
if (this.hash == null) return;
|
||||
const pixels = decode(this.hash, this.size, this.size);
|
||||
const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
|
||||
const imageData = ctx!.createImageData(this.size, this.size);
|
||||
@ -70,9 +76,23 @@ export default Vue.extend({
|
||||
|
||||
> canvas,
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
> img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&.cover {
|
||||
> img {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Vue from 'vue';
|
||||
import { App } from 'vue';
|
||||
|
||||
import mfm from './misskey-flavored-markdown.vue';
|
||||
import acct from './acct.vue';
|
||||
@ -12,14 +12,16 @@ import loading from './loading.vue';
|
||||
import error from './error.vue';
|
||||
import streamIndicator from './stream-indicator.vue';
|
||||
|
||||
Vue.component('mfm', mfm);
|
||||
Vue.component('mk-acct', acct);
|
||||
Vue.component('mk-avatar', avatar);
|
||||
Vue.component('mk-emoji', emoji);
|
||||
Vue.component('mk-user-name', userName);
|
||||
Vue.component('mk-ellipsis', ellipsis);
|
||||
Vue.component('mk-time', time);
|
||||
Vue.component('mk-url', url);
|
||||
Vue.component('mk-loading', loading);
|
||||
Vue.component('mk-error', error);
|
||||
Vue.component('stream-indicator', streamIndicator);
|
||||
export default function(app: App) {
|
||||
app.component('Mfm', mfm);
|
||||
app.component('MkAcct', acct);
|
||||
app.component('MkAvatar', avatar);
|
||||
app.component('MkEmoji', emoji);
|
||||
app.component('MkUserName', userName);
|
||||
app.component('MkEllipsis', ellipsis);
|
||||
app.component('MkTime', time);
|
||||
app.component('MkUrl', url);
|
||||
app.component('MkLoading', loading);
|
||||
app.component('MkError', error);
|
||||
app.component('StreamIndicator', streamIndicator);
|
||||
}
|
||||
|
@ -1,93 +1,93 @@
|
||||
<template>
|
||||
<div class="zbcjwnqg" v-size="{ max: [550, 1200] }">
|
||||
<div class="zbcjwnqg" v-size="{ max: [550, 1000] }">
|
||||
<div class="stats" v-if="info">
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
|
||||
<b><Fa :icon="faUser"/>{{ $t('users') }}</b>
|
||||
<small>{{ $t('local') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ info.originalUsersCount | number }}</dd>
|
||||
<dd>{{ number(info.originalUsersCount) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersLocalDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ usersLocalDoD | number }}</dd>
|
||||
<dd>{{ number(usersLocalDoD) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersLocalWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ usersLocalWoW | number }}</dd>
|
||||
<dd>{{ number(usersLocalWoW) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
|
||||
<b><Fa :icon="faUser"/>{{ $t('users') }}</b>
|
||||
<small>{{ $t('remote') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd>
|
||||
<dd>{{ number((info.usersCount - info.originalUsersCount)) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ usersRemoteDoD | number }}</dd>
|
||||
<dd>{{ number(usersRemoteDoD) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ usersRemoteWoW | number }}</dd>
|
||||
<dd>{{ number(usersRemoteWoW) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
|
||||
<b><Fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
|
||||
<small>{{ $t('local') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ info.originalNotesCount | number }}</dd>
|
||||
<dd>{{ number(info.originalNotesCount) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesLocalDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ notesLocalDoD | number }}</dd>
|
||||
<dd>{{ number(notesLocalDoD) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesLocalWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ notesLocalWoW | number }}</dd>
|
||||
<dd>{{ number(notesLocalWoW) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
|
||||
<b><Fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
|
||||
<small>{{ $t('remote') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd>
|
||||
<dd>{{ number((info.notesCount - info.originalNotesCount)) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ notesRemoteDoD | number }}</dd>
|
||||
<dd>{{ number(notesRemoteDoD) }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ notesRemoteWoW | number }}</dd>
|
||||
<dd>{{ number(notesRemoteWoW) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title" style="position: relative;"><fa :icon="faChartBar"/> {{ $t('statistics') }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><fa :icon="faSync"/></button></div>
|
||||
<div class="_title" style="position: relative;"><Fa :icon="faChartBar"/> {{ $t('statistics') }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><Fa :icon="faSync"/></button></div>
|
||||
<div class="_content" style="margin-top: -8px;">
|
||||
<div class="selects" style="display: flex;">
|
||||
<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup :label="$t('federation')">
|
||||
<option value="federation-instances">{{ $t('_charts.federationInstancesIncDec') }}</option>
|
||||
<option value="federation-instances-total">{{ $t('_charts.federationInstancesTotal') }}</option>
|
||||
@ -109,11 +109,11 @@
|
||||
<option value="drive">{{ $t('_charts.storageUsageIncDec') }}</option>
|
||||
<option value="drive-total">{{ $t('_charts.storageUsageTotal') }}</option>
|
||||
</optgroup>
|
||||
</mk-select>
|
||||
<mk-select v-model="chartSpan" style="margin: 0;">
|
||||
</MkSelect>
|
||||
<MkSelect v-model:value="chartSpan" style="margin: 0;">
|
||||
<option value="hour">{{ $t('perHour') }}</option>
|
||||
<option value="day">{{ $t('perDay') }}</option>
|
||||
</mk-select>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<canvas ref="chart"></canvas>
|
||||
</div>
|
||||
@ -122,10 +122,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import { faChartBar, faUser, faPencilAlt, faSync } from '@fortawesome/free-solid-svg-icons';
|
||||
import Chart from 'chart.js';
|
||||
import MkSelect from './ui/select.vue';
|
||||
import number from '@/filters/number';
|
||||
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
@ -136,8 +137,9 @@ const alpha = (hex, a) => {
|
||||
const b = parseInt(result[3], 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSelect
|
||||
},
|
||||
@ -216,7 +218,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.info = await this.$root.api('stats');
|
||||
this.info = await os.api('stats');
|
||||
|
||||
this.now = new Date();
|
||||
|
||||
@ -226,17 +228,17 @@ export default Vue.extend({
|
||||
methods: {
|
||||
async fetchChart() {
|
||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
||||
this.$root.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
|
||||
]), Promise.all([
|
||||
this.$root.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/users', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
|
||||
this.$root.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
|
||||
])]);
|
||||
|
||||
const chart = {
|
||||
@ -279,7 +281,7 @@ export default Vue.extend({
|
||||
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
this.chartInstance = new Chart(this.$refs.chart, {
|
||||
this.chartInstance = markRaw(new Chart(this.$refs.chart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
@ -344,7 +346,7 @@ export default Vue.extend({
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
getDate(ago: number) {
|
||||
@ -622,13 +624,15 @@ export default Vue.extend({
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
number
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zbcjwnqg {
|
||||
&.max-width_1200px {
|
||||
&.max-width_1000px {
|
||||
> .stats {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
@ -5,18 +5,18 @@
|
||||
:title="url"
|
||||
>
|
||||
<slot></slot>
|
||||
<fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/>
|
||||
<Fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { url as local } from '../config';
|
||||
import MkUrlPreview from './url-preview-popup.vue';
|
||||
import { isDeviceTouch } from '../scripts/is-device-touch';
|
||||
import { url as local } from '@/config';
|
||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
@ -36,29 +36,34 @@ export default Vue.extend({
|
||||
target: self ? null : '_blank',
|
||||
showTimer: null,
|
||||
hideTimer: null,
|
||||
preview: null,
|
||||
checkTimer: null,
|
||||
close: null,
|
||||
faExternalLinkSquareAlt
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showPreview() {
|
||||
async showPreview() {
|
||||
if (!document.body.contains(this.$el)) return;
|
||||
if (this.preview) return;
|
||||
if (this.close) return;
|
||||
|
||||
this.preview = new MkUrlPreview({
|
||||
parent: this,
|
||||
propsData: {
|
||||
const { dispose } = os.popup(await import('@/components/url-preview-popup.vue'), {
|
||||
url: this.url,
|
||||
source: this.$el
|
||||
}
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
document.body.appendChild(this.preview.$el);
|
||||
this.close = () => {
|
||||
dispose();
|
||||
};
|
||||
|
||||
this.checkTimer = setInterval(() => {
|
||||
if (!document.body.contains(this.$el)) this.closePreview();
|
||||
}, 1000);
|
||||
},
|
||||
closePreview() {
|
||||
if (this.preview) {
|
||||
this.preview.destroyDom();
|
||||
this.preview = null;
|
||||
if (this.close) {
|
||||
clearInterval(this.checkTimer);
|
||||
this.close();
|
||||
this.close = null;
|
||||
}
|
||||
},
|
||||
onMouseover() {
|
||||
|
@ -5,9 +5,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
inline: {
|
||||
type: Boolean,
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="mk-media-banner">
|
||||
<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
|
||||
<span class="icon"><fa :icon="faExclamationTriangle"/></span>
|
||||
<span class="icon"><Fa :icon="faExclamationTriangle"/></span>
|
||||
<b>{{ $t('sensitive') }}</b>
|
||||
<span>{{ $t('clickToShow') }}</span>
|
||||
</div>
|
||||
@ -19,17 +19,18 @@
|
||||
:title="media.name"
|
||||
:download="media.name"
|
||||
>
|
||||
<span class="icon"><fa icon="download"/></span>
|
||||
<span class="icon"><Fa icon="download"/></span>
|
||||
<b>{{ media.name }}</b>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
media: {
|
||||
type: Object,
|
||||
|
@ -1,34 +1,36 @@
|
||||
<template>
|
||||
<div class="qjewsnkg" v-if="hide" @click="hide = false">
|
||||
<img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/>
|
||||
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.name"/>
|
||||
<div class="text">
|
||||
<div>
|
||||
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
||||
<b><Fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
||||
<span>{{ $t('clickToShow') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gqnyydlz" v-else>
|
||||
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
|
||||
<div class="gqnyydlz" :style="{ background: color }" v-else>
|
||||
<i><Fa :icon="faEyeSlash" @click="hide = true"/></i>
|
||||
<a
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
@click.prevent="onClick"
|
||||
>
|
||||
<img-with-blurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name"/>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name" :cover="false"/>
|
||||
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||
import ImageViewer from './image-viewer.vue';
|
||||
import ImgWithBlurhash from './img-with-blurhash.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ImgWithBlurhash
|
||||
},
|
||||
@ -44,8 +46,8 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
hide: true,
|
||||
faExclamationTriangle,
|
||||
faEyeSlash
|
||||
color: null,
|
||||
faExclamationTriangle, faEyeSlash,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -67,6 +69,9 @@ export default Vue.extend({
|
||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||
this.$watch('image', () => {
|
||||
this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw;
|
||||
if (this.image.blurhash) {
|
||||
this.color = extractAvgColorFromBlurhash(this.image.blurhash);
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
@ -77,12 +82,9 @@ export default Vue.extend({
|
||||
if (this.$store.state.device.imageNewTab) {
|
||||
window.open(this.image.url, '_blank');
|
||||
} else {
|
||||
const viewer = this.$root.new(ImageViewer, {
|
||||
os.popup(ImageViewer, {
|
||||
image: this.image
|
||||
});
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
viewer.close();
|
||||
});
|
||||
}, {}, 'closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,6 +125,7 @@ export default Vue.extend({
|
||||
|
||||
.gqnyydlz {
|
||||
position: relative;
|
||||
border: solid 1px var(--divider);
|
||||
|
||||
> i {
|
||||
display: block;
|
||||
|
@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<div class="mk-media-list">
|
||||
<template v-for="media in mediaList.filter(media => !previewable(media))">
|
||||
<x-banner :media="media" :key="media.id"/>
|
||||
</template>
|
||||
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :media="media" :key="media.id"/>
|
||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" ref="gridOuter">
|
||||
<div :data-count="mediaList.filter(media => previewable(media)).length" :style="gridInnerStyle">
|
||||
<template v-for="media in mediaList">
|
||||
<x-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
|
||||
<x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
|
||||
<XVideo :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
|
||||
<XImage :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@ -15,12 +13,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import XBanner from './media-banner.vue';
|
||||
import XImage from './media-image.vue';
|
||||
import XVideo from './media-video.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XBanner,
|
||||
XImage,
|
||||
@ -46,7 +45,7 @@ export default Vue.extend({
|
||||
this.size();
|
||||
window.addEventListener('resize', this.size);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('resize', this.size);
|
||||
},
|
||||
activated() {
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="hide" @click="hide = false">
|
||||
<div>
|
||||
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
||||
<b><Fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
|
||||
<span>{{ $t('clickToShow') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else>
|
||||
<i><fa :icon="faEyeSlash" @click="hide = true"/></i>
|
||||
<i><Fa :icon="faEyeSlash" @click="hide = true"/></i>
|
||||
<a
|
||||
:href="video.url"
|
||||
rel="nofollow noopener"
|
||||
@ -14,17 +14,18 @@
|
||||
:style="imageStyle"
|
||||
:title="video.name"
|
||||
>
|
||||
<fa :icon="faPlayCircle"/>
|
||||
<Fa :icon="faPlayCircle"/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
video: {
|
||||
type: Object,
|
||||
|
@ -15,12 +15,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { toUnicode } from 'punycode';
|
||||
import { host as localHost } from '../config';
|
||||
import { host as localHost } from '@/config';
|
||||
import { wellKnownServices } from '../../well-known-services';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
username: {
|
||||
type: String,
|
||||
|
@ -1,191 +0,0 @@
|
||||
<template>
|
||||
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
|
||||
<div class="rrevdjwt" :class="{ left: align === 'left' }" ref="items">
|
||||
<template v-for="(item, i) in items.filter(item => item !== undefined)">
|
||||
<div v-if="item === null" class="divider" :key="i"></div>
|
||||
<span v-else-if="item.type === 'label'" class="label item" :key="i">
|
||||
<span>{{ item.text }}</span>
|
||||
</span>
|
||||
<router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i">
|
||||
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
|
||||
<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
||||
<span>{{ item.text }}</span>
|
||||
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
|
||||
</router-link>
|
||||
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i">
|
||||
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
|
||||
<span>{{ item.text }}</span>
|
||||
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i">
|
||||
<mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/>
|
||||
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
|
||||
</button>
|
||||
<button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i">
|
||||
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
|
||||
<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
|
||||
<span>{{ item.text }}</span>
|
||||
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</x-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import XPopup from './popup.vue';
|
||||
import { focusPrev, focusNext } from '../scripts/focus';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPopup
|
||||
},
|
||||
props: {
|
||||
source: {
|
||||
required: true
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
noCenter: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
viaKeyboard: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
faCircle
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'up|k|shift+tab': this.focusUp,
|
||||
'down|j|tab': this.focusDown,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.viaKeyboard) {
|
||||
this.$nextTick(() => {
|
||||
focusNext(this.$refs.items.children[0], true);
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clicked(fn) {
|
||||
fn();
|
||||
this.close();
|
||||
},
|
||||
close() {
|
||||
this.$refs.popup.close();
|
||||
},
|
||||
focusUp() {
|
||||
focusPrev(document.activeElement);
|
||||
},
|
||||
focusDown() {
|
||||
focusNext(document.activeElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rrevdjwt {
|
||||
padding: 8px 0;
|
||||
|
||||
&.left {
|
||||
> .item {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
> .item {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9em;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: #fff;
|
||||
background: var(--accentDarken);
|
||||
}
|
||||
|
||||
&:not(:active):focus {
|
||||
box-shadow: 0 0 0 2px var(--focus) inset;
|
||||
}
|
||||
|
||||
&.label {
|
||||
pointer-events: none;
|
||||
font-size: 0.7em;
|
||||
padding-bottom: 4px;
|
||||
|
||||
> span {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
> i {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 13px;
|
||||
color: var(--indicator);
|
||||
font-size: 12px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
> .divider {
|
||||
margin: 8px 0;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,16 +1,18 @@
|
||||
import Vue, { VNode } from 'vue';
|
||||
import { VNode, defineComponent, h } from 'vue';
|
||||
import { MfmForest } from '../../mfm/prelude';
|
||||
import { parse, parsePlain } from '../../mfm/parse';
|
||||
import MkUrl from './url.vue';
|
||||
import MkLink from './link.vue';
|
||||
import MkMention from './mention.vue';
|
||||
import MkEmoji from './emoji.vue';
|
||||
import { concat } from '../../prelude/array';
|
||||
import MkFormula from './formula.vue';
|
||||
import MkCode from './code.vue';
|
||||
import MkGoogle from './google.vue';
|
||||
import { host } from '../config';
|
||||
import { host } from '@/config';
|
||||
import { RouterLink } from 'vue-router';
|
||||
|
||||
export default Vue.component('misskey-flavored-markdown', {
|
||||
export default defineComponent({
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
@ -41,7 +43,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
},
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
render() {
|
||||
if (this.text == null || this.text == '') return;
|
||||
|
||||
const ast = (this.plain ? parsePlain : parse)(this.text);
|
||||
@ -53,67 +55,49 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
|
||||
if (!this.plain) {
|
||||
const x = text.split('\n')
|
||||
.map(t => t == '' ? [createElement('br')] : [this._v(t), createElement('br')]); // NOTE: this._vはHACK SEE: https://github.com/syuilo/misskey/pull/6399#issuecomment-632820283
|
||||
.map(t => t == '' ? [h('br')] : [t, h('br')]);
|
||||
x[x.length - 1].pop();
|
||||
return x;
|
||||
} else {
|
||||
return [this._v(text.replace(/\n/g, ' '))];
|
||||
return [text.replace(/\n/g, ' ')];
|
||||
}
|
||||
}
|
||||
|
||||
case 'bold': {
|
||||
return [createElement('b', genEl(token.children))];
|
||||
return [h('b', genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'strike': {
|
||||
return [createElement('del', genEl(token.children))];
|
||||
return [h('del', genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'italic': {
|
||||
return (createElement as any)('i', {
|
||||
attrs: {
|
||||
return h('i', {
|
||||
style: 'font-style: oblique;'
|
||||
},
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
case 'big': {
|
||||
return (createElement as any)('strong', {
|
||||
attrs: {
|
||||
style: `display: inline-block; font-size: 150%;`
|
||||
},
|
||||
directives: [this.$store.state.device.animatedMfm ? {
|
||||
name: 'animate-css',
|
||||
value: { classes: 'tada', iteration: 'infinite' }
|
||||
}: {}]
|
||||
return h('strong', {
|
||||
style: `display: inline-block; font-size: 150%;` + (this.$store.state.device.animatedMfm ? 'animation: anime-tada 1s linear infinite both;' : ''),
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
case 'small': {
|
||||
return [createElement('small', {
|
||||
attrs: {
|
||||
return [h('small', {
|
||||
style: 'opacity: 0.7;'
|
||||
},
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'center': {
|
||||
return [createElement('div', {
|
||||
attrs: {
|
||||
return [h('div', {
|
||||
style: 'text-align:center;'
|
||||
}
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'motion': {
|
||||
return (createElement as any)('span', {
|
||||
attrs: {
|
||||
style: 'display: inline-block;'
|
||||
},
|
||||
directives: [this.$store.state.device.animatedMfm ? {
|
||||
name: 'animate-css',
|
||||
value: { classes: 'rubberBand', iteration: 'infinite' }
|
||||
} : {}]
|
||||
return h('span', {
|
||||
style: 'display: inline-block;' + (this.$store.state.device.animatedMfm ? 'animation: anime-rubberBand 1s linear infinite both;' : ''),
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
@ -123,163 +107,126 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
token.node.props.attr == 'alternate' ? 'alternate' :
|
||||
'normal';
|
||||
const style = this.$store.state.device.animatedMfm
|
||||
? `animation: spin 1.5s linear infinite; animation-direction: ${direction};` : '';
|
||||
return (createElement as any)('span', {
|
||||
attrs: {
|
||||
? `animation: anime-spin 1.5s linear infinite; animation-direction: ${direction};` : '';
|
||||
return h('span', {
|
||||
style: 'display: inline-block;' + style
|
||||
},
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
case 'jump': {
|
||||
return (createElement as any)('span', {
|
||||
attrs: {
|
||||
style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: jump 0.75s linear infinite;' : 'display: inline-block;'
|
||||
},
|
||||
return h('span', {
|
||||
style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: anime-jump 0.75s linear infinite;' : 'display: inline-block;'
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
case 'flip': {
|
||||
return (createElement as any)('span', {
|
||||
attrs: {
|
||||
return h('span', {
|
||||
style: 'display: inline-block; transform: scaleX(-1);'
|
||||
},
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
case 'url': {
|
||||
return [createElement(MkUrl, {
|
||||
return [h(MkUrl, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
url: token.node.props.url,
|
||||
rel: 'nofollow noopener',
|
||||
},
|
||||
})];
|
||||
}
|
||||
|
||||
case 'link': {
|
||||
return [createElement(MkLink, {
|
||||
return [h(MkLink, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
url: token.node.props.url,
|
||||
rel: 'nofollow noopener',
|
||||
},
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'mention': {
|
||||
return [createElement(MkMention, {
|
||||
return [h(MkMention, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host,
|
||||
username: token.node.props.username
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'hashtag': {
|
||||
return [createElement('router-link', {
|
||||
return [h(RouterLink, {
|
||||
key: Math.random(),
|
||||
attrs: {
|
||||
to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
|
||||
style: 'color:var(--hashtag);'
|
||||
}
|
||||
}, `#${token.node.props.hashtag}`)];
|
||||
}
|
||||
|
||||
case 'blockCode': {
|
||||
return [createElement(MkCode, {
|
||||
return [h(MkCode, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
code: token.node.props.code,
|
||||
lang: token.node.props.lang,
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'inlineCode': {
|
||||
return [createElement(MkCode, {
|
||||
return [h(MkCode, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
code: token.node.props.code,
|
||||
lang: token.node.props.lang,
|
||||
inline: true
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'quote': {
|
||||
if (this.shouldBreak) {
|
||||
return [createElement('div', {
|
||||
attrs: {
|
||||
if (!this.nowrap) {
|
||||
return [h('div', {
|
||||
class: 'quote'
|
||||
}
|
||||
}, genEl(token.children))];
|
||||
} else {
|
||||
return [createElement('span', {
|
||||
attrs: {
|
||||
return [h('span', {
|
||||
class: 'quote'
|
||||
}
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
}
|
||||
|
||||
case 'title': {
|
||||
return [createElement('div', {
|
||||
attrs: {
|
||||
return [h('div', {
|
||||
class: 'title'
|
||||
}
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'emoji': {
|
||||
return [createElement('mk-emoji', {
|
||||
return [h(MkEmoji, {
|
||||
key: Math.random(),
|
||||
attrs: {
|
||||
emoji: token.node.props.emoji,
|
||||
name: token.node.props.name
|
||||
},
|
||||
props: {
|
||||
name: token.node.props.name,
|
||||
customEmojis: this.customEmojis,
|
||||
normal: this.plain
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'mathInline': {
|
||||
//const MkFormula = () => import('./formula.vue').then(m => m.default);
|
||||
return [createElement(MkFormula, {
|
||||
return [h(MkFormula, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
formula: token.node.props.formula,
|
||||
block: false
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'mathBlock': {
|
||||
//const MkFormula = () => import('./formula.vue').then(m => m.default);
|
||||
return [createElement(MkFormula, {
|
||||
return [h(MkFormula, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
formula: token.node.props.formula,
|
||||
block: true
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'search': {
|
||||
//const MkGoogle = () => import('./google.vue').then(m => m.default);
|
||||
return [createElement(MkGoogle, {
|
||||
return [h(MkGoogle, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
q: token.node.props.query
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
default: {
|
||||
console.log('unrecognized ast type:', token.node.type);
|
||||
console.error('unrecognized ast type:', token.node.type);
|
||||
|
||||
return [];
|
||||
}
|
||||
@ -287,6 +234,6 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
}));
|
||||
|
||||
// Parse ast to DOM
|
||||
return createElement('span', genEl(ast));
|
||||
return h('span', genEl(ast));
|
||||
}
|
||||
});
|
||||
|
@ -30,10 +30,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: Array,
|
||||
@ -64,7 +65,7 @@ export default Vue.extend({
|
||||
// Vueが何故かWatchを発動させない場合があるので
|
||||
this.clock = setInterval(this.draw, 1000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
|
@ -3,10 +3,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MfmCore from './mfm';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MfmCore
|
||||
}
|
||||
@ -24,7 +24,7 @@ export default Vue.extend({
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
::v-deep .quote {
|
||||
::v-deep(.quote) {
|
||||
display: block;
|
||||
margin: 8px;
|
||||
padding: 6px 0 6px 12px;
|
||||
@ -33,15 +33,15 @@ export default Vue.extend({
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
::v-deep pre {
|
||||
::v-deep(pre) {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
::v-deep > code {
|
||||
> ::v-deep(code) {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
::v-deep .title {
|
||||
::v-deep(.title) {
|
||||
text-align: center;
|
||||
border-bottom: solid 1px var(--divider);
|
||||
}
|
||||
|
@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div class="mk-modal" v-hotkey.global="keymap">
|
||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
||||
<div class="bg _modalBg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div>
|
||||
</transition>
|
||||
<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
|
||||
<div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
canClose: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'esc': this.close,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.show = false;
|
||||
(this.$refs.bg as any).style.pointerEvents = 'none';
|
||||
(this.$refs.content as any).style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-enter-active, .modal-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s !important;
|
||||
}
|
||||
.modal-enter, .modal-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.bg-fade-enter-active, .bg-fade-leave-active {
|
||||
transition: opacity 0.3s !important;
|
||||
}
|
||||
.bg-fade-enter, .bg-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mk-modal {
|
||||
> .bg {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
> .content {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: calc(100% - 16px);
|
||||
max-height: calc(100% - 16px);
|
||||
overflow: auto;
|
||||
margin: auto;
|
||||
|
||||
::v-deep > * {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,33 +1,36 @@
|
||||
<template>
|
||||
<header class="kkwtjztg">
|
||||
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">
|
||||
<mk-user-name :user="note.user"/>
|
||||
<router-link class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
|
||||
<MkUserName :user="note.user"/>
|
||||
</router-link>
|
||||
<span class="is-bot" v-if="note.user.isBot">bot</span>
|
||||
<span class="username"><mk-acct :user="note.user"/></span>
|
||||
<span class="admin" v-if="note.user.isAdmin"><fa :icon="faBookmark"/></span>
|
||||
<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><fa :icon="farBookmark"/></span>
|
||||
<span class="username"><MkAcct :user="note.user"/></span>
|
||||
<span class="admin" v-if="note.user.isAdmin"><Fa :icon="faBookmark"/></span>
|
||||
<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><Fa :icon="farBookmark"/></span>
|
||||
<div class="info">
|
||||
<span class="mobile" v-if="note.viaMobile"><fa :icon="faMobileAlt"/></span>
|
||||
<router-link class="created-at" :to="note | notePage">
|
||||
<mk-time :time="note.createdAt"/>
|
||||
<span class="mobile" v-if="note.viaMobile"><Fa :icon="faMobileAlt"/></span>
|
||||
<router-link class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</router-link>
|
||||
<span class="visibility" v-if="note.visibility !== 'public'">
|
||||
<fa v-if="note.visibility === 'home'" :icon="faHome"/>
|
||||
<fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
|
||||
<fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
|
||||
<Fa v-if="note.visibility === 'home'" :icon="faHome"/>
|
||||
<Fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
|
||||
<Fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
|
||||
</span>
|
||||
<span class="localOnly" v-if="note.localOnly"><fa :icon="faBiohazard"/></span>
|
||||
<span class="localOnly" v-if="note.localOnly"><Fa :icon="faBiohazard"/></span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, faBiohazard } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
|
||||
import notePage from '../filters/note';
|
||||
import { userPage } from '../filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
@ -39,6 +42,11 @@ export default Vue.extend({
|
||||
return {
|
||||
faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, farBookmark, faBiohazard
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
notePage,
|
||||
userPage
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="yohlumlk">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="main">
|
||||
<x-note-header class="header" :note="note" :mini="true"/>
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
|
||||
<x-cw-button v-model="showContent" :note="note"/>
|
||||
<XCwButton v-model:value="showContent" :note="note"/>
|
||||
</p>
|
||||
<div class="content" v-show="note.cw == null || showContent">
|
||||
<x-sub-note-content class="text" :note="note"/>
|
||||
<XSubNote-content class="text" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -17,12 +17,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XSubNoteContent from './sub-note-content.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNoteHeader,
|
||||
XSubNoteContent,
|
||||
|
@ -1,32 +1,33 @@
|
||||
<template>
|
||||
<div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }">
|
||||
<div class="main">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="body">
|
||||
<x-note-header class="header" :note="note" :mini="true"/>
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
|
||||
<x-cw-button v-model="showContent" :note="note"/>
|
||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
|
||||
<XCwButton v-model:value="showContent" :note="note"/>
|
||||
</p>
|
||||
<div class="content" v-show="note.cw == null || showContent">
|
||||
<x-sub-note-content class="text" :note="note"/>
|
||||
<XSubNote-content class="text" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-sub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
|
||||
<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XSubNoteContent from './sub-note-content.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'x-sub',
|
||||
export default defineComponent({
|
||||
name: 'XSub',
|
||||
|
||||
components: {
|
||||
XNoteHeader,
|
||||
@ -65,7 +66,7 @@ export default Vue.extend({
|
||||
|
||||
created() {
|
||||
if (this.detail) {
|
||||
this.$root.api('notes/children', {
|
||||
os.api('notes/children', {
|
||||
noteId: this.note.id,
|
||||
limit: 5
|
||||
}).then(replies => {
|
||||
|
@ -8,95 +8,99 @@
|
||||
v-hotkey="keymap"
|
||||
v-size="{ max: [500, 450, 350, 300] }"
|
||||
>
|
||||
<x-sub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
|
||||
<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||
<div class="info" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
|
||||
<div class="info" v-if="appearNote._prId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <fa :icon="faTimes"/></button></div>
|
||||
<div class="info" v-if="appearNote._featuredId_"><fa :icon="faBolt"/> {{ $t('featured') }}</div>
|
||||
<XSub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
|
||||
<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||
<div class="info" v-if="pinned"><Fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
|
||||
<div class="info" v-if="appearNote._prId_"><Fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <Fa :icon="faTimes"/></button></div>
|
||||
<div class="info" v-if="appearNote._featuredId_"><Fa :icon="faBolt"/> {{ $t('featured') }}</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<fa :icon="faRetweet"/>
|
||||
<i18n path="renotedBy" tag="span">
|
||||
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user">
|
||||
<mk-user-name :user="note.user"/>
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<Fa :icon="faRetweet"/>
|
||||
<i18n-t keypath="renotedBy" tag="span">
|
||||
<template #user>
|
||||
<router-link class="name" :to="userPage(note.user)" v-user-preview="note.userId">
|
||||
<MkUserName :user="note.user"/>
|
||||
</router-link>
|
||||
</i18n>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div class="info">
|
||||
<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
|
||||
<fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
<Fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<span class="visibility" v-if="note.visibility !== 'public'">
|
||||
<fa v-if="note.visibility === 'home'" :icon="faHome"/>
|
||||
<fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
|
||||
<fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
|
||||
<Fa v-if="note.visibility === 'home'" :icon="faHome"/>
|
||||
<Fa v-if="note.visibility === 'followers'" :icon="faUnlock"/>
|
||||
<Fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/>
|
||||
</span>
|
||||
<span class="localOnly" v-if="note.localOnly"><fa :icon="faBiohazard"/></span>
|
||||
<span class="localOnly" v-if="note.localOnly"><Fa :icon="faBiohazard"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article">
|
||||
<mk-avatar class="avatar" :user="appearNote.user"/>
|
||||
<article class="article" @contextmenu="onContextmenu">
|
||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
<x-note-header class="header" :note="appearNote" :mini="true"/>
|
||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||
<div class="body" ref="noteBody">
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<x-cw-button v-model="showContent" :note="appearNote"/>
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<XCwButton v-model:value="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
|
||||
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><Fa :icon="faReply"/></router-link>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||
</div>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
<x-media-list :media-list="appearNote.files" :parent-element="noteBody"/>
|
||||
<XMediaList :media-list="appearNote.files" :parent-element="noteBody"/>
|
||||
</div>
|
||||
<x-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
|
||||
<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
|
||||
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
|
||||
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
|
||||
</div>
|
||||
<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link>
|
||||
<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><Fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
|
||||
<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
|
||||
<button @click="reply()" class="button _button">
|
||||
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
|
||||
<template v-else><fa :icon="faReply"/></template>
|
||||
<template v-if="appearNote.reply"><Fa :icon="faReplyAll"/></template>
|
||||
<template v-else><Fa :icon="faReply"/></template>
|
||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
|
||||
<fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||
<Fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="button _button">
|
||||
<fa :icon="faBan"/>
|
||||
<Fa :icon="faBan"/>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
|
||||
<fa :icon="faPlus"/>
|
||||
<Fa :icon="faPlus"/>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
|
||||
<fa :icon="faMinus"/>
|
||||
<Fa :icon="faMinus"/>
|
||||
</button>
|
||||
<button class="button _button" @click="menu()" ref="menuButton">
|
||||
<fa :icon="faEllipsisH"/>
|
||||
<Fa :icon="faEllipsisH"/>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||
<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||
</div>
|
||||
<div v-else class="_panel muted" @click="muted = false">
|
||||
<i18n path="userSaysSomething" tag="small">
|
||||
<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.userId" place="name">
|
||||
<mk-user-name :user="appearNote.user"/>
|
||||
<i18n-t keypath="userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<router-link class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</router-link>
|
||||
</i18n>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
|
||||
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import { parse } from '../../mfm/parse';
|
||||
@ -108,21 +112,24 @@ import XReactionsViewer from './reactions-viewer.vue';
|
||||
import XMediaList from './media-list.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
import XPoll from './poll.vue';
|
||||
import MkUrlPreview from './url-preview.vue';
|
||||
import MkReactionPicker from './reaction-picker.vue';
|
||||
import pleaseLogin from '../scripts/please-login';
|
||||
import { focusPrev, focusNext } from '../scripts/focus';
|
||||
import { url } from '../config';
|
||||
import copyToClipboard from '../scripts/copy-to-clipboard';
|
||||
import { checkWordMute } from '../scripts/check-word-mute';
|
||||
import { utils } from '@syuilo/aiscript';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import { url } from '@/config';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
import { userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { noteActions, noteViewInterruptors } from '@/store';
|
||||
|
||||
export default Vue.extend({
|
||||
model: {
|
||||
prop: 'note',
|
||||
event: 'updated'
|
||||
},
|
||||
function markRawAll(...xs) {
|
||||
for (const x of xs) {
|
||||
markRaw(x);
|
||||
}
|
||||
}
|
||||
|
||||
markRawAll(faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish);
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XSub,
|
||||
XNoteHeader,
|
||||
@ -131,7 +138,7 @@ export default Vue.extend({
|
||||
XMediaList,
|
||||
XCwButton,
|
||||
XPoll,
|
||||
MkUrlPreview,
|
||||
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
|
||||
},
|
||||
|
||||
inject: {
|
||||
@ -157,6 +164,8 @@ export default Vue.extend({
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['update:note'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
@ -171,6 +180,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
computed: {
|
||||
rs() {
|
||||
return this.$store.state.settings.reactions;
|
||||
},
|
||||
keymap(): any {
|
||||
return {
|
||||
'r': () => this.reply(true),
|
||||
@ -184,16 +196,16 @@ export default Vue.extend({
|
||||
'esc': this.blur,
|
||||
'm|o': () => this.menu(true),
|
||||
's': this.toggleShowContent,
|
||||
'1': () => this.reactDirectly(this.$store.state.settings.reactions[0]),
|
||||
'2': () => this.reactDirectly(this.$store.state.settings.reactions[1]),
|
||||
'3': () => this.reactDirectly(this.$store.state.settings.reactions[2]),
|
||||
'4': () => this.reactDirectly(this.$store.state.settings.reactions[3]),
|
||||
'5': () => this.reactDirectly(this.$store.state.settings.reactions[4]),
|
||||
'6': () => this.reactDirectly(this.$store.state.settings.reactions[5]),
|
||||
'7': () => this.reactDirectly(this.$store.state.settings.reactions[6]),
|
||||
'8': () => this.reactDirectly(this.$store.state.settings.reactions[7]),
|
||||
'9': () => this.reactDirectly(this.$store.state.settings.reactions[8]),
|
||||
'0': () => this.reactDirectly(this.$store.state.settings.reactions[9]),
|
||||
'1': () => this.reactDirectly(this.rs[0]),
|
||||
'2': () => this.reactDirectly(this.rs[1]),
|
||||
'3': () => this.reactDirectly(this.rs[2]),
|
||||
'4': () => this.reactDirectly(this.rs[3]),
|
||||
'5': () => this.reactDirectly(this.rs[4]),
|
||||
'6': () => this.reactDirectly(this.rs[5]),
|
||||
'7': () => this.reactDirectly(this.rs[6]),
|
||||
'8': () => this.reactDirectly(this.rs[7]),
|
||||
'9': () => this.reactDirectly(this.rs[8]),
|
||||
'0': () => this.reactDirectly(this.rs[9]),
|
||||
};
|
||||
},
|
||||
|
||||
@ -251,22 +263,22 @@ export default Vue.extend({
|
||||
|
||||
async created() {
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection = this.$root.stream;
|
||||
this.connection = os.stream;
|
||||
}
|
||||
|
||||
// plugin
|
||||
if (this.$store.state.noteViewInterruptors.length > 0) {
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
let result = this.note;
|
||||
for (const interruptor of this.$store.state.noteViewInterruptors) {
|
||||
result = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(result))));
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
|
||||
}
|
||||
this.$emit('updated', Object.freeze(result));
|
||||
this.$emit('update:note', Object.freeze(result));
|
||||
}
|
||||
|
||||
this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
|
||||
|
||||
if (this.detail) {
|
||||
this.$root.api('notes/children', {
|
||||
os.api('notes/children', {
|
||||
noteId: this.appearNote.id,
|
||||
limit: 30
|
||||
}).then(replies => {
|
||||
@ -274,7 +286,7 @@ export default Vue.extend({
|
||||
});
|
||||
|
||||
if (this.appearNote.replyId) {
|
||||
this.$root.api('notes/conversation', {
|
||||
os.api('notes/conversation', {
|
||||
noteId: this.appearNote.replyId
|
||||
}).then(conversation => {
|
||||
this.conversation = conversation.reverse();
|
||||
@ -293,7 +305,7 @@ export default Vue.extend({
|
||||
this.noteBody = this.$refs.noteBody;
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.decapture(true);
|
||||
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
@ -303,7 +315,7 @@ export default Vue.extend({
|
||||
|
||||
methods: {
|
||||
updateAppearNote(v) {
|
||||
this.$emit('updated', Object.freeze(this.isRenote ? {
|
||||
this.$emit('update:note', Object.freeze(this.isRenote ? {
|
||||
...this.note,
|
||||
renote: {
|
||||
...this.note.renote,
|
||||
@ -316,7 +328,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
readPromo() {
|
||||
(this as any).$root.api('promo/read', {
|
||||
os.api('promo/read', {
|
||||
noteId: this.appearNote.id
|
||||
});
|
||||
this.isDeleted = true;
|
||||
@ -439,8 +451,8 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
reply(viaKeyboard = false) {
|
||||
pleaseLogin(this.$root);
|
||||
this.$root.post({
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
reply: this.appearNote,
|
||||
animation: !viaKeyboard,
|
||||
}, () => {
|
||||
@ -449,14 +461,13 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
renote(viaKeyboard = false) {
|
||||
pleaseLogin(this.$root);
|
||||
pleaseLogin();
|
||||
this.blur();
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
os.modalMenu([{
|
||||
text: this.$t('renote'),
|
||||
icon: faRetweet,
|
||||
action: () => {
|
||||
(this as any).$root.api('notes/create', {
|
||||
os.api('notes/create', {
|
||||
renoteId: this.appearNote.id
|
||||
});
|
||||
}
|
||||
@ -464,42 +475,42 @@ export default Vue.extend({
|
||||
text: this.$t('quote'),
|
||||
icon: faQuoteRight,
|
||||
action: () => {
|
||||
this.$root.post({
|
||||
os.post({
|
||||
renote: this.appearNote,
|
||||
});
|
||||
}
|
||||
}]
|
||||
source: this.$refs.renoteButton,
|
||||
}], this.$refs.renoteButton, {
|
||||
viaKeyboard
|
||||
});
|
||||
},
|
||||
|
||||
renoteDirectly() {
|
||||
(this as any).$root.api('notes/create', {
|
||||
os.api('notes/create', {
|
||||
renoteId: this.appearNote.id
|
||||
});
|
||||
},
|
||||
|
||||
react(viaKeyboard = false) {
|
||||
pleaseLogin(this.$root);
|
||||
pleaseLogin();
|
||||
this.blur();
|
||||
const picker = this.$root.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
os.popup(defineAsyncComponent(() => import('@/components/reaction-picker.vue')), {
|
||||
showFocus: viaKeyboard,
|
||||
});
|
||||
picker.$once('chosen', reaction => {
|
||||
this.$root.api('notes/reactions/create', {
|
||||
src: this.$refs.reactButton,
|
||||
}, {
|
||||
done: reaction => {
|
||||
if (reaction) {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: this.appearNote.id,
|
||||
reaction: reaction
|
||||
}).then(() => {
|
||||
picker.close();
|
||||
});
|
||||
});
|
||||
picker.$once('closed', this.focus);
|
||||
}
|
||||
this.focus();
|
||||
},
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
reactDirectly(reaction) {
|
||||
this.$root.api('notes/reactions/create', {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: this.appearNote.id,
|
||||
reaction: reaction
|
||||
});
|
||||
@ -508,81 +519,67 @@ export default Vue.extend({
|
||||
undoReact(note) {
|
||||
const oldReaction = note.myReaction;
|
||||
if (!oldReaction) return;
|
||||
this.$root.api('notes/reactions/delete', {
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: note.id
|
||||
});
|
||||
},
|
||||
|
||||
favorite() {
|
||||
pleaseLogin(this.$root);
|
||||
this.$root.api('notes/favorites/create', {
|
||||
pleaseLogin();
|
||||
os.apiWithDialog('notes/favorites/create', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
del() {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('noteDeleteConfirm'),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('notes/delete', {
|
||||
os.api('notes/delete', {
|
||||
noteId: this.appearNote.id
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
delEdit() {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('deleteAndEditConfirm'),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('notes/delete', {
|
||||
os.api('notes/delete', {
|
||||
noteId: this.appearNote.id
|
||||
});
|
||||
|
||||
this.$root.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
|
||||
os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
|
||||
});
|
||||
},
|
||||
|
||||
toggleFavorite(favorite: boolean) {
|
||||
this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
|
||||
os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
toggleWatch(watch: boolean) {
|
||||
this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', {
|
||||
os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async menu(viaKeyboard = false) {
|
||||
getMenu() {
|
||||
let menu;
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
const state = await this.$root.api('notes/state', {
|
||||
const statePromise = os.api('notes/state', {
|
||||
noteId: this.appearNote.id
|
||||
});
|
||||
|
||||
menu = [{
|
||||
type: 'link',
|
||||
icon: faInfoCircle,
|
||||
@ -604,7 +601,7 @@ export default Vue.extend({
|
||||
}
|
||||
} : undefined,
|
||||
null,
|
||||
state.isFavorited ? {
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
icon: faStar,
|
||||
text: this.$t('unfavorite'),
|
||||
action: () => this.toggleFavorite(false)
|
||||
@ -612,8 +609,8 @@ export default Vue.extend({
|
||||
icon: faStar,
|
||||
text: this.$t('favorite'),
|
||||
action: () => this.toggleFavorite(true)
|
||||
},
|
||||
this.appearNote.userId != this.$store.state.i.id ? state.isWatching ? {
|
||||
}),
|
||||
(this.appearNote.userId != this.$store.state.i.id) ? statePromise.then(state => state.isWatching ? {
|
||||
icon: faEyeSlash,
|
||||
text: this.$t('unwatch'),
|
||||
action: () => this.toggleWatch(false)
|
||||
@ -621,7 +618,7 @@ export default Vue.extend({
|
||||
icon: faEye,
|
||||
text: this.$t('watch'),
|
||||
action: () => this.toggleWatch(true)
|
||||
} : undefined,
|
||||
}) : undefined,
|
||||
this.appearNote.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
|
||||
icon: faThumbtack,
|
||||
text: this.$t('unpin'),
|
||||
@ -650,6 +647,7 @@ export default Vue.extend({
|
||||
{
|
||||
icon: faTrashAlt,
|
||||
text: this.$t('delete'),
|
||||
danger: true,
|
||||
action: this.del
|
||||
}]
|
||||
: []
|
||||
@ -674,8 +672,8 @@ export default Vue.extend({
|
||||
.filter(x => x !== undefined);
|
||||
}
|
||||
|
||||
if (this.$store.state.noteActions.length > 0) {
|
||||
menu = menu.concat([null, ...this.$store.state.noteActions.map(action => ({
|
||||
if (noteActions.length > 0) {
|
||||
menu = menu.concat([null, ...noteActions.map(action => ({
|
||||
icon: faPlug,
|
||||
text: action.title,
|
||||
action: () => {
|
||||
@ -684,27 +682,39 @@ export default Vue.extend({
|
||||
}))]);
|
||||
}
|
||||
|
||||
this.$root.menu({
|
||||
items: menu,
|
||||
source: this.$refs.menuButton,
|
||||
return menu;
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(e.target)) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
os.contextMenu(this.getMenu(), e).then(this.focus);
|
||||
},
|
||||
|
||||
menu(viaKeyboard = false) {
|
||||
os.modalMenu(this.getMenu(), this.$refs.menuButton, {
|
||||
viaKeyboard
|
||||
}).then(this.focus);
|
||||
},
|
||||
|
||||
showRenoteMenu(viaKeyboard = false) {
|
||||
if (!this.isMyRenote) return;
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
os.modalMenu([{
|
||||
text: this.$t('unrenote'),
|
||||
icon: faTrashAlt,
|
||||
action: () => {
|
||||
this.$root.api('notes/delete', {
|
||||
os.api('notes/delete', {
|
||||
noteId: this.note.id
|
||||
});
|
||||
this.isDeleted = true;
|
||||
}
|
||||
}],
|
||||
source: this.$refs.renoteTime,
|
||||
}], this.$refs.renoteTime, {
|
||||
viaKeyboard: viaKeyboard
|
||||
});
|
||||
},
|
||||
@ -715,31 +725,20 @@ export default Vue.extend({
|
||||
|
||||
copyContent() {
|
||||
copyToClipboard(this.appearNote.text);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
os.success();
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
copyToClipboard(`${url}/notes/${this.appearNote.id}`);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
os.success();
|
||||
},
|
||||
|
||||
togglePin(pin: boolean) {
|
||||
this.$root.api(pin ? 'i/pin' : 'i/unpin', {
|
||||
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}).catch(e => {
|
||||
}, undefined, null, e => {
|
||||
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('pinLimitExceeded')
|
||||
});
|
||||
@ -748,26 +747,16 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
async promote() {
|
||||
const { canceled, result: days } = await this.$root.dialog({
|
||||
const { canceled, result: days } = await os.dialog({
|
||||
title: this.$t('numberOfDays'),
|
||||
input: { type: 'number' }
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('admin/promo/create', {
|
||||
os.apiWithDialog('admin/promo/create', {
|
||||
noteId: this.appearNote.id,
|
||||
expiresAt: Date.now() + (86400000 * days)
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -785,7 +774,9 @@ export default Vue.extend({
|
||||
|
||||
focusAfter() {
|
||||
focusNext(this.$el);
|
||||
}
|
||||
},
|
||||
|
||||
userPage
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -795,10 +786,28 @@ export default Vue.extend({
|
||||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
overflow: hidden;
|
||||
contain: content;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--focus);
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
border: dashed 1px var(--focus);
|
||||
border-radius: var(--radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .article > .main > .footer > .button {
|
||||
|
@ -1,42 +1,41 @@
|
||||
<template>
|
||||
<div class="mk-notes">
|
||||
<div class="_list_">
|
||||
<div class="_fullinfo" v-if="empty">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $t('noNotes') }}</div>
|
||||
</div>
|
||||
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
<MkError v-if="error" @retry="init()"/>
|
||||
|
||||
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
|
||||
<button class="_panel _button" ref="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
|
||||
<template v-if="moreFetching"><mk-loading inline/></template>
|
||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
||||
<x-note :note="note" @updated="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||
</x-list>
|
||||
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
||||
<XNote :note="note" @update:note="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||
</XList>
|
||||
|
||||
<div v-show="more && !reversed" style="margin-top: var(--margin);">
|
||||
<button class="_panel _button" ref="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
|
||||
<template v-if="moreFetching"><mk-loading inline/></template>
|
||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import paging from '../scripts/paging';
|
||||
import { defineComponent } from 'vue';
|
||||
import paging from '@/scripts/paging';
|
||||
import XNote from './note.vue';
|
||||
import XList from './date-separated-list.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNote, XList, MkButton
|
||||
XNote, XList,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
@ -68,6 +67,8 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['before', 'after'],
|
||||
|
||||
computed: {
|
||||
notes(): any[] {
|
||||
return this.prop ? this.items.map(item => item[this.prop]) : this.items;
|
||||
@ -82,9 +83,9 @@ export default Vue.extend({
|
||||
updated(oldValue, newValue) {
|
||||
const i = this.notes.findIndex(n => n === oldValue);
|
||||
if (this.prop) {
|
||||
Vue.set(this.items[i], this.prop, newValue);
|
||||
this.items[i][this.prop] = newValue;
|
||||
} else {
|
||||
Vue.set(this.items, i, newValue);
|
||||
this.items[i] = newValue;
|
||||
}
|
||||
},
|
||||
|
||||
@ -94,4 +95,3 @@ export default Vue.extend({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,34 +1,40 @@
|
||||
<template>
|
||||
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()">
|
||||
<XModalWindow ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
@ok="ok()"
|
||||
@close="$refs.dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>{{ $t('notificationSetting') }}</template>
|
||||
<div class="vv94n3oa">
|
||||
<div v-if="showGlobalToggle">
|
||||
<mk-switch v-model="useGlobalSetting">
|
||||
<div v-if="showGlobalToggle" class="_section">
|
||||
<MkSwitch v-model:value="useGlobalSetting">
|
||||
{{ $t('useGlobalSetting') }}
|
||||
<template #desc>{{ $t('useGlobalSettingDesc') }}</template>
|
||||
</mk-switch>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
<div v-if="!useGlobalSetting">
|
||||
<mk-info>{{ $t('notificationSettingDesc') }}</mk-info>
|
||||
<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button>
|
||||
<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button>
|
||||
<mk-switch v-for="type in notificationTypes" :key="type" v-model="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</mk-switch>
|
||||
<div v-if="!useGlobalSetting" class="_section">
|
||||
<MkInfo>{{ $t('notificationSettingDesc') }}</MkInfo>
|
||||
<MkButton inline @click="disableAll">{{ $t('disableAll') }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ $t('enableAll') }}</MkButton>
|
||||
<MkSwitch v-for="type in notificationTypes" :key="type" v-model:value="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</x-window>
|
||||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, { PropType } from 'vue';
|
||||
import XWindow from './window.vue';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import MkSwitch from './ui/switch.vue';
|
||||
import MkInfo from './ui/info.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
import { notificationTypes } from '../../types';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XWindow,
|
||||
XModalWindow,
|
||||
MkSwitch,
|
||||
MkInfo,
|
||||
MkButton
|
||||
@ -48,6 +54,8 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
typesMap: {} as Record<typeof notificationTypes[number], boolean>,
|
||||
@ -60,7 +68,7 @@ export default Vue.extend({
|
||||
this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle;
|
||||
|
||||
for (const type of this.notificationTypes) {
|
||||
Vue.set(this.typesMap, type, this.includingTypes === null || this.includingTypes.includes(type));
|
||||
this.typesMap[type] = this.includingTypes === null || this.includingTypes.includes(type);
|
||||
}
|
||||
},
|
||||
|
||||
@ -69,8 +77,8 @@ export default Vue.extend({
|
||||
const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][])
|
||||
.filter(type => this.typesMap[type]);
|
||||
|
||||
this.$emit('ok', { includingTypes });
|
||||
this.$refs.window.close();
|
||||
this.$emit('done', { includingTypes });
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
|
||||
disableAll() {
|
||||
@ -87,12 +95,3 @@ export default Vue.extend({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vv94n3oa {
|
||||
> div {
|
||||
border-top: solid 1px var(--divider);
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,71 +1,75 @@
|
||||
<template>
|
||||
<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }">
|
||||
<div class="head">
|
||||
<mk-avatar v-if="notification.user" class="icon" :user="notification.user"/>
|
||||
<img v-else class="icon" :src="notification.icon" alt=""/>
|
||||
<MkAvatar v-if="notification.user" class="icon" :user="notification.user"/>
|
||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
||||
<div class="sub-icon" :class="notification.type">
|
||||
<fa :icon="faPlus" v-if="notification.type === 'follow'"/>
|
||||
<fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/>
|
||||
<fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/>
|
||||
<fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/>
|
||||
<fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/>
|
||||
<fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
|
||||
<fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
|
||||
<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
|
||||
<fa :icon="faPollH" v-else-if="notification.type === 'pollVote'"/>
|
||||
<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/>
|
||||
<Fa :icon="faPlus" v-if="notification.type === 'follow'"/>
|
||||
<Fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/>
|
||||
<Fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/>
|
||||
<Fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/>
|
||||
<Fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/>
|
||||
<Fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
|
||||
<Fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
|
||||
<Fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
|
||||
<Fa :icon="faPollH" v-else-if="notification.type === 'pollVote'"/>
|
||||
<XReactionIcon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tail">
|
||||
<header>
|
||||
<router-link v-if="notification.user" class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link>
|
||||
<router-link v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></router-link>
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<mk-time :time="notification.createdAt" v-if="withTime"/>
|
||||
<MkTime :time="notification.createdAt" v-if="withTime"/>
|
||||
</header>
|
||||
<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
|
||||
<fa :icon="faQuoteLeft"/>
|
||||
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<fa :icon="faQuoteRight"/>
|
||||
<router-link v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Fa :icon="faQuoteLeft"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<Fa :icon="faQuoteRight"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'renote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
|
||||
<fa :icon="faQuoteLeft"/>
|
||||
<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
|
||||
<fa :icon="faQuoteRight"/>
|
||||
<router-link v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||
<Fa :icon="faQuoteLeft"/>
|
||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
|
||||
<Fa :icon="faQuoteRight"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'reply'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
|
||||
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<router-link v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'mention'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
|
||||
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<router-link v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'quote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
|
||||
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<router-link v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
</router-link>
|
||||
<router-link v-if="notification.type === 'pollVote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
|
||||
<fa :icon="faQuoteLeft"/>
|
||||
<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<fa :icon="faQuoteRight"/>
|
||||
<router-link v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Fa :icon="faQuoteLeft"/>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||
<Fa :icon="faQuoteRight"/>
|
||||
</router-link>
|
||||
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}<div v-if="full"><mk-follow-button :user="notification.user" :full="true"/></div></span>
|
||||
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
||||
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span>
|
||||
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span>
|
||||
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span>
|
||||
<span v-if="notification.type === 'app'" class="text">
|
||||
<mfm :text="notification.body" :nowrap="!full"/>
|
||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faIdCardAlt, faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faCheck, faPollH } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faClock } from '@fortawesome/free-regular-svg-icons';
|
||||
import noteSummary from '../../misc/get-note-summary';
|
||||
import XReactionIcon from './reaction-icon.vue';
|
||||
import MkFollowButton from './follow-button.vue';
|
||||
import notePage from '../filters/note';
|
||||
import { userPage } from '../filters/user';
|
||||
import { locale } from '../i18n';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XReactionIcon, MkFollowButton
|
||||
},
|
||||
@ -87,7 +91,7 @@ export default Vue.extend({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
getNoteSummary: (text: string) => noteSummary(text, this.$root.i18n.messages[this.$root.i18n.locale]),
|
||||
getNoteSummary: (text: string) => noteSummary(text, locale),
|
||||
followRequestDone: false,
|
||||
groupInviteDone: false,
|
||||
connection: null,
|
||||
@ -100,7 +104,7 @@ export default Vue.extend({
|
||||
if (!this.notification.isRead) {
|
||||
this.readObserver = new IntersectionObserver((entries, observer) => {
|
||||
if (!entries.some(entry => entry.isIntersecting)) return;
|
||||
this.$root.stream.send('readNotification', {
|
||||
os.stream.send('readNotification', {
|
||||
id: this.notification.id
|
||||
});
|
||||
entries.map(({ target }) => observer.unobserve(target));
|
||||
@ -108,12 +112,12 @@ export default Vue.extend({
|
||||
|
||||
this.readObserver.observe(this.$el);
|
||||
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el));
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (!this.notification.isRead) {
|
||||
this.readObserver.unobserve(this.$el);
|
||||
this.connection.dispose();
|
||||
@ -123,24 +127,22 @@ export default Vue.extend({
|
||||
methods: {
|
||||
acceptFollowRequest() {
|
||||
this.followRequestDone = true;
|
||||
this.$root.api('following/requests/accept', { userId: this.notification.user.id });
|
||||
os.api('following/requests/accept', { userId: this.notification.user.id });
|
||||
},
|
||||
rejectFollowRequest() {
|
||||
this.followRequestDone = true;
|
||||
this.$root.api('following/requests/reject', { userId: this.notification.user.id });
|
||||
os.api('following/requests/reject', { userId: this.notification.user.id });
|
||||
},
|
||||
acceptGroupInvitation() {
|
||||
this.groupInviteDone = true;
|
||||
this.$root.api('users/groups/invitations/accept', { invitationId: this.notification.invitation.id });
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
os.apiWithDialog('users/groups/invitations/accept', { invitationId: this.notification.invitation.id });
|
||||
},
|
||||
rejectGroupInvitation() {
|
||||
this.groupInviteDone = true;
|
||||
this.$root.api('users/groups/invitations/reject', { invitationId: this.notification.invitation.id });
|
||||
os.api('users/groups/invitations/reject', { invitationId: this.notification.invitation.id });
|
||||
},
|
||||
notePage,
|
||||
userPage
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -153,6 +155,7 @@ export default Vue.extend({
|
||||
font-size: 0.9em;
|
||||
overflow-wrap: break-word;
|
||||
display: flex;
|
||||
contain: content;
|
||||
|
||||
&.max-width_600px {
|
||||
padding: 16px;
|
||||
|
@ -1,30 +1,31 @@
|
||||
<template>
|
||||
<div class="mfcuwfyp">
|
||||
<x-list class="notifications" :items="items" v-slot="{ item: notification }">
|
||||
<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @updated="noteUpdated(notification.note, $event)" :key="notification.id"/>
|
||||
<x-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
|
||||
</x-list>
|
||||
<XList class="notifications" :items="items" v-slot="{ item: notification }">
|
||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
|
||||
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
|
||||
</XList>
|
||||
|
||||
<button class="_panel _button" ref="loadMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
|
||||
<template v-if="moreFetching"><mk-loading inline/></template>
|
||||
<template v-if="moreFetching"><MkLoading inline/></template>
|
||||
</button>
|
||||
|
||||
<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p>
|
||||
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
<MkError v-if="error" @retry="init()"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, { PropType } from 'vue';
|
||||
import paging from '../scripts/paging';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import paging from '@/scripts/paging';
|
||||
import XNotification from './notification.vue';
|
||||
import XList from './date-separated-list.vue';
|
||||
import XNote from './note.vue';
|
||||
import { notificationTypes } from '../../types';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotification,
|
||||
XList,
|
||||
@ -63,22 +64,30 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
watch: {
|
||||
includeTypes() {
|
||||
includeTypes: {
|
||||
handler() {
|
||||
this.reload();
|
||||
},
|
||||
'$store.state.i.mutingNotificationTypes'() {
|
||||
deep: true
|
||||
},
|
||||
// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $store.state.i が更新されると、
|
||||
// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
|
||||
'$store.state.i.mutingNotificationTypes': {
|
||||
handler() {
|
||||
if (this.includeTypes === null) {
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection = os.stream.useSharedConnection('main');
|
||||
this.connection.on('notification', this.onNotification);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
@ -86,7 +95,7 @@ export default Vue.extend({
|
||||
onNotification(notification) {
|
||||
const isMuted = !this.allIncludeTypes.includes(notification.type);
|
||||
if (isMuted || document.visibilityState === 'visible') {
|
||||
this.$root.stream.send('readNotification', {
|
||||
os.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
}
|
||||
@ -101,10 +110,10 @@ export default Vue.extend({
|
||||
|
||||
noteUpdated(oldValue, newValue) {
|
||||
const i = this.items.findIndex(n => n.note === oldValue);
|
||||
Vue.set(this.items, i, {
|
||||
this.items[i] = {
|
||||
...this.items[i],
|
||||
note: newValue
|
||||
});
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
@ -8,22 +8,27 @@
|
||||
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
|
||||
<footer>
|
||||
<img class="icon" :src="page.user.avatarUrl"/>
|
||||
<p>{{ page.user | userName }}</p>
|
||||
<p>{{ userName(page.user) }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { userName } from '../filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
userName
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
86
src/client/components/page-window.vue
Normal file
86
src/client/components/page-window.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<XWindow ref="window" :initial-width="400" :initial-height="450" :can-resize="true" @closed="$emit('closed')">
|
||||
<template #header>
|
||||
<XHeader :info="pageInfo" :with-back="false"/>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<button class="_button" @click="expand" v-tooltip="$t('showInPage')"><Fa :icon="faExpandAlt"/></button>
|
||||
<button class="_button" @click="popout" v-tooltip="$t('popout')"><Fa :icon="faExternalLinkAlt"/></button>
|
||||
</template>
|
||||
<div style="min-height: 100%; background: var(--bg);">
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</div>
|
||||
</XWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import { faExternalLinkAlt, faExpandAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import XWindow from '@/components/ui/window.vue';
|
||||
import XHeader from '@/ui/_common_/header.vue';
|
||||
import { popout } from '@/scripts/popout';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XWindow,
|
||||
XHeader,
|
||||
},
|
||||
|
||||
props: {
|
||||
initialUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialComponent: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
initialProps: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
pageInfo: null,
|
||||
url: this.initialUrl,
|
||||
component: this.initialComponent,
|
||||
props: this.initialProps,
|
||||
faExternalLinkAlt, faExpandAlt,
|
||||
};
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
navHook: (url, component, props) => {
|
||||
this.url = url;
|
||||
this.component = markRaw(component);
|
||||
this.props = props;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePage(page) {
|
||||
if (page == null) return;
|
||||
if (page.INFO) {
|
||||
this.pageInfo = page.INFO;
|
||||
}
|
||||
},
|
||||
|
||||
expand() {
|
||||
this.$router.push(this.url);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
|
||||
popout() {
|
||||
popout(this.url, this.$el);
|
||||
this.$refs.window.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import XText from './page.text.vue';
|
||||
import XSection from './page.section.vue';
|
||||
import XImage from './page.image.vue';
|
||||
@ -19,7 +19,7 @@ import XCounter from './page.counter.vue';
|
||||
import XRadioButton from './page.radio-button.vue';
|
||||
import XCanvas from './page.canvas.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas
|
||||
},
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-button class="kudkigyw" @click="click()" :primary="value.primary">{{ hpml.interpolate(value.text) }}</mk-button>
|
||||
<MkButton class="kudkigyw" @click="click()" :primary="value.primary">{{ hpml.interpolate(value.text) }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '../ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
@ -24,14 +25,14 @@ export default Vue.extend({
|
||||
click() {
|
||||
if (this.value.action === 'dialog') {
|
||||
this.hpml.eval();
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
text: this.hpml.interpolate(this.value.content)
|
||||
});
|
||||
} else if (this.value.action === 'resetRandom') {
|
||||
this.hpml.updateRandomSeed(Math.random());
|
||||
this.hpml.eval();
|
||||
} else if (this.value.action === 'pushEvent') {
|
||||
this.$root.api('page-push', {
|
||||
os.api('page-push', {
|
||||
pageId: this.hpml.page.id,
|
||||
event: this.value.event,
|
||||
...(this.value.var ? {
|
||||
@ -39,7 +40,7 @@ export default Vue.extend({
|
||||
} : {})
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'success',
|
||||
text: this.hpml.interpolate(this.value.message)
|
||||
});
|
||||
|
@ -5,9 +5,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-button class="llumlmnx" @click="click()">{{ hpml.interpolate(value.text) }}</mk-button>
|
||||
<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(value.text) }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '../ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
|
@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div v-show="hpml.vars[value.var]">
|
||||
<x-block v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h"/>
|
||||
<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
|
@ -5,9 +5,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-input class="kudkigyw" v-model="v" type="number">{{ hpml.interpolate(value.text) }}</mk-input>
|
||||
<MkInput class="kudkigyw" v-model:value="v" type="number">{{ hpml.interpolate(value.text) }}</MkInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkInput from '../ui/input.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInput
|
||||
},
|
||||
|
@ -1,18 +1,19 @@
|
||||
<template>
|
||||
<div class="ngbfujlo">
|
||||
<mk-textarea :value="text" readonly style="margin: 0;"></mk-textarea>
|
||||
<mk-button class="button" primary @click="post()" :disabled="posting || posted"><fa v-if="posted" :icon="faCheck"/><fa v-else :icon="faPaperPlane"/></mk-button>
|
||||
<MkTextarea :value="text" readonly style="margin: 0;"></MkTextarea>
|
||||
<MkButton class="button" primary @click="post()" :disabled="posting || posted"><Fa v-if="posted" :icon="faCheck"/><Fa v-else :icon="faPaperPlane"/></MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faCheck, faPaperPlane } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkTextarea from '../ui/textarea.vue';
|
||||
import MkButton from '../ui/button.vue';
|
||||
import { apiUrl } from '../../config';
|
||||
import { apiUrl } from '@/config';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkTextarea,
|
||||
MkButton,
|
||||
@ -44,7 +45,7 @@ export default Vue.extend({
|
||||
methods: {
|
||||
upload() {
|
||||
return new Promise((ok) => {
|
||||
const dialog = this.$root.dialog({
|
||||
const dialog = os.dialog({
|
||||
type: 'waiting',
|
||||
text: this.$t('uploading') + '...',
|
||||
showOkButton: false,
|
||||
@ -75,15 +76,11 @@ export default Vue.extend({
|
||||
async post() {
|
||||
this.posting = true;
|
||||
const file = this.value.attachCanvasImage ? await this.upload() : null;
|
||||
this.$root.api('notes/create', {
|
||||
os.apiWithDialog('notes/create', {
|
||||
text: this.text === '' ? null : this.text,
|
||||
fileIds: file ? [file.id] : undefined,
|
||||
}).then(() => {
|
||||
this.posted = true;
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>{{ hpml.interpolate(value.title) }}</div>
|
||||
<mk-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</mk-radio>
|
||||
<MkRadio v-for="x in value.values" v-model:value="v" :value="x" :key="x">{{ x }}</MkRadio>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkRadio from '../ui/radio.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkRadio
|
||||
},
|
||||
|
@ -3,15 +3,16 @@
|
||||
<component :is="'h' + h">{{ value.title }}</component>
|
||||
|
||||
<div class="children">
|
||||
<x-block v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h + 1"/>
|
||||
<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h + 1"/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div class="hkcxmtwj">
|
||||
<mk-switch v-model="v">{{ hpml.interpolate(value.text) }}</mk-switch>
|
||||
<MkSwitch v-model:value="v">{{ hpml.interpolate(value.text) }}</MkSwitch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkSwitch from '../ui/switch.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSwitch
|
||||
},
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-input class="kudkigyw" v-model="v" type="text">{{ hpml.interpolate(value.text) }}</mk-input>
|
||||
<MkInput class="kudkigyw" v-model:value="v" type="text">{{ hpml.interpolate(value.text) }}</MkInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkInput from '../ui/input.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInput
|
||||
},
|
||||
|
@ -1,16 +1,19 @@
|
||||
<template>
|
||||
<div class="mrdgzndn">
|
||||
<mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" class="url"/>
|
||||
<Mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
|
||||
<MkUrlPreview v-for="url in urls" :url="url" :key="url" class="url"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { parse } from '../../../mfm/parse';
|
||||
import { unique } from '../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-textarea v-model="v">{{ hpml.interpolate(value.text) }}</mk-textarea>
|
||||
<MkTextarea v-model:value="v">{{ hpml.interpolate(value.text) }}</MkTextarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkTextarea from '../ui/textarea.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkTextarea
|
||||
},
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<mk-textarea :value="text" readonly></mk-textarea>
|
||||
<MkTextarea :value="text" readonly></MkTextarea>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkTextarea from '../ui/textarea.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkTextarea
|
||||
},
|
||||
|
@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }" v-if="hpml">
|
||||
<x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :hpml="hpml" :key="child.id" :h="2"/>
|
||||
<XBlock v-for="child in page.content" :value="child" @update:value="v => updateBlock(v)" :page="page" :hpml="hpml" :key="child.id" :h="2"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { parse } from '@syuilo/aiscript';
|
||||
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faHeart } from '@fortawesome/free-regular-svg-icons';
|
||||
import XBlock from './page.block.vue';
|
||||
import { Hpml } from '../../scripts/hpml/evaluator';
|
||||
import { url } from '../../config';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
import { url } from '@/config';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XBlock
|
||||
},
|
||||
@ -33,7 +34,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
created() {
|
||||
this.hpml = new Hpml(this, this.page, {
|
||||
this.hpml = new Hpml(this.page, {
|
||||
randomSeed: Math.random(),
|
||||
visitor: this.$store.state.i,
|
||||
url: url,
|
||||
@ -49,7 +50,7 @@ export default Vue.extend({
|
||||
ast = parse(this.page.script);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
/*this.$root.dialog({
|
||||
/*os.dialog({
|
||||
type: 'error',
|
||||
text: 'Syntax error :('
|
||||
});*/
|
||||
@ -59,7 +60,7 @@ export default Vue.extend({
|
||||
this.hpml.eval();
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
/*this.$root.dialog({
|
||||
/*os.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});*/
|
||||
@ -70,7 +71,7 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
if (this.hpml.aiscript) this.hpml.aiscript.abort();
|
||||
},
|
||||
});
|
||||
|
@ -8,14 +8,16 @@
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="1" />
|
||||
repeatCount="1"
|
||||
/>
|
||||
<animate attributeName="stroke-width"
|
||||
begin="0s" dur="0.5s"
|
||||
values="16; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="1" />
|
||||
repeatCount="1"
|
||||
/>
|
||||
</circle>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
|
||||
@ -25,21 +27,24 @@
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="1" />
|
||||
repeatCount="1"
|
||||
/>
|
||||
<animate attributeName="cx"
|
||||
begin="0s" dur="0.8s"
|
||||
:values="`${particle.xA}; ${particle.xB}`"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="1" />
|
||||
repeatCount="1"
|
||||
/>
|
||||
<animate attributeName="cy"
|
||||
begin="0s" dur="0.8s"
|
||||
:values="`${particle.yA}; ${particle.yB}`"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="1" />
|
||||
repeatCount="1"
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
@ -47,9 +52,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
x: {
|
||||
type: Number,
|
||||
@ -60,6 +65,7 @@ export default Vue.extend({
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['end'],
|
||||
data() {
|
||||
const particles = [];
|
||||
const origin = 64;
|
||||
@ -85,7 +91,7 @@ export default Vue.extend({
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.destroyDom();
|
||||
this.$emit('end');
|
||||
}, 1100);
|
||||
}
|
||||
});
|
||||
|
@ -1,47 +1,47 @@
|
||||
<template>
|
||||
<div class="zmdxowus">
|
||||
<p class="caution" v-if="choices.length < 2">
|
||||
<fa :icon="faExclamationTriangle"/>{{ $t('_poll.noOnlyOneChoice') }}
|
||||
<Fa :icon="faExclamationTriangle"/>{{ $t('_poll.noOnlyOneChoice') }}
|
||||
</p>
|
||||
<ul ref="choices">
|
||||
<li v-for="(choice, i) in choices" :key="i">
|
||||
<mk-input class="input" :value="choice" @input="onInput(i, $event)">
|
||||
<MkInput class="input" :value="choice" @update:value="onInput(i, $event)">
|
||||
<span>{{ $t('_poll.choiceN', { n: i + 1 }) }}</span>
|
||||
</mk-input>
|
||||
</MkInput>
|
||||
<button @click="remove(i)" class="_button">
|
||||
<fa :icon="faTimes"/>
|
||||
<Fa :icon="faTimes"/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<mk-button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</mk-button>
|
||||
<mk-button class="add" v-else disabled>{{ $t('_poll.noMore') }}</mk-button>
|
||||
<MkButton class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</MkButton>
|
||||
<MkButton class="add" v-else disabled>{{ $t('_poll.noMore') }}</MkButton>
|
||||
<section>
|
||||
<mk-switch v-model="multiple">{{ $t('_poll.canMultipleVote') }}</mk-switch>
|
||||
<MkSwitch v-model:value="multiple">{{ $t('_poll.canMultipleVote') }}</MkSwitch>
|
||||
<div>
|
||||
<mk-select v-model="expiration">
|
||||
<MkSelect v-model:value="expiration">
|
||||
<template #label>{{ $t('_poll.expiration') }}</template>
|
||||
<option value="infinite">{{ $t('_poll.infinite') }}</option>
|
||||
<option value="at">{{ $t('_poll.at') }}</option>
|
||||
<option value="after">{{ $t('_poll.after') }}</option>
|
||||
</mk-select>
|
||||
</MkSelect>
|
||||
<section v-if="expiration === 'at'">
|
||||
<mk-input v-model="atDate" type="date" class="input">
|
||||
<MkInput v-model:value="atDate" type="date" class="input">
|
||||
<span>{{ $t('_poll.deadlineDate') }}</span>
|
||||
</mk-input>
|
||||
<mk-input v-model="atTime" type="time" class="input">
|
||||
</MkInput>
|
||||
<MkInput v-model:value="atTime" type="time" class="input">
|
||||
<span>{{ $t('_poll.deadlineTime') }}</span>
|
||||
</mk-input>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section v-if="expiration === 'after'">
|
||||
<mk-input v-model="after" type="number" class="input">
|
||||
<MkInput v-model:value="after" type="number" class="input">
|
||||
<span>{{ $t('_poll.duration') }}</span>
|
||||
</mk-input>
|
||||
<mk-select v-model="unit">
|
||||
</MkInput>
|
||||
<MkSelect v-model:value="unit">
|
||||
<option value="second">{{ $t('_time.second') }}</option>
|
||||
<option value="minute">{{ $t('_time.minute') }}</option>
|
||||
<option value="hour">{{ $t('_time.hour') }}</option>
|
||||
<option value="day">{{ $t('_time.day') }}</option>
|
||||
</mk-select>
|
||||
</MkSelect>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
@ -49,23 +49,33 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { erase } from '../../prelude/array';
|
||||
import { addTime } from '../../prelude/time';
|
||||
import { formatDateTimeString } from '../../misc/format-time-string';
|
||||
import MkInput from './ui/input.vue';
|
||||
import MkSelect from './ui/select.vue';
|
||||
import MkSwitch from './ui/switch.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInput,
|
||||
MkSelect,
|
||||
MkSwitch,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
poll: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['updated'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
choices: ['', ''],
|
||||
@ -78,20 +88,66 @@ export default Vue.extend({
|
||||
faExclamationTriangle, faTimes
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
choices() {
|
||||
this.$emit('updated');
|
||||
poll: {
|
||||
handler(poll) {
|
||||
if (poll == null) return;
|
||||
if (poll.choices.length == 0) return;
|
||||
this.choices = poll.choices;
|
||||
if (poll.choices.length == 1) this.choices = this.choices.concat('');
|
||||
this.multiple = poll.multiple;
|
||||
if (poll.expiresAt) {
|
||||
this.expiration = 'at';
|
||||
this.atDate = this.atTime = poll.expiresAt;
|
||||
} else if (typeof poll.expiredAfter === 'number') {
|
||||
this.expiration = 'after';
|
||||
this.after = poll.expiredAfter;
|
||||
} else {
|
||||
this.expiration = 'infinite';
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
choices: {
|
||||
handler() {
|
||||
this.$emit('updated', this.get());
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
multiple: {
|
||||
handler() {
|
||||
this.$emit('updated', this.get());
|
||||
},
|
||||
},
|
||||
expiration: {
|
||||
handler() {
|
||||
this.$emit('updated', this.get());
|
||||
},
|
||||
},
|
||||
atDate: {
|
||||
handler() {
|
||||
this.$emit('updated', this.get());
|
||||
},
|
||||
},
|
||||
after: {
|
||||
handler() {
|
||||
this.$emit('updated', this.get());
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onInput(i, e) {
|
||||
Vue.set(this.choices, i, e);
|
||||
this.choices[i] = e;
|
||||
},
|
||||
|
||||
add() {
|
||||
this.choices.push('');
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
|
||||
// TODO
|
||||
//(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
|
||||
});
|
||||
},
|
||||
|
||||
@ -116,29 +172,14 @@ export default Vue.extend({
|
||||
};
|
||||
|
||||
return {
|
||||
choices: erase('', this.choices),
|
||||
choices: this.choices,
|
||||
multiple: this.multiple,
|
||||
...(
|
||||
this.expiration === 'at' ? { expiresAt: at() } :
|
||||
this.expiration === 'after' ? { expiredAfter: after() } : {})
|
||||
this.expiration === 'after' ? { expiredAfter: after() } : {}
|
||||
)
|
||||
};
|
||||
},
|
||||
|
||||
set(data) {
|
||||
if (data.choices.length == 0) return;
|
||||
this.choices = data.choices;
|
||||
if (data.choices.length == 1) this.choices = this.choices.concat('');
|
||||
this.multiple = data.multiple;
|
||||
if (data.expiresAt) {
|
||||
this.expiration = 'at';
|
||||
this.atDate = this.atTime = data.expiresAt;
|
||||
} else if (typeof data.expiredAfter === 'number') {
|
||||
this.expiration = 'after';
|
||||
this.after = data.expiredAfter;
|
||||
} else {
|
||||
this.expiration = 'infinite';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="tivcixzd" :data-done="closed || isVoted">
|
||||
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
||||
<ul>
|
||||
<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }">
|
||||
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||
<span>
|
||||
<template v-if="choice.isVoted"><fa :icon="faCheck"/></template>
|
||||
<mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
|
||||
<template v-if="choice.isVoted"><Fa :icon="faCheck"/></template>
|
||||
<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
|
||||
<span class="votes" v-if="showResult">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
@ -22,11 +22,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { sum } from '../../prelude/array';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
@ -85,7 +86,7 @@ export default Vue.extend({
|
||||
},
|
||||
vote(id) {
|
||||
if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
|
||||
this.$root.api('notes/polls/vote', {
|
||||
os.api('notes/polls/vote', {
|
||||
noteId: this.note.id,
|
||||
choice: id
|
||||
}).then(() => {
|
||||
@ -153,7 +154,7 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
|
||||
&[data-done] {
|
||||
&.done {
|
||||
> ul > li {
|
||||
cursor: default;
|
||||
|
||||
|
@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div class="mk-popup" v-hotkey.global="keymap">
|
||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
||||
<div class="bg _modalBg" ref="bg" @click="close()" v-if="show"></div>
|
||||
</transition>
|
||||
<transition :name="$store.state.device.animation ? 'popup' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
|
||||
<div class="content" :class="{ fixed }" ref="content" v-if="show" :style="{ width: width ? width + 'px' : 'auto' }"><slot></slot></div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
source: {
|
||||
required: true
|
||||
},
|
||||
noCenter: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'esc': this.close,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const popover = this.$refs.content as any;
|
||||
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
const width = popover.offsetWidth;
|
||||
const height = popover.offsetHeight;
|
||||
|
||||
let left;
|
||||
let top;
|
||||
|
||||
if (this.$root.isMobile && !this.noCenter) {
|
||||
const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.source.offsetHeight / 2);
|
||||
left = (x - (width / 2));
|
||||
top = (y - (height / 2));
|
||||
popover.style.transformOrigin = 'center';
|
||||
} else {
|
||||
const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.source.offsetHeight;
|
||||
left = (x - (width / 2));
|
||||
top = y;
|
||||
}
|
||||
|
||||
if (this.fixed) {
|
||||
if (left + width > window.innerWidth) {
|
||||
left = window.innerWidth - width;
|
||||
popover.style.transformOrigin = 'center';
|
||||
}
|
||||
|
||||
if (top + height > window.innerHeight) {
|
||||
top = window.innerHeight - height;
|
||||
popover.style.transformOrigin = 'center';
|
||||
}
|
||||
} else {
|
||||
if (left + width - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - width + window.pageXOffset;
|
||||
popover.style.transformOrigin = 'center';
|
||||
}
|
||||
|
||||
if (top + height - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - height + window.pageYOffset;
|
||||
popover.style.transformOrigin = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
|
||||
popover.style.left = left + 'px';
|
||||
popover.style.top = top + 'px';
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.show = false;
|
||||
if (this.$refs.bg) (this.$refs.bg as any).style.pointerEvents = 'none';
|
||||
if (this.$refs.content) (this.$refs.content as any).style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-enter-active, .popup-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s !important;
|
||||
}
|
||||
.popup-enter, .popup-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.bg-fade-enter-active, .bg-fade-leave-active {
|
||||
transition: opacity 0.3s !important;
|
||||
}
|
||||
.bg-fade-enter, .bg-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mk-popup {
|
||||
> .bg {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
> .content {
|
||||
position: absolute;
|
||||
z-index: 10001;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
|
||||
overflow: hidden;
|
||||
transform-origin: center top;
|
||||
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<div class="skeikyzd" v-show="files.length != 0">
|
||||
<x-draggable class="files" :list="files" animation="150" delay="100" delayOnTouchOnly="true">
|
||||
<XDraggable class="files" :list="files" animation="150" delay="100" delay-on-touch-only="true">
|
||||
<div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)">
|
||||
<x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/>
|
||||
<MkDriveFileThumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/>
|
||||
<div class="sensitive" v-if="file.isSensitive">
|
||||
<fa class="icon" :icon="faExclamationTriangle"/>
|
||||
<Fa class="icon" :icon="faExclamationTriangle"/>
|
||||
</div>
|
||||
</div>
|
||||
</x-draggable>
|
||||
</XDraggable>
|
||||
<p class="remain">{{ 4 - files.length }}/4</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as XDraggable from 'vuedraggable';
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faExclamationTriangle, faICursor } from '@fortawesome/free-solid-svg-icons';
|
||||
import XFileThumbnail from './drive-file-thumbnail.vue'
|
||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue'
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XDraggable,
|
||||
XFileThumbnail
|
||||
XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)),
|
||||
MkDriveFileThumbnail
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -36,6 +36,8 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['updated', 'detach'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
menu: null as Promise<null> | null,
|
||||
@ -48,21 +50,21 @@ export default Vue.extend({
|
||||
detachMedia(id) {
|
||||
if (this.detachMediaFn) {
|
||||
this.detachMediaFn(id);
|
||||
} else if (this.$parent.detachMedia) {
|
||||
this.$parent.detachMedia(id);
|
||||
} else {
|
||||
this.$emit('detach', id);
|
||||
}
|
||||
},
|
||||
toggleSensitive(file) {
|
||||
this.$root.api('drive/files/update', {
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
isSensitive: !file.isSensitive
|
||||
}).then(() => {
|
||||
file.isSensitive = !file.isSensitive;
|
||||
this.$parent.updateMedia(file);
|
||||
this.$emit('updated', file);
|
||||
});
|
||||
},
|
||||
async rename(file) {
|
||||
const { canceled, result } = await this.$root.dialog({
|
||||
const { canceled, result } = await os.dialog({
|
||||
title: this.$t('enterFileName'),
|
||||
input: {
|
||||
default: file.name
|
||||
@ -70,18 +72,17 @@ export default Vue.extend({
|
||||
allowEmpty: false
|
||||
});
|
||||
if (canceled) return;
|
||||
this.$root.api('drive/files/update', {
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
name: result
|
||||
}).then(() => {
|
||||
file.name = result;
|
||||
this.$parent.updateMedia(file);
|
||||
this.$emit('updated', file);
|
||||
});
|
||||
},
|
||||
showFileMenu(file, ev: MouseEvent) {
|
||||
if (this.menu) return;
|
||||
this.menu = this.$root.menu({
|
||||
items: [{
|
||||
this.menu = os.modalMenu([{
|
||||
text: this.$t('renameFile'),
|
||||
icon: faICursor,
|
||||
action: () => { this.rename(file) }
|
||||
@ -93,9 +94,7 @@ export default Vue.extend({
|
||||
text: this.$t('attachCancel'),
|
||||
icon: faTimesCircle,
|
||||
action: () => { this.detachMedia(file.id) }
|
||||
}],
|
||||
source: ev.currentTarget || ev.target
|
||||
}).then(() => this.menu = null);
|
||||
}], ev.currentTarget || ev.target).then(() => this.menu = null);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -103,7 +102,7 @@ export default Vue.extend({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.skeikyzd {
|
||||
padding: 4px;
|
||||
padding: 8px 16px;
|
||||
position: relative;
|
||||
|
||||
> .files {
|
||||
@ -114,7 +113,9 @@ export default Vue.extend({
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 4px;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
|
||||
&:hover > .remove {
|
||||
|
@ -1,156 +1,19 @@
|
||||
<template>
|
||||
<div class="ulveipgl">
|
||||
<transition :name="$store.state.device.animation ? 'form-fade' : ''" appear @after-leave="$emit('closed');">
|
||||
<div class="bg _modalBg" ref="bg" v-if="show" @click="close()"></div>
|
||||
</transition>
|
||||
<div class="main" ref="main" @click.self="close()" @keydown="onKeydown">
|
||||
<transition :name="$store.state.device.animation ? 'form' : ''" appear
|
||||
@after-leave="destroyDom"
|
||||
>
|
||||
<x-post-form ref="form"
|
||||
v-if="show"
|
||||
:reply="reply"
|
||||
:renote="renote"
|
||||
:mention="mention"
|
||||
:specified="specified"
|
||||
:initial-text="initialText"
|
||||
:initial-note="initialNote"
|
||||
:instant="instant"
|
||||
:channel="channel"
|
||||
@posted="onPosted"
|
||||
@cancel="onCanceled"
|
||||
style="border-radius: var(--radius);"/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')" :position="'top'">
|
||||
<MkPostForm @done="$refs.modal.close()" @esc="$refs.modal.close()" v-bind="$attrs"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XPostForm from './post-form.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
import MkPostForm from '@/components/post-form.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPostForm
|
||||
MkModal,
|
||||
MkPostForm,
|
||||
},
|
||||
|
||||
props: {
|
||||
reply: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
renote: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
mention: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
specified: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
initialText: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
initialNote: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
instant: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
channel: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
show: true
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.form.focus();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.show = false;
|
||||
(this.$refs.bg as any).style.pointerEvents = 'none';
|
||||
(this.$refs.main as any).style.pointerEvents = 'none';
|
||||
},
|
||||
|
||||
onPosted() {
|
||||
this.$emit('posted');
|
||||
this.close();
|
||||
},
|
||||
|
||||
onCanceled() {
|
||||
this.$emit('cancel');
|
||||
this.close();
|
||||
},
|
||||
|
||||
onKeydown(e) {
|
||||
if (e.which === 27) { // Esc
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
}
|
||||
emits: ['closed'],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-enter-active, .form-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s !important;
|
||||
}
|
||||
.form-enter, .form-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.form-fade-enter-active, .form-fade-leave-active {
|
||||
transition: opacity 0.3s !important;
|
||||
}
|
||||
.form-fade-enter, .form-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ulveipgl {
|
||||
> .bg {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
> .main {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
top: 32px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(100% - 64px);
|
||||
width: 500px;
|
||||
max-width: calc(100% - 16px);
|
||||
overflow: auto;
|
||||
margin: 0 auto 0 auto;
|
||||
|
||||
@media (max-width: 550px) {
|
||||
top: 16px;
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
top: 8px;
|
||||
height: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,84 +1,84 @@
|
||||
<template>
|
||||
<div class="gafaadew"
|
||||
<div class="gafaadew" :class="{ modal, _popup: modal }"
|
||||
v-size="{ max: [500] }"
|
||||
@dragover.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.stop="onDrop"
|
||||
>
|
||||
<header>
|
||||
<button v-if="!fixed" class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button>
|
||||
<button v-if="!fixed" class="cancel _button" @click="cancel"><Fa :icon="faTimes"/></button>
|
||||
<div>
|
||||
<span class="local-only" v-if="localOnly" v-text="$t('_visibility.localOnly')" />
|
||||
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
|
||||
<span class="local-only" v-if="localOnly"><Fa :icon="faBiohazard"/></span>
|
||||
<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')" :disabled="channel != null">
|
||||
<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span>
|
||||
<span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
|
||||
<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span>
|
||||
<span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span>
|
||||
<span v-if="visibility === 'public'"><Fa :icon="faGlobe"/></span>
|
||||
<span v-if="visibility === 'home'"><Fa :icon="faHome"/></span>
|
||||
<span v-if="visibility === 'followers'"><Fa :icon="faUnlock"/></span>
|
||||
<span v-if="visibility === 'specified'"><Fa :icon="faEnvelope"/></span>
|
||||
</button>
|
||||
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button>
|
||||
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<Fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="form" :class="{ fixed }">
|
||||
<x-note-preview class="preview" v-if="reply" :note="reply"/>
|
||||
<x-note-preview class="preview" v-if="renote" :note="renote"/>
|
||||
<div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('quoteAttached') }}<button @click="quoteId = null"><fa icon="times"/></button></div>
|
||||
<XNotePreview class="preview" v-if="reply" :note="reply"/>
|
||||
<XNotePreview class="preview" v-if="renote" :note="renote"/>
|
||||
<div class="with-quote" v-if="quoteId"><Fa icon="quote-left"/> {{ $t('quoteAttached') }}<button @click="quoteId = null"><Fa icon="times"/></button></div>
|
||||
<div v-if="visibility === 'specified'" class="to-specified">
|
||||
<span style="margin-right: 8px;">{{ $t('recipient') }}</span>
|
||||
<div class="visibleUsers">
|
||||
<span v-for="u in visibleUsers" :key="u.id">
|
||||
<mk-acct :user="u"/>
|
||||
<button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button>
|
||||
<MkAcct :user="u"/>
|
||||
<button class="_button" @click="removeVisibleUser(u)"><Fa :icon="faTimes"/></button>
|
||||
</span>
|
||||
<button @click="addVisibleUser" class="_buttonPrimary"><fa :icon="faPlus" fixed-width/></button>
|
||||
<button @click="addVisibleUser" class="_buttonPrimary"><Fa :icon="faPlus" fixed-width/></button>
|
||||
</div>
|
||||
</div>
|
||||
<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$t('annotation')" v-autocomplete="{ model: 'cw' }" @keydown="onKeydown">
|
||||
<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @keydown="onKeydown" @paste="onPaste"></textarea>
|
||||
<x-post-form-attaches class="attaches" :files="files"/>
|
||||
<x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
|
||||
<x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
|
||||
<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$t('annotation')" @keydown="onKeydown">
|
||||
<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste"></textarea>
|
||||
<XPostFormAttaches class="attaches" :files="files" @updated="updateMedia" @detach="detachMedia"/>
|
||||
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
|
||||
<footer>
|
||||
<button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><fa :icon="faPhotoVideo"/></button>
|
||||
<button class="_button" @click="poll = !poll" :class="{ active: poll }" v-tooltip="$t('poll')"><fa :icon="faPollH"/></button>
|
||||
<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><fa :icon="faEyeSlash"/></button>
|
||||
<button class="_button" @click="insertMention" v-tooltip="$t('mention')"><fa :icon="faAt"/></button>
|
||||
<button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><fa :icon="faLaughSquint"/></button>
|
||||
<button class="_button" @click="showActions" v-tooltip="$t('plugin')" v-if="$store.state.postFormActions.length > 0"><fa :icon="faPlug"/></button>
|
||||
<button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><Fa :icon="faPhotoVideo"/></button>
|
||||
<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$t('poll')"><Fa :icon="faPollH"/></button>
|
||||
<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><Fa :icon="faEyeSlash"/></button>
|
||||
<button class="_button" @click="insertMention" v-tooltip="$t('mention')"><Fa :icon="faAt"/></button>
|
||||
<button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><Fa :icon="faLaughSquint"/></button>
|
||||
<button class="_button" @click="showActions" v-tooltip="$t('plugin')" v-if="postFormActions.length > 0"><Fa :icon="faPlug"/></button>
|
||||
</footer>
|
||||
<input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { length } from 'stringz';
|
||||
import { toASCII } from 'punycode';
|
||||
import MkVisibilityChooser from './visibility-chooser.vue';
|
||||
import MkUserSelect from './user-select.vue';
|
||||
import XNotePreview from './note-preview.vue';
|
||||
import { parse } from '../../mfm/parse';
|
||||
import { host, url } from '../config';
|
||||
import { host, url } from '@/config';
|
||||
import { erase, unique } from '../../prelude/array';
|
||||
import extractMentions from '../../misc/extract-mentions';
|
||||
import getAcct from '../../misc/acct/render';
|
||||
import { formatTimeString } from '../../misc/format-time-string';
|
||||
import { selectDriveFile } from '../scripts/select-drive-file';
|
||||
import { Autocomplete } from '@/scripts/autocomplete';
|
||||
import { noteVisibilities } from '../../types';
|
||||
import { utils } from '@syuilo/aiscript';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import { notePostInterruptors, postFormActions } from '@/store';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotePreview,
|
||||
XUploader: () => import('./uploader.vue').then(m => m.default),
|
||||
XPostFormAttaches: () => import('./post-form-attaches.vue').then(m => m.default),
|
||||
XPollEditor: () => import('./poll-editor.vue').then(m => m.default)
|
||||
XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
|
||||
XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue'))
|
||||
},
|
||||
|
||||
inject: ['modal'],
|
||||
|
||||
props: {
|
||||
reply: {
|
||||
type: Object,
|
||||
@ -117,19 +117,22 @@ export default Vue.extend({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['posted', 'done', 'esc'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
posting: false,
|
||||
text: '',
|
||||
files: [],
|
||||
uploadings: [],
|
||||
poll: false,
|
||||
pollChoices: [],
|
||||
pollMultiple: false,
|
||||
pollExpiration: [],
|
||||
poll: null,
|
||||
useCw: false,
|
||||
cw: null,
|
||||
localOnly: false,
|
||||
@ -139,7 +142,8 @@ export default Vue.extend({
|
||||
draghover: false,
|
||||
quoteId: null,
|
||||
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
|
||||
faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard, faPlug
|
||||
postFormActions,
|
||||
faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug
|
||||
};
|
||||
},
|
||||
|
||||
@ -190,7 +194,7 @@ export default Vue.extend({
|
||||
return !this.posting &&
|
||||
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
|
||||
(length(this.text.trim()) <= this.max) &&
|
||||
(!this.poll || this.pollChoices.length >= 2);
|
||||
(!this.poll || this.poll.choices.length >= 2);
|
||||
},
|
||||
|
||||
max(): number {
|
||||
@ -246,14 +250,14 @@ export default Vue.extend({
|
||||
if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
|
||||
this.visibility = this.reply.visibility;
|
||||
if (this.reply.visibility === 'specified') {
|
||||
this.$root.api('users/show', {
|
||||
os.api('users/show', {
|
||||
userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId)
|
||||
}).then(users => {
|
||||
this.visibleUsers.push(...users);
|
||||
});
|
||||
|
||||
if (this.reply.userId !== this.$store.state.i.id) {
|
||||
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
|
||||
os.api('users/show', { userId: this.reply.userId }).then(user => {
|
||||
this.visibleUsers.push(user);
|
||||
});
|
||||
}
|
||||
@ -271,15 +275,21 @@ export default Vue.extend({
|
||||
this.cw = this.reply.cw;
|
||||
}
|
||||
|
||||
if (this.autofocus) {
|
||||
this.focus();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: detach when unmount
|
||||
new Autocomplete(this.$refs.text, this, { model: 'text' });
|
||||
new Autocomplete(this.$refs.cw, this, { model: 'cw' });
|
||||
|
||||
this.$nextTick(() => {
|
||||
// 書きかけの投稿を復元
|
||||
if (!this.instant && !this.mention) {
|
||||
if (!this.instant && !this.mention && !this.specified) {
|
||||
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
|
||||
if (draft) {
|
||||
this.text = draft.data.text;
|
||||
@ -289,10 +299,7 @@ export default Vue.extend({
|
||||
this.localOnly = draft.data.localOnly;
|
||||
this.files = (draft.data.files || []).filter(e => e);
|
||||
if (draft.data.poll) {
|
||||
this.poll = true;
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.poll as any).set(draft.data.poll);
|
||||
});
|
||||
this.poll = draft.data.poll;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -305,13 +312,7 @@ export default Vue.extend({
|
||||
this.cw = init.cw;
|
||||
this.useCw = init.cw != null;
|
||||
if (init.poll) {
|
||||
this.poll = true;
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.poll as any).set({
|
||||
choices: init.poll.choices.map(c => c.text),
|
||||
multiple: init.poll.multiple
|
||||
});
|
||||
});
|
||||
this.poll = init.poll;
|
||||
}
|
||||
this.visibility = init.visibility;
|
||||
this.localOnly = init.localOnly;
|
||||
@ -328,11 +329,24 @@ export default Vue.extend({
|
||||
this.$watch('useCw', () => this.saveDraft());
|
||||
this.$watch('cw', () => this.saveDraft());
|
||||
this.$watch('poll', () => this.saveDraft());
|
||||
this.$watch('files', () => this.saveDraft());
|
||||
this.$watch('files', () => this.saveDraft(), { deep: true });
|
||||
this.$watch('visibility', () => this.saveDraft());
|
||||
this.$watch('localOnly', () => this.saveDraft());
|
||||
},
|
||||
|
||||
togglePoll() {
|
||||
if (this.poll) {
|
||||
this.poll = null;
|
||||
} else {
|
||||
this.poll = {
|
||||
choices: ['', ''],
|
||||
multiple: false,
|
||||
expiresAt: null,
|
||||
expiredAfter: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
trimmedLength(text: string) {
|
||||
return length(text.trim());
|
||||
},
|
||||
@ -346,85 +360,50 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
chooseFileFrom(ev) {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
type: 'label',
|
||||
text: this.$t('attachFile'),
|
||||
}, {
|
||||
text: this.$t('upload'),
|
||||
icon: faUpload,
|
||||
action: () => { this.chooseFileFromPc() }
|
||||
}, {
|
||||
text: this.$t('fromDrive'),
|
||||
icon: faCloud,
|
||||
action: () => { this.chooseFileFromDrive() }
|
||||
}, {
|
||||
text: this.$t('fromUrl'),
|
||||
icon: faLink,
|
||||
action: () => { this.chooseFileFromUrl() }
|
||||
}],
|
||||
source: ev.currentTarget || ev.target
|
||||
});
|
||||
},
|
||||
|
||||
chooseFileFromPc() {
|
||||
(this.$refs.file as any).click();
|
||||
},
|
||||
|
||||
chooseFileFromDrive() {
|
||||
selectDriveFile(this.$root, true).then(files => {
|
||||
selectFile(ev.currentTarget || ev.target, this.$t('attachFile'), true).then(files => {
|
||||
for (const file of files) {
|
||||
this.attachMedia(file);
|
||||
this.files.push(file);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
attachMedia(driveFile) {
|
||||
this.files.push(driveFile);
|
||||
},
|
||||
|
||||
detachMedia(id) {
|
||||
this.files = this.files.filter(x => x.id != id);
|
||||
},
|
||||
|
||||
updateMedia(file) {
|
||||
Vue.set(this.files, this.files.findIndex(x => x.id === file.id), file);
|
||||
},
|
||||
|
||||
onChangeFile() {
|
||||
for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
|
||||
this.files[this.files.findIndex(x => x.id === file.id)] = file;
|
||||
},
|
||||
|
||||
upload(file: File, name?: string) {
|
||||
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
|
||||
os.upload(file, this.$store.state.settings.uploadFolder, name).then(res => {
|
||||
this.files.push(res);
|
||||
});
|
||||
},
|
||||
|
||||
onChangeUploadings(uploads) {
|
||||
this.$emit('change-uploadings', uploads);
|
||||
},
|
||||
|
||||
onPollUpdate() {
|
||||
const got = this.$refs.poll.get();
|
||||
this.pollChoices = got.choices;
|
||||
this.pollMultiple = got.multiple;
|
||||
this.pollExpiration = [got.expiration, got.expiresAt || got.expiredAfter];
|
||||
onPollUpdate(poll) {
|
||||
this.poll = poll;
|
||||
this.saveDraft();
|
||||
},
|
||||
|
||||
setVisibility() {
|
||||
async setVisibility() {
|
||||
if (this.channel) {
|
||||
// TODO: information dialog
|
||||
return;
|
||||
}
|
||||
const w = this.$root.new(MkVisibilityChooser, {
|
||||
source: this.$refs.visibilityButton,
|
||||
|
||||
os.popup(await import('./visibility-picker.vue'), {
|
||||
currentVisibility: this.visibility,
|
||||
currentLocalOnly: this.localOnly
|
||||
});
|
||||
w.$once('chosen', ({ visibility, localOnly }) => {
|
||||
currentLocalOnly: this.localOnly,
|
||||
src: this.$refs.visibilityButton
|
||||
}, {
|
||||
changeVisibility: visibility => {
|
||||
this.applyVisibility(visibility);
|
||||
},
|
||||
changeLocalOnly: localOnly => {
|
||||
this.localOnly = localOnly;
|
||||
});
|
||||
}
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
applyVisibility(v: string) {
|
||||
@ -432,8 +411,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
addVisibleUser() {
|
||||
const vm = this.$root.new(MkUserSelect, {});
|
||||
vm.$once('selected', user => {
|
||||
os.selectUser().then(user => {
|
||||
this.visibleUsers.push(user);
|
||||
});
|
||||
},
|
||||
@ -445,12 +423,13 @@ export default Vue.extend({
|
||||
clear() {
|
||||
this.text = '';
|
||||
this.files = [];
|
||||
this.poll = false;
|
||||
this.poll = null;
|
||||
this.quoteId = null;
|
||||
},
|
||||
|
||||
onKeydown(e) {
|
||||
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
|
||||
if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
|
||||
if (e.which === 27) this.$emit('esc');
|
||||
},
|
||||
|
||||
async onPaste(e: ClipboardEvent) {
|
||||
@ -469,7 +448,7 @@ export default Vue.extend({
|
||||
if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
|
||||
e.preventDefault();
|
||||
|
||||
this.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('quoteQuestion'),
|
||||
showCancelButton: true
|
||||
@ -487,7 +466,7 @@ export default Vue.extend({
|
||||
onDragover(e) {
|
||||
if (!e.dataTransfer.items[0]) return;
|
||||
const isFile = e.dataTransfer.items[0].kind == 'file';
|
||||
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
|
||||
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
||||
if (isFile || isDriveFile) {
|
||||
e.preventDefault();
|
||||
this.draghover = true;
|
||||
@ -514,7 +493,7 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = e.dataTransfer.getData('mk_drive_file');
|
||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile != '') {
|
||||
const file = JSON.parse(driveFile);
|
||||
this.files.push(file);
|
||||
@ -537,7 +516,7 @@ export default Vue.extend({
|
||||
visibility: this.visibility,
|
||||
localOnly: this.localOnly,
|
||||
files: this.files,
|
||||
poll: this.poll && this.$refs.poll ? (this.$refs.poll as any).get() : undefined
|
||||
poll: this.poll
|
||||
}
|
||||
};
|
||||
|
||||
@ -559,29 +538,30 @@ export default Vue.extend({
|
||||
replyId: this.reply ? this.reply.id : undefined,
|
||||
renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
|
||||
channelId: this.channel ? this.channel.id : undefined,
|
||||
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
|
||||
poll: this.poll,
|
||||
cw: this.useCw ? this.cw || '' : undefined,
|
||||
localOnly: this.localOnly,
|
||||
visibility: this.visibility,
|
||||
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
|
||||
viaMobile: this.$root.isMobile
|
||||
viaMobile: os.isMobile
|
||||
};
|
||||
|
||||
// plugin
|
||||
if (this.$store.state.notePostInterruptors.length > 0) {
|
||||
for (const interruptor of this.$store.state.notePostInterruptors) {
|
||||
data = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(data))));
|
||||
if (notePostInterruptors.length > 0) {
|
||||
for (const interruptor of notePostInterruptors) {
|
||||
data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
|
||||
}
|
||||
}
|
||||
|
||||
this.posting = true;
|
||||
this.$root.api('notes/create', data).then(() => {
|
||||
os.api('notes/create', data).then(() => {
|
||||
this.clear();
|
||||
this.deleteDraft();
|
||||
this.$emit('posted');
|
||||
}).catch(err => {
|
||||
}).then(() => {
|
||||
this.posting = false;
|
||||
this.$emit('done');
|
||||
});
|
||||
|
||||
if (this.text && this.text != '') {
|
||||
@ -592,28 +572,23 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.$emit('cancel');
|
||||
this.$emit('done');
|
||||
},
|
||||
|
||||
insertMention() {
|
||||
const vm = this.$root.new(MkUserSelect, {});
|
||||
vm.$once('selected', user => {
|
||||
insertTextAtCursor(this.$refs.text, getAcct(user) + ' ');
|
||||
os.selectUser().then(user => {
|
||||
insertTextAtCursor(this.$refs.text, '@' + getAcct(user) + ' ');
|
||||
});
|
||||
},
|
||||
|
||||
async insertEmoji(ev) {
|
||||
const vm = this.$root.new(await import('./emoji-picker.vue').then(m => m.default), {
|
||||
source: ev.currentTarget || ev.target
|
||||
}).$once('chosen', emoji => {
|
||||
os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
|
||||
insertTextAtCursor(this.$refs.text, emoji);
|
||||
vm.close();
|
||||
});
|
||||
},
|
||||
|
||||
showActions(ev) {
|
||||
this.$root.menu({
|
||||
items: this.$store.state.postFormActions.map(action => ({
|
||||
os.modalMenu(postFormActions.map(action => ({
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler({
|
||||
@ -622,9 +597,7 @@ export default Vue.extend({
|
||||
if (key === 'text') { this.text = value; }
|
||||
});
|
||||
}
|
||||
})),
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
})), ev.currentTarget || ev.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -632,26 +605,22 @@ export default Vue.extend({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gafaadew {
|
||||
background: var(--panel);
|
||||
position: relative;
|
||||
|
||||
&.modal {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
> header {
|
||||
z-index: 1000;
|
||||
height: 66px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
> .cancel {
|
||||
padding: 0;
|
||||
font-size: 20px;
|
||||
width: 64px;
|
||||
line-height: 66px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
width: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
@ -662,10 +631,6 @@ export default Vue.extend({
|
||||
> .text-count {
|
||||
opacity: 0.7;
|
||||
line-height: 66px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
> .visibility {
|
||||
@ -678,8 +643,9 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
|
||||
.local-only {
|
||||
margin: 0 8px;
|
||||
> .local-only {
|
||||
margin: 0 0 0 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .submit {
|
||||
@ -690,10 +656,6 @@ export default Vue.extend({
|
||||
vertical-align: bottom;
|
||||
border-radius: 4px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
@ -706,13 +668,6 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
> .form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
|
||||
&.fixed {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
> .preview {
|
||||
padding: 16px;
|
||||
}
|
||||
@ -741,10 +696,6 @@ export default Vue.extend({
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
> .visibleUsers {
|
||||
display: inline;
|
||||
top: -1px;
|
||||
@ -782,10 +733,6 @@ export default Vue.extend({
|
||||
color: var(--fg);
|
||||
font-family: inherit;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
@ -806,31 +753,14 @@ export default Vue.extend({
|
||||
min-width: 100%;
|
||||
min-height: 90px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
&.withCw {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .mk-uploader {
|
||||
margin: 8px 0 0 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
> .file {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> footer {
|
||||
padding: 0 16px 16px 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 0 8px 8px 8px;
|
||||
}
|
||||
|
||||
> button {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
@ -850,5 +780,45 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
> header {
|
||||
height: 50px;
|
||||
|
||||
> .cancel {
|
||||
width: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
> div {
|
||||
> .text-count {
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
> .submit {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .form {
|
||||
> .to-specified {
|
||||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
> .cw,
|
||||
> .text {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
> .text {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
> footer {
|
||||
padding: 0 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :customEmojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
|
||||
<MkEmoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :customEmojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
import { defineComponent } from 'vue';import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
reaction: {
|
||||
type: String,
|
||||
|
@ -1,31 +1,28 @@
|
||||
<template>
|
||||
<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
|
||||
<div class="rdfaahpb">
|
||||
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
|
||||
<div class="rdfaahpb _popup" v-hotkey="keymap">
|
||||
<div class="buttons" ref="buttons" :class="{ showFocus }">
|
||||
<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><x-reaction-icon :reaction="reaction"/></button>
|
||||
<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><XReactionIcon :reaction="reaction"/></button>
|
||||
</div>
|
||||
<input class="text" v-model.trim="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
|
||||
<input class="text" ref="text" v-model.trim="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText">
|
||||
</div>
|
||||
</x-popup>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { emojiRegex } from '../../misc/emoji-regex';
|
||||
import XReactionIcon from './reaction-icon.vue';
|
||||
import XPopup from './popup.vue';
|
||||
import XReactionIcon from '@/components/reaction-icon.vue';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
import { Autocomplete } from '@/scripts/autocomplete';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPopup,
|
||||
XReactionIcon,
|
||||
MkModal,
|
||||
},
|
||||
|
||||
props: {
|
||||
source: {
|
||||
required: true
|
||||
},
|
||||
|
||||
reactions: {
|
||||
required: false
|
||||
},
|
||||
@ -35,7 +32,13 @@ export default Vue.extend({
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
|
||||
src: {
|
||||
required: false
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
@ -70,21 +73,30 @@ export default Vue.extend({
|
||||
|
||||
watch: {
|
||||
focus(i) {
|
||||
this.$refs.buttons.children[i].focus();
|
||||
this.$refs.buttons.children[i].focus({
|
||||
preventScroll: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.focus = 0;
|
||||
});
|
||||
|
||||
// TODO: detach when unmount
|
||||
new Autocomplete(this.$refs.text, this, { model: 'text' });
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$refs.popup.close();
|
||||
this.$emit('done');
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
|
||||
react(reaction) {
|
||||
this.$emit('chosen', reaction);
|
||||
this.$emit('done', reaction);
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
|
||||
reactText() {
|
||||
@ -136,6 +148,7 @@ export default Vue.extend({
|
||||
|
||||
&.showFocus {
|
||||
> button:focus {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:after {
|
||||
|
@ -1,28 +1,36 @@
|
||||
<template>
|
||||
<mk-tooltip :source="source" ref="tooltip">
|
||||
<MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')">
|
||||
<div class="bqxuuuey">
|
||||
<div class="info">
|
||||
<div>{{ reaction.replace('@.', '') }}</div>
|
||||
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon"/>
|
||||
</div>
|
||||
<template v-if="users.length <= 10">
|
||||
<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
|
||||
<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
</b>
|
||||
</template>
|
||||
<template v-if="10 < users.length">
|
||||
<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
|
||||
<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
</b>
|
||||
<span slot="omitted">+{{ count - 10 }}</span>
|
||||
</template>
|
||||
</mk-tooltip>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import MkTooltip from './ui/tooltip.vue';
|
||||
import XReactionIcon from './reaction-icon.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkTooltip
|
||||
MkTooltip,
|
||||
XReactionIcon
|
||||
},
|
||||
props: {
|
||||
reaction: {
|
||||
@ -37,15 +45,30 @@ export default Vue.extend({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
emojis: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
source: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$refs.tooltip.close();
|
||||
}
|
||||
}
|
||||
emits: ['closed'],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bqxuuuey {
|
||||
> .info {
|
||||
padding: 0 0 8px 0;
|
||||
text-align: center;
|
||||
|
||||
> .icon {
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -4,24 +4,25 @@
|
||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||
@click="toggleReaction(reaction)"
|
||||
v-if="count > 0"
|
||||
@touchstart="onMouseover"
|
||||
@touchstart.passive="onMouseover"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
@touchend="onMouseleave"
|
||||
ref="reaction"
|
||||
v-particle="canToggle"
|
||||
>
|
||||
<x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/>
|
||||
<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
|
||||
<span>{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XDetails from './reactions-viewer.details.vue';
|
||||
import XReactionIcon from './reaction-icon.vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import XDetails from '@/components/reactions-viewer.details.vue';
|
||||
import XReactionIcon from '@/components/reaction-icon.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XReactionIcon
|
||||
},
|
||||
@ -45,7 +46,7 @@ export default Vue.extend({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
details: null,
|
||||
close: null,
|
||||
detailsTimeoutId: null,
|
||||
isHovering: false
|
||||
};
|
||||
@ -58,7 +59,7 @@ export default Vue.extend({
|
||||
watch: {
|
||||
count(newCount, oldCount) {
|
||||
if (oldCount < newCount) this.anime();
|
||||
if (this.details != null) this.openDetails();
|
||||
if (this.close != null) this.openDetails();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -70,18 +71,18 @@ export default Vue.extend({
|
||||
|
||||
const oldReaction = this.note.myReaction;
|
||||
if (oldReaction) {
|
||||
this.$root.api('notes/reactions/delete', {
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
if (oldReaction !== this.reaction) {
|
||||
this.$root.api('notes/reactions/create', {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: this.note.id,
|
||||
reaction: this.reaction
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$root.api('notes/reactions/create', {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: this.note.id,
|
||||
reaction: this.reaction
|
||||
});
|
||||
@ -99,7 +100,7 @@ export default Vue.extend({
|
||||
this.closeDetails();
|
||||
},
|
||||
openDetails() {
|
||||
this.$root.api('notes/reactions', {
|
||||
os.api('notes/reactions', {
|
||||
noteId: this.note.id,
|
||||
type: this.reaction,
|
||||
limit: 11
|
||||
@ -110,18 +111,26 @@ export default Vue.extend({
|
||||
|
||||
this.closeDetails();
|
||||
if (!this.isHovering) return;
|
||||
this.details = this.$root.new(XDetails, {
|
||||
|
||||
const showing = ref(true);
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
reaction: this.reaction,
|
||||
emojis: this.note.emojis,
|
||||
users,
|
||||
count: this.count,
|
||||
source: this.$refs.reaction
|
||||
});
|
||||
}, {}, 'closed');
|
||||
|
||||
this.close = () => {
|
||||
showing.value = false;
|
||||
};
|
||||
});
|
||||
},
|
||||
closeDetails() {
|
||||
if (this.details != null) {
|
||||
this.details.close();
|
||||
this.details = null;
|
||||
if (this.close != null) {
|
||||
this.close();
|
||||
this.close = null;
|
||||
}
|
||||
},
|
||||
anime() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user