Merge branch 'develop' into pizzax-indexeddb

This commit is contained in:
tamaina
2022-06-21 07:49:52 +00:00
459 changed files with 11027 additions and 9888 deletions

View File

@@ -1,68 +1,85 @@
module.exports = {
root: true,
env: {
"node": false
'node': false,
},
parser: "vue-eslint-parser",
parser: 'vue-eslint-parser',
parserOptions: {
"parser": "@typescript-eslint/parser",
'parser': '@typescript-eslint/parser',
tsconfigRootDir: __dirname,
//project: ['./tsconfig.json'],
project: ['./tsconfig.json'],
extraFileExtensions: ['.vue'],
},
extends: [
//"../shared/.eslintrc.js",
"plugin:vue/vue3-recommended"
'../shared/.eslintrc.js',
'plugin:vue/vue3-recommended',
],
rules: {
'@typescript-eslint/no-empty-interface': [
'error',
{
'allowSingleExtends': true,
},
],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// data の禁止理由: 抽象的すぎるため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
"id-denylist": ["error", "window", "data", "e"],
'id-denylist': ['error', 'window', 'data', 'e'],
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
"no-shadow": ["warn"],
"vue/attributes-order": ["error", {
"alphabetical": false
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
'alphabetical': false,
}],
"vue/no-use-v-if-with-v-for": ["error", {
"allowUsingIterationVar": false
'vue/no-use-v-if-with-v-for': ['error', {
'allowUsingIterationVar': false,
}],
"vue/no-ref-as-operand": "error",
"vue/no-multi-spaces": ["error", {
"ignoreProperties": false
'vue/no-ref-as-operand': 'error',
'vue/no-multi-spaces': ['error', {
'ignoreProperties': false,
}],
"vue/no-v-html": "error",
"vue/order-in-components": "error",
"vue/html-indent": ["warn", "tab", {
"attribute": 1,
"baseIndent": 0,
"closeBracket": 0,
"alignAttributesVertically": true,
"ignores": []
'vue/no-v-html': 'error',
'vue/order-in-components': 'error',
'vue/html-indent': ['warn', 'tab', {
'attribute': 1,
'baseIndent': 0,
'closeBracket': 0,
'alignAttributesVertically': true,
'ignores': [],
}],
"vue/html-closing-bracket-spacing": ["warn", {
"startTag": "never",
"endTag": "never",
"selfClosingTag": "never"
'vue/html-closing-bracket-spacing': ['warn', {
'startTag': 'never',
'endTag': 'never',
'selfClosingTag': 'never',
}],
"vue/multi-word-component-names": "warn",
"vue/require-v-for-key": "warn",
"vue/no-unused-components": "warn",
"vue/valid-v-for": "warn",
"vue/return-in-computed-property": "warn",
"vue/no-setup-props-destructure": "warn",
"vue/max-attributes-per-line": "off",
"vue/html-self-closing": "off",
"vue/singleline-html-element-content-newline": "off",
'vue/multi-word-component-names': 'warn',
'vue/require-v-for-key': 'warn',
'vue/no-unused-components': 'warn',
'vue/valid-v-for': 'warn',
'vue/return-in-computed-property': 'warn',
'vue/no-setup-props-destructure': 'warn',
'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off',
},
globals: {
"require": false,
"_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
}
}
// Node.js
'module': false,
'require': false,
'__dirname': false,
// Vue
'$$': false,
'$ref': false,
'$computed': false,
// Misskey
'_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,
},
};

View File

@@ -1,5 +1,7 @@
import { Theme } from '../src/scripts/theme';
declare module '@/themes/*.json5' {
export = Theme;
import { Theme } from "@/scripts/theme";
const theme: Theme;
export default theme;
}

View File

@@ -12,13 +12,17 @@
"dependencies": {
"@discordapp/twemoji": "14.0.2",
"@fortawesome/fontawesome-free": "6.1.1",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"@syuilo/aiscript": "0.11.1",
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.37",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
"autosize": "5.0.1",
"autwh": "0.1.0",
"blurhash": "1.1.5",
"broadcast-channel": "4.12.0",
"broadcast-channel": "4.13.0",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
"chart.js": "3.8.0",
"chartjs-adapter-date-fns": "2.0.0",
@@ -26,6 +30,7 @@
"chartjs-plugin-zoom": "1.2.1",
"compare-versions": "4.1.3",
"content-disposition": "0.5.4",
"cropperjs": "2.0.0-beta",
"date-fns": "2.28.0",
"deepcopy": "2.1.0",
"escape-regexp": "0.0.1",
@@ -52,35 +57,30 @@
"random-seed": "0.3.0",
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
"rollup": "2.75.6",
"s-age": "1.1.2",
"sass": "1.52.1",
"sass": "1.52.3",
"seedrandom": "3.0.5",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.140.2",
"three": "0.141.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.4.2",
"tsc-alias": "1.6.7",
"tsc-alias": "1.6.9",
"tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0",
"typescript": "4.7.3",
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vue": "3.2.36",
"vite": "2.9.10",
"vue": "3.2.37",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.15",
"vuedraggable": "4.0.1",
"websocket": "1.0.34",
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.36",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"rollup": "2.74.1",
"typescript": "4.7.2",
"vite": "2.9.9",
"ws": "8.6.0"
"ws": "8.8.0"
},
"devDependencies": {
"@types/escape-regexp": "0.0.1",
@@ -101,13 +101,13 @@
"@types/uuid": "8.3.4",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.26.0",
"@typescript-eslint/parser": "5.26.0",
"eslint": "8.16.0",
"eslint-plugin-vue": "9.0.1",
"@typescript-eslint/eslint-plugin": "5.27.1",
"@typescript-eslint/parser": "5.27.1",
"cross-env": "7.0.3",
"cypress": "9.7.0",
"cypress": "10.0.3",
"eslint": "8.17.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.1.0",
"start-server-and-test": "1.14.0"
}
}

View File

@@ -1,11 +1,11 @@
import { del, get, set } from '@/scripts/idb-proxy';
import { defineAsyncComponent, reactive } from 'vue';
import * as misskey from 'misskey-js';
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
import { i18n } from './i18n';
import { del, get, set } from '@/scripts/idb-proxy';
import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
import { i18n } from './i18n';
// TODO: 他のタブと永続化されたstateを同期
@@ -22,13 +22,7 @@ export async function signout() {
waiting();
localStorage.removeItem('account');
//#region Remove account
const accounts = await getAccounts();
accounts.splice(accounts.findIndex(x => x.id === $i.id), 1);
if (accounts.length > 0) await set('accounts', accounts);
else await del('accounts');
//#endregion
await removeAccount($i.id);
//#region Remove service worker registration
try {
@@ -55,7 +49,7 @@ export async function signout() {
} catch (err) {}
//#endregion
document.cookie = `igi=; path=/`;
document.cookie = 'igi=; path=/';
if (accounts.length > 0) login(accounts[0].token);
else unisonReload('/');
@@ -72,14 +66,22 @@ export async function addAccount(id: Account['id'], token: Account['token']) {
}
}
export async function removeAccount(id: Account['id']) {
const accounts = await getAccounts();
accounts.splice(accounts.findIndex(x => x.id === id), 1);
if (accounts.length > 0) await set('accounts', accounts);
else await del('accounts');
}
function fetchAccount(token: string): Promise<Account> {
return new Promise((done, fail) => {
// Fetch user
fetch(`${apiUrl}/i`, {
method: 'POST',
body: JSON.stringify({
i: token
})
i: token,
}),
})
.then(res => res.json())
.then(res => {
@@ -216,13 +218,13 @@ export async function openAccountMenu(opts: {
type: 'link',
icon: 'fas fa-users',
text: i18n.ts.manageAccounts,
to: `/settings/accounts`,
to: '/settings/accounts',
}]], ev.currentTarget ?? ev.target, {
align: 'left'
align: 'left',
});
} else {
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
align: 'left'
align: 'left',
});
}
}

View File

@@ -1,13 +1,19 @@
<template>
<div class="bcekxzvu _card _gap">
<div class="_content target">
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
<MkA v-user-preview="report.targetUserId" class="info" :to="userPage(report.targetUser)">
<MkUserName class="name" :user="report.targetUser"/>
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
<div class="bcekxzvu _gap _panel">
<div class="target">
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`" :behavior="'window'">
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true" :disable-link="true"/>
<div class="names">
<MkUserName class="name" :user="report.targetUser"/>
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
</div>
</MkA>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.registeredDate }}</template>
<template #value>{{ new Date(report.targetUser.createdAt).toLocaleString() }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
</MkKeyValue>
</div>
<div class="_content">
<div class="detail">
<div>
<Mfm :text="report.comment"/>
</div>
@@ -18,85 +24,84 @@
<MkAcct :user="report.assignee"/>
</div>
<div><MkTime :time="report.createdAt"/></div>
</div>
<div class="_footer">
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
{{ $ts.forwardReport }}
<template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
</MkSwitch>
<MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
<div class="action">
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
{{ $ts.forwardReport }}
<template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
</MkSwitch>
<MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/form/switch.vue';
import MkKeyValue from '@/components/key-value.vue';
import { acct, userPage } from '@/filters/user';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
},
const props = defineProps<{
report: any;
}>();
props: {
report: {
type: Object,
required: true,
}
},
const emit = defineEmits<{
(ev: 'resolved', reportId: string): void;
}>();
emits: ['resolved'],
let forward = $ref(props.report.forwarded);
data() {
return {
forward: this.report.forwarded,
};
},
methods: {
acct,
userPage,
resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', {
forward: this.forward,
reportId: this.report.id,
}).then(() => {
this.$emit('resolved', this.report.id);
});
}
}
});
function resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', {
forward: forward,
reportId: props.report.id,
}).then(() => {
emit('resolved', props.report.id);
});
}
</script>
<style lang="scss" scoped>
.bcekxzvu {
display: flex;
> .target {
display: flex;
width: 100%;
width: 35%;
box-sizing: border-box;
text-align: left;
align-items: center;
> .avatar {
width: 42px;
height: 42px;
}
padding: 24px;
border-right: solid 1px var(--divider);
> .info {
margin-left: 0.3em;
padding: 0 8px;
flex: 1;
display: flex;
box-sizing: border-box;
align-items: center;
padding: 14px;
border-radius: 8px;
background-image: linear-gradient(45deg, rgb(255 196 0 / 15%) 16.67%, transparent 16.67%, transparent 50%, rgb(255 196 0 / 15%) 50%, rgb(255 196 0 / 15%) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
> .name {
font-weight: bold;
> .avatar {
width: 42px;
height: 42px;
}
> .names {
margin-left: 0.3em;
padding: 0 8px;
flex: 1;
> .name {
font-weight: bold;
}
}
}
}
> .detail {
flex: 1;
padding: 24px;
}
}
</style>

View File

@@ -35,6 +35,7 @@
<script lang="ts">
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import contains from '@/scripts/contains';
import { char2filePath } from '@/scripts/twemoji-base';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { acct } from '@/filters/user';
import * as os from '@/os';
@@ -42,7 +43,6 @@ import { MFM_TAGS } from '@/scripts/mfm-tags';
import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist';
import { instance } from '@/instance';
import { twemojiSvgBase } from '@/scripts/twemoji-base';
import { i18n } from '@/i18n';
type EmojiDef = {
@@ -55,16 +55,10 @@ type EmojiDef = {
const lib = emojilist.filter(x => x.category !== 'flags');
const char2file = (char: string) => {
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
return codes.filter(x => x && x.length).join('-');
};
const emjdb: EmojiDef[] = lib.map(x => ({
emoji: x.char,
name: x.name,
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
url: char2filePath(x.char),
}));
for (const x of lib) {
@@ -74,7 +68,7 @@ for (const x of lib) {
emoji: x.char,
name: k,
aliasOf: x.name,
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
url: char2filePath(x.char),
});
}
}

View File

@@ -27,8 +27,7 @@ type CaptchaContainer = {
};
declare global {
interface Window extends CaptchaContainer {
}
interface Window extends CaptchaContainer { }
}
const props = defineProps<{

View File

@@ -13,7 +13,7 @@
id-denylist violation when setting it. This is causing about 60+ lint issues.
As this is part of Chart.js's API it makes sense to disable the check here.
*/
import { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue';
import { onMounted, ref, watch, PropType, onUnmounted } from 'vue';
import {
Chart,
ArcElement,
@@ -53,7 +53,7 @@ const props = defineProps({
limit: {
type: Number,
required: false,
default: 90
default: 90,
},
span: {
type: String as PropType<'hour' | 'day'>,
@@ -62,22 +62,22 @@ const props = defineProps({
detailed: {
type: Boolean,
required: false,
default: false
default: false,
},
stacked: {
type: Boolean,
required: false,
default: false
default: false,
},
bar: {
type: Boolean,
required: false,
default: false
default: false,
},
aspectRatio: {
type: Number,
required: false,
default: null
default: null,
},
});
@@ -156,7 +156,7 @@ const getDate = (ago: number) => {
const format = (arr) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v
y: v,
}));
};
@@ -343,7 +343,7 @@ const render = () => {
min: 'original',
max: 'original',
},
}
},
} : undefined,
//gradient,
},
@@ -367,8 +367,8 @@ const render = () => {
ctx.stroke();
ctx.restore();
}
}
}]
},
}],
});
};
@@ -433,18 +433,18 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => {
name: 'In',
type: 'area',
color: '#008FFB',
data: format(raw.inboxReceived)
data: format(raw.inboxReceived),
}, {
name: 'Out (succ)',
type: 'area',
color: '#00E396',
data: format(raw.deliverSucceeded)
data: format(raw.deliverSucceeded),
}, {
name: 'Out (fail)',
type: 'area',
color: '#FEB019',
data: format(raw.deliverFailed)
}]
data: format(raw.deliverFailed),
}],
};
};
@@ -456,7 +456,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'line',
data: format(type === 'combined'
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
: sum(raw[type].inc, negate(raw[type].dec))
: sum(raw[type].inc, negate(raw[type].dec)),
),
color: '#888888',
}, {
@@ -464,7 +464,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
: raw[type].diffs.renote
: raw[type].diffs.renote,
),
color: colors.green,
}, {
@@ -472,7 +472,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
: raw[type].diffs.reply
: raw[type].diffs.reply,
),
color: colors.yellow,
}, {
@@ -480,7 +480,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
: raw[type].diffs.normal
: raw[type].diffs.normal,
),
color: colors.blue,
}, {
@@ -488,7 +488,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
type: 'area',
data: format(type === 'combined'
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
: raw[type].diffs.withFile
: raw[type].diffs.withFile,
),
color: colors.purple,
}],
@@ -522,21 +522,21 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
type: 'line',
data: format(total
? sum(raw.local.total, raw.remote.total)
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)),
),
}, {
name: 'Local',
type: 'area',
data: format(total
? raw.local.total
: sum(raw.local.inc, negate(raw.local.dec))
: sum(raw.local.inc, negate(raw.local.dec)),
),
}, {
name: 'Remote',
type: 'area',
data: format(total
? raw.remote.total
: sum(raw.remote.inc, negate(raw.remote.dec))
: sum(raw.remote.inc, negate(raw.remote.dec)),
),
}],
};
@@ -607,8 +607,8 @@ const fetchDriveChart = async (): Promise<typeof chartData> => {
raw.local.incSize,
negate(raw.local.decSize),
raw.remote.incSize,
negate(raw.remote.decSize)
)
negate(raw.remote.decSize),
),
),
}, {
name: 'Local +',
@@ -642,8 +642,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
raw.local.incCount,
negate(raw.local.decCount),
raw.remote.incCount,
negate(raw.remote.decCount)
)
negate(raw.remote.decCount),
),
),
}, {
name: 'Local +',
@@ -672,18 +672,18 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
name: 'In',
type: 'area',
color: '#008FFB',
data: format(raw.requests.received)
data: format(raw.requests.received),
}, {
name: 'Out (succ)',
type: 'area',
color: '#00E396',
data: format(raw.requests.succeeded)
data: format(raw.requests.succeeded),
}, {
name: 'Out (fail)',
type: 'area',
color: '#FEB019',
data: format(raw.requests.failed)
}]
data: format(raw.requests.failed),
}],
};
};
@@ -696,9 +696,9 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
color: '#008FFB',
data: format(total
? raw.users.total
: sum(raw.users.inc, negate(raw.users.dec))
)
}]
: sum(raw.users.inc, negate(raw.users.dec)),
),
}],
};
};
@@ -711,9 +711,9 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
color: '#008FFB',
data: format(total
? raw.notes.total
: sum(raw.notes.inc, negate(raw.notes.dec))
)
}]
: sum(raw.notes.inc, negate(raw.notes.dec)),
),
}],
};
};
@@ -726,17 +726,17 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
color: '#008FFB',
data: format(total
? raw.following.total
: sum(raw.following.inc, negate(raw.following.dec))
)
: sum(raw.following.inc, negate(raw.following.dec)),
),
}, {
name: 'Followers',
type: 'area',
color: '#00E396',
data: format(total
? raw.followers.total
: sum(raw.followers.inc, negate(raw.followers.dec))
)
}]
: sum(raw.followers.inc, negate(raw.followers.dec)),
),
}],
};
};
@@ -750,9 +750,9 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
color: '#008FFB',
data: format(total
? raw.drive.totalUsage
: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
)
}]
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
),
}],
};
};
@@ -765,9 +765,9 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
color: '#008FFB',
data: format(total
? raw.drive.totalFiles
: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
)
}]
: sum(raw.drive.incFiles, negate(raw.drive.decFiles)),
),
}],
};
};

View File

@@ -0,0 +1,175 @@
<template>
<XModalWindow
ref="dialogEl"
:width="800"
:height="500"
:scroll="false"
:with-ok-button="true"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
>
<template #header>{{ $ts.cropImage }}</template>
<template #default="{ width, height }">
<div class="mk-cropper-dialog" :style="`--vw: ${width}px; --vh: ${height}px;`">
<Transition name="fade">
<div v-if="loading" class="loading">
<MkLoading/>
</div>
</Transition>
<div class="container">
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
</div>
</div>
</template>
</XModalWindow>
</template>
<script lang="ts" setup>
import { nextTick, onMounted } from 'vue';
import * as misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
import XModalWindow from '@/components/ui/modal-window.vue';
import * as os from '@/os';
import { $i } from '@/account';
import { defaultStore } from '@/store';
import { apiUrl, url } from '@/config';
import { query } from '@/scripts/url';
const emit = defineEmits<{
(ev: 'ok', cropped: misskey.entities.DriveFile): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const props = defineProps<{
file: misskey.entities.DriveFile;
aspectRatio: number;
}>();
const imgUrl = `${url}/proxy/image.webp?${query({
url: props.file.url,
})}`;
let dialogEl = $ref<InstanceType<typeof XModalWindow>>();
let imgEl = $ref<HTMLImageElement>();
let cropper: Cropper | null = null;
let loading = $ref(true);
const ok = async () => {
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
croppedCanvas.toBlob(blob => {
const formData = new FormData();
formData.append('file', blob);
formData.append('i', $i.token);
if (defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
}
fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(f => {
res(f);
});
});
});
os.promiseDialog(promise);
const f = await promise;
emit('ok', f);
dialogEl.close();
};
const cancel = () => {
emit('cancel');
dialogEl.close();
};
const onImageLoad = () => {
loading = false;
if (cropper) {
cropper.getCropperImage()!.$center('contain');
cropper.getCropperSelection()!.$center();
}
};
onMounted(() => {
cropper = new Cropper(imgEl, {
});
const computedStyle = getComputedStyle(document.documentElement);
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
selection.aspectRatio = props.aspectRatio;
selection.initialAspectRatio = props.aspectRatio;
selection.outlined = true;
window.setTimeout(() => {
cropper.getCropperImage()!.$center('contain');
selection.$center();
}, 100);
// モーダルオープンアニメーションが終わったあとで再度調整
window.setTimeout(() => {
cropper.getCropperImage()!.$center('contain');
selection.$center();
}, 500);
});
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.mk-cropper-dialog {
display: flex;
flex-direction: column;
width: var(--vw);
height: var(--vh);
position: relative;
> .loading {
position: absolute;
z-index: 10;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
-webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px));
background: rgba(0, 0, 0, 0.5);
}
> .container {
flex: 1;
width: 100%;
height: 100%;
> ::v-deep(cropper-canvas) {
width: 100%;
height: 100%;
> cropper-selection > cropper-handle[action="move"] {
background: transparent;
}
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div ref="thumbnail" class="zdjebgpv">
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
<i v-else-if="is === 'image'" class="fas fa-file-image icon"></i>
<i v-else-if="is === 'video'" class="fas fa-file-video icon"></i>
<i v-else-if="is === 'audio' || is === 'midi'" class="fas fa-music icon"></i>
@@ -33,16 +33,16 @@ const is = computed(() => {
if (props.file.type.endsWith('/pdf')) return 'pdf';
if (props.file.type.startsWith('text/')) return 'textfile';
if ([
"application/zip",
"application/x-cpio",
"application/x-bzip",
"application/x-bzip2",
"application/java-archive",
"application/x-rar-compressed",
"application/x-tar",
"application/gzip",
"application/x-7z-compressed"
].some(archiveType => archiveType === props.file.type)) return 'archive';
'application/zip',
'application/x-cpio',
'application/x-bzip',
'application/x-bzip2',
'application/java-archive',
'application/x-rar-compressed',
'application/x-tar',
'application/gzip',
'application/x-7z-compressed',
].some(archiveType => archiveType === props.file.type)) return 'archive';
return 'unknown';
});
@@ -57,7 +57,7 @@ const isThumbnailAvailable = computed(() => {
.zdjebgpv {
position: relative;
display: flex;
background: #e1e1e1;
background: var(--panel);
border-radius: 8px;
overflow: clip;

View File

@@ -71,7 +71,7 @@ function onMouseover() {
}
function onMouseout() {
hover.value = false
hover.value = false;
}
function onDragover(ev: DragEvent) {
@@ -204,7 +204,7 @@ function deleteFolder() {
defaultStore.set('uploadFolder', null);
}
}).catch(err => {
switch(err.id) {
switch (err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',

View File

@@ -143,7 +143,7 @@ const fetching = ref(true);
const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
)
);
watch(folder, () => emit('cd', folder.value));
@@ -332,7 +332,7 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
// 削除時に親フォルダに移動
move(folderToDelete.parentId);
}).catch(err => {
switch(err.id) {
switch (err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',
@@ -607,7 +607,7 @@ function onContextmenu(ev: MouseEvent) {
onMounted(() => {
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
nextTick(() => {
ilFilesObserver.observe(loadMoreFiles.value?.$el)
ilFilesObserver.observe(loadMoreFiles.value?.$el);
});
}
@@ -628,7 +628,7 @@ onMounted(() => {
onActivated(() => {
if (defaultStore.state.enableInfiniteScroll) {
nextTick(() => {
ilFilesObserver.observe(loadMoreFiles.value?.$el)
ilFilesObserver.observe(loadMoreFiles.value?.$el);
});
}
});

View File

@@ -1,6 +1,6 @@
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0">

View File

@@ -28,7 +28,7 @@
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { onBeforeUnmount, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
@@ -43,32 +43,30 @@ const props = withDefaults(defineProps<{
large: false,
});
const isFollowing = ref(props.user.isFollowing);
const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
const wait = ref(false);
let isFollowing = $ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
let wait = $ref(false);
const connection = stream.useChannel('main');
if (props.user.isFollowing == null) {
os.api('users/show', {
userId: props.user.id
}).then(u => {
isFollowing.value = u.isFollowing;
hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
});
})
.then(onFollowChange);
}
function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id === props.user.id) {
isFollowing.value = user.isFollowing;
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
isFollowing = user.isFollowing;
hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
}
async function onClick() {
wait.value = true;
wait = true;
try {
if (isFollowing.value) {
if (isFollowing) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
@@ -80,26 +78,22 @@ async function onClick() {
userId: props.user.id
});
} else {
if (hasPendingFollowRequestFromYou.value) {
if (hasPendingFollowRequestFromYou) {
await os.api('following/requests/cancel', {
userId: props.user.id
});
} else if (props.user.isLocked) {
await os.api('following/create', {
userId: props.user.id
});
hasPendingFollowRequestFromYou.value = true;
hasPendingFollowRequestFromYou = false;
} else {
await os.api('following/create', {
userId: props.user.id
});
hasPendingFollowRequestFromYou.value = true;
hasPendingFollowRequestFromYou = true;
}
}
} catch (err) {
console.error(err);
} finally {
wait.value = false;
wait = false;
}
}

View File

@@ -9,13 +9,13 @@
<i v-else class="fas fa-angle-down icon"></i>
</span>
</div>
<keep-alive>
<KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22">
<slot></slot>
</MkSpacer>
</div>
</keep-alive>
</KeepAlive>
</div>
</template>
@@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
defaultOpen: boolean;
}>(), {
defaultOpen: false,
})
});
let opened = $ref(props.defaultOpen);
let openedAtLeastOnce = $ref(props.defaultOpen);

View File

@@ -14,7 +14,7 @@ export default defineComponent({
data() {
return {
value: this.modelValue,
}
};
},
watch: {
value() {

View File

@@ -1,7 +1,7 @@
<template>
<div class="timctyfi" :class="{ disabled }">
<div class="label"><slot name="label"></slot></div>
<div v-panel class="body">
<div v-adaptive-border class="body">
<div ref="containerEl" class="container">
<div class="track">
<div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
@@ -192,6 +192,8 @@ export default defineComponent({
> .body {
padding: 12px;
background: var(--panel);
border: solid 1px var(--panel);
border-radius: 6px;
> .container {

View File

@@ -5,13 +5,13 @@
</template>
<script lang="ts" setup>
import { inject } from 'vue';
import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { router } from '@/router';
import { url } from '@/config';
import { popout as popout_ } from '@/scripts/popout';
import { i18n } from '@/i18n';
import { MisskeyNavigator } from '@/scripts/navigate';
import { useRouter } from '@/router';
const props = withDefaults(defineProps<{
to: string;
@@ -22,15 +22,16 @@ const props = withDefaults(defineProps<{
behavior: null,
});
const mkNav = new MisskeyNavigator();
const router = useRouter();
const active = $computed(() => {
if (props.activeClass == null) return false;
const resolved = router.resolve(props.to);
if (resolved.path === router.currentRoute.value.path) return true;
if (resolved.name == null) return false;
if (resolved == null) return false;
if (resolved.route.path === router.currentRoute.value.path) return true;
if (resolved.route.name == null) return false;
if (router.currentRoute.value.name == null) return false;
return resolved.name === router.currentRoute.value.name;
return resolved.route.name === router.currentRoute.value.name;
});
function onContextmenu(ev) {
@@ -44,31 +45,25 @@ function onContextmenu(ev) {
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(props.to);
}
}, mkNav.sideViewHook ? {
icon: 'fas fa-columns',
text: i18n.ts.openInSideView,
action: () => {
if (mkNav.sideViewHook) mkNav.sideViewHook(props.to);
}
} : undefined, {
},
}, {
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: () => {
router.push(props.to);
}
},
}, null, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(props.to, '_blank');
}
},
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(`${url}${props.to}`);
}
},
}], ev);
}
@@ -98,6 +93,6 @@ function nav() {
}
}
mkNav.push(props.to);
router.push(props.to);
}
</script>

View File

@@ -1,4 +1,4 @@
<template>
char2filePath<template>
<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/>
<span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
@@ -8,7 +8,7 @@
<script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { twemojiSvgBase } from '@/scripts/twemoji-base';
import { char2filePath } from '@/scripts/twemoji-base';
import { defaultStore } from '@/store';
import { instance } from '@/instance';
@@ -45,10 +45,7 @@ export default defineComponent({
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
const url = computed(() => {
if (char.value) {
let codes = Array.from(char.value).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
return `${twemojiSvgBase}/${codes.join('-')}.svg`;
return char2filePath(char.value);
} else {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url)

View File

@@ -1,361 +0,0 @@
<template>
<div ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
<template v-if="info">
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
<MkAvatar v-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
<i v-else-if="info.icon" class="icon" :class="info.icon"></i>
<div class="title">
<MkUserName v-if="info.userName" :user="info.userName" :nowrap="true" class="title"/>
<div v-else-if="info.title" class="title">{{ info.title }}</div>
<div v-if="!narrow && info.subtitle" class="subtitle">
{{ info.subtitle }}
</div>
<div v-if="narrow && hasTabs" class="subtitle activeTab">
{{ info.tabs.find(tab => tab.active)?.title }}
<i class="chevron fas fa-chevron-down"></i>
</div>
</div>
</div>
<div v-if="!narrow || hideTitle" class="tabs">
<button v-for="tab in info.tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
</div>
</template>
<div class="buttons right">
<template v-if="info && info.actions && !narrow">
<template v-for="action in info.actions">
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</template>
<button v-if="shouldShowMenu" v-tooltip="$ts.menu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag"><i class="fas fa-ellipsis-h"></i></button>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os';
import { url } from '@/config';
import { scrollToTop } from '@/scripts/scroll';
import MkButton from '@/components/ui/button.vue';
import { i18n } from '@/i18n';
import { globalEvents } from '@/events';
export default defineComponent({
components: {
MkButton
},
props: {
info: {
type: Object as PropType<{
actions?: {}[];
tabs?: {}[];
}>,
required: true
},
menu: {
required: false
},
thin: {
required: false,
default: false
},
},
setup(props) {
const el = ref<HTMLElement>(null);
const bg = ref(null);
const narrow = ref(false);
const height = ref(0);
const hasTabs = computed(() => {
return props.info.tabs && props.info.tabs.length > 0;
});
const shouldShowMenu = computed(() => {
if (props.info == null) return false;
if (props.info.actions != null && narrow.value) return true;
if (props.info.menu != null) return true;
if (props.info.share != null) return true;
if (props.menu != null) return true;
return false;
});
const share = () => {
navigator.share({
url: url + props.info.path,
...props.info.share,
});
};
const showMenu = (ev: MouseEvent) => {
let menu = props.info.menu ? props.info.menu() : [];
if (narrow.value && props.info.actions) {
menu = [...props.info.actions.map(x => ({
text: x.text,
icon: x.icon,
action: x.handler
})), menu.length > 0 ? null : undefined, ...menu];
}
if (props.info.share) {
if (menu.length > 0) menu.push(null);
menu.push({
text: i18n.ts.share,
icon: 'fas fa-share-alt',
action: share
});
}
if (props.menu) {
if (menu.length > 0) menu.push(null);
menu = menu.concat(props.menu);
}
popupMenu(menu, ev.currentTarget ?? ev.target);
};
const showTabsPopup = (ev: MouseEvent) => {
if (!hasTabs.value) return;
if (!narrow.value) return;
ev.preventDefault();
ev.stopPropagation();
const menu = props.info.tabs.map(tab => ({
text: tab.title,
icon: tab.icon,
action: tab.onClick,
}));
popupMenu(menu, ev.currentTarget ?? ev.target);
};
const preventDrag = (ev: TouchEvent) => {
ev.stopPropagation();
};
const onClick = () => {
scrollToTop(el.value, { behavior: 'smooth' });
};
const calcBg = () => {
const rawBg = props.info?.bg || 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
};
onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
onUnmounted(() => {
globalEvents.off('themeChanged', calcBg);
});
if (el.value.parentElement) {
narrow.value = el.value.parentElement.offsetWidth < 500;
const ro = new ResizeObserver((entries, observer) => {
if (el.value) {
narrow.value = el.value.parentElement.offsetWidth < 500;
}
});
ro.observe(el.value.parentElement);
onUnmounted(() => {
ro.disconnect();
});
}
});
return {
el,
bg,
narrow,
height,
hasTabs,
shouldShowMenu,
share,
showMenu,
showTabsPopup,
preventDrag,
onClick,
hideTitle: inject('shouldOmitHeaderTitle', false),
thin_: props.thin || inject('shouldHeaderThin', false)
};
},
});
</script>
<style lang="scss" scoped>
.fdidabkb {
--height: 60px;
display: flex;
position: sticky;
top: var(--stickyTop, 0);
z-index: 1000;
width: 100%;
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
border-bottom: solid 0.5px var(--divider);
&.thin {
--height: 50px;
> .buttons {
> .button {
font-size: 0.9em;
}
}
}
&.slim {
text-align: center;
> .titleContainer {
flex: 1;
margin: 0 auto;
margin-left: var(--height);
> *:first-child {
margin-left: auto;
}
> *:last-child {
margin-right: auto;
}
}
}
> .buttons {
--margin: 8px;
display: flex;
align-items: center;
height: var(--height);
margin: 0 var(--margin);
&.right {
margin-left: auto;
}
&:empty {
width: var(--height);
}
> .button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&.highlighted {
color: var(--accent);
}
}
> .fullButton {
& + .fullButton {
margin-left: 12px;
}
}
}
> .titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
> .subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
margin-left: 6px;
}
}
}
}
}
> .tabs {
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
> .tab {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
&:after {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
width: 100%;
height: 3px;
background: var(--accent);
}
}
> .icon + .title {
margin-left: 8px;
}
}
}
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<div class="yxspomdl" :class="{ inline, colored, mini }">
<div class="container">
<svg class="spinner bg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<div :class="[$style.root, { [$style.inline]: inline, [$style.colored]: colored, [$style.mini]: mini }]">
<div :class="$style.container">
<svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<svg :class="[$style.spinner, $style.fg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
@@ -16,7 +16,9 @@
</template>
<script lang="ts" setup>
import { } from 'vue';
import { useCssModule } from 'vue';
useCssModule();
const props = withDefaults(defineProps<{
inline?: boolean;
@@ -29,7 +31,7 @@ const props = withDefaults(defineProps<{
});
</script>
<style lang="scss" scoped>
<style lang="scss" module>
@keyframes spinner {
0% {
transform: rotate(0deg);
@@ -39,7 +41,7 @@ const props = withDefaults(defineProps<{
}
}
.yxspomdl {
.root {
padding: 32px;
text-align: center;
cursor: wait;
@@ -60,33 +62,33 @@ const props = withDefaults(defineProps<{
padding: 16px;
--size: 32px;
}
}
> .container {
position: relative;
width: var(--size);
height: var(--size);
margin: 0 auto;
.container {
position: relative;
width: var(--size);
height: var(--size);
margin: 0 auto;
}
> .spinner {
position: absolute;
top: 0;
left: 0;
width: var(--size);
height: var(--size);
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
.spinner {
position: absolute;
top: 0;
left: 0;
width: var(--size);
height: var(--size);
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
> .bg {
opacity: 0.275;
}
.bg {
opacity: 0.275;
}
> .fg {
animation: spinner 0.5s linear infinite;
}
}
.fg {
animation: spinner 0.5s linear infinite;
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
<template v-if="metadata">
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
<i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i>
<div class="title">
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/>
<div v-else-if="metadata.title" class="title">{{ metadata.title }}</div>
<div v-if="!narrow && metadata.subtitle" class="subtitle">
{{ metadata.subtitle }}
</div>
<div v-if="narrow && hasTabs" class="subtitle activeTab">
{{ tabs.find(tab => tab.active)?.title }}
<i class="chevron fas fa-chevron-down"></i>
</div>
</div>
</div>
<div v-if="!narrow || hideTitle" class="tabs">
<button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
</div>
</template>
<div class="buttons right">
<template v-for="action in actions">
<button v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, inject } from 'vue';
import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os';
import { scrollToTop } from '@/scripts/scroll';
import { i18n } from '@/i18n';
import { globalEvents } from '@/events';
import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{
tabs?: {
title: string;
active: boolean;
icon?: string;
iconOnly?: boolean;
onClick: () => void;
}[];
actions?: {
text: string;
icon: string;
handler: (ev: MouseEvent) => void;
}[];
thin?: boolean;
}>();
const metadata = injectPageMetadata();
const hideTitle = inject('shouldOmitHeaderTitle', false);
const thin_ = props.thin || inject('shouldHeaderThin', false);
const el = $ref<HTMLElement | null>(null);
const bg = ref(null);
let narrow = $ref(false);
const height = ref(0);
const hasTabs = $computed(() => props.tabs && props.tabs.length > 0);
const hasActions = $computed(() => props.actions && props.actions.length > 0);
const show = $computed(() => {
return !hideTitle || hasTabs || hasActions;
});
const showTabsPopup = (ev: MouseEvent) => {
if (!hasTabs) return;
if (!narrow) return;
ev.preventDefault();
ev.stopPropagation();
const menu = props.tabs.map(tab => ({
text: tab.title,
icon: tab.icon,
action: tab.onClick,
}));
popupMenu(menu, ev.currentTarget ?? ev.target);
};
const preventDrag = (ev: TouchEvent) => {
ev.stopPropagation();
};
const onClick = () => {
scrollToTop(el, { behavior: 'smooth' });
};
const calcBg = () => {
const rawBg = metadata?.bg || 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
};
let ro: ResizeObserver | null;
onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
if (el && el.parentElement) {
narrow = el.parentElement.offsetWidth < 500;
ro = new ResizeObserver((entries, observer) => {
if (el.parentElement) {
narrow = el.parentElement.offsetWidth < 500;
}
});
ro.observe(el.parentElement);
}
});
onUnmounted(() => {
globalEvents.off('themeChanged', calcBg);
if (ro) ro.disconnect();
});
</script>
<style lang="scss" scoped>
.fdidabkb {
--height: 60px;
display: flex;
position: sticky;
top: var(--stickyTop, 0);
z-index: 1000;
width: 100%;
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
border-bottom: solid 0.5px var(--divider);
&.thin {
--height: 50px;
> .buttons {
> .button {
font-size: 0.9em;
}
}
}
&.slim {
text-align: center;
> .titleContainer {
flex: 1;
margin: 0 auto;
margin-left: var(--height);
> *:first-child {
margin-left: auto;
}
> *:last-child {
margin-right: auto;
}
}
}
> .buttons {
--margin: 8px;
display: flex;
align-items: center;
height: var(--height);
margin: 0 var(--margin);
&.right {
margin-left: auto;
}
&:empty {
width: var(--height);
}
> .button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&.highlighted {
color: var(--accent);
}
}
> .fullButton {
& + .fullButton {
margin-left: 12px;
}
}
}
> .titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
> .subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
margin-left: 6px;
}
}
}
}
}
> .tabs {
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
> .tab {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
&:after {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
width: 100%;
height: 3px;
background: var(--accent);
}
}
> .icon + .title {
margin-left: 8px;
}
}
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<KeepAlive max="5">
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
</KeepAlive>
</template>
<script lang="ts" setup>
import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue';
import { Router } from '@/nirax';
const props = defineProps<{
router?: Router;
}>();
const emit = defineEmits<{
}>();
const router = props.router ?? inject('router');
if (router == null) {
throw new Error('no router provided');
}
let currentPageComponent = $ref(router.getCurrentComponent());
let currentPageProps = $ref(router.getCurrentProps());
let key = $ref(router.getCurrentKey());
function onChange({ route, props: newProps, key: newKey }) {
currentPageComponent = route.component;
currentPageProps = newProps;
key = newKey;
}
router.addListener('change', onChange);
onUnmounted(() => {
router.removeListener('change', onChange);
});
</script>

View File

@@ -1,71 +1,63 @@
<template>
<div ref="rootEl">
<slot name="header"></slot>
<div ref="bodyEl">
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
export default defineComponent({
props: {
autoSticky: {
type: Boolean,
required: false,
default: false,
},
},
const props = withDefaults(defineProps<{
autoSticky?: boolean;
}>(), {
autoSticky: false,
});
setup(props, context) {
const rootEl = ref<HTMLElement>(null);
const bodyEl = ref<HTMLElement>(null);
const rootEl = $ref<HTMLElement>();
const bodyEl = $ref<HTMLElement>();
const calc = () => {
const currentStickyTop = getComputedStyle(rootEl.value).getPropertyValue('--stickyTop') || '0px';
let headerHeight = $ref<string | undefined>();
const header = rootEl.value.children[0];
if (header === bodyEl.value) {
bodyEl.value.style.setProperty('--stickyTop', currentStickyTop);
} else {
bodyEl.value.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
const calc = () => {
const currentStickyTop = getComputedStyle(rootEl).getPropertyValue('--stickyTop') || '0px';
if (props.autoSticky) {
header.style.setProperty('--stickyTop', currentStickyTop);
header.style.position = 'sticky';
header.style.top = 'var(--stickyTop)';
header.style.zIndex = '1';
}
}
};
const header = rootEl.children[0] as HTMLElement;
if (header === bodyEl) {
bodyEl.style.setProperty('--stickyTop', currentStickyTop);
} else {
bodyEl.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
headerHeight = header.offsetHeight.toString();
onMounted(() => {
calc();
if (props.autoSticky) {
header.style.setProperty('--stickyTop', currentStickyTop);
header.style.position = 'sticky';
header.style.top = 'var(--stickyTop)';
header.style.zIndex = '1';
}
}
};
const observer = new MutationObserver(() => {
window.setTimeout(() => {
calc();
}, 100);
});
const observer = new MutationObserver(() => {
window.setTimeout(() => {
calc();
}, 100);
});
observer.observe(rootEl.value, {
attributes: false,
childList: true,
subtree: false,
});
onMounted(() => {
calc();
onUnmounted(() => {
observer.disconnect();
});
});
observer.observe(rootEl, {
attributes: false,
childList: true,
subtree: false,
});
});
return {
rootEl,
bodyEl,
};
},
onUnmounted(() => {
observer.disconnect();
});
</script>

View File

@@ -32,8 +32,7 @@ const relative = $computed(() => {
ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
ago >= -1 ? i18n.ts._ago.justNow :
ago < -1 ? i18n.ts._ago.future :
i18n.ts._ago.unknown);
i18n.ts._ago.future);
});
function tick() {

View File

@@ -10,15 +10,17 @@ import MkEllipsis from './global/ellipsis.vue';
import MkTime from './global/time.vue';
import MkUrl from './global/url.vue';
import I18n from './global/i18n';
import RouterView from './global/router-view.vue';
import MkLoading from './global/loading.vue';
import MkError from './global/error.vue';
import MkAd from './global/ad.vue';
import MkHeader from './global/header.vue';
import MkPageHeader from './global/page-header.vue';
import MkSpacer from './global/spacer.vue';
import MkStickyContainer from './global/sticky-container.vue';
export default function(app: App) {
app.component('I18n', I18n);
app.component('RouterView', RouterView);
app.component('Mfm', Mfm);
app.component('MkA', MkA);
app.component('MkAcct', MkAcct);
@@ -31,7 +33,7 @@ export default function(app: App) {
app.component('MkLoading', MkLoading);
app.component('MkError', MkError);
app.component('MkAd', MkAd);
app.component('MkHeader', MkHeader);
app.component('MkPageHeader', MkPageHeader);
app.component('MkSpacer', MkSpacer);
app.component('MkStickyContainer', MkStickyContainer);
}
@@ -39,6 +41,7 @@ export default function(app: App) {
declare module '@vue/runtime-core' {
export interface GlobalComponents {
I18n: typeof I18n;
RouterView: typeof RouterView;
Mfm: typeof Mfm;
MkA: typeof MkA;
MkAcct: typeof MkAcct;
@@ -51,7 +54,7 @@ declare module '@vue/runtime-core' {
MkLoading: typeof MkLoading;
MkError: typeof MkError;
MkAd: typeof MkAd;
MkHeader: typeof MkHeader;
MkPageHeader: typeof MkPageHeader;
MkSpacer: typeof MkSpacer;
MkStickyContainer: typeof MkStickyContainer;
}

View File

@@ -8,7 +8,8 @@
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
<video
:poster="video.thumbnailUrl"
:title="video.name"
:title="video.comment"
:alt="video.comment"
preload="none"
controls
@contextmenu.stop

View File

@@ -9,82 +9,71 @@
<polygon
:points="polygonPoints"
fill="#fff"
fill-opacity="0.5"/>
fill-opacity="0.5"
/>
<polyline
:points="polylinePoints"
fill="none"
stroke="#fff"
stroke-width="2"/>
stroke-width="2"
/>
<circle
:cx="headX"
:cy="headY"
r="3"
fill="#fff"/>
fill="#fff"
/>
</mask>
</defs>
<rect
x="-10" y="-10"
:width="viewBoxX + 20" :height="viewBoxY + 20"
:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"
/>
</svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onUnmounted, watch } from 'vue';
import { v4 as uuid } from 'uuid';
import * as os from '@/os';
export default defineComponent({
props: {
src: {
type: Array,
required: true
}
},
data() {
return {
viewBoxX: 50,
viewBoxY: 30,
gradientId: uuid(),
maskId: uuid(),
polylinePoints: '',
polygonPoints: '',
headX: null,
headY: null,
clock: null
};
},
watch: {
src() {
this.draw();
}
},
created() {
this.draw();
const props = defineProps<{
src: number[];
}>();
// Vueが何故かWatchを発動させない場合があるので
this.clock = window.setInterval(this.draw, 1000);
},
beforeUnmount() {
window.clearInterval(this.clock);
},
methods: {
draw() {
const stats = this.src.slice().reverse();
const peak = Math.max.apply(null, stats) || 1;
const viewBoxX = 50;
const viewBoxY = 50;
const gradientId = uuid();
const maskId = uuid();
let polylinePoints = $ref('');
let polygonPoints = $ref('');
let headX = $ref<number | null>(null);
let headY = $ref<number | null>(null);
let clock = $ref<number | null>(null);
const polylinePoints = stats.map((n, i) => [
i * (this.viewBoxX / (stats.length - 1)),
(1 - (n / peak)) * this.viewBoxY
]);
function draw(): void {
const stats = props.src.slice().reverse();
const peak = Math.max.apply(null, stats) || 1;
this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
const _polylinePoints = stats.map((n, i) => [
i * (viewBoxX / (stats.length - 1)),
(1 - (n / peak)) * viewBoxY,
]);
this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
polylinePoints = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.headX = polylinePoints[polylinePoints.length - 1][0];
this.headY = polylinePoints[polylinePoints.length - 1][1];
}
}
polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`;
headX = _polylinePoints[_polylinePoints.length - 1][0];
headY = _polylinePoints[_polylinePoints.length - 1][1];
}
watch(() => props.src, draw, { immediate: true });
// Vueが何故かWatchを発動させない場合があるので
clock = window.setInterval(draw, 1000);
onUnmounted(() => {
window.clearInterval(clock);
});
</script>

View File

@@ -1,163 +1,118 @@
<template>
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div ref="rootEl" class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu">
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
<span v-else style="display: inline-block; width: 20px"></span>
<span v-if="pageInfo" class="title">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
<span>{{ pageInfo.title }}</span>
<span v-if="pageMetadata?.value" class="title">
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
<span>{{ pageMetadata?.value.title }}</span>
</span>
<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
</div>
<div class="body">
<MkStickyContainer>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<keep-alive>
<component :is="component" v-bind="props" :ref="changePage"/>
</keep-alive>
<template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template>
<RouterView :router="router"/>
</MkStickyContainer>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ComputedRef, provide } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import { popout } from '@/scripts/popout';
import { popout as _popout } from '@/scripts/popout';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { mainRouter, routes } from '@/router';
import { i18n } from '@/i18n';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
import { Router } from '@/nirax';
export default defineComponent({
components: {
MkModal,
},
const props = defineProps<{
initialPath: string;
}>();
inject: {
sideViewHook: {
default: null
}
},
defineEmits<{
(ev: 'closed'): void;
(ev: 'click'): void;
}>();
provide() {
return {
navHook: (path) => {
this.navigate(path);
},
shouldHeaderThin: true,
};
},
const router = new Router(routes, props.initialPath);
props: {
initialPath: {
type: String,
required: true,
},
initialComponent: {
type: Object,
required: true,
},
initialProps: {
type: Object,
required: false,
default: () => {},
},
},
emits: ['closed'],
data() {
return {
width: 860,
height: 660,
pageInfo: null,
path: this.initialPath,
component: this.initialComponent,
props: this.initialProps,
history: [],
};
},
computed: {
url(): string {
return url + this.path;
},
contextmenu() {
return [{
type: 'label',
text: this.path,
}, {
icon: 'fas fa-expand-alt',
text: this.$ts.showInPage,
action: this.expand
}, this.sideViewHook ? {
icon: 'fas fa-columns',
text: this.$ts.openInSideView,
action: () => {
this.sideViewHook(this.path);
this.$refs.window.close();
}
} : undefined, {
icon: 'fas fa-external-link-alt',
text: this.$ts.popout,
action: this.popout
}, null, {
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
}];
},
},
methods: {
changePage(page) {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
this.pageInfo = page[symbols.PAGE_INFO];
}
},
navigate(path, record = true) {
if (record) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
back() {
this.navigate(this.history.pop(), false);
},
expand() {
this.$router.push(this.path);
this.$refs.window.close();
},
popout() {
popout(this.path, this.$el);
this.$refs.window.close();
},
onContextmenu(ev: MouseEvent) {
os.contextMenu(this.contextmenu, ev);
}
},
router.addListener('push', ctx => {
});
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
let rootEl = $ref();
let modal = $ref<InstanceType<typeof MkModal>>();
let path = $ref(props.initialPath);
let width = $ref(860);
let height = $ref(660);
const history = [];
provide('router', router);
provideMetadataReceiver((info) => {
pageMetadata = info;
});
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
const pageUrl = $computed(() => url + path);
const contextmenu = $computed(() => {
return [{
type: 'label',
text: path,
}, {
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.popout,
action: popout,
}, null, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(pageUrl, '_blank');
modal.close();
},
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(pageUrl);
},
}];
});
function navigate(path, record = true) {
if (record) history.push(router.getCurrentPath());
router.push(path);
}
function back() {
navigate(history.pop(), false);
}
function expand() {
mainRouter.push(path);
modal.close();
}
function popout() {
_popout(path, rootEl);
modal.close();
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu(contextmenu, ev);
}
</script>
<style lang="scss" scoped>

View File

@@ -222,7 +222,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction
reaction: reaction,
});
}, () => {
focus();
@@ -233,7 +233,7 @@ function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api('notes/reactions/delete', {
noteId: note.id
noteId: note.id,
});
}
@@ -251,13 +251,13 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton }), ev).then(focus);
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
viaKeyboard,
}).then(focus);
}
@@ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
noteId: note.id
noteId: note.id,
});
isDeleted.value = true;
}
},
}], renoteTime.value, {
viaKeyboard: viaKeyboard
viaKeyboard: viaKeyboard,
});
}
@@ -288,14 +288,14 @@ function blur() {
os.api('notes/children', {
noteId: appearNote.id,
limit: 30
limit: 30,
}).then(res => {
replies.value = res;
});
if (appearNote.replyId) {
os.api('notes/conversation', {
noteId: appearNote.replyId
noteId: appearNote.replyId,
}).then(res => {
conversation.value = res.reverse();
});

View File

@@ -5,7 +5,7 @@
<XNoteHeader class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<span v-if="note.cw != ''" class="text">{{ note.cw }}</span>
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
<XCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">

View File

@@ -105,7 +105,7 @@
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
import { computed, inject, onMounted, onUnmounted, reactive, ref, Ref } from 'vue';
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import MkNoteSub from './MkNoteSub.vue';
@@ -210,7 +210,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction
reaction: reaction,
});
}, () => {
focus();
@@ -221,10 +221,12 @@ function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api('notes/reactions/delete', {
noteId: note.id
noteId: note.id,
});
}
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
@@ -239,13 +241,13 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton }), ev).then(focus);
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
viaKeyboard,
}).then(focus);
}
@@ -257,12 +259,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
noteId: note.id
noteId: note.id,
});
isDeleted.value = true;
}
},
}], renoteTime.value, {
viaKeyboard: viaKeyboard
viaKeyboard: viaKeyboard,
});
}
@@ -284,7 +286,7 @@ function focusAfter() {
function readPromo() {
os.api('promo/read', {
noteId: appearNote.id
noteId: appearNote.id,
});
isDeleted.value = true;
}

View File

@@ -1,5 +1,6 @@
<template>
<XModalWindow ref="dialog"
<XModalWindow
ref="dialog"
:width="400"
:height="450"
:with-ok-button="true"
@@ -28,18 +29,18 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import { notificationTypes } from 'misskey-js';
import MkSwitch from './form/switch.vue';
import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue';
import { notificationTypes } from 'misskey-js';
import XModalWindow from '@/components/ui/modal-window.vue';
export default defineComponent({
components: {
XModalWindow,
MkSwitch,
MkInfo,
MkButton
MkButton,
},
props: {
@@ -53,7 +54,7 @@ export default defineComponent({
type: Boolean,
required: false,
default: true,
}
},
},
emits: ['done', 'closed'],
@@ -93,7 +94,7 @@ export default defineComponent({
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = true;
}
}
}
},
},
});
</script>

View File

@@ -16,7 +16,8 @@
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon v-else-if="notification.type === 'reaction'"
<XReactionIcon
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
:custom-emojis="notification.note.emojis"
@@ -74,10 +75,10 @@
<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js';
import { getNoteSummary } from '@/scripts/get-note-summary';
import XReactionIcon from './reaction-icon.vue';
import MkFollowButton from './follow-button.vue';
import XReactionTooltip from './reaction-tooltip.vue';
import { getNoteSummary } from '@/scripts/get-note-summary';
import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
@@ -87,7 +88,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
export default defineComponent({
components: {
XReactionIcon, MkFollowButton
XReactionIcon, MkFollowButton,
},
props: {
@@ -116,7 +117,7 @@ export default defineComponent({
const readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return;
stream.send('readNotification', {
id: props.notification.id
id: props.notification.id,
});
observer.disconnect();
});

View File

@@ -19,8 +19,7 @@
<script lang="ts" setup>
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
import { notificationTypes } from 'misskey-js';
import MkPagination from '@/components/ui/pagination.vue';
import { Paging } from '@/components/ui/pagination.vue';
import MkPagination, { Paging } from '@/components/ui/pagination.vue';
import XNotification from '@/components/notification.vue';
import XList from '@/components/date-separated-list.vue';
import XNote from '@/components/note.vue';
@@ -49,14 +48,14 @@ const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
stream.send('readNotification', {
id: notification.id
id: notification.id,
});
}
if (!isMuted) {
pagingComponent.value.prepend({
...notification,
isRead: document.visibilityState === 'visible'
isRead: document.visibilityState === 'visible',
});
}
};

View File

@@ -12,7 +12,7 @@ export default defineComponent({
props: {
value: {
type: Number,
required: true
required: true,
},
},
@@ -26,7 +26,7 @@ export default defineComponent({
isZero,
number,
};
}
},
});
</script>

View File

@@ -1,186 +1,135 @@
<template>
<XWindow ref="window"
<XWindow
ref="windowEl"
:initial-width="500"
:initial-height="500"
:can-resize="true"
:close-button="true"
:buttons-left="buttonsLeft"
:buttons-right="buttonsRight"
:contextmenu="contextmenu"
@closed="$emit('closed')"
>
<template #header>
<template v-if="pageInfo">
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageInfo.title }}</span>
<template v-if="pageMetadata?.value">
<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageMetadata.value.title }}</span>
</template>
</template>
<template #headerLeft>
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
</template>
<template #headerRight>
<button v-tooltip="$ts.showInPage" class="_button" @click="expand()"><i class="fas fa-expand-alt"></i></button>
<button v-tooltip="$ts.popout" class="_button" @click="popout()"><i class="fas fa-external-link-alt"></i></button>
<button class="_button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
</template>
<div class="yrolvcoq" :style="{ background: pageInfo?.bg }">
<MkStickyContainer>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<component :is="component" v-bind="props" :ref="changePage"/>
</MkStickyContainer>
<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }">
<RouterView :router="router"/>
</div>
</XWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ComputedRef, inject, provide } from 'vue';
import RouterView from './global/router-view.vue';
import XWindow from '@/components/ui/window.vue';
import { popout } from '@/scripts/popout';
import { popout as _popout } from '@/scripts/popout';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { mainRouter, routes } from '@/router';
import { Router } from '@/nirax';
import { i18n } from '@/i18n';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
export default defineComponent({
components: {
XWindow,
const props = defineProps<{
initialPath: string;
}>();
defineEmits<{
(ev: 'closed'): void;
}>();
const router = new Router(routes, props.initialPath);
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
let windowEl = $ref<InstanceType<typeof XWindow>>();
const history = $ref<string[]>([props.initialPath]);
const buttonsLeft = $computed(() => {
const buttons = [];
if (history.length > 1) {
buttons.push({
icon: 'fas fa-arrow-left',
onClick: back,
});
}
return buttons;
});
const buttonsRight = $computed(() => {
const buttons = [{
icon: 'fas fa-expand-alt',
title: i18n.ts.showInPage,
onClick: expand,
}];
return buttons;
});
router.addListener('push', ctx => {
history.push(router.getCurrentPath());
});
provide('router', router);
provideMetadataReceiver((info) => {
pageMetadata = info;
});
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
const contextmenu = $computed(() => ([{
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.popout,
action: popout,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(url + router.getCurrentPath(), '_blank');
windowEl.close();
},
inject: {
sideViewHook: {
default: null
}
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(url + router.getCurrentPath());
},
}]));
provide() {
return {
navHook: (path) => {
this.navigate(path);
},
shouldHeaderThin: true,
};
},
function menu(ev) {
os.popupMenu(contextmenu, ev.currentTarget ?? ev.target);
}
props: {
initialPath: {
type: String,
required: true,
},
initialComponent: {
type: Object,
required: true,
},
initialProps: {
type: Object,
required: false,
default: () => {},
},
},
function back() {
history.pop();
router.change(history[history.length - 1]);
}
emits: ['closed'],
function close() {
windowEl.close();
}
data() {
return {
pageInfo: null,
path: this.initialPath,
component: this.initialComponent,
props: this.initialProps,
history: [],
};
},
function expand() {
mainRouter.push(router.getCurrentPath());
windowEl.close();
}
computed: {
url(): string {
return url + this.path;
},
function popout() {
_popout(router.getCurrentPath(), windowEl.$el);
windowEl.close();
}
contextmenu() {
return [{
type: 'label',
text: this.path,
}, {
icon: 'fas fa-expand-alt',
text: this.$ts.showInPage,
action: this.expand
}, this.sideViewHook ? {
icon: 'fas fa-columns',
text: this.$ts.openInSideView,
action: () => {
this.sideViewHook(this.path);
this.$refs.window.close();
}
} : undefined, {
icon: 'fas fa-external-link-alt',
text: this.$ts.popout,
action: this.popout
}, null, {
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
}];
},
},
methods: {
changePage(page) {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
this.pageInfo = page[symbols.PAGE_INFO];
}
},
navigate(path, record = true) {
if (record) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
menu(ev) {
os.popupMenu([{
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
}], ev.currentTarget ?? ev.target);
},
back() {
this.navigate(this.history.pop(), false);
},
close() {
this.$refs.window.close();
},
expand() {
this.$router.push(this.path);
this.$refs.window.close();
},
popout() {
popout(this.path, this.$el);
this.$refs.window.close();
},
},
defineExpose({
close,
});
</script>

View File

@@ -66,7 +66,7 @@ export default defineComponent({
.then(response => response.json())
.then(f => {
ok(f);
})
});
});
});
os.promiseDialog(promise);

View File

@@ -16,7 +16,7 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue'
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
import * as os from '@/os';
export default defineComponent({
@@ -114,19 +114,19 @@ export default defineComponent({
this.menu = os.popupMenu([{
text: this.$ts.renameFile,
icon: 'fas fa-i-cursor',
action: () => { this.rename(file) }
action: () => { this.rename(file); }
}, {
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
action: () => { this.toggleSensitive(file) }
action: () => { this.toggleSensitive(file); }
}, {
text: this.$ts.describeFile,
icon: 'fas fa-i-cursor',
action: () => { this.describe(file) }
action: () => { this.describe(file); }
}, {
text: this.$ts.attachCancel,
icon: 'fas fa-times-circle',
action: () => { this.detachMedia(file.id) }
action: () => { this.detachMedia(file.id); }
}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
}
}

View File

@@ -442,7 +442,7 @@ function onCompositionEnd(ev: CompositionEvent) {
}
async function onPaste(ev: ClipboardEvent) {
for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({item, i}))) {
for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({ item, i }))) {
if (item.kind === 'file') {
const file = item.getAsFile();
const lio = file.name.lastIndexOf('.');

View File

@@ -222,7 +222,7 @@ export default defineComponent({
return {
chartEl,
}
};
},
});
</script>

View File

@@ -52,7 +52,7 @@ export default defineComponent({
flag: true,
radio: 'misskey',
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
}
};
},
methods: {

View File

@@ -14,8 +14,6 @@
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
@@ -64,8 +62,6 @@ import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
const MkCaptcha = defineAsyncComponent(() => import('./captcha.vue'));
let signing = $ref(false);
let user = $ref(null);
let username = $ref('');
@@ -163,7 +159,7 @@ function queryKey() {
function onSubmit() {
signing = true;
console.log('submit')
console.log('submit');
if (!totpLogin && user && user.twoFactorEnabled) {
if (window.PublicKeyCredential && user.securityKeys) {
os.api('signin', {
@@ -217,8 +213,16 @@ function loginFailed(err) {
showSuspendedDialog();
break;
}
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.rateLimitExceeded,
});
break;
}
default: {
console.log(err)
console.log(err);
os.alert({
type: 'error',
title: i18n.ts.loginFailed,

View File

@@ -1,11 +1,11 @@
<template>
<form class="qlvuhzng _formRoot" :autocomplete="Math.random()" @submit.prevent="onSubmit">
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
<template v-if="meta">
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</MkInput>
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
@@ -19,7 +19,7 @@
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix><i class="fas fa-envelope"></i></template>
<template #caption>
@@ -34,7 +34,7 @@
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password @update:modelValue="onChangePassword">
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
@@ -43,7 +43,7 @@
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
@@ -111,7 +111,7 @@ export default defineComponent({
ToSAgreement: false,
hCaptchaResponse: null,
reCaptchaResponse: null,
}
};
},
computed: {

View File

@@ -96,11 +96,11 @@ export default defineComponent({
}
function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
const origin = {x: circleCenterX, y: circleCenterY};
const dist1 = distance({x: 0, y: 0}, origin);
const dist2 = distance({x: boxW, y: 0}, origin);
const dist3 = distance({x: 0, y: boxH}, origin);
const dist4 = distance({x: boxW, y: boxH }, origin);
const origin = { x: circleCenterX, y: circleCenterY };
const dist1 = distance({ x: 0, y: 0 }, origin);
const dist2 = distance({ x: boxW, y: 0 }, origin);
const dist3 = distance({ x: 0, y: boxH }, origin);
const dist4 = distance({ x: boxW, y: boxH }, origin);
return Math.max(dist1, dist2, dist3, dist4) * 2;
}

View File

@@ -1,5 +1,6 @@
<template>
<div ref="itemsEl" v-hotkey="keymap"
<div
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@@ -162,6 +163,15 @@ function focusDown() {
position: relative;
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&.danger {
color: #ff2a2a;
@@ -191,15 +201,6 @@ function focusDown() {
}
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&:not(:active):focus-visible {
box-shadow: 0 0 0 2px var(--focus) inset;
}

View File

@@ -1,7 +1,7 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" @click="$emit('click')" @closed="$emit('closed')">
<div class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
<div class="header">
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
<div ref="rootEl" class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
<div ref="headerEl" class="header">
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button>
<span class="title">
<slot name="header"></slot>
@@ -11,82 +11,82 @@
</div>
<div v-if="padding" class="body">
<div class="_section">
<slot></slot>
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
<div v-else class="body">
<slot></slot>
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
import MkModal from './modal.vue';
export default defineComponent({
components: {
MkModal
},
props: {
withOkButton: {
type: Boolean,
required: false,
default: false
},
okButtonDisabled: {
type: Boolean,
required: false,
default: false
},
padding: {
type: Boolean,
required: false,
default: false
},
width: {
type: Number,
required: false,
default: 400
},
height: {
type: Number,
required: false,
default: null
},
canClose: {
type: Boolean,
required: false,
default: true,
},
scroll: {
type: Boolean,
required: false,
default: true,
},
},
const props = withDefaults(defineProps<{
withOkButton: boolean;
okButtonDisabled: boolean;
padding: boolean;
width: number;
height: number | null;
scroll: boolean;
}>(), {
withOkButton: false,
okButtonDisabled: false,
padding: false,
width: 400,
height: null,
scroll: true,
});
emits: ['click', 'close', 'closed', 'ok'],
const emit = defineEmits<{
(event: 'click'): void;
(event: 'close'): void;
(event: 'closed'): void;
(event: 'ok'): void;
}>();
data() {
return {
};
},
let modal = $ref<InstanceType<typeof MkModal>>();
let rootEl = $ref<HTMLElement>();
let headerEl = $ref<HTMLElement>();
let bodyWidth = $ref(0);
let bodyHeight = $ref(0);
methods: {
close() {
this.$refs.modal.close();
},
const close = () => {
modal.close();
};
onKeydown(evt) {
if (evt.which === 27) { // Esc
evt.preventDefault();
evt.stopPropagation();
this.close();
}
},
const onBgClick = () => {
emit('click');
};
const onKeydown = (evt) => {
if (evt.which === 27) { // Esc
evt.preventDefault();
evt.stopPropagation();
close();
}
};
const ro = new ResizeObserver((entries, observer) => {
bodyWidth = rootEl.offsetWidth;
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
});
onMounted(() => {
bodyWidth = rootEl.offsetWidth;
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
ro.observe(rootEl);
});
onUnmounted(() => {
ro.disconnect();
});
defineExpose({
close,
});
</script>

View File

@@ -1,5 +1,5 @@
<template>
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered">
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
@@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(ev: 'opening'): void;
(ev: 'opened'): void;
(ev: 'click'): void;
(ev: 'esc'): void;
(ev: 'close'): void;
@@ -212,7 +213,9 @@ const align = () => {
popover.style.top = top + 'px';
};
const childRendered = () => {
const onOpened = () => {
emit('opened');
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
const el = content.value!.children[0];
el.addEventListener('mousedown', ev => {
@@ -234,10 +237,10 @@ onMounted(() => {
}
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick()
await nextTick();
align();
}, { immediate: true, });
}, { immediate: true });
nextTick(() => {
const popover = content.value;

View File

@@ -1,7 +1,10 @@
<template>
<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')">
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>{{ text }}</slot>
<slot>
<Mfm v-if="asMfm" :text="text"/>
<span v-else>{{ text }}</span>
</slot>
</div>
</transition>
</template>
@@ -16,6 +19,7 @@ const props = withDefaults(defineProps<{
x?: number;
y?: number;
text?: string;
asMfm?: boolean;
maxWidth?: number;
direction?: 'top' | 'bottom' | 'right' | 'left';
innerMargin?: number;
@@ -63,7 +67,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calcPosWhenBottom = () => {
let left: number;
@@ -84,7 +88,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calcPosWhenLeft = () => {
let left: number;
@@ -105,7 +109,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calcPosWhenRight = () => {
let left: number;
@@ -126,7 +130,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calc = (): {
left: number;
@@ -170,9 +174,7 @@ const setPosition = () => {
return { left, top, transformOrigin: 'left center' };
}
}
return null as never;
}
};
const { left, top, transformOrigin } = calc();
el.value.style.transformOrigin = transformOrigin;

View File

@@ -4,14 +4,14 @@
<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
<span class="left">
<slot name="headerLeft"></slot>
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
</span>
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
<slot name="header"></slot>
</span>
<span class="right">
<slot name="headerRight"></slot>
<button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
<button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button>
</span>
</div>
<div v-if="padding" class="body">
@@ -46,41 +46,41 @@ const minHeight = 50;
const minWidth = 250;
function dragListen(fn) {
window.addEventListener('mousemove', fn);
window.addEventListener('touchmove', fn);
window.addEventListener('mousemove', fn);
window.addEventListener('touchmove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn));
window.addEventListener('touchend', dragClear.bind(null, fn));
window.addEventListener('mouseup', dragClear.bind(null, fn));
window.addEventListener('touchend', dragClear.bind(null, fn));
}
function dragClear(fn) {
window.removeEventListener('mousemove', fn);
window.removeEventListener('touchmove', fn);
window.removeEventListener('mousemove', fn);
window.removeEventListener('touchmove', fn);
window.removeEventListener('mouseleave', dragClear);
window.removeEventListener('mouseup', dragClear);
window.removeEventListener('touchend', dragClear);
window.removeEventListener('mouseup', dragClear);
window.removeEventListener('touchend', dragClear);
}
export default defineComponent({
provide: {
inWindow: true
inWindow: true,
},
props: {
padding: {
type: Boolean,
required: false,
default: false
default: false,
},
initialWidth: {
type: Number,
required: false,
default: 400
default: 400,
},
initialHeight: {
type: Number,
required: false,
default: null
default: null,
},
canResize: {
type: Boolean,
@@ -105,7 +105,17 @@ export default defineComponent({
contextmenu: {
type: Array,
required: false,
}
},
buttonsLeft: {
type: Array,
required: false,
default: [],
},
buttonsRight: {
type: Array,
required: false,
default: [],
},
},
emits: ['closed'],
@@ -162,7 +172,10 @@ export default defineComponent({
this.top();
},
onHeaderMousedown(evt) {
onHeaderMousedown(evt: MouseEvent) {
// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
if (evt.button === 2) return;
const main = this.$el as any;
if (!contains(main, document.activeElement)) main.focus();
@@ -356,12 +369,12 @@ export default defineComponent({
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
const windowHeight = main.offsetHeight;
if (position.left < 0) main.style.left = 0; // 左はみ出し
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
if (position.top < 0) main.style.top = 0; // 上はみ出し
}
}
if (position.left < 0) main.style.left = 0; // 左はみ出し
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し
if (position.top < 0) main.style.top = 0; // 上はみ出し
},
},
});
</script>
@@ -404,17 +417,25 @@ export default defineComponent({
border-bottom: solid 1px var(--divider);
> .left, > .right {
> ::v-deep(button) {
> .button {
height: var(--height);
width: var(--height);
&:hover {
color: var(--fgHighlighted);
}
&.highlighted {
color: var(--accent);
}
}
}
> .left {
margin-right: 16px;
}
> .right {
min-width: 16px;
}

View File

@@ -90,7 +90,7 @@ fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).the
sitename = info.sitename;
fetching = false;
player = info.player;
})
});
});
function adjustTweetHeight(message: any) {

View File

@@ -2,11 +2,11 @@
<div class="vjoppmmu">
<template v-if="edit">
<header>
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" class="mk-widget-select">
<template #label>{{ $ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ $t(`_widgets.${widget}`) }}</option>
</MkSelect>
<MkButton inline primary @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
</header>
<XDraggable

View File

@@ -9,7 +9,7 @@ export default {
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
}
};
const parentBg = getBgColor(src.parentElement);

View File

@@ -25,12 +25,12 @@ function calc(src: Element) {
return;
}
if (info.intersection) {
info.intersection.disconnect()
info.intersection.disconnect();
delete info.intersection;
};
}
info.fn(width, height);
};
}
export default {
mounted(src, binding, vn) {

View File

@@ -9,7 +9,7 @@ export default {
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
}
};
const parentBg = getBgColor(src.parentElement);

View File

@@ -60,9 +60,9 @@ function calc(el: Element) {
return;
}
if (info.intersection) {
info.intersection.disconnect()
info.intersection.disconnect();
delete info.intersection;
};
}
mountings.set(el, Object.assign(info, { previousWidth: width }));

View File

@@ -5,8 +5,10 @@ export default {
//const query = binding.value;
const header = src.children[0];
const body = src.children[1];
const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
if (body) body.dataset.stickyContainerHeaderHeight = header.offsetHeight.toString();
header.style.setProperty('--stickyTop', currentStickyTop);
header.style.position = 'sticky';
header.style.top = 'var(--stickyTop)';

View File

@@ -48,6 +48,7 @@ export default {
popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
showing,
text: self.text,
asMfm: binding.modifiers.mfm,
targetElement: el,
}, {}, 'closed');

View File

@@ -21,7 +21,6 @@ import widgets from '@/widgets';
import directives from '@/directives';
import components from '@/components';
import { version, ui, lang, host } from '@/config';
import { router } from '@/router';
import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n } from '@/i18n';
@@ -173,11 +172,10 @@ fetchInstanceMetaPromise.then(() => {
const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'desktop' ? defineAsyncComponent(() => import('@/ui/desktop.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue'))
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue')),
);
if (_DEV_) {
@@ -192,14 +190,10 @@ app.config.globalProperties = {
$ts: i18n.ts,
};
app.use(router);
widgets(app);
directives(app);
components(app);
await router.isReady();
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {

View File

@@ -1,11 +1,11 @@
import { computed, ref, reactive } from 'vue';
import { $i } from './account';
import { mainRouter } from '@/router';
import { search } from '@/scripts/search';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { ui } from '@/config';
import { $i } from './account';
import { unisonReload } from '@/scripts/unison-reload';
import { router } from './router';
export const menuDef = reactive({
notifications: {
@@ -60,16 +60,16 @@ export const menuDef = reactive({
title: 'lists',
icon: 'fas fa-list-ul',
show: computed(() => $i != null),
active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')),
active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/list/') || mainRouter.currentRoute.value.path === '/my/lists' || mainRouter.currentRoute.value.path.startsWith('/my/lists/')),
action: (ev) => {
const items = ref([{
type: 'pending'
type: 'pending',
}]);
os.api('users/lists/list').then(lists => {
const _items = [...lists.map(list => ({
type: 'link',
text: list.name,
to: `/timeline/list/${list.id}`
to: `/timeline/list/${list.id}`,
})), null, {
type: 'link',
to: '/my/lists',
@@ -91,16 +91,16 @@ export const menuDef = reactive({
title: 'antennas',
icon: 'fas fa-satellite',
show: computed(() => $i != null),
active: computed(() => router.currentRoute.value.path.startsWith('/timeline/antenna/') || router.currentRoute.value.path === '/my/antennas' || router.currentRoute.value.path.startsWith('/my/antennas/')),
active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/antenna/') || mainRouter.currentRoute.value.path === '/my/antennas' || mainRouter.currentRoute.value.path.startsWith('/my/antennas/')),
action: (ev) => {
const items = ref([{
type: 'pending'
type: 'pending',
}]);
os.api('antennas/list').then(antennas => {
const _items = [...antennas.map(antenna => ({
type: 'link',
text: antenna.name,
to: `/timeline/antenna/${antenna.id}`
to: `/timeline/antenna/${antenna.id}`,
})), null, {
type: 'link',
to: '/my/antennas',
@@ -178,29 +178,22 @@ export const menuDef = reactive({
action: () => {
localStorage.setItem('ui', 'default');
unisonReload();
}
},
}, {
text: i18n.ts.deck,
active: ui === 'deck',
action: () => {
localStorage.setItem('ui', 'deck');
unisonReload();
}
},
}, {
text: i18n.ts.classic,
active: ui === 'classic',
action: () => {
localStorage.setItem('ui', 'classic');
unisonReload();
}
}, /*{
text: i18n.ts.desktop + ' (β)',
active: ui === 'desktop',
action: () => {
localStorage.setItem('ui', 'desktop');
unisonReload();
}
}*/], ev.currentTarget ?? ev.target);
},
}], ev.currentTarget ?? ev.target);
},
},
});

View File

@@ -0,0 +1,207 @@
// NIRAX --- A lightweight router
import { EventEmitter } from 'eventemitter3';
import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue';
type RouteDef = {
path: string;
component: Component;
query?: Record<string, string>;
name?: string;
globalCacheKey?: string;
};
type ParsedPath = (string | {
name: string;
startsWith?: string;
wildcard?: boolean;
optional?: boolean;
})[];
function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath;
path = path.substring(1);
for (const part of path.split('/')) {
if (part.includes(':')) {
const prefix = part.substring(0, part.indexOf(':'));
const placeholder = part.substring(part.indexOf(':') + 1);
const wildcard = placeholder.includes('(*)');
const optional = placeholder.endsWith('?');
res.push({
name: placeholder.replace('(*)', '').replace('?', ''),
startsWith: prefix !== '' ? prefix : undefined,
wildcard,
optional,
});
} else {
res.push(part);
}
}
return res;
}
export class Router extends EventEmitter<{
change: (ctx: {
beforePath: string;
path: string;
route: RouteDef | null;
props: Map<string, string> | null;
key: string;
}) => void;
push: (ctx: {
beforePath: string;
path: string;
route: RouteDef | null;
props: Map<string, string> | null;
key: string;
}) => void;
}> {
private routes: RouteDef[];
private currentPath: string;
private currentComponent: Component | null = null;
private currentProps: Map<string, string> | null = null;
private currentKey = Date.now().toString();
public currentRoute: ShallowRef<RouteDef | null> = shallowRef(null);
constructor(routes: Router['routes'], currentPath: Router['currentPath']) {
super();
this.routes = routes;
this.currentPath = currentPath;
this.navigate(currentPath, null, true);
}
public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null {
let queryString: string | null = null;
if (path[0] === '/') path = path.substring(1);
if (path.includes('?')) {
queryString = path.substring(path.indexOf('?') + 1);
path = path.substring(0, path.indexOf('?'));
}
if (_DEV_) console.log('Routing: ', path, queryString);
forEachRouteLoop:
for (const route of this.routes) {
let parts = path.split('/');
const props = new Map<string, string>();
pathMatchLoop:
for (const p of parsePath(route.path)) {
if (typeof p === 'string') {
if (p === parts[0]) {
parts.shift();
} else {
continue forEachRouteLoop;
}
} else {
if (parts[0] == null && !p.optional) {
continue forEachRouteLoop;
}
if (p.wildcard) {
if (parts.length !== 0) {
props.set(p.name, parts.join('/'));
parts = [];
}
break pathMatchLoop;
} else {
if (p.startsWith) {
if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop;
props.set(p.name, parts[0].substring(p.startsWith.length));
parts.shift();
} else {
props.set(p.name, parts[0]);
parts.shift();
}
}
}
}
if (parts.length !== 0) continue forEachRouteLoop;
if (route.query != null && queryString != null) {
const queryObject = [...new URLSearchParams(queryString).entries()]
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
for (const q in route.query) {
const as = route.query[q];
if (queryObject[q]) {
props.set(as, queryObject[q]);
}
}
}
return {
route,
props,
};
}
return null;
}
private navigate(path: string, key: string | null | undefined, initial = false) {
const beforePath = this.currentPath;
const beforeRoute = this.currentRoute.value;
this.currentPath = path;
const res = this.resolve(this.currentPath);
if (res == null) {
throw new Error('no route found for: ' + path);
}
const isSamePath = beforePath === path;
if (isSamePath && key == null) key = this.currentKey;
this.currentComponent = res.route.component;
this.currentProps = res.props;
this.currentRoute.value = res.route;
this.currentKey = this.currentRoute.value.globalCacheKey ?? key ?? Date.now().toString();
if (!initial) {
this.emit('change', {
beforePath,
path,
route: this.currentRoute.value,
props: this.currentProps,
key: this.currentKey,
});
}
}
public getCurrentComponent() {
return this.currentComponent;
}
public getCurrentProps() {
return this.currentProps;
}
public getCurrentPath() {
return this.currentPath;
}
public getCurrentKey() {
return this.currentKey;
}
public push(path: string) {
const beforePath = this.currentPath;
this.navigate(path, null);
this.emit('push', {
beforePath,
path,
route: this.currentRoute.value,
props: this.currentProps,
key: this.currentKey,
});
}
public change(path: string, key?: string | null) {
this.navigate(path, key);
}
}

View File

@@ -8,7 +8,6 @@ import { apiUrl, url } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { MenuItem } from '@/types/menu';
import { resolve } from '@/router';
import { $i } from '@/account';
export const pendingApiRequestsCount = ref(0);
@@ -34,7 +33,7 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
cache: 'no-cache',
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
@@ -142,7 +141,7 @@ export async function popup(component: Component, props: Record<string, any>, ev
props,
events: disposeEvent ? {
...events,
[disposeEvent]: dispose
[disposeEvent]: dispose,
} : events,
id,
};
@@ -155,26 +154,20 @@ export async function popup(component: Component, props: Record<string, any>, ev
}
export function pageWindow(path: string) {
const { component, props } = resolve(path);
popup(defineAsyncComponent(() => import('@/components/page-window.vue')), {
initialPath: path,
initialComponent: markRaw(component),
initialProps: props,
}, {}, 'closed');
}
export function modalPageWindow(path: string) {
const { component, props } = resolve(path);
popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), {
initialPath: path,
initialComponent: markRaw(component),
initialProps: props,
}, {}, 'closed');
}
export function toast(message: string) {
popup(defineAsyncComponent(() => import('@/components/toast.vue')), {
message
message,
}, {}, 'closed');
}
@@ -226,7 +219,7 @@ export function inputText(props: {
type: props.type,
placeholder: props.placeholder,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? result : { canceled: true });
@@ -251,7 +244,7 @@ export function inputNumber(props: {
type: 'number',
placeholder: props.placeholder,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? result : { canceled: true });
@@ -276,7 +269,7 @@ export function inputDate(props: {
type: 'date',
placeholder: props.placeholder,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true });
@@ -285,7 +278,7 @@ export function inputDate(props: {
});
}
export function select<C extends any = any>(props: {
export function select<C = any>(props: {
title?: string | null;
text?: string | null;
default?: string | null;
@@ -313,7 +306,7 @@ export function select<C extends any = any>(props: {
items: props.items,
groupedItems: props.groupedItems,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? result : { canceled: true });
@@ -330,7 +323,7 @@ export function success() {
}, 1000);
popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), {
success: true,
showing: showing
showing: showing,
}, {
done: () => resolve(),
}, 'closed');
@@ -342,7 +335,7 @@ export function waiting() {
const showing = ref(true);
popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), {
success: false,
showing: showing
showing: showing,
}, {
done: () => resolve(),
}, 'closed');
@@ -373,7 +366,7 @@ export async function selectDriveFile(multiple: boolean) {
return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), {
type: 'file',
multiple
multiple,
}, {
done: files => {
if (files) {
@@ -388,7 +381,7 @@ export async function selectDriveFolder(multiple: boolean) {
return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), {
type: 'folder',
multiple
multiple,
}, {
done: folders => {
if (folders) {
@@ -403,7 +396,7 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), {
src,
...opts
...opts,
}, {
done: emoji => {
resolve(emoji);
@@ -412,6 +405,21 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
});
}
export async function cropImage(image: Misskey.entities.DriveFile, options: {
aspectRatio: number;
}): Promise<Misskey.entities.DriveFile> {
return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/cropper-dialog.vue')), {
file: image,
aspectRatio: options.aspectRatio,
}, {
ok: x => {
resolve(x);
},
}, 'closed');
});
}
type AwaitType<T> =
T extends Promise<infer U> ? U :
T extends (...args: any[]) => Promise<infer V> ? V :
@@ -453,7 +461,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
openingEmojiPicker = await popup(defineAsyncComponent(() => import('@/components/emoji-picker-window.vue')), {
src,
...opts
...opts,
}, {
chosen: emoji => {
insertTextAtCursor(activeTextarea, emoji);
@@ -462,7 +470,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
openingEmojiPicker!.dispose();
openingEmojiPicker = null;
observer.disconnect();
}
},
});
}
@@ -478,7 +486,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
src,
width: options?.width,
align: options?.align,
viaKeyboard: options?.viaKeyboard
viaKeyboard: options?.viaKeyboard,
}, {
closed: () => {
resolve();

View File

@@ -21,11 +21,11 @@
import { } from 'vue';
import * as misskey from 'misskey-js';
import MkButton from '@/components/ui/button.vue';
import * as symbols from '@/symbols';
import { version } from '@/config';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const props = withDefaults(defineProps<{
error?: Error;
@@ -52,11 +52,13 @@ function reload() {
unisonReload();
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.error,
icon: 'fas fa-exclamation-triangle',
},
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.error,
icon: 'fas fa-exclamation-triangle',
});
</script>

View File

@@ -1,62 +1,65 @@
<template>
<div style="overflow: clip;">
<MkSpacer :content-max="600" :margin-min="20">
<div class="_formRoot znqjceqz">
<div id="debug"></div>
<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
<div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
</div>
<div class="_formBlock" style="text-align: center;">
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
</div>
<div class="_formBlock" style="text-align: center;">
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
</div>
<FormSection>
<div class="_formLinks">
<FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="fas fa-code"></i></template>
{{ i18n.ts._aboutMisskey.source }}
<template #suffix>GitHub</template>
</FormLink>
<FormLink to="https://crowdin.com/project/misskey" external>
<template #icon><i class="fas fa-language"></i></template>
{{ i18n.ts._aboutMisskey.translation }}
<template #suffix>Crowdin</template>
</FormLink>
<FormLink to="https://www.patreon.com/syuilo" external>
<template #icon><i class="fas fa-hand-holding-medical"></i></template>
{{ i18n.ts._aboutMisskey.donate }}
<template #suffix>Patreon</template>
</FormLink>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div style="overflow: clip;">
<MkSpacer :content-max="600" :margin-min="20">
<div class="_formRoot znqjceqz">
<div id="debug"></div>
<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
<div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
</div>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
<div class="_formLinks">
<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
<div class="_formBlock" style="text-align: center;">
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
</div>
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
</FormSection>
<FormSection>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
</FormSection>
</div>
</MkSpacer>
</div>
<div class="_formBlock" style="text-align: center;">
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
</div>
<FormSection>
<div class="_formLinks">
<FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="fas fa-code"></i></template>
{{ i18n.ts._aboutMisskey.source }}
<template #suffix>GitHub</template>
</FormLink>
<FormLink to="https://crowdin.com/project/misskey" external>
<template #icon><i class="fas fa-language"></i></template>
{{ i18n.ts._aboutMisskey.translation }}
<template #suffix>Crowdin</template>
</FormLink>
<FormLink to="https://www.patreon.com/syuilo" external>
<template #icon><i class="fas fa-hand-holding-medical"></i></template>
{{ i18n.ts._aboutMisskey.donate }}
<template #suffix>Patreon</template>
</FormLink>
</div>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
<div class="_formLinks">
<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
</div>
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
</FormSection>
<FormSection>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
</FormSection>
</div>
</MkSpacer>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -67,10 +70,10 @@ import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
import MkLink from '@/components/link.vue';
import { physics } from '@/scripts/physics';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata';
const patrons = [
'まっちゃとーにゅ',
@@ -194,12 +197,14 @@ onBeforeUnmount(() => {
}
});
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.aboutMisskey,
icon: null,
bg: 'var(--bg)',
},
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.aboutMisskey,
icon: null,
bg: 'var(--bg)',
});
</script>

View File

@@ -1,78 +1,81 @@
<template>
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
<div class="_formRoot">
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
<div class="content">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
<div class="name">
<b>{{ $instance.name || host }}</b>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
<div class="_formRoot">
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
<div class="content">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
<div class="name">
<b>{{ $instance.name || host }}</b>
</div>
</div>
</div>
</div>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.description }}</template>
<template #value>{{ $instance.description }}</template>
</MkKeyValue>
<FormSection>
<MkKeyValue class="_formBlock" :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.description }}</template>
<template #value>{{ $instance.description }}</template>
</MkKeyValue>
<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
</FormSection>
<FormSection>
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.administrator }}</template>
<template #value>{{ $instance.maintainerName }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.contact }}</template>
<template #value>{{ $instance.maintainerEmail }}</template>
</MkKeyValue>
</FormSplit>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
</FormSection>
<FormSuspense :p="initStats">
<FormSection>
<template #label>{{ $ts.statistics }}</template>
<MkKeyValue class="_formBlock" :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
</MkKeyValue>
<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
</FormSection>
<FormSection>
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
<template #key>{{ $ts.administrator }}</template>
<template #value>{{ $instance.maintainerName }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
<template #key>{{ $ts.contact }}</template>
<template #value>{{ $instance.maintainerEmail }}</template>
</MkKeyValue>
</FormSplit>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
</FormSection>
</FormSuspense>
<FormSection>
<template #label>Well-known resources</template>
<div class="_formLinks">
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
</div>
</FormSection>
</div>
</MkSpacer>
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
<MkInstanceStats :chart-limit="500" :detailed="true"/>
</MkSpacer>
<FormSuspense :p="initStats">
<FormSection>
<template #label>{{ $ts.statistics }}</template>
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
<FormSection>
<template #label>Well-known resources</template>
<div class="_formLinks">
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
</div>
</FormSection>
</div>
</MkSpacer>
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
<MkInstanceStats :chart-limit="500" :detailed="true"/>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { version, instanceName } from '@/config';
import { version, instanceName , host } from '@/config';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue';
@@ -81,9 +84,8 @@ import MkKeyValue from '@/components/key-value.vue';
import MkInstanceStats from '@/components/instance-stats.vue';
import * as os from '@/os';
import number from '@/filters/number';
import * as symbols from '@/symbols';
import { host } from '@/config';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let stats = $ref(null);
let tab = $ref('overview');
@@ -93,23 +95,24 @@ const initStats = () => os.api('stats', {
stats = res;
});
defineExpose({
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.ts.instanceInfo,
icon: 'fas fa-info-circle',
bg: 'var(--bg)',
tabs: [{
active: tab === 'overview',
title: i18n.ts.overview,
onClick: () => { tab = 'overview'; },
}, {
active: tab === 'charts',
title: i18n.ts.charts,
icon: 'fas fa-chart-bar',
onClick: () => { tab = 'charts'; },
},],
})),
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => [{
active: tab === 'overview',
title: i18n.ts.overview,
onClick: () => { tab = 'overview'; },
}, {
active: tab === 'charts',
title: i18n.ts.charts,
icon: 'fas fa-chart-bar',
onClick: () => { tab = 'charts'; },
}]);
definePageMetadata(computed(() => ({
title: i18n.ts.instanceInfo,
icon: 'fas fa-info-circle',
bg: 'var(--bg)',
})));
</script>
<style lang="scss" scoped>

View File

@@ -1,48 +1,45 @@
<template>
<XModalWindow ref="dialog"
:width="370"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template v-if="file" #header>{{ file.name }}</template>
<div v-if="file" class="cxqhhsmd">
<div class="_section">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div class="info">
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
<div v-if="file" class="cxqhhsmd _formRoot">
<div class="_formBlock">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div class="info">
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
</div>
</div>
</div>
<div class="_section">
<div class="_content">
<div class="_formBlock">
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch>
</div>
</div>
<div class="_section">
<div class="_content">
<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton>
<FormLink class="_formBlock" :to="file.url" :external="true">Open</FormLink>
<FormLink class="_formBlock" :to="`/user-info/${file.userId}`">{{ $ts.user }}</FormLink>
<div class="_formBlock">
<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
</div>
<div v-if="info" class="_formBlock">
<details class="_content rawdata">
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
</details>
</div>
</div>
<div v-if="info" class="_section">
<details class="_content rawdata">
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
</details>
</div>
</div>
</XModalWindow>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import { computed } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/form/switch.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import FormLink from '@/components/form/link.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let file: any = $ref(null);
let info: any = $ref(null);
@@ -60,10 +57,6 @@ async function fetch() {
fetch();
function showUser() {
os.pageWindow(`/user-info/${file.userId}`);
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
@@ -72,7 +65,7 @@ async function del() {
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: file.id
fileId: file.id,
});
}
@@ -80,6 +73,16 @@ async function toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v });
isSensitive = v;
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
icon: 'fas fa-file',
bg: 'var(--bg)',
})));
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,249 @@
<template>
<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick">
<template v-if="metadata">
<div class="titleContainer" @click="showTabsPopup">
<i v-if="metadata.icon" class="icon" :class="metadata.icon"></i>
<div class="title">
<div class="title">{{ metadata.title }}</div>
</div>
</div>
<div class="tabs">
<button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
</button>
</div>
</template>
<div class="buttons right">
<template v-if="actions">
<template v-for="action in actions">
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, inject } from 'vue';
import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os';
import { url } from '@/config';
import { scrollToTop } from '@/scripts/scroll';
import MkButton from '@/components/ui/button.vue';
import { i18n } from '@/i18n';
import { globalEvents } from '@/events';
import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{
tabs?: {
title: string;
active: boolean;
icon?: string;
iconOnly?: boolean;
onClick: () => void;
}[];
actions?: {
text: string;
icon: string;
asFullButton?: boolean;
handler: (ev: MouseEvent) => void;
}[];
thin?: boolean;
}>();
const metadata = injectPageMetadata();
const el = ref<HTMLElement>(null);
const bg = ref(null);
const height = ref(0);
const hasTabs = computed(() => {
return props.tabs && props.tabs.length > 0;
});
const showTabsPopup = (ev: MouseEvent) => {
if (!hasTabs.value) return;
if (!narrow.value) return;
ev.preventDefault();
ev.stopPropagation();
const menu = props.tabs.map(tab => ({
text: tab.title,
icon: tab.icon,
action: tab.onClick,
}));
popupMenu(menu, ev.currentTarget ?? ev.target);
};
const preventDrag = (ev: TouchEvent) => {
ev.stopPropagation();
};
const onClick = () => {
scrollToTop(el.value, { behavior: 'smooth' });
};
const calcBg = () => {
const rawBg = metadata?.bg || 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
};
onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
});
onUnmounted(() => {
globalEvents.off('themeChanged', calcBg);
});
</script>
<style lang="scss" scoped>
.fdidabkc {
--height: 60px;
display: flex;
position: sticky;
top: var(--stickyTop, 0);
z-index: 1000;
width: 100%;
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
> .buttons {
--margin: 8px;
display: flex;
align-items: center;
height: var(--height);
margin: 0 var(--margin);
&.right {
margin-left: auto;
}
&:empty {
width: var(--height);
}
> .button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&.highlighted {
color: var(--accent);
}
}
> .fullButton {
& + .fullButton {
margin-left: 12px;
}
}
}
> .titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
> .subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
margin-left: 6px;
}
}
}
}
}
> .tabs {
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
> .tab {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
&:after {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
width: 100%;
height: 3px;
background: var(--accent);
}
}
> .icon + .title {
margin-left: 8px;
}
}
}
}
</style>

View File

@@ -1,28 +1,31 @@
<template>
<div class="lcixvhis">
<div class="_section reports">
<div class="_content">
<div class="inputs" style="display: flex;">
<MkSelect v-model="state" style="margin: 0; flex: 1;">
<template #label>{{ $ts.state }}</template>
<option value="all">{{ $ts.all }}</option>
<option value="unresolved">{{ $ts.unresolved }}</option>
<option value="resolved">{{ $ts.resolved }}</option>
</MkSelect>
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
<template #label>{{ $ts.reporteeOrigin }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
<template #label>{{ $ts.reporterOrigin }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
</div>
<!-- TODO
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="lcixvhis">
<div class="_section reports">
<div class="_content">
<div class="inputs" style="display: flex;">
<MkSelect v-model="state" style="margin: 0; flex: 1;">
<template #label>{{ $ts.state }}</template>
<option value="all">{{ $ts.all }}</option>
<option value="unresolved">{{ $ts.unresolved }}</option>
<option value="resolved">{{ $ts.resolved }}</option>
</MkSelect>
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
<template #label>{{ $ts.reporteeOrigin }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
<template #label>{{ $ts.reporterOrigin }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
</div>
<!-- TODO
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
<span>{{ $ts.username }}</span>
@@ -33,24 +36,27 @@
</div>
-->
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
</MkPagination>
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
</MkPagination>
</div>
</div>
</div>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import XAbuseReport from '@/components/abuse-report.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let reports = $ref<InstanceType<typeof MkPagination>>();
@@ -74,12 +80,14 @@ function resolved(reportId) {
reports.removeItem(item => item.id === reportId);
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.abuseReports,
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)',
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.abuseReports,
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,21 +1,23 @@
<template>
<MkSpacer :content-max="900">
<div class="uqshojas">
<div v-for="ad in ads" class="_panel _formRoot ad">
<MkAd v-if="ad.url" :specify="ad"/>
<MkInput v-model="ad.url" type="url" class="_formBlock">
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl" class="_formBlock">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<FormRadios v-model="ad.place" class="_formBlock">
<template #label>Form</template>
<option value="square">square</option>
<option value="horizontal">horizontal</option>
<option value="horizontal-big">horizontal-big</option>
</FormRadios>
<!--
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="uqshojas">
<div v-for="ad in ads" class="_panel _formRoot ad">
<MkAd v-if="ad.url" :specify="ad"/>
<MkInput v-model="ad.url" type="url" class="_formBlock">
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl" class="_formBlock">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<FormRadios v-model="ad.place" class="_formBlock">
<template #label>Form</template>
<option value="square">square</option>
<option value="horizontal">horizontal</option>
<option value="horizontal-big">horizontal-big</option>
</FormRadios>
<!--
<div style="margin: 32px 0;">
{{ i18n.ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
@@ -23,36 +25,38 @@
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div>
-->
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="buttons _formBlock">
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="buttons _formBlock">
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</div>
</div>
</MkSpacer>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let ads: any[] = $ref([]);
@@ -81,7 +85,7 @@ function remove(ad) {
if (canceled) return;
ads = ads.filter(x => x !== ad);
os.apiWithDialog('admin/ad/delete', {
id: ad.id
id: ad.id,
});
});
}
@@ -90,28 +94,29 @@ function save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
expiresAt: new Date(ad.expiresAt).getTime(),
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
expiresAt: new Date(ad.expiresAt).getTime(),
});
}
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,34 +1,40 @@
<template>
<div class="ztgjmzrw">
<section v-for="announcement in announcements" class="_card _gap announcements">
<div class="_content announcement">
<MkInput v-model="announcement.title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="announcement.text">
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkInput v-model="announcement.imageUrl">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
<div class="buttons">
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="ztgjmzrw">
<section v-for="announcement in announcements" class="_card _gap announcements">
<div class="_content announcement">
<MkInput v-model="announcement.title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="announcement.text">
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkInput v-model="announcement.imageUrl">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
<div class="buttons">
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</section>
</div>
</section>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let announcements: any[] = $ref([]);
@@ -41,7 +47,7 @@ function add() {
id: null,
title: '',
text: '',
imageUrl: null
imageUrl: null,
});
}
@@ -61,41 +67,42 @@ function save(announcement) {
os.api('admin/announcements/create', announcement).then(() => {
os.alert({
type: 'success',
text: i18n.ts.saved
text: i18n.ts.saved,
});
}).catch(err => {
os.alert({
type: 'error',
text: err
text: err,
});
});
} else {
os.api('admin/announcements/update', announcement).then(() => {
os.alert({
type: 'success',
text: i18n.ts.saved
text: i18n.ts.saved,
});
}).catch(err => {
os.alert({
type: 'error',
text: err
text: err,
});
});
}
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
});
</script>

View File

@@ -51,7 +51,6 @@ import FormButton from '@/components/ui/button.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue'));

View File

@@ -1,12 +1,13 @@
<template>
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
<template><MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
<template #key>{{ table[0] }}</template>
<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
</MkKeyValue>
</FormSuspense>
</MkSpacer>
</MkSpacer></MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -14,18 +15,20 @@ import { } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.database,
icon: 'fas fa-database',
bg: 'var(--bg)',
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.database,
icon: 'fas fa-database',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,49 +1,53 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="enableEmail" class="_formBlock">
<template #label>{{ i18n.ts.enableEmail }}</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</FormSwitch>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="enableEmail" class="_formBlock">
<template #label>{{ i18n.ts.enableEmail }}</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</FormSwitch>
<template v-if="enableEmail">
<FormInput v-model="email" type="email" class="_formBlock">
<template #label>{{ i18n.ts.emailAddress }}</template>
</FormInput>
<template v-if="enableEmail">
<FormInput v-model="email" type="email" class="_formBlock">
<template #label>{{ i18n.ts.emailAddress }}</template>
</FormInput>
<FormSection>
<template #label>{{ i18n.ts.smtpConfig }}</template>
<FormSplit :min-width="280">
<FormInput v-model="smtpHost" class="_formBlock">
<template #label>{{ i18n.ts.smtpHost }}</template>
</FormInput>
<FormInput v-model="smtpPort" type="number" class="_formBlock">
<template #label>{{ i18n.ts.smtpPort }}</template>
</FormInput>
</FormSplit>
<FormSplit :min-width="280">
<FormInput v-model="smtpUser" class="_formBlock">
<template #label>{{ i18n.ts.smtpUser }}</template>
</FormInput>
<FormInput v-model="smtpPass" type="password" class="_formBlock">
<template #label>{{ i18n.ts.smtpPass }}</template>
</FormInput>
</FormSplit>
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormSwitch v-model="smtpSecure" class="_formBlock">
<template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</FormSwitch>
</FormSection>
</template>
</div>
</FormSuspense>
</MkSpacer>
<FormSection>
<template #label>{{ i18n.ts.smtpConfig }}</template>
<FormSplit :min-width="280">
<FormInput v-model="smtpHost" class="_formBlock">
<template #label>{{ i18n.ts.smtpHost }}</template>
</FormInput>
<FormInput v-model="smtpPort" type="number" class="_formBlock">
<template #label>{{ i18n.ts.smtpPort }}</template>
</FormInput>
</FormSplit>
<FormSplit :min-width="280">
<FormInput v-model="smtpUser" class="_formBlock">
<template #label>{{ i18n.ts.smtpUser }}</template>
</FormInput>
<FormInput v-model="smtpPass" type="password" class="_formBlock">
<template #label>{{ i18n.ts.smtpPass }}</template>
</FormInput>
</FormSplit>
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormSwitch v-model="smtpSecure" class="_formBlock">
<template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</FormSwitch>
</FormSection>
</template>
</div>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormInfo from '@/components/ui/info.vue';
@@ -51,9 +55,9 @@ import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance, instance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let enableEmail: boolean = $ref(false);
let email: any = $ref(null);
@@ -78,13 +82,13 @@ async function testEmail() {
const { canceled, result: destination } = await os.inputText({
title: i18n.ts.destination,
type: 'email',
placeholder: instance.maintainerEmail
placeholder: instance.maintainerEmail,
});
if (canceled) return;
os.apiWithDialog('admin/send-email', {
to: destination,
subject: 'Test email',
text: 'Yo'
text: 'Yo',
});
}
@@ -102,21 +106,22 @@ function save() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
text: i18n.ts.testEmail,
handler: testEmail,
}, {
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
text: i18n.ts.testEmail,
handler: testEmail,
}, {
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,69 +1,75 @@
<template>
<MkSpacer :content-max="900">
<div class="ogwlenmc">
<div v-if="tab === 'local'" class="local">
<MkInput v-model="query" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
<template #label>Select mode</template>
</MkSwitch>
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkButton inline @click="selectAll">Select all</MkButton>
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
<MkButton inline @click="addTagBulk">Add tag</MkButton>
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
<MkButton inline @click="setTagBulk">Set tag</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton>
</div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.category }}</div>
</div>
</button>
<div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="ogwlenmc">
<div v-if="tab === 'local'" class="local">
<MkInput v-model="query" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
<template #label>Select mode</template>
</MkSwitch>
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkButton inline @click="selectAll">Select all</MkButton>
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
<MkButton inline @click="addTagBulk">Add tag</MkButton>
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
<MkButton inline @click="setTagBulk">Set tag</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton>
</div>
</template>
</MkPagination>
</div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template #default="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.category }}</div>
</div>
</button>
</div>
</template>
</MkPagination>
</div>
<div v-else-if="tab === 'remote'" class="remote">
<FormSplit>
<MkInput v-model="queryRemote" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkInput v-model="host" :debounce="true">
<template #label>{{ $ts.host }}</template>
</MkInput>
</FormSplit>
<MkPagination :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div>
<div v-else-if="tab === 'remote'" class="remote">
<FormSplit>
<MkInput v-model="queryRemote" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkInput v-model="host" :debounce="true">
<template #label>{{ $ts.host }}</template>
</MkInput>
</FormSplit>
<MkPagination :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template #default="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
</MkPagination>
</div>
</div>
</MkSpacer>
</template>
</MkPagination>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue';
@@ -72,8 +78,8 @@ import MkSwitch from '@/components/form/switch.vue';
import FormSplit from '@/components/form/split.vue';
import { selectFile, selectFiles } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
@@ -131,13 +137,13 @@ const add = async (ev: MouseEvent) => {
const edit = (emoji) => {
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
emoji: emoji
emoji: emoji,
}, {
done: result => {
if (result.updated) {
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
...oldEmoji,
...result.updated
...result.updated,
}));
} else if (result.deleted) {
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
@@ -159,7 +165,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
}, {
text: i18n.ts.import,
icon: 'fas fa-plus',
action: () => { im(emoji) }
action: () => { im(emoji); },
}], ev.currentTarget ?? ev.target);
};
@@ -181,7 +187,7 @@ const menu = (ev: MouseEvent) => {
text: err.message,
});
});
}
},
}, {
icon: 'fas fa-upload',
text: i18n.ts.import,
@@ -201,7 +207,7 @@ const menu = (ev: MouseEvent) => {
text: err.message,
});
});
}
},
}], ev.currentTarget ?? ev.target);
};
@@ -265,31 +271,31 @@ const delBulk = async () => {
emojisPaginationComponent.value.reload();
};
defineExpose({
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addEmoji,
handler: add,
}, {
icon: 'fas fa-ellipsis-h',
handler: menu,
}],
tabs: [{
active: tab.value === 'local',
title: i18n.ts.local,
onClick: () => { tab.value = 'local'; },
}, {
active: tab.value === 'remote',
title: i18n.ts.remote,
onClick: () => { tab.value = 'remote'; },
},]
})),
});
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addEmoji,
handler: add,
}, {
icon: 'fas fa-ellipsis-h',
handler: menu,
}]);
const headerTabs = $computed(() => [{
active: tab.value === 'local',
title: i18n.ts.local,
onClick: () => { tab.value = 'local'; },
}, {
active: tab.value === 'remote',
title: i18n.ts.remote,
onClick: () => { tab.value = 'remote'; },
}]);
definePageMetadata(computed(() => ({
title: i18n.ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
})));
</script>
<style lang="scss" scoped>

View File

@@ -1,61 +1,58 @@
<template>
<div class="xrmjdkdw">
<MkContainer :foldable="true" class="lookup">
<template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template>
<div class="xrmjdkdw-lookup">
<MkInput v-model="q" class="item" type="text" @enter="find()">
<template #label>{{ $ts.fileIdOrUrl }}</template>
</MkInput>
<MkButton primary @click="find()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
</div>
</MkContainer>
<div class="_section">
<div class="_content">
<div class="inputs" style="display: flex;">
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
<template #label>{{ $ts.instance }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
<template #label>{{ $ts.host }}</template>
</MkInput>
</div>
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
<template #label>MIME type</template>
</MkInput>
</div>
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief">
<button v-for="file in items" :key="file.id" class="file _panel _button _gap" @click="show(file, $event)">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div class="body">
<div>
<small style="opacity: 0.7;">{{ file.name }}</small>
</div>
<div>
<MkAcct v-if="file.user" :user="file.user"/>
<div v-else>{{ $ts.system }}</div>
</div>
<div>
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
</div>
<div>
<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
</div>
<div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions"/></template>
<MkSpacer :content-max="900">
<div class="xrmjdkdw">
<div>
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
<template #label>{{ $ts.instance }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
<template #label>{{ $ts.host }}</template>
</MkInput>
</div>
</button>
</MkPagination>
</div>
</div>
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
<template #label>MIME type</template>
</MkInput>
</div>
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _button" @click="show(file, $event)">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div v-if="viewMode === 'list'" class="body">
<div>
<small style="opacity: 0.7;">{{ file.name }}</small>
</div>
<div>
<MkAcct v-if="file.user" :user="file.user"/>
<div v-else>{{ $ts.system }}</div>
</div>
<div>
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
</div>
<div>
<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
</div>
</div>
</button>
</MkPagination>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent } from 'vue';
import * as Acct from 'misskey-js/built/acct';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@@ -64,13 +61,13 @@ import MkContainer from '@/components/ui/container.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let q = $ref(null);
let origin = $ref('local');
let type = $ref(null);
let searchHost = $ref('');
let viewMode = $ref('grid');
const pagination = {
endpoint: 'admin/drive/files' as const,
limit: 10,
@@ -93,83 +90,98 @@ function clear() {
}
function show(file) {
os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), {
fileId: file.id
}, {}, 'closed');
os.pageWindow(`/admin/file/${file.id}`);
}
function find() {
async function find() {
const { canceled, result: q } = await os.inputText({
title: i18n.ts.fileIdOrUrl,
allowEmpty: false,
});
if (canceled) return;
os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
show(file);
}).catch(err => {
if (err.code === 'NO_SUCH_FILE') {
os.alert({
type: 'error',
text: i18n.ts.notFound
text: i18n.ts.notFound,
});
}
});
}
defineExpose({
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.ts.files,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
text: i18n.ts.clearCachedFiles,
icon: 'fas fa-trash-alt',
handler: clear,
}],
})),
});
const headerActions = $computed(() => [{
text: i18n.ts.lookup,
icon: 'fas fa-search',
handler: find,
}, {
text: i18n.ts.clearCachedFiles,
icon: 'fas fa-trash-alt',
handler: clear,
}]);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.files,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
})));
</script>
<style lang="scss" scoped>
.xrmjdkdw {
margin: var(--margin);
> .lookup {
margin-bottom: 16px;
}
.urempief {
margin-top: var(--margin);
> .file {
display: flex;
width: 100%;
box-sizing: border-box;
text-align: left;
align-items: center;
&.list {
> .file {
display: flex;
width: 100%;
box-sizing: border-box;
text-align: left;
align-items: center;
&:hover {
color: var(--accent);
&:hover {
color: var(--accent);
}
> .thumbnail {
width: 128px;
height: 128px;
}
> .body {
margin-left: 0.3em;
padding: 8px;
flex: 1;
@media (max-width: 500px) {
font-size: 14px;
}
}
}
}
> .thumbnail {
width: 128px;
height: 128px;
}
&.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-gap: 12px;
margin: var(--margin) 0;
> .body {
margin-left: 0.3em;
padding: 8px;
flex: 1;
@media (max-width: 500px) {
font-size: 14px;
> .file {
aspect-ratio: 1;
> .thumbnail {
width: 100%;
height: 100%;
}
}
}
}
}
.xrmjdkdw-lookup {
padding: 16px;
> .item {
margin-bottom: 16px;
}
}
</style>

View File

@@ -1,8 +1,6 @@
<template>
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
<div v-if="!narrow || initialPage == null" class="nav">
<MkHeader :info="header"></MkHeader>
<div v-if="!narrow || initialPage == null" class="nav">
<MkSpacer :content-max="700" :margin-min="16">
<div class="lxpfedzu">
<div class="banner">
@@ -17,29 +15,26 @@
</MkSpacer>
</div>
<div v-if="!(narrow && initialPage == null)" class="main">
<MkStickyContainer>
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
</MkStickyContainer>
<component :is="component" :key="initialPage" v-bind="pageProps"/>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
import { i18n } from '@/i18n';
import MkSuperMenu from '@/components/ui/super-menu.vue';
import MkInfo from '@/components/ui/info.vue';
import { scroll } from '@/scripts/scroll';
import { instance } from '@/instance';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { lookupUser } from '@/scripts/lookup-user';
import { MisskeyNavigator } from '@/scripts/navigate';
import { useRouter } from '@/router';
import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
const isEmpty = (x: string | null) => x == null || x === '';
const nav = new MisskeyNavigator();
const router = useRouter();
const indexInfo = {
title: i18n.ts.controlPanel,
@@ -224,7 +219,7 @@ watch(component, () => {
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
router.push('/admin/overview');
} else {
if (props.initialPage == null) {
INFO = indexInfo;
@@ -234,7 +229,7 @@ watch(() => props.initialPage, () => {
watch(narrow, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
router.push('/admin/overview');
}
});
@@ -243,7 +238,7 @@ onMounted(() => {
narrow = el.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
router.push('/admin/overview');
}
});
@@ -251,19 +246,19 @@ onUnmounted(() => {
ro.disconnect();
});
const pageChanged = (page) => {
if (page == null) {
provideMetadataReceiver((info) => {
if (info == null) {
childInfo = null;
} else {
childInfo = page[symbols.PAGE_INFO];
childInfo = info;
}
};
});
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
text: x.code
text: x.code,
});
}).catch(err => {
os.alert({
@@ -279,33 +274,38 @@ const lookup = (ev) => {
icon: 'fas fa-user',
action: () => {
lookupUser();
}
},
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
}
},
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
}
},
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
}
},
}], ev.currentTarget ?? ev.target);
};
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(INFO);
defineExpose({
[symbols.PAGE_INFO]: INFO,
header: {
title: i18n.ts.controlPanel,
}
},
});
</script>

View File

@@ -1,25 +1,29 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock">
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</FormTextarea>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock">
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</FormTextarea>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</FormSuspense>
</MkSpacer>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import FormButton from '@/components/ui/button.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let blockedHosts: string = $ref('');
@@ -36,11 +40,13 @@ function save() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,5 +1,6 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<template><MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-twitter"></i></template>
@@ -20,19 +21,19 @@
<XDiscord/>
</FormFolder>
</FormSuspense>
</MkSpacer>
</MkSpacer></MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import FormFolder from '@/components/form/folder.vue';
import FormSuspense from '@/components/form/suspense.vue';
import XTwitter from './integrations.twitter.vue';
import XGithub from './integrations.github.vue';
import XDiscord from './integrations.discord.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormFolder from '@/components/form/folder.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let enableTwitterIntegration: boolean = $ref(false);
let enableGithubIntegration: boolean = $ref(false);
@@ -45,11 +46,13 @@ async function init() {
enableDiscordIntegration = meta.enableDiscordIntegration;
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
});
</script>

View File

@@ -132,7 +132,7 @@ export default defineComponent({
overviewHeight: '1fr',
queueHeight: '1fr',
paused: false,
}
};
},
computed: {

View File

@@ -1,72 +1,76 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
<template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageBucket" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</FormInput>
<FormInput v-model="objectStoragePrefix" class="_formBlock">
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageRegion" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput>
<FormSplit :min-width="280">
<FormInput v-model="objectStorageAccessKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Access key</template>
<template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageSecretKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Secret key</template>
<FormInput v-model="objectStorageBucket" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</FormInput>
</FormSplit>
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</FormSwitch>
<FormInput v-model="objectStoragePrefix" class="_formBlock">
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</FormInput>
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</FormSwitch>
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormInput>
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</FormSwitch>
<FormInput v-model="objectStorageRegion" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput>
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
<template #label>s3ForcePathStyle</template>
</FormSwitch>
</template>
</div>
</FormSuspense>
</MkSpacer>
<FormSplit :min-width="280">
<FormInput v-model="objectStorageAccessKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Access key</template>
</FormInput>
<FormInput v-model="objectStorageSecretKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Secret key</template>
</FormInput>
</FormSplit>
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
<template #label>s3ForcePathStyle</template>
</FormSwitch>
</template>
</div>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormGroup from '@/components/form/group.vue';
@@ -74,9 +78,9 @@ import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let useObjectStorage: boolean = $ref(false);
let objectStorageBaseUrl: string | null = $ref(null);
@@ -129,17 +133,18 @@ function save() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,18 +1,22 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
none
</FormSuspense>
</MkSpacer>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
none
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
async function init() {
await os.api('admin/meta');
@@ -24,17 +28,18 @@ function save() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
});
</script>

View File

@@ -35,7 +35,7 @@
</MkContainer>
</div>
<!--<XMetrics/>-->
<!--<XMetrics/>-->
<MkFolder style="margin: var(--margin)">
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
@@ -67,6 +67,7 @@
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import XMetrics from './metrics.vue';
import MkInstanceStats from '@/components/instance-stats.vue';
import MkNumberDiff from '@/components/number-diff.vue';
import MkContainer from '@/components/ui/container.vue';
@@ -74,11 +75,10 @@ import MkFolder from '@/components/ui/folder.vue';
import MkQueueChart from '@/components/queue-chart.vue';
import { version, url } from '@/config';
import number from '@/filters/number';
import XMetrics from './metrics.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let stats: any = $ref(null);
let serverInfo: any = $ref(null);
@@ -106,7 +106,7 @@ onMounted(async () => {
nextTick(() => {
queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
length: 200,
});
});
});
@@ -115,12 +115,14 @@ onBeforeUnmount(() => {
queueStatsConnection.dispose();
});
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.dashboard,
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)',
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.dashboard,
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,5 +1,6 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<template><MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue class="_formBlock">
@@ -9,7 +10,7 @@
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
</FormSuspense>
</MkSpacer>
</MkSpacer></MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -19,9 +20,9 @@ import FormButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let proxyAccount: any = $ref(null);
let proxyAccountId: any = $ref(null);
@@ -50,11 +51,13 @@ function save() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,26 +1,30 @@
<template>
<MkSpacer :content-max="800">
<XQueue :connection="connection" domain="inbox">
<template #title>In</template>
</XQueue>
<XQueue :connection="connection" domain="deliver">
<template #title>Out</template>
</XQueue>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
</MkSpacer>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<XQueue :connection="connection" domain="inbox">
<template #title>In</template>
</XQueue>
<XQueue :connection="connection" domain="deliver">
<template #title>Out</template>
</XQueue>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols';
import * as config from '@/config';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const connection = markRaw(stream.useChannel('queueStats'))
const connection = markRaw(stream.useChannel('queueStats'));
function clear() {
os.confirm({
@@ -38,28 +42,29 @@ onMounted(() => {
nextTick(() => {
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
length: 200,
});
});
})
});
onBeforeUnmount(() => {
connection.dispose();
});
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: i18n.ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: i18n.ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,24 +1,28 @@
<template>
<MkSpacer :content-max="800">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div class="status">
<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
<i v-else class="fas fa-clock icon requesting"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div class="status">
<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
<i v-else class="fas fa-clock icon requesting"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</MkSpacer>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let relays: any[] = $ref([]);
@@ -26,30 +30,30 @@ async function addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: i18n.ts.addRelay,
type: 'url',
placeholder: i18n.ts.inboxUrl
placeholder: i18n.ts.inboxUrl,
});
if (canceled) return;
os.api('admin/relays/add', {
inbox
inbox,
}).then((relay: any) => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
text: err.message || err,
});
});
}
function remove(inbox: string) {
os.api('admin/relays/remove', {
inbox
inbox,
}).then(() => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
text: err.message || err,
});
});
}
@@ -62,18 +66,19 @@ function refresh() {
refresh();
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addRelay,
handler: addRelay,
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addRelay,
handler: addRelay,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,36 +1,41 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormFolder class="_formBlock">
<template #icon><i class="fas fa-shield-alt"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormFolder class="_formBlock">
<template #icon><i class="fas fa-shield-alt"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<XBotProtection/>
</FormFolder>
<XBotProtection/>
</FormFolder>
<FormFolder class="_formBlock">
<template #label>Summaly Proxy</template>
<FormFolder class="_formBlock">
<template #label>Summaly Proxy</template>
<div class="_formRoot">
<FormInput v-model="summalyProxy" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>Summaly Proxy URL</template>
</FormInput>
<div class="_formRoot">
<FormInput v-model="summalyProxy" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>Summaly Proxy URL</template>
</FormInput>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</div>
</FormFolder>
</div>
</FormSuspense>
</MkSpacer>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</div>
</FormFolder>
</div>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XBotProtection from './bot-protection.vue';
import XHeader from './_header_.vue';
import FormFolder from '@/components/form/folder.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInfo from '@/components/ui/info.vue';
@@ -38,11 +43,10 @@ import FormSuspense from '@/components/form/suspense.vue';
import FormSection from '@/components/form/section.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import XBotProtection from './bot-protection.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let summalyProxy: string = $ref('');
let enableHcaptcha: boolean = $ref(false);
@@ -63,11 +67,13 @@ function save() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,149 +1,155 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormInput v-model="name" class="_formBlock">
<template #label>{{ i18n.ts.instanceName }}</template>
</FormInput>
<FormTextarea v-model="description" class="_formBlock">
<template #label>{{ i18n.ts.instanceDescription }}</template>
</FormTextarea>
<FormInput v-model="tosUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.tosUrl }}</template>
</FormInput>
<FormSplit :min-width="300">
<FormInput v-model="maintainerName" class="_formBlock">
<template #label>{{ i18n.ts.maintainerName }}</template>
</FormInput>
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
<template #prefix><i class="fas fa-envelope"></i></template>
<template #label>{{ i18n.ts.maintainerEmail }}</template>
</FormInput>
</FormSplit>
<FormTextarea v-model="pinnedUsers" class="_formBlock">
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</FormTextarea>
<FormSection>
<FormSwitch v-model="enableRegistration" class="_formBlock">
<template #label>{{ i18n.ts.enableRegistration }}</template>
</FormSwitch>
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</FormSwitch>
</FormSection>
<FormSection>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts.theme }}</template>
<FormInput v-model="iconUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.iconUrl }}</template>
</FormInput>
<FormInput v-model="bannerUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</FormInput>
<FormInput v-model="backgroundImageUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</FormInput>
<FormInput v-model="themeColor" class="_formBlock">
<template #prefix><i class="fas fa-palette"></i></template>
<template #label>{{ i18n.ts.themeColor }}</template>
<template #caption>#RRGGBB</template>
</FormInput>
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts.files }}</template>
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
<FormSplit :min-width="280">
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ i18n.ts.inMb }}</template>
<div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormInput v-model="name" class="_formBlock">
<template #label>{{ i18n.ts.instanceName }}</template>
</FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
</FormSplit>
</FormSection>
<FormTextarea v-model="description" class="_formBlock">
<template #label>{{ i18n.ts.instanceDescription }}</template>
</FormTextarea>
<FormSection>
<template #label>ServiceWorker</template>
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
<template #label>{{ i18n.ts.enableServiceworker }}</template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</FormSwitch>
<template v-if="enableServiceWorker">
<FormInput v-model="swPublicKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Public key</template>
<FormInput v-model="tosUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.tosUrl }}</template>
</FormInput>
<FormInput v-model="swPrivateKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Private key</template>
</FormInput>
</template>
</FormSection>
<FormSplit :min-width="300">
<FormInput v-model="maintainerName" class="_formBlock">
<template #label>{{ i18n.ts.maintainerName }}</template>
</FormInput>
<FormSection>
<template #label>DeepL Translation</template>
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
<template #prefix><i class="fas fa-envelope"></i></template>
<template #label>{{ i18n.ts.maintainerEmail }}</template>
</FormInput>
</FormSplit>
<FormInput v-model="deeplAuthKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>DeepL Auth Key</template>
</FormInput>
<FormSwitch v-model="deeplIsPro" class="_formBlock">
<template #label>Pro account</template>
</FormSwitch>
</FormSection>
</div>
</FormSuspense>
</MkSpacer>
<FormTextarea v-model="pinnedUsers" class="_formBlock">
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</FormTextarea>
<FormSection>
<FormSwitch v-model="enableRegistration" class="_formBlock">
<template #label>{{ i18n.ts.enableRegistration }}</template>
</FormSwitch>
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</FormSwitch>
</FormSection>
<FormSection>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts.theme }}</template>
<FormInput v-model="iconUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.iconUrl }}</template>
</FormInput>
<FormInput v-model="bannerUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</FormInput>
<FormInput v-model="backgroundImageUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</FormInput>
<FormInput v-model="themeColor" class="_formBlock">
<template #prefix><i class="fas fa-palette"></i></template>
<template #label>{{ i18n.ts.themeColor }}</template>
<template #caption>#RRGGBB</template>
</FormInput>
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts.files }}</template>
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
<FormSplit :min-width="280">
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
</FormSplit>
</FormSection>
<FormSection>
<template #label>ServiceWorker</template>
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
<template #label>{{ i18n.ts.enableServiceworker }}</template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</FormSwitch>
<template v-if="enableServiceWorker">
<FormInput v-model="swPublicKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Public key</template>
</FormInput>
<FormInput v-model="swPrivateKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>Private key</template>
</FormInput>
</template>
</FormSection>
<FormSection>
<template #label>DeepL Translation</template>
<FormInput v-model="deeplAuthKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
<template #label>DeepL Auth Key</template>
</FormInput>
<FormSwitch v-model="deeplIsPro" class="_formBlock">
<template #label>Pro account</template>
</FormSwitch>
</FormSection>
</div>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@@ -152,9 +158,9 @@ import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let name: string | null = $ref(null);
let description: string | null = $ref(null);
@@ -240,17 +246,18 @@ function save() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,76 +1,84 @@
<template>
<div class="lknzcolw">
<div class="users">
<div class="inputs">
<MkSelect v-model="sort" style="flex: 1;">
<template #label>{{ $ts.sort }}</template>
<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
</MkSelect>
<MkSelect v-model="state" style="flex: 1;">
<template #label>{{ $ts.state }}</template>
<option value="all">{{ $ts.all }}</option>
<option value="available">{{ $ts.normal }}</option>
<option value="admin">{{ $ts.administrator }}</option>
<option value="moderator">{{ $ts.moderator }}</option>
<option value="silenced">{{ $ts.silence }}</option>
<option value="suspended">{{ $ts.suspend }}</option>
</MkSelect>
<MkSelect v-model="origin" style="flex: 1;">
<template #label>{{ $ts.instance }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
</div>
<div class="inputs">
<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
<template #prefix>@</template>
<template #label>{{ $ts.username }}</template>
</MkInput>
<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
<template #prefix>@</template>
<template #label>{{ $ts.host }}</template>
</MkInput>
</div>
<div>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="lknzcolw">
<div class="users">
<div class="inputs">
<MkSelect v-model="sort" style="flex: 1;">
<template #label>{{ $ts.sort }}</template>
<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
</MkSelect>
<MkSelect v-model="state" style="flex: 1;">
<template #label>{{ $ts.state }}</template>
<option value="all">{{ $ts.all }}</option>
<option value="available">{{ $ts.normal }}</option>
<option value="admin">{{ $ts.administrator }}</option>
<option value="moderator">{{ $ts.moderator }}</option>
<option value="silenced">{{ $ts.silence }}</option>
<option value="suspended">{{ $ts.suspend }}</option>
</MkSelect>
<MkSelect v-model="origin" style="flex: 1;">
<template #label>{{ $ts.instance }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
</div>
<div class="inputs">
<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
<template #prefix>@</template>
<template #label>{{ $ts.username }}</template>
</MkInput>
<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
<template #prefix>@</template>
<template #label>{{ $ts.host }}</template>
</MkInput>
</div>
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
<div class="body">
<header>
<MkUserName class="name" :user="user"/>
<span class="acct">@{{ acct(user) }}</span>
<span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span>
<span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span>
<span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span>
<span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span>
</header>
<div>
<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
</div>
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
<div class="body">
<header>
<MkUserName class="name" :user="user"/>
<span class="acct">@{{ acct(user) }}</span>
<span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span>
<span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span>
<span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span>
<span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span>
</header>
<div>
<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
</div>
</div>
</button>
</MkPagination>
</div>
</button>
</MkPagination>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import { acct } from '@/filters/user';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { lookupUser } from '@/scripts/lookup-user';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let paginationComponent = $ref<InstanceType<typeof MkPagination>>();
@@ -89,7 +97,7 @@ const pagination = {
username: searchUsername,
hostname: searchHost,
})),
offsetMode: true
offsetMode: true,
};
function searchUser() {
@@ -106,7 +114,7 @@ async function addUser() {
const { canceled: canceled2, result: password } = await os.inputText({
title: i18n.ts.password,
type: 'password'
type: 'password',
});
if (canceled2) return;
@@ -122,34 +130,34 @@ function show(user) {
os.pageWindow(`/user-info/${user.id}`);
}
defineExpose({
[symbols.PAGE_INFO]: computed(() => ({
title: i18n.ts.users,
icon: 'fas fa-users',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-search',
text: i18n.ts.search,
handler: searchUser
}, {
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addUser,
handler: addUser
}, {
asFullButton: true,
icon: 'fas fa-search',
text: i18n.ts.lookup,
handler: lookupUser
}],
})),
});
const headerActions = $computed(() => [{
icon: 'fas fa-search',
text: i18n.ts.search,
handler: searchUser,
}, {
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addUser,
handler: addUser,
}, {
asFullButton: true,
icon: 'fas fa-search',
text: i18n.ts.lookup,
handler: lookupUser,
}]);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.users,
icon: 'fas fa-users',
bg: 'var(--bg)',
})));
</script>
<style lang="scss" scoped>
.lknzcolw {
> .users {
margin: var(--margin);
> .inputs {
display: flex;

View File

@@ -1,57 +1,53 @@
<template>
<MkSpacer :content-max="800">
<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content">
<Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div>
<div v-if="$i && !announcement.isRead" class="_footer">
<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
</div>
</section>
</MkPagination>
</MkSpacer>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content">
<Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div>
<div v-if="$i && !announcement.isRead" class="_footer">
<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
</div>
</section>
</MkPagination>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
export default defineComponent({
components: {
MkPagination,
MkButton
},
const pagination = {
endpoint: 'announcements' as const,
limit: 10,
};
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
},
pagination: {
endpoint: 'announcements' as const,
limit: 10,
},
};
},
// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
function read(items, announcement, i) {
items[i] = {
...announcement,
isRead: true,
};
os.api('i/read-announcement', { announcementId: announcement.id });
}
methods: {
// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
read(items, announcement, i) {
items[i] = {
...announcement,
isRead: true,
};
os.api('i/read-announcement', { announcementId: announcement.id });
},
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
});
</script>

View File

@@ -1,8 +1,9 @@
<template>
<div v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
<div class="tl _block">
<XTimeline ref="tl" :key="antennaId"
<XTimeline
ref="tlEl" :key="antennaId"
class="tl"
src="antenna"
:antenna="antennaId"
@@ -13,92 +14,78 @@
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, computed } from 'vue';
<script lang="ts" setup>
import { computed, inject, watch } from 'vue';
import XTimeline from '@/components/timeline.vue';
import { scroll } from '@/scripts/scroll';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { useRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata';
import i18n from '@/components/global/i18n';
export default defineComponent({
components: {
XTimeline,
},
const router = useRouter();
props: {
antennaId: {
type: String,
required: true
}
},
const props = defineProps<{
antennaId: string;
}>();
data() {
return {
antenna: null,
queue: 0,
[symbols.PAGE_INFO]: computed(() => this.antenna ? {
title: this.antenna.name,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-calendar-alt',
text: this.$ts.jumpToSpecifiedDate,
handler: this.timetravel
}, {
icon: 'fas fa-cog',
text: this.$ts.settings,
handler: this.settings
}],
} : null),
};
},
let antenna = $ref(null);
let queue = $ref(0);
let rootEl = $ref<HTMLElement>();
let tlEl = $ref<InstanceType<typeof XTimeline>>();
const keymap = $computed(() => ({
't': focus,
}));
computed: {
keymap(): any {
return {
't': this.focus
};
},
},
function queueUpdated(q) {
queue = q;
}
watch: {
antennaId: {
async handler() {
this.antenna = await os.api('antennas/show', {
antennaId: this.antennaId
});
},
immediate: true
}
},
function top() {
scroll(rootEl, { top: 0 });
}
methods: {
queueUpdated(q) {
this.queue = q;
},
async function timetravel() {
const { canceled, result: date } = await os.inputDate({
title: i18n.ts.date,
});
if (canceled) return;
top() {
scroll(this.$el, { top: 0 });
},
tlEl.timetravel(date);
}
async timetravel() {
const { canceled, result: date } = await os.inputDate({
title: this.$ts.date,
});
if (canceled) return;
function settings() {
router.push(`/my/antennas/${props.antennaId}`);
}
this.$refs.tl.timetravel(date);
},
function focus() {
tlEl.focus();
}
settings() {
this.$router.push(`/my/antennas/${this.antennaId}`);
},
watch(() => props.antennaId, async () => {
antenna = await os.api('antennas/show', {
antennaId: props.antennaId,
});
}, { immediate: true });
focus() {
(this.$refs.tl as any).focus();
}
}
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => antenna ? {
title: antenna.name,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-calendar-alt',
text: i18n.ts.jumpToSpecifiedDate,
handler: timetravel,
}, {
icon: 'fas fa-cog',
text: i18n.ts.settings,
handler: settings,
}],
} : null));
</script>
<style lang="scss" scoped>

View File

@@ -1,40 +1,43 @@
<template>
<MkSpacer :content-max="700">
<div class="_formRoot">
<div class="_formBlock">
<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
<template #label>Endpoint</template>
</MkInput>
<MkTextarea v-model="body" class="_formBlock" code>
<template #label>Params (JSON or JSON5)</template>
</MkTextarea>
<MkSwitch v-model="withCredential" class="_formBlock">
With credential
</MkSwitch>
<MkButton class="_formBlock" primary :disabled="sending" @click="send">
<template v-if="sending"><MkEllipsis/></template>
<template v-else><i class="fas fa-paper-plane"></i> Send</template>
</MkButton>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="_formRoot">
<div class="_formBlock">
<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
<template #label>Endpoint</template>
</MkInput>
<MkTextarea v-model="body" class="_formBlock" code>
<template #label>Params (JSON or JSON5)</template>
</MkTextarea>
<MkSwitch v-model="withCredential" class="_formBlock">
With credential
</MkSwitch>
<MkButton class="_formBlock" primary :disabled="sending" @click="send">
<template v-if="sending"><MkEllipsis/></template>
<template v-else><i class="fas fa-paper-plane"></i> Send</template>
</MkButton>
</div>
<div v-if="res" class="_formBlock">
<MkTextarea v-model="res" code readonly tall>
<template #label>Response</template>
</MkTextarea>
</div>
</div>
<div v-if="res" class="_formBlock">
<MkTextarea v-model="res" code readonly tall>
<template #label>Response</template>
</MkTextarea>
</div>
</div>
</MkSpacer>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import JSON5 from 'json5';
import { Endpoints } from 'misskey-js';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { Endpoints } from 'misskey-js';
import { definePageMetadata } from '@/scripts/page-metadata';
const body = ref('{}');
const endpoint = ref('');
@@ -75,10 +78,12 @@ function onEndpointChange() {
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: 'API console',
icon: 'fas fa-terminal'
},
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: 'API console',
icon: 'fas fa-terminal',
});
</script>

View File

@@ -32,7 +32,7 @@ export default defineComponent({
computed: {
name(): string {
const el = document.createElement('div');
el.textContent = this.app.name
el.textContent = this.app.name;
return el.innerHTML;
},
app(): any {

View File

@@ -15,7 +15,7 @@
<h1>{{ $ts._auth.denied }}</h1>
</div>
<div v-if="state == 'accepted'" class="accepted">
<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$ts.allowed }}</h1>
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : $ts.allowed }}</h1>
<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p>
<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
</div>
@@ -40,24 +40,20 @@ export default defineComponent({
XForm,
MkSignin,
},
props: ['token'],
data() {
return {
state: null,
session: null,
fetching: true
fetching: true,
};
},
computed: {
token(): string {
return this.$route.params.token;
}
},
mounted() {
if (!this.$i) return;
// Fetch session
os.api('auth/session/show', {
token: this.token
token: this.token,
}).then(session => {
this.session = session;
this.fetching = false;
@@ -65,7 +61,7 @@ export default defineComponent({
// 既に連携していた場合
if (this.session.app.isAuthorized) {
os.api('auth/accept', {
token: this.session.token
token: this.session.token,
}).then(() => {
this.accepted();
});
@@ -85,8 +81,8 @@ export default defineComponent({
}
}, onLogin(res) {
login(res.i);
}
}
},
},
});
</script>

View File

@@ -1,127 +1,122 @@
<template>
<MkSpacer :content-max="700">
<div class="_formRoot">
<MkInput v-model="name" class="_formBlock">
<template #label>{{ $ts.name }}</template>
</MkInput>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="_formRoot">
<MkInput v-model="name" class="_formBlock">
<template #label>{{ $ts.name }}</template>
</MkInput>
<MkTextarea v-model="description" class="_formBlock">
<template #label>{{ $ts.description }}</template>
</MkTextarea>
<MkTextarea v-model="description" class="_formBlock">
<template #label>{{ $ts.description }}</template>
</MkTextarea>
<div class="banner">
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
<div v-else-if="bannerUrl">
<img :src="bannerUrl" style="width: 100%;"/>
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
<div class="banner">
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
<div v-else-if="bannerUrl">
<img :src="bannerUrl" style="width: 100%;"/>
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
</div>
</div>
<div class="_formBlock">
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
</div>
</div>
<div class="_formBlock">
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
</div>
</div>
</MkSpacer>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, inject, watch } from 'vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { useRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkTextarea, MkButton, MkInput,
},
const router = useRouter();
props: {
channelId: {
type: String,
required: false
},
},
const props = defineProps<{
channelId?: string;
}>();
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.channelId ? {
title: this.$ts._channel.edit,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
} : {
title: this.$ts._channel.create,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
}),
channel: null,
name: null,
description: null,
bannerUrl: null,
bannerId: null,
};
},
let channel = $ref(null);
let name = $ref(null);
let description = $ref(null);
let bannerUrl = $ref<string | null>(null);
let bannerId = $ref<string | null>(null);
watch: {
async bannerId() {
if (this.bannerId == null) {
this.bannerUrl = null;
} else {
this.bannerUrl = (await os.api('drive/files/show', {
fileId: this.bannerId,
})).url;
}
},
},
async created() {
if (this.channelId) {
this.channel = await os.api('channels/show', {
channelId: this.channelId,
});
this.name = this.channel.name;
this.description = this.channel.description;
this.bannerId = this.channel.bannerId;
this.bannerUrl = this.channel.bannerUrl;
}
},
methods: {
save() {
const params = {
name: this.name,
description: this.description,
bannerId: this.bannerId,
};
if (this.channelId) {
params.channelId = this.channelId;
os.api('channels/update', params)
.then(channel => {
os.success();
});
} else {
os.api('channels/create', params)
.then(channel => {
os.success();
this.$router.push(`/channels/${channel.id}`);
});
}
},
setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
this.bannerId = file.id;
});
},
removeBannerImage() {
this.bannerId = null;
}
watch(() => bannerId, async () => {
if (bannerId == null) {
bannerUrl = null;
} else {
bannerUrl = (await os.api('drive/files/show', {
fileId: bannerId,
})).url;
}
});
async function fetchChannel() {
if (props.channelId == null) return;
channel = await os.api('channels/show', {
channelId: props.channelId,
});
name = channel.name;
description = channel.description;
bannerId = channel.bannerId;
bannerUrl = channel.bannerUrl;
}
fetchChannel();
function save() {
const params = {
name: name,
description: description,
bannerId: bannerId,
};
if (props.channelId) {
params.channelId = props.channelId;
os.api('channels/update', params).then(() => {
os.success();
});
} else {
os.api('channels/create', params).then(created => {
os.success();
router.push(`/channels/${created.id}`);
});
}
}
function setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
bannerId = file.id;
});
}
function removeBannerImage() {
bannerId = null;
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => props.channelId ? {
title: i18n.ts._channel.edit,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
} : {
title: i18n.ts._channel.create,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
}));
</script>
<style lang="scss" scoped>

View File

@@ -1,98 +1,87 @@
<template>
<MkSpacer :content-max="700">
<div v-if="channel">
<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
<button class="_button toggle" @click="() => showBanner = !showBanner">
<template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
<template v-else><i class="fas fa-angle-down"></i></template>
</button>
<div v-if="!showBanner" class="hideOverlay">
</div>
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
<div class="status">
<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div v-if="channel">
<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
<button class="_button toggle" @click="() => showBanner = !showBanner">
<template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
<template v-else><i class="fas fa-angle-down"></i></template>
</button>
<div v-if="!showBanner" class="hideOverlay">
</div>
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
<div class="status">
<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
</div>
<div class="fade"></div>
</div>
<div v-if="channel.description" class="description">
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
</div>
<div class="fade"></div>
</div>
<div v-if="channel.description" class="description">
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
</div>
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/>
<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/>
<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>
</MkSpacer>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, inject, watch } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import XPostForm from '@/components/post-form.vue';
import XTimeline from '@/components/timeline.vue';
import XChannelFollowButton from '@/components/channel-follow-button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { useRouter } from '@/router';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
export default defineComponent({
components: {
MkContainer,
XPostForm,
XTimeline,
XChannelFollowButton
},
const router = useRouter();
props: {
channelId: {
type: String,
required: true
}
},
const props = defineProps<{
channelId: string;
}>();
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.channel ? {
title: this.channel.name,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
actions: [...(this.$i && this.$i.id === this.channel.userId ? [{
icon: 'fas fa-cog',
text: this.$ts.edit,
handler: this.edit,
}] : [])],
} : null),
channel: null,
showBanner: true,
pagination: {
endpoint: 'channels/timeline' as const,
limit: 10,
params: computed(() => ({
channelId: this.channelId,
}))
},
};
},
let channel = $ref(null);
let showBanner = $ref(true);
const pagination = {
endpoint: 'channels/timeline' as const,
limit: 10,
params: computed(() => ({
channelId: props.channelId,
})),
};
watch: {
channelId: {
async handler() {
this.channel = await os.api('channels/show', {
channelId: this.channelId,
});
},
immediate: true
}
},
watch(() => props.channelId, async () => {
channel = await os.api('channels/show', {
channelId: props.channelId,
});
}, { immediate: true });
methods: {
edit() {
this.$router.push(`/channels/${this.channel.id}/edit`);
}
},
});
function edit() {
router.push(`/channels/${channel.id}/edit`);
}
const headerActions = $computed(() => channel && channel.userId ? [{
icon: 'fas fa-cog',
text: i18n.ts.edit,
handler: edit,
}] : null);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => channel ? {
title: channel.name,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
} : null));
</script>
<style lang="scss" scoped>

View File

@@ -1,82 +1,83 @@
<template>
<MkSpacer :content-max="700">
<div v-if="tab === 'featured'" class="_content grwlizim featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-else-if="tab === 'following'" class="_content grwlizim following">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-else-if="tab === 'owned'" class="_content grwlizim owned">
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
</MkSpacer>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div v-if="tab === 'featured'" class="_content grwlizim featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-else-if="tab === 'following'" class="_content grwlizim following">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-else-if="tab === 'owned'" class="_content grwlizim owned">
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, defineComponent, inject } from 'vue';
import MkChannelPreview from '@/components/channel-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import * as symbols from '@/symbols';
import { useRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkChannelPreview, MkPagination, MkButton,
},
data() {
return {
[symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.channel,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-plus',
text: this.$ts.create,
handler: this.create,
}],
tabs: [{
active: this.tab === 'featured',
title: this.$ts._channel.featured,
icon: 'fas fa-fire-alt',
onClick: () => { this.tab = 'featured'; },
}, {
active: this.tab === 'following',
title: this.$ts._channel.following,
icon: 'fas fa-heart',
onClick: () => { this.tab = 'following'; },
}, {
active: this.tab === 'owned',
title: this.$ts._channel.owned,
icon: 'fas fa-edit',
onClick: () => { this.tab = 'owned'; },
},]
})),
tab: 'featured',
featuredPagination: {
endpoint: 'channels/featured' as const,
noPaging: true,
},
followingPagination: {
endpoint: 'channels/followed' as const,
limit: 5,
},
ownedPagination: {
endpoint: 'channels/owned' as const,
limit: 5,
},
};
},
methods: {
create() {
this.$router.push(`/channels/new`);
}
}
});
const router = useRouter();
let tab = $ref('featured');
const featuredPagination = {
endpoint: 'channels/featured' as const,
noPaging: true,
};
const followingPagination = {
endpoint: 'channels/followed' as const,
limit: 5,
};
const ownedPagination = {
endpoint: 'channels/owned' as const,
limit: 5,
};
function create() {
router.push('/channels/new');
}
const headerActions = $computed(() => [{
icon: 'fas fa-plus',
text: i18n.ts.create,
handler: create,
}]);
const headerTabs = $computed(() => [{
active: tab === 'featured',
title: i18n.ts._channel.featured,
icon: 'fas fa-fire-alt',
onClick: () => { tab = 'featured'; },
}, {
active: tab === 'following',
title: i18n.ts._channel.following,
icon: 'fas fa-heart',
onClick: () => { tab = 'following'; },
}, {
active: tab === 'owned',
title: i18n.ts._channel.owned,
icon: 'fas fa-edit',
onClick: () => { tab = 'owned'; },
}]);
definePageMetadata(computed(() => ({
title: i18n.ts.channel,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
})));
</script>

View File

@@ -1,136 +1,109 @@
<template>
<MkSpacer :content-max="800">
<div v-if="clip">
<div class="okzinsic _panel">
<div v-if="clip.description" class="description">
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions"/></template>
<MkSpacer :content-max="800">
<div v-if="clip">
<div class="okzinsic _panel">
<div v-if="clip.description" class="description">
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
</div>
<div class="user">
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
</div>
<div class="user">
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
</div>
<XNotes :pagination="pagination" :detail="true"/>
</div>
</MkSpacer>
<XNotes :pagination="pagination" :detail="true"/>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import MkContainer from '@/components/ui/container.vue';
import XPostForm from '@/components/post-form.vue';
<script lang="ts" setup>
import { computed, watch, provide } from 'vue';
import * as misskey from 'misskey-js';
import XNotes from '@/components/notes.vue';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { definePageMetadata } from '@/scripts/page-metadata';
export default defineComponent({
components: {
MkContainer,
XPostForm,
XNotes,
},
const props = defineProps<{
clipId: string,
}>();
props: {
clipId: {
type: String,
required: true
}
},
let clip: misskey.entities.Clip = $ref<misskey.entities.Clip>();
const pagination = {
endpoint: 'clips/notes' as const,
limit: 10,
params: computed(() => ({
clipId: props.clipId,
})),
};
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.clip ? {
title: this.clip.name,
icon: 'fas fa-paperclip',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-ellipsis-h',
handler: this.menu
}],
} : null),
clip: null,
pagination: {
endpoint: 'clips/notes' as const,
limit: 10,
params: computed(() => ({
clipId: this.clipId,
}))
const isOwned: boolean | null = $computed<boolean | null>(() => $i && clip && ($i.id === clip.userId));
watch(() => props.clipId, async () => {
clip = await os.api('clips/show', {
clipId: props.clipId,
});
}, {
immediate: true,
});
provide('currentClipPage', $$(clip));
const headerActions = $computed(() => clip && isOwned ? [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.edit,
handler: async (): Promise<void> => {
const { canceled, result } = await os.form(clip.name, {
name: {
type: 'string',
label: i18n.ts.name,
default: clip.name,
},
};
},
computed: {
isOwned(): boolean {
return this.$i && this.clip && (this.$i.id === this.clip.userId);
}
},
watch: {
clipId: {
async handler() {
this.clip = await os.api('clips/show', {
clipId: this.clipId,
});
description: {
type: 'string',
required: false,
multiline: true,
label: i18n.ts.description,
default: clip.description,
},
immediate: true
}
isPublic: {
type: 'boolean',
label: i18n.ts.public,
default: clip.isPublic,
},
});
if (canceled) return;
os.apiWithDialog('clips/update', {
clipId: clip.id,
...result,
});
},
}, {
icon: 'fas fa-trash-alt',
text: i18n.ts.delete,
danger: true,
handler: async (): Promise<void> => {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('deleteAreYouSure', { x: clip.name }),
});
if (canceled) return;
created() {
await os.apiWithDialog('clips/delete', {
clipId: clip.id,
});
},
}] : null);
methods: {
menu(ev) {
os.popupMenu([this.isOwned ? {
icon: 'fas fa-pencil-alt',
text: this.$ts.edit,
action: async () => {
const { canceled, result } = await os.form(this.clip.name, {
name: {
type: 'string',
label: this.$ts.name,
default: this.clip.name
},
description: {
type: 'string',
required: false,
multiline: true,
label: this.$ts.description,
default: this.clip.description
},
isPublic: {
type: 'boolean',
label: this.$ts.public,
default: this.clip.isPublic
}
});
if (canceled) return;
os.apiWithDialog('clips/update', {
clipId: this.clip.id,
...result
});
}
} : undefined, this.isOwned ? {
icon: 'fas fa-trash-alt',
text: this.$ts.delete,
danger: true,
action: async () => {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('deleteAreYouSure', { x: this.clip.name }),
});
if (canceled) return;
await os.apiWithDialog('clips/delete', {
clipId: this.clip.id,
});
}
} : undefined], ev.currentTarget ?? ev.target);
}
}
});
definePageMetadata(computed(() => clip ? {
title: clip.name,
icon: 'fas fa-paperclip',
bg: 'var(--bg)',
} : null));
</script>
<style lang="scss" scoped>

View File

@@ -8,17 +8,19 @@
import { computed } from 'vue';
import XDrive from '@/components/drive.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
let folder = $ref(null);
defineExpose({
[symbols.PAGE_INFO]: computed(() => ({
title: folder ? folder.name : i18n.ts.drive,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
hideHeader: true,
})),
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => ({
title: folder ? folder.name : i18n.ts.drive,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
hideHeader: true,
})));
</script>

View File

@@ -36,7 +36,6 @@ import MkSelect from '@/components/form/select.vue';
import MkFolder from '@/components/ui/folder.vue';
import MkTab from '@/components/tab.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { emojiCategories, emojiTags } from '@/instance';
import XEmoji from './emojis.emoji.vue';
@@ -58,7 +57,7 @@ export default defineComponent({
tags: emojiTags,
selectedTags: new Set(),
searchEmojis: null,
}
};
},
watch: {

View File

@@ -1,15 +1,18 @@
<template>
<div :class="$style.root">
<XCategory v-if="tab === 'category'"/>
</div>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<div :class="$style.root">
<XCategory v-if="tab === 'category'"/>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import XCategory from './emojis.category.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const tab = ref('category');
@@ -31,20 +34,21 @@ function menu(ev) {
text: err.message,
});
});
}
},
}], ev.currentTarget ?? ev.target);
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-ellipsis-h',
handler: menu,
}],
},
const headerActions = $computed(() => [{
icon: 'fas fa-ellipsis-h',
handler: menu,
}]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
});
</script>

Some files were not shown because too many files have changed in this diff Show More