Merge branch 'develop' into pr/ThatOneCalculator/8764
This commit is contained in:
commit
9cd1526073
249 changed files with 4798 additions and 6822 deletions
|
@ -22,9 +22,8 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// data の禁止理由: 抽象的すぎるため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
'id-denylist': ['error', 'window', 'data', 'e'],
|
||||
'id-denylist': ['error', 'window', 'e'],
|
||||
'no-shadow': ['warn'],
|
||||
'vue/attributes-order': ['error', {
|
||||
'alphabetical': false,
|
||||
|
@ -69,6 +68,7 @@ module.exports = {
|
|||
// Vue
|
||||
'$$': false,
|
||||
'$ref': false,
|
||||
'$shallowRef': false,
|
||||
'$computed': false,
|
||||
|
||||
// Misskey
|
||||
|
|
21
packages/client/assets/tagcanvas.min.js
vendored
Normal file
21
packages/client/assets/tagcanvas.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -17,7 +17,7 @@
|
|||
"@rollup/plugin-json": "4.1.0",
|
||||
"@rollup/pluginutils": "^4.2.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@vitejs/plugin-vue": "2.3.3",
|
||||
"@vitejs/plugin-vue": "3.0.0-beta.0",
|
||||
"@vue/compiler-sfc": "3.2.37",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
|
@ -37,7 +37,7 @@
|
|||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
"idb-keyval": "6.1.0",
|
||||
"idb-keyval": "6.2.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.1",
|
||||
"katex": "0.15.6",
|
||||
|
@ -47,7 +47,7 @@
|
|||
"mocha": "10.0.0",
|
||||
"ms": "2.1.3",
|
||||
"nested-property": "4.0.0",
|
||||
"photoswipe": "5.2.7",
|
||||
"photoswipe": "5.2.8",
|
||||
"prismjs": "1.28.0",
|
||||
"private-ip": "2.3.3",
|
||||
"promise-limit": "2.7.0",
|
||||
|
@ -58,25 +58,25 @@
|
|||
"random-seed": "0.3.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "2.75.6",
|
||||
"rollup": "2.75.7",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.52.3",
|
||||
"sass": "1.53.0",
|
||||
"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.141.0",
|
||||
"three": "0.142.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tsc-alias": "1.6.9",
|
||||
"tsc-alias": "1.6.11",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "4.7.3",
|
||||
"typescript": "4.7.4",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vite": "2.9.10",
|
||||
"vite": "3.0.0-beta.6",
|
||||
"vue": "3.2.37",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
|
@ -102,13 +102,13 @@
|
|||
"@types/uuid": "8.3.4",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.27.1",
|
||||
"@typescript-eslint/parser": "5.27.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.0",
|
||||
"@typescript-eslint/parser": "5.30.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.0.3",
|
||||
"eslint": "8.17.0",
|
||||
"cypress": "10.3.0",
|
||||
"eslint": "8.18.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-vue": "9.1.0",
|
||||
"eslint-plugin-vue": "9.1.1",
|
||||
"start-server-and-test": "1.14.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ const accountData = localStorage.getItem('account');
|
|||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||
|
||||
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
|
||||
export const iAmAdmin = $i != null && $i.isAdmin;
|
||||
|
||||
export async function signout() {
|
||||
waiting();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
||||
<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
|
||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||
|
@ -40,7 +40,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const window = ref<InstanceType<typeof XWindow>>();
|
||||
const uiWindow = ref<InstanceType<typeof XWindow>>();
|
||||
const comment = ref(props.initialComment || '');
|
||||
|
||||
function send() {
|
||||
|
@ -52,7 +52,7 @@ function send() {
|
|||
type: 'success',
|
||||
text: i18n.ts.abuseReported
|
||||
});
|
||||
window.value?.close();
|
||||
uiWindow.value?.close();
|
||||
emit('closed');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
|
||||
<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
||||
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
|
||||
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
|
||||
</li>
|
||||
|
|
|
@ -51,7 +51,7 @@ const variable = computed(() => {
|
|||
}
|
||||
});
|
||||
|
||||
const loaded = computed(() => !!window[variable.value]);
|
||||
const loaded = !!window[variable.value];
|
||||
|
||||
const src = computed(() => {
|
||||
switch (props.provider) {
|
||||
|
@ -62,7 +62,7 @@ const src = computed(() => {
|
|||
|
||||
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
||||
|
||||
if (loaded.value) {
|
||||
if (loaded) {
|
||||
available.value = true;
|
||||
} else {
|
||||
(document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||
|
@ -74,7 +74,7 @@ if (loaded.value) {
|
|||
}
|
||||
|
||||
function reset() {
|
||||
if (captcha.value?.reset) captcha.value.reset();
|
||||
if (captcha.value.reset) captcha.value.reset();
|
||||
}
|
||||
|
||||
function requestRender() {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
|
||||
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
|
||||
|
@ -5,7 +6,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import 'prismjs';
|
||||
import { Prism } from 'prismjs';
|
||||
import 'prismjs/themes/prism-okaidia.css';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
@ -59,7 +59,7 @@ const isThumbnailAvailable = computed(() => {
|
|||
display: flex;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
|
||||
> .icon-sub {
|
||||
position: absolute;
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
|
||||
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
||||
<div class="main _formRoot">
|
||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
|
||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
|
||||
<MkInput v-model="email" class="_formBlock" type="email" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<XModalWindow ref="dialog"
|
||||
<XModalWindow
|
||||
ref="dialog"
|
||||
:width="450"
|
||||
:can-close="false"
|
||||
:with-ok-button="true"
|
||||
|
@ -37,10 +38,10 @@
|
|||
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</FormSelect>
|
||||
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
|
||||
<template #caption><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</FormRadios>
|
||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
|
||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormRange>
|
||||
|
@ -55,7 +56,6 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import FormInput from './form/input.vue';
|
||||
import FormTextarea from './form/textarea.vue';
|
||||
import FormSwitch from './form/switch.vue';
|
||||
|
@ -63,6 +63,7 @@ import FormSelect from './form/select.vue';
|
|||
import FormRange from './form/range.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
import FormRadios from './form/radios.vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -91,31 +92,31 @@ export default defineComponent({
|
|||
|
||||
data() {
|
||||
return {
|
||||
values: {}
|
||||
values: {},
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
for (const item in this.form) {
|
||||
this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null;
|
||||
this.values[item] = this.form[item].default ?? null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.$emit('done', {
|
||||
result: this.values
|
||||
result: this.values,
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.$emit('done', {
|
||||
canceled: true
|
||||
canceled: true,
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
<template>
|
||||
<div v-sticky-container class="adfeebaf _formBlock">
|
||||
<div class="label"><slot name="label"></slot></div>
|
||||
<div class="main _formRoot">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.adfeebaf {
|
||||
padding: 24px 24px;
|
||||
border: solid 1px var(--divider);
|
||||
border-radius: var(--radius);
|
||||
|
||||
> .label {
|
||||
font-weight: bold;
|
||||
padding: 0 0 16px 0;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,165 +4,139 @@
|
|||
<div v-adaptive-border class="body">
|
||||
<div ref="containerEl" class="container">
|
||||
<div class="track">
|
||||
<div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
|
||||
<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div v-if="steps" class="ticks">
|
||||
<div v-if="steps && showTicks" class="ticks">
|
||||
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineAsyncComponent, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 100,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
textConverter: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: (v) => v.toString(),
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
const containerEl = ref<HTMLElement>();
|
||||
const thumbEl = ref<HTMLElement>();
|
||||
|
||||
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
|
||||
const steppedValue = computed(() => {
|
||||
if (props.step) {
|
||||
const step = props.step / (props.max - props.min);
|
||||
return (step * Math.round(rawValue.value / step));
|
||||
} else {
|
||||
return rawValue.value;
|
||||
}
|
||||
});
|
||||
const finalValue = computed(() => {
|
||||
return (steppedValue.value * (props.max - props.min)) + props.min;
|
||||
});
|
||||
watch(finalValue, () => {
|
||||
context.emit('update:modelValue', finalValue.value);
|
||||
});
|
||||
|
||||
const thumbWidth = computed(() => {
|
||||
if (thumbEl.value == null) return 0;
|
||||
return thumbEl.value!.offsetWidth;
|
||||
});
|
||||
const thumbPosition = ref(0);
|
||||
const calcThumbPosition = () => {
|
||||
if (containerEl.value == null) {
|
||||
thumbPosition.value = 0;
|
||||
} else {
|
||||
thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
|
||||
}
|
||||
};
|
||||
watch([steppedValue, containerEl], calcThumbPosition);
|
||||
|
||||
let ro: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
calcThumbPosition();
|
||||
});
|
||||
ro.observe(containerEl.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ro) ro.disconnect();
|
||||
});
|
||||
|
||||
const steps = computed(() => {
|
||||
if (props.step) {
|
||||
return (props.max - props.min) / props.step;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const tooltipShowing = ref(true);
|
||||
os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
|
||||
showing: tooltipShowing,
|
||||
text: computed(() => {
|
||||
return props.textConverter(finalValue.value);
|
||||
}),
|
||||
targetElement: thumbEl,
|
||||
}, {}, 'closed');
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
|
||||
document.head.appendChild(style);
|
||||
|
||||
const onDrag = (ev: MouseEvent | TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
const containerRect = containerEl.value!.getBoundingClientRect();
|
||||
const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
|
||||
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2));
|
||||
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
|
||||
};
|
||||
|
||||
const onMouseup = () => {
|
||||
document.head.removeChild(style);
|
||||
tooltipShowing.value = false;
|
||||
window.removeEventListener('mousemove', onDrag);
|
||||
window.removeEventListener('touchmove', onDrag);
|
||||
window.removeEventListener('mouseup', onMouseup);
|
||||
window.removeEventListener('touchend', onMouseup);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onDrag);
|
||||
window.addEventListener('touchmove', onDrag);
|
||||
window.addEventListener('mouseup', onMouseup, { once: true });
|
||||
window.addEventListener('touchend', onMouseup, { once: true });
|
||||
};
|
||||
|
||||
return {
|
||||
rawValue,
|
||||
finalValue,
|
||||
steppedValue,
|
||||
onMousedown,
|
||||
containerEl,
|
||||
thumbEl,
|
||||
thumbPosition,
|
||||
steps,
|
||||
};
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: number;
|
||||
disabled?: boolean;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
textConverter?: (value: number) => string,
|
||||
showTicks?: boolean;
|
||||
}>(), {
|
||||
step: 1,
|
||||
textConverter: (v) => v.toString(),
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: number): void;
|
||||
}>();
|
||||
|
||||
const containerEl = ref<HTMLElement>();
|
||||
const thumbEl = ref<HTMLElement>();
|
||||
|
||||
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
|
||||
const steppedRawValue = computed(() => {
|
||||
if (props.step) {
|
||||
const step = props.step / (props.max - props.min);
|
||||
return (step * Math.round(rawValue.value / step));
|
||||
} else {
|
||||
return rawValue.value;
|
||||
}
|
||||
});
|
||||
const finalValue = computed(() => {
|
||||
if (Number.isInteger(props.step)) {
|
||||
return Math.round((steppedRawValue.value * (props.max - props.min)) + props.min);
|
||||
} else {
|
||||
return (steppedRawValue.value * (props.max - props.min)) + props.min;
|
||||
}
|
||||
});
|
||||
|
||||
const thumbWidth = computed(() => {
|
||||
if (thumbEl.value == null) return 0;
|
||||
return thumbEl.value!.offsetWidth;
|
||||
});
|
||||
const thumbPosition = ref(0);
|
||||
const calcThumbPosition = () => {
|
||||
if (containerEl.value == null) {
|
||||
thumbPosition.value = 0;
|
||||
} else {
|
||||
thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedRawValue.value;
|
||||
}
|
||||
};
|
||||
watch([steppedRawValue, containerEl], calcThumbPosition);
|
||||
|
||||
let ro: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
calcThumbPosition();
|
||||
});
|
||||
ro.observe(containerEl.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ro) ro.disconnect();
|
||||
});
|
||||
|
||||
const steps = computed(() => {
|
||||
if (props.step) {
|
||||
return (props.max - props.min) / props.step;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const tooltipShowing = ref(true);
|
||||
os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
|
||||
showing: tooltipShowing,
|
||||
text: computed(() => {
|
||||
return props.textConverter(finalValue.value);
|
||||
}),
|
||||
targetElement: thumbEl,
|
||||
}, {}, 'closed');
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
|
||||
document.head.appendChild(style);
|
||||
|
||||
const onDrag = (ev: MouseEvent | TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
const containerRect = containerEl.value!.getBoundingClientRect();
|
||||
const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
|
||||
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2));
|
||||
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
|
||||
};
|
||||
|
||||
let beforeValue = finalValue.value;
|
||||
|
||||
const onMouseup = () => {
|
||||
document.head.removeChild(style);
|
||||
tooltipShowing.value = false;
|
||||
window.removeEventListener('mousemove', onDrag);
|
||||
window.removeEventListener('touchmove', onDrag);
|
||||
window.removeEventListener('mouseup', onMouseup);
|
||||
window.removeEventListener('touchend', onMouseup);
|
||||
|
||||
// 値が変わってたら通知
|
||||
if (beforeValue !== finalValue.value) {
|
||||
emit('update:modelValue', finalValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onDrag);
|
||||
window.addEventListener('touchmove', onDrag);
|
||||
window.addEventListener('mouseup', onMouseup, { once: true });
|
||||
window.addEventListener('touchend', onMouseup, { once: true });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -215,7 +189,7 @@ export default defineComponent({
|
|||
height: 3px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 999px;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
|
||||
> .highlight {
|
||||
position: absolute;
|
||||
|
|
|
@ -214,6 +214,7 @@ const onClick = (ev: MouseEvent) => {
|
|||
cursor: pointer;
|
||||
transition: border-color 0.1s ease-out;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> .prefix,
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{
|
||||
minWidth: number;
|
||||
minWidth?: number;
|
||||
}>(), {
|
||||
minWidth: 210,
|
||||
minWidth: 210,
|
||||
});
|
||||
|
||||
const minWidth = props.minWidth + 'px';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div v-if="block" v-html="compiledFormula"></div>
|
||||
<span v-else v-html="compiledFormula"></span>
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@/os';
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive, nextTick, reactive } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { popupMenu } from '@/os';
|
||||
import { scrollToTop } from '@/scripts/scroll';
|
||||
|
@ -137,16 +137,18 @@ onMounted(() => {
|
|||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
|
||||
watch(() => props.tab, () => {
|
||||
const tabEl = tabRefs[props.tab];
|
||||
if (tabEl && tabHighlightEl) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect = tabEl.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.style.width = rect.width + 'px';
|
||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
||||
}
|
||||
watch(() => [props.tab, props.tabs], () => {
|
||||
nextTick(() => {
|
||||
const tabEl = tabRefs[props.tab];
|
||||
if (tabEl && tabHighlightEl) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect = tabEl.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.style.width = rect.width + 'px';
|
||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
||||
}
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
@ -170,11 +172,8 @@ onUnmounted(() => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.fdidabkb {
|
||||
--height: 60px;
|
||||
--height: 55px;
|
||||
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));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<KeepAlive max="5">
|
||||
<KeepAlive :max="defaultStore.state.numberOfPageCache">
|
||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||
</KeepAlive>
|
||||
</template>
|
||||
|
@ -7,21 +7,19 @@
|
|||
<script lang="ts" setup>
|
||||
import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Router } from '@/nirax';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
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 currentPageComponent = $shallowRef(router.getCurrentComponent());
|
||||
let currentPageProps = $ref(router.getCurrentProps());
|
||||
let key = $ref(router.getCurrentKey());
|
||||
|
||||
|
|
|
@ -1,46 +1,38 @@
|
|||
<template>
|
||||
<div ref="rootEl">
|
||||
<slot name="header"></slot>
|
||||
<div ref="headerEl">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
<script lang="ts">
|
||||
// なんか動かない
|
||||
//const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
|
||||
const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
|
||||
</script>
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSticky?: boolean;
|
||||
}>(), {
|
||||
autoSticky: false,
|
||||
});
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
|
||||
|
||||
const rootEl = $ref<HTMLElement>();
|
||||
const headerEl = $ref<HTMLElement>();
|
||||
const bodyEl = $ref<HTMLElement>();
|
||||
|
||||
let headerHeight = $ref<string | undefined>();
|
||||
let childStickyTop = $ref(0);
|
||||
const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
|
||||
provide(CURRENT_STICKY_TOP, $$(childStickyTop));
|
||||
|
||||
const calc = () => {
|
||||
const currentStickyTop = getComputedStyle(rootEl).getPropertyValue('--stickyTop') || '0px';
|
||||
|
||||
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();
|
||||
|
||||
if (props.autoSticky) {
|
||||
header.style.setProperty('--stickyTop', currentStickyTop);
|
||||
header.style.position = 'sticky';
|
||||
header.style.top = 'var(--stickyTop)';
|
||||
header.style.zIndex = '1';
|
||||
}
|
||||
}
|
||||
childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
|
||||
headerHeight = headerEl.offsetHeight.toString();
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
window.setTimeout(() => {
|
||||
calc();
|
||||
}, 100);
|
||||
|
@ -49,11 +41,19 @@ const observer = new MutationObserver(() => {
|
|||
onMounted(() => {
|
||||
calc();
|
||||
|
||||
observer.observe(rootEl, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: false,
|
||||
watch(parentStickyTop, calc);
|
||||
|
||||
watch($$(childStickyTop), () => {
|
||||
bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
headerEl.style.position = 'sticky';
|
||||
headerEl.style.top = 'var(--stickyTop, 0)';
|
||||
headerEl.style.zIndex = '1000';
|
||||
|
||||
observer.observe(headerEl);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -1,81 +1,219 @@
|
|||
<template>
|
||||
<div class="zbcjwnqg">
|
||||
<div class="selects" style="display: flex;">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup :label="$ts.federation">
|
||||
<option value="federation">{{ $ts._charts.federation }}</option>
|
||||
<option value="ap-request">{{ $ts._charts.apRequest }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.users">
|
||||
<option value="users">{{ $ts._charts.usersIncDec }}</option>
|
||||
<option value="users-total">{{ $ts._charts.usersTotal }}</option>
|
||||
<option value="active-users">{{ $ts._charts.activeUsers }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.notes">
|
||||
<option value="notes">{{ $ts._charts.notesIncDec }}</option>
|
||||
<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
|
||||
<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
|
||||
<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.drive">
|
||||
<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
|
||||
<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
<option value="day">{{ $ts.perDay }}</option>
|
||||
</MkSelect>
|
||||
<div class="main">
|
||||
<div class="body">
|
||||
<div class="selects" style="display: flex;">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup :label="$ts.federation">
|
||||
<option value="federation">{{ $ts._charts.federation }}</option>
|
||||
<option value="ap-request">{{ $ts._charts.apRequest }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.users">
|
||||
<option value="users">{{ $ts._charts.usersIncDec }}</option>
|
||||
<option value="users-total">{{ $ts._charts.usersTotal }}</option>
|
||||
<option value="active-users">{{ $ts._charts.activeUsers }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.notes">
|
||||
<option value="notes">{{ $ts._charts.notesIncDec }}</option>
|
||||
<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
|
||||
<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
|
||||
<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.drive">
|
||||
<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
|
||||
<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
<option value="day">{{ $ts.perDay }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||
<div class="subpub">
|
||||
<div class="sub">
|
||||
<div class="title">Sub</div>
|
||||
<canvas ref="subDoughnutEl"></canvas>
|
||||
</div>
|
||||
<div class="pub">
|
||||
<div class="title">Pub</div>
|
||||
<canvas ref="pubDoughnutEl"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
DoughnutController,
|
||||
} from 'chart.js';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkChart from '@/components/chart.vue';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSelect,
|
||||
MkChart,
|
||||
},
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
DoughnutController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
);
|
||||
|
||||
props: {
|
||||
chartLimit: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 90
|
||||
const props = withDefaults(defineProps<{
|
||||
chartLimit?: number;
|
||||
detailed?: boolean;
|
||||
}>(), {
|
||||
chartLimit: 90,
|
||||
});
|
||||
|
||||
const chartSpan = $ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = $ref('active-users');
|
||||
let subDoughnutEl = $ref<HTMLCanvasElement>();
|
||||
let pubDoughnutEl = $ref<HTMLCanvasElement>();
|
||||
|
||||
const { handler: externalTooltipHandler1 } = useChartTooltip();
|
||||
const { handler: externalTooltipHandler2 } = useChartTooltip();
|
||||
|
||||
function createDoughnut(chartEl, tooltip, data) {
|
||||
const chartInstance = new Chart(chartEl, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: data.map(x => x.color),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: data.map(x => x.value),
|
||||
}],
|
||||
},
|
||||
detailed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
},
|
||||
},
|
||||
onClick: (ev) => {
|
||||
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
|
||||
if (hit && data[hit.index].onClick) {
|
||||
data[hit.index].onClick();
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: tooltip,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setup() {
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref('active-users');
|
||||
return chartInstance;
|
||||
}
|
||||
|
||||
return {
|
||||
chartSrc,
|
||||
chartSpan,
|
||||
};
|
||||
},
|
||||
onMounted(() => {
|
||||
os.apiGet('federation/stats', { limit: 15 }).then(fedStats => {
|
||||
createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
|
||||
name: x.host,
|
||||
color: x.themeColor,
|
||||
value: x.followersCount,
|
||||
onClick: () => {
|
||||
os.pageWindow(`/instance-info/${x.host}`);
|
||||
},
|
||||
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
|
||||
|
||||
createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
|
||||
name: x.host,
|
||||
color: x.themeColor,
|
||||
value: x.followingCount,
|
||||
onClick: () => {
|
||||
os.pageWindow(`/instance-info/${x.host}`);
|
||||
},
|
||||
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zbcjwnqg {
|
||||
> .selects {
|
||||
> .main {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
> .body {
|
||||
> .chart {
|
||||
padding: 8px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .chart {
|
||||
padding: 8px 0 0 0;
|
||||
> .subpub {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
> .sub, > .pub {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
max-height: 300px;
|
||||
|
||||
> .title {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,8 +16,8 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
|
|||
import * as os from '@/os';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
copy: string | null;
|
||||
oneline: boolean;
|
||||
copy?: string | null;
|
||||
oneline?: boolean;
|
||||
}>(), {
|
||||
copy: null,
|
||||
oneline: false,
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
</template>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<a v-click-anime href="https://misskey-hub.net/help.html" target="_blank" @click.passive="close()">
|
||||
<button v-click-anime class="_button" @click="help">
|
||||
<i class="fas fa-question-circle icon"></i>
|
||||
<div class="text">{{ $ts.help }}</div>
|
||||
</a>
|
||||
</button>
|
||||
<MkA v-click-anime to="/about" @click.passive="close()">
|
||||
<i class="fas fa-info-circle icon"></i>
|
||||
<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
|
||||
<div class="text">{{ $ts.instanceInfo }}</div>
|
||||
</MkA>
|
||||
<MkA v-click-anime to="/about-misskey" @click.passive="close()">
|
||||
<img src="/static-assets/favicon.png" class="icon"/>
|
||||
|
@ -34,13 +34,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { } from 'vue';
|
||||
import MkModal from '@/components/ui/modal.vue';
|
||||
import { menuDef } from '@/menu';
|
||||
import { instanceName } from '@/config';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src?: HTMLElement;
|
||||
|
@ -73,6 +74,28 @@ const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuD
|
|||
function close() {
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function help(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
type: 'link',
|
||||
to: '/mfm-cheat-sheet',
|
||||
text: i18n.ts._mfm.cheatSheet,
|
||||
icon: 'fas fa-code',
|
||||
}, {
|
||||
type: 'link',
|
||||
to: '/scratchpad',
|
||||
text: i18n.ts.scratchpad,
|
||||
icon: 'fas fa-terminal',
|
||||
}, null, {
|
||||
text: i18n.ts.document,
|
||||
icon: 'fas fa-question-circle',
|
||||
action: () => {
|
||||
window.open('https://misskey-hub.net/help.html', '_blank');
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
||||
close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
99
packages/client/src/components/marquee.vue
Normal file
99
packages/client/src/components/marquee.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<script lang="ts">
|
||||
import { h, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'MarqueeText',
|
||||
props: {
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
repeat: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
paused: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const contentEl = ref();
|
||||
|
||||
function calc() {
|
||||
const eachLength = contentEl.value.offsetWidth / props.repeat;
|
||||
const factor = 3000;
|
||||
const duration = props.duration / ((1 / eachLength) * factor);
|
||||
|
||||
contentEl.value.style.animationDuration = `${duration}s`;
|
||||
}
|
||||
|
||||
watch(() => props.duration, calc);
|
||||
|
||||
onMounted(() => {
|
||||
calc();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
});
|
||||
|
||||
return {
|
||||
contentEl,
|
||||
};
|
||||
},
|
||||
render({
|
||||
$slots, $style, $props: {
|
||||
duration, repeat, paused, reverse,
|
||||
},
|
||||
}) {
|
||||
return h('div', { class: [$style.wrap] }, [
|
||||
h('span', {
|
||||
ref: 'contentEl',
|
||||
class: [
|
||||
paused
|
||||
? $style.paused
|
||||
: undefined,
|
||||
$style.content,
|
||||
],
|
||||
}, Array(repeat).fill(
|
||||
h('span', {
|
||||
class: $style.text,
|
||||
style: {
|
||||
animationDirection: reverse
|
||||
? 'reverse'
|
||||
: undefined,
|
||||
},
|
||||
}, $slots.default()),
|
||||
)),
|
||||
]);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrap {
|
||||
overflow: hidden; overflow: clip;
|
||||
}
|
||||
.content {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.text {
|
||||
display: inline-block;
|
||||
animation-name: marquee;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: inherit;
|
||||
}
|
||||
.paused .text {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
@keyframes marquee {
|
||||
0% { transform:translateX(0); }
|
||||
100% { transform:translateX(-100%); }
|
||||
}
|
||||
</style>
|
|
@ -143,7 +143,6 @@ function onContextmenu(ev: MouseEvent) {
|
|||
background: var(--windowHeader);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
box-shadow: 0px 1px var(--divider);
|
||||
|
||||
> button {
|
||||
height: $height;
|
||||
|
|
|
@ -27,7 +27,7 @@ const props = defineProps<{
|
|||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
font-size: 0.95em;
|
||||
|
||||
&.min-width_350px {
|
||||
|
|
|
@ -36,7 +36,7 @@ const showContent = $ref(false);
|
|||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
font-size: 0.95em;
|
||||
|
||||
&.min-width_350px {
|
||||
|
|
|
@ -297,7 +297,7 @@ function readPromo() {
|
|||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
font-size: 1.05em;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
contain: content;
|
||||
|
||||
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
||||
|
|
|
@ -1,31 +1,35 @@
|
|||
<template>
|
||||
<div class="igpposuu _monospace">
|
||||
<div v-if="value === null" class="null">null</div>
|
||||
<div v-else-if="typeof value === 'boolean'" class="boolean">{{ value ? 'true' : 'false' }}</div>
|
||||
<div v-else-if="typeof value === 'boolean'" class="boolean" :class="{ true: value, false: !value }">{{ value ? 'true' : 'false' }}</div>
|
||||
<div v-else-if="typeof value === 'string'" class="string">"{{ value }}"</div>
|
||||
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
|
||||
<div v-else-if="Array.isArray(value)" class="array">
|
||||
<button @click="collapsed_ = !collapsed_">[ {{ collapsed_ ? '+' : '-' }} ]</button>
|
||||
<template v-if="!collapsed_">
|
||||
<div v-for="i in value.length" class="element">
|
||||
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="isArray(value) && isEmpty(value)" class="array empty">[]</div>
|
||||
<div v-else-if="isArray(value)" class="array">
|
||||
<div v-for="i in value.length" class="element">
|
||||
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="typeof value === 'object'" class="object">
|
||||
<button @click="collapsed_ = !collapsed_">{ {{ collapsed_ ? '+' : '-' }} }</button>
|
||||
<template v-if="!collapsed_">
|
||||
<div v-for="k in Object.keys(value)" class="kv">
|
||||
<div class="k">{{ k }}:</div>
|
||||
<div class="v"><XValue :value="value[k]" collapsed/></div>
|
||||
<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
|
||||
<div v-else-if="isObject(value)" class="object">
|
||||
<div v-for="k in Object.keys(value)" class="kv">
|
||||
<button class="toggle _button" :class="{ visible: collapsable(value[k]) }" @click="collapsed[k] = !collapsed[k]">{{ collapsed[k] ? '+' : '-' }}</button>
|
||||
<div class="k">{{ k }}:</div>
|
||||
<div v-if="collapsed[k]" class="v">
|
||||
<button class="_button" @click="collapsed[k] = !collapsed[k]">
|
||||
<template v-if="typeof value[k] === 'string'">"..."</template>
|
||||
<template v-else-if="isArray(value[k])">[...]</template>
|
||||
<template v-else-if="isObject(value[k])">{...}</template>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="v"><XValue :value="value[k]"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
import number from '@/filters/number';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -33,24 +37,44 @@ export default defineComponent({
|
|||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const collapsed_ = ref(props.collapsed);
|
||||
const collapsed = reactive({});
|
||||
|
||||
if (isObject(props.value)) {
|
||||
for (const key in props.value) {
|
||||
collapsed[key] = collapsable(props.value[key]);
|
||||
}
|
||||
}
|
||||
|
||||
function isObject(v): boolean {
|
||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||
}
|
||||
|
||||
function isArray(v): boolean {
|
||||
return Array.isArray(v);
|
||||
}
|
||||
|
||||
function isEmpty(v): boolean {
|
||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||
}
|
||||
|
||||
function collapsable(v): boolean {
|
||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||
}
|
||||
|
||||
return {
|
||||
number,
|
||||
collapsed_,
|
||||
collapsed,
|
||||
isObject,
|
||||
isArray,
|
||||
isEmpty,
|
||||
collapsable,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -66,6 +90,14 @@ export default defineComponent({
|
|||
> .boolean {
|
||||
display: inline;
|
||||
color: var(--codeBoolean);
|
||||
|
||||
&.true {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.false {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> .string {
|
||||
|
@ -78,7 +110,12 @@ export default defineComponent({
|
|||
color: var(--codeNumber);
|
||||
}
|
||||
|
||||
> .array {
|
||||
> .array.empty {
|
||||
display: inline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .array:not(.empty) {
|
||||
display: inline;
|
||||
|
||||
> .element {
|
||||
|
@ -87,13 +124,28 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
> .object {
|
||||
> .object.empty {
|
||||
display: inline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .object:not(.empty) {
|
||||
display: inline;
|
||||
|
||||
> .kv {
|
||||
display: block;
|
||||
padding-left: 16px;
|
||||
|
||||
> .toggle {
|
||||
width: 16px;
|
||||
color: var(--accent);
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
> .k {
|
||||
display: inline;
|
||||
margin-right: 8px;
|
||||
|
|
|
@ -4,26 +4,13 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XValue from './object-view.value.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XValue
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
value: Record<string, unknown>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -48,7 +48,10 @@ 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 history = $ref<{ path: string; key: any; }[]>([{
|
||||
path: router.getCurrentPath(),
|
||||
key: router.getCurrentKey(),
|
||||
}]);
|
||||
const buttonsLeft = $computed(() => {
|
||||
const buttons = [];
|
||||
|
||||
|
@ -72,7 +75,7 @@ const buttonsRight = $computed(() => {
|
|||
});
|
||||
|
||||
router.addListener('push', ctx => {
|
||||
history.push(router.getCurrentPath());
|
||||
history.push({ path: ctx.path, key: ctx.key });
|
||||
});
|
||||
|
||||
provide('router', router);
|
||||
|
@ -111,7 +114,7 @@ function menu(ev) {
|
|||
|
||||
function back() {
|
||||
history.pop();
|
||||
router.change(history[history.length - 1]);
|
||||
router.change(history[history.length - 1].path, history[history.length - 1].key);
|
||||
}
|
||||
|
||||
function close() {
|
||||
|
@ -136,5 +139,6 @@ defineExpose({
|
|||
<style lang="scss" scoped>
|
||||
.yrolvcoq {
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,7 +24,6 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup(props, ctx) {
|
||||
|
||||
const hpml = new Hpml(props.page, {
|
||||
randomSeed: Math.random(),
|
||||
visitor: $i,
|
||||
|
|
|
@ -116,8 +116,11 @@ function get() {
|
|||
let base = parseInt(after.value);
|
||||
switch (unit.value) {
|
||||
case 'day': base *= 24;
|
||||
// fallthrough
|
||||
case 'hour': base *= 60;
|
||||
// fallthrough
|
||||
case 'minute': base *= 60;
|
||||
// fallthrough
|
||||
case 'second': return base *= 1000;
|
||||
default: return null;
|
||||
}
|
||||
|
|
|
@ -12,106 +12,81 @@
|
|||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XDetails from '@/components/reactions-viewer.details.vue';
|
||||
import XReactionIcon from '@/components/reaction-icon.vue';
|
||||
import * as os from '@/os';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XReactionIcon
|
||||
},
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
count: number;
|
||||
isInitial: boolean;
|
||||
note: misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
reaction: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isInitial: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
note: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
const buttonRef = ref<HTMLElement>();
|
||||
|
||||
setup(props) {
|
||||
const buttonRef = ref<HTMLElement>();
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
const toggleReaction = () => {
|
||||
if (!canToggle.value) return;
|
||||
|
||||
const toggleReaction = () => {
|
||||
if (!canToggle.value) return;
|
||||
|
||||
const oldReaction = props.note.myReaction;
|
||||
if (oldReaction) {
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: props.note.id
|
||||
}).then(() => {
|
||||
if (oldReaction !== props.reaction) {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: props.note.id,
|
||||
reaction: props.reaction
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const oldReaction = props.note.myReaction;
|
||||
if (oldReaction) {
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: props.note.id,
|
||||
}).then(() => {
|
||||
if (oldReaction !== props.reaction) {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: props.note.id,
|
||||
reaction: props.reaction
|
||||
reaction: props.reaction,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const anime = () => {
|
||||
if (document.hidden) return;
|
||||
|
||||
// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション
|
||||
};
|
||||
|
||||
watch(() => props.count, (newCount, oldCount) => {
|
||||
if (oldCount < newCount) anime();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.isInitial) anime();
|
||||
} else {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: props.note.id,
|
||||
reaction: props.reaction,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
const reactions = await os.api('notes/reactions', {
|
||||
noteId: props.note.id,
|
||||
type: props.reaction,
|
||||
limit: 11
|
||||
});
|
||||
const anime = () => {
|
||||
if (document.hidden) return;
|
||||
|
||||
const users = reactions.map(x => x.user);
|
||||
// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション
|
||||
};
|
||||
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
reaction: props.reaction,
|
||||
emojis: props.note.emojis,
|
||||
users,
|
||||
count: props.count,
|
||||
targetElement: buttonRef.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
return {
|
||||
buttonRef,
|
||||
canToggle,
|
||||
toggleReaction,
|
||||
};
|
||||
},
|
||||
watch(() => props.count, (newCount, oldCount) => {
|
||||
if (oldCount < newCount) anime();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.isInitial) anime();
|
||||
});
|
||||
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
const reactions = await os.api('notes/reactions', {
|
||||
noteId: props.note.id,
|
||||
type: props.reaction,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = reactions.map(x => x.user);
|
||||
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
reaction: props.reaction,
|
||||
emojis: props.note.emojis,
|
||||
users,
|
||||
count: props.count,
|
||||
targetElement: buttonRef.value,
|
||||
}, {}, 'closed');
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="!totpLogin" class="normal-signin">
|
||||
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.token }}</template>
|
||||
<template #prefix><i class="fas fa-gavel"></i></template>
|
||||
</MkInput>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<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" 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}$" 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" 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>
|
||||
|
|
88
packages/client/src/components/tag-cloud.vue
Normal file
88
packages/client/src/components/tag-cloud.vue
Normal file
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div ref="rootEl" class="meijqfqm">
|
||||
<canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
|
||||
<div :id="idForTags" ref="tagsEl" class="tags">
|
||||
<ul>
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch, PropType, onBeforeUnmount } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
const loaded = !!window.TagCanvas;
|
||||
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||
const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||
let available = $ref(false);
|
||||
let rootEl = $ref<HTMLElement | null>(null);
|
||||
let canvasEl = $ref<HTMLCanvasElement | null>(null);
|
||||
let tagsEl = $ref<HTMLElement | null>(null);
|
||||
let width = $ref(300);
|
||||
|
||||
watch($$(available), () => {
|
||||
window.TagCanvas.Start(idForCanvas, idForTags, {
|
||||
textColour: '#ffffff',
|
||||
outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(),
|
||||
outlineRadius: 10,
|
||||
initial: [-0.030, -0.010],
|
||||
frontSelect: true,
|
||||
imageRadius: 8,
|
||||
//dragControl: true,
|
||||
dragThreshold: 3,
|
||||
wheelZoom: false,
|
||||
reverse: true,
|
||||
depth: 0.5,
|
||||
maxSpeed: 0.2,
|
||||
minSpeed: 0.003,
|
||||
stretchX: 0.8,
|
||||
stretchY: 0.8,
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
width = rootEl.offsetWidth;
|
||||
|
||||
if (loaded) {
|
||||
available = true;
|
||||
} else {
|
||||
document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||
async: true,
|
||||
src: '/client-assets/tagcanvas.min.js',
|
||||
})).addEventListener('load', () => available = true);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.TagCanvas.Delete(idForCanvas);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
update: () => {
|
||||
window.TagCanvas.Update(idForCanvas);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.meijqfqm {
|
||||
position: relative;
|
||||
overflow: hidden; overflow: clip;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
> .canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .tags {
|
||||
position: absolute;
|
||||
top: 999px;
|
||||
left: 999px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -54,7 +54,7 @@ onMounted(() => {
|
|||
width: min-content;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@ export default defineComponent({
|
|||
text-decoration: none;
|
||||
background: var(--buttonBg);
|
||||
border-radius: 5px;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
box-sizing: border-box;
|
||||
transition: background 0.1s ease;
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<transition :name="$store.state.animation ? 'container-toggle' : ''"
|
||||
<transition
|
||||
:name="$store.state.animation ? 'container-toggle' : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@leave="leave"
|
||||
|
@ -34,37 +35,37 @@ export default defineComponent({
|
|||
showHeader: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
thin: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
naked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
foldable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
@ -79,12 +80,12 @@ export default defineComponent({
|
|||
const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0;
|
||||
this.$el.style.minHeight = `${headerHeight}px`;
|
||||
if (showBody) {
|
||||
this.$el.style.flexBasis = `auto`;
|
||||
this.$el.style.flexBasis = 'auto';
|
||||
} else {
|
||||
this.$el.style.flexBasis = `${headerHeight}px`;
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px');
|
||||
|
@ -124,7 +125,7 @@ export default defineComponent({
|
|||
afterLeave(el) {
|
||||
el.style.height = null;
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -142,7 +143,7 @@ export default defineComponent({
|
|||
|
||||
.ukygtjoj {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
|
||||
&.naked {
|
||||
background: transparent !important;
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';import * as os from '@/os';
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({});
|
||||
</script>
|
||||
|
|
|
@ -136,11 +136,11 @@ function focusDown() {
|
|||
> .item {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 8px 18px;
|
||||
padding: 6px 18px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.85em;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -105,7 +105,6 @@ defineExpose({
|
|||
background: var(--windowHeader);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
box-shadow: 0px 1px var(--divider);
|
||||
|
||||
> button {
|
||||
height: $height;
|
||||
|
|
|
@ -389,7 +389,7 @@ defineExpose({
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
|
||||
> .content {
|
||||
position: fixed;
|
||||
|
|
|
@ -99,12 +99,12 @@ export default defineComponent({
|
|||
buttonsLeft: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: [],
|
||||
default: () => [],
|
||||
},
|
||||
buttonsRight: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: [],
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -410,6 +410,7 @@ export default defineComponent({
|
|||
backdrop-filter: var(--blur, blur(15px));
|
||||
//border-bottom: solid 1px var(--divider);
|
||||
font-size: 95%;
|
||||
font-weight: bold;
|
||||
|
||||
> .left, > .right {
|
||||
> .button {
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
<div class="customize-container">
|
||||
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
|
||||
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
|
||||
<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="handle" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
||||
<div class="handle">
|
||||
<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</XDraggable>
|
||||
|
@ -141,6 +143,12 @@ export default defineComponent({
|
|||
> .remove {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
> .handle {
|
||||
> .widget {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,7 +34,6 @@ function calc(src: Element) {
|
|||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
|
||||
const resize = new ResizeObserver((entries, observer) => {
|
||||
calc(src);
|
||||
});
|
||||
|
|
|
@ -35,11 +35,6 @@ export const menuDef = reactive({
|
|||
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
|
||||
to: '/my/follow-requests',
|
||||
},
|
||||
featured: {
|
||||
title: 'featured',
|
||||
icon: 'fas fa-fire-alt',
|
||||
to: '/featured',
|
||||
},
|
||||
explore: {
|
||||
title: 'explore',
|
||||
icon: 'fas fa-hashtag',
|
||||
|
@ -81,12 +76,14 @@ export const menuDef = reactive({
|
|||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
/*
|
||||
groups: {
|
||||
title: 'groups',
|
||||
icon: 'fas fa-users',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/groups',
|
||||
},
|
||||
*/
|
||||
antennas: {
|
||||
title: 'antennas',
|
||||
icon: 'fas fa-satellite',
|
||||
|
@ -112,20 +109,6 @@ export const menuDef = reactive({
|
|||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
mentions: {
|
||||
title: 'mentions',
|
||||
icon: 'fas fa-at',
|
||||
show: computed(() => $i != null),
|
||||
indicated: computed(() => $i != null && $i.hasUnreadMentions),
|
||||
to: '/my/mentions',
|
||||
},
|
||||
messages: {
|
||||
title: 'directNotes',
|
||||
icon: 'fas fa-envelope',
|
||||
show: computed(() => $i != null),
|
||||
indicated: computed(() => $i != null && $i.hasUnreadSpecifiedNotes),
|
||||
to: '/my/messages',
|
||||
},
|
||||
favorites: {
|
||||
title: 'favorites',
|
||||
icon: 'fas fa-star',
|
||||
|
@ -153,21 +136,6 @@ export const menuDef = reactive({
|
|||
icon: 'fas fa-satellite-dish',
|
||||
to: '/channels',
|
||||
},
|
||||
federation: {
|
||||
title: 'federation',
|
||||
icon: 'fas fa-globe',
|
||||
to: '/federation',
|
||||
},
|
||||
emojis: {
|
||||
title: 'emojis',
|
||||
icon: 'fas fa-laugh',
|
||||
to: '/emojis',
|
||||
},
|
||||
scratchpad: {
|
||||
title: 'scratchpad',
|
||||
icon: 'fas fa-terminal',
|
||||
to: '/scratchpad',
|
||||
},
|
||||
ui: {
|
||||
title: 'switchUi',
|
||||
icon: 'fas fa-columns',
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
|
||||
type RouteDef = {
|
||||
path: string;
|
||||
component: Component;
|
||||
query?: Record<string, string>;
|
||||
loginRequired?: boolean;
|
||||
name?: string;
|
||||
hash?: string;
|
||||
globalCacheKey?: string;
|
||||
};
|
||||
|
||||
|
@ -78,7 +81,12 @@ export class Router extends EventEmitter<{
|
|||
|
||||
public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null {
|
||||
let queryString: string | null = null;
|
||||
let hash: string | null = null;
|
||||
if (path[0] === '/') path = path.substring(1);
|
||||
if (path.includes('#')) {
|
||||
hash = path.substring(path.indexOf('#') + 1);
|
||||
path = path.substring(0, path.indexOf('#'));
|
||||
}
|
||||
if (path.includes('?')) {
|
||||
queryString = path.substring(path.indexOf('?') + 1);
|
||||
path = path.substring(0, path.indexOf('?'));
|
||||
|
@ -127,6 +135,10 @@ export class Router extends EventEmitter<{
|
|||
|
||||
if (parts.length !== 0) continue forEachRouteLoop;
|
||||
|
||||
if (route.hash != null && hash != null) {
|
||||
props.set(route.hash, hash);
|
||||
}
|
||||
|
||||
if (route.query != null && queryString != null) {
|
||||
const queryObject = [...new URLSearchParams(queryString).entries()]
|
||||
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
|
||||
|
@ -138,6 +150,7 @@ export class Router extends EventEmitter<{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
route,
|
||||
props,
|
||||
|
@ -158,6 +171,10 @@ export class Router extends EventEmitter<{
|
|||
throw new Error('no route found for: ' + path);
|
||||
}
|
||||
|
||||
if (res.route.loginRequired) {
|
||||
pleaseLogin('/');
|
||||
}
|
||||
|
||||
const isSamePath = beforePath === path;
|
||||
if (isSamePath && key == null) key = this.currentKey;
|
||||
this.currentComponent = res.route.component;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div style="overflow: clip;">
|
||||
<div style="overflow: hidden; overflow: clip;">
|
||||
<MkSpacer :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot znqjceqz">
|
||||
<div id="debug"></div>
|
||||
|
@ -204,7 +204,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.aboutMisskey,
|
||||
icon: null,
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
106
packages/client/src/pages/about.federation.vue
Normal file
106
packages/client/src/pages/about.federation.vue
Normal file
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="taeiyria">
|
||||
<div class="query">
|
||||
<MkInput v-model="host" :debounce="true" class="">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
<FormSplit style="margin-top: var(--margin);">
|
||||
<MkSelect v-model="state">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="federating">{{ $ts.federating }}</option>
|
||||
<option value="subscribing">{{ $ts.subscribing }}</option>
|
||||
<option value="publishing">{{ $ts.publishing }}</option>
|
||||
<option value="suspended">{{ $ts.suspended }}</option>
|
||||
<option value="blocked">{{ $ts.blocked }}</option>
|
||||
<option value="notResponding">{{ $ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkInstanceCardMini from '@/components/instance-card-mini.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let host = $ref('');
|
||||
let state = $ref('federating');
|
||||
let sort = $ref('+pubSub');
|
||||
const pagination = {
|
||||
endpoint: 'federation/instances' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: computed(() => ({
|
||||
sort: sort,
|
||||
host: host !== '' ? host : null,
|
||||
...(
|
||||
state === 'federating' ? { federating: true } :
|
||||
state === 'subscribing' ? { subscribing: true } :
|
||||
state === 'publishing' ? { publishing: true } :
|
||||
state === 'suspended' ? { suspended: true } :
|
||||
state === 'blocked' ? { blocked: true } :
|
||||
state === 'notResponding' ? { notResponding: true } :
|
||||
{}),
|
||||
})),
|
||||
};
|
||||
|
||||
function getStatus(instance) {
|
||||
if (instance.isSuspended) return 'Suspended';
|
||||
if (instance.isBlocked) return 'Blocked';
|
||||
if (instance.isNotResponding) return 'Error';
|
||||
return 'Alive';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.taeiyria {
|
||||
> .query {
|
||||
background: var(--bg);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dqokceoi {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
grid-gap: 12px;
|
||||
|
||||
> .instance:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -67,7 +67,13 @@
|
|||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :content-max="1000" :margin-min="20">
|
||||
<XEmojis/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1000" :margin-min="20">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
@ -75,6 +81,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
import XFederation from './about.federation.vue';
|
||||
import { version, instanceName , host } from '@/config';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
|
@ -87,8 +95,14 @@ import number from '@/filters/number';
|
|||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialTab?: string;
|
||||
}>(), {
|
||||
initialTab: 'overview',
|
||||
});
|
||||
|
||||
let stats = $ref(null);
|
||||
let tab = $ref('overview');
|
||||
let tab = $ref(props.initialTab);
|
||||
|
||||
const initStats = () => os.api('stats', {
|
||||
}).then((res) => {
|
||||
|
@ -100,16 +114,23 @@ const headerActions = $computed(() => []);
|
|||
const headerTabs = $computed(() => [{
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
}, {
|
||||
key: 'emojis',
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
}, {
|
||||
key: 'federation',
|
||||
title: i18n.ts.federation,
|
||||
icon: 'fas fa-globe',
|
||||
}, {
|
||||
key: 'charts',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'fas fa-chart-bar',
|
||||
icon: 'fas fa-chart-simple',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.instanceInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
@ -117,7 +138,7 @@ definePageMetadata(computed(() => ({
|
|||
.fwhjspax {
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="file" :content-max="500" :margin-min="16" :margin-max="32">
|
||||
<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32">
|
||||
<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot">
|
||||
<a class="_formBlock thumbnail" :href="file.url" target="_blank">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
|
@ -39,6 +39,20 @@
|
|||
<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'ip' && info" class="_formRoot">
|
||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||
<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline>
|
||||
<template #key>IP</template>
|
||||
<template #value>{{ info.requestIp }}</template>
|
||||
</MkKeyValue>
|
||||
<FormSection v-if="info.requestHeaders">
|
||||
<template #label>Headers</template>
|
||||
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace">
|
||||
<template #key>{{ k }}</template>
|
||||
<template #value>{{ v }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'raw'" class="_formRoot">
|
||||
<MkObjectView v-if="info" tall :value="info">
|
||||
</MkObjectView>
|
||||
|
@ -54,13 +68,15 @@ import MkSwitch from '@/components/form/switch.vue';
|
|||
import MkObjectView from '@/components/object-view.vue';
|
||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkUserCardMini from '@/components/user-card-mini.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { acct } from '@/filters/user';
|
||||
import { iAmAdmin, iAmModerator } from '@/account';
|
||||
|
||||
let tab = $ref('overview');
|
||||
let file: any = $ref(null);
|
||||
|
@ -108,7 +124,11 @@ const headerTabs = $computed(() => [{
|
|||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'fas fa-info-circle',
|
||||
}, {
|
||||
}, iAmModerator ? {
|
||||
key: 'ip',
|
||||
title: 'IP',
|
||||
icon: 'fas fa-bars-staggered',
|
||||
} : null, {
|
||||
key: 'raw',
|
||||
title: 'Raw data',
|
||||
icon: 'fas fa-code',
|
||||
|
@ -117,7 +137,6 @@ const headerTabs = $computed(() => [{
|
|||
definePageMetadata(computed(() => ({
|
||||
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
|
||||
icon: 'fas fa-file',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, inject, watch, nextTick } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { popupMenu } from '@/os';
|
||||
import { url } from '@/config';
|
||||
|
@ -75,7 +75,6 @@ const hasTabs = computed(() => {
|
|||
|
||||
const showTabsPopup = (ev: MouseEvent) => {
|
||||
if (!hasTabs.value) return;
|
||||
if (!narrow.value) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const menu = props.tabs.map(tab => ({
|
||||
|
@ -126,16 +125,18 @@ onMounted(() => {
|
|||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
|
||||
watch(() => props.tab, () => {
|
||||
const tabEl = tabRefs[props.tab];
|
||||
if (tabEl && tabHighlightEl) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect = tabEl.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.style.width = rect.width + 'px';
|
||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
||||
}
|
||||
watch(() => [props.tab, props.tabs], () => {
|
||||
nextTick(() => {
|
||||
const tabEl = tabRefs[props.tab];
|
||||
if (tabEl && tabHighlightEl) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect = tabEl.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.style.width = rect.width + 'px';
|
||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
||||
}
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
@ -150,9 +151,6 @@ onUnmounted(() => {
|
|||
.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));
|
||||
|
|
|
@ -27,10 +27,10 @@
|
|||
</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">
|
||||
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false">
|
||||
<span>{{ $ts.username }}</span>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'">
|
||||
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params().origin === 'local'">
|
||||
<span>{{ $ts.host }}</span>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
@ -87,7 +87,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -116,7 +116,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.ads,
|
||||
icon: 'fas fa-audio-description',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -102,7 +102,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -61,27 +61,22 @@ let hcaptchaSecretKey: string | null = $ref(null);
|
|||
let recaptchaSiteKey: string | null = $ref(null);
|
||||
let recaptchaSecretKey: string | null = $ref(null);
|
||||
|
||||
const enableHcaptcha = $computed(() => provider === 'hcaptcha');
|
||||
const enableRecaptcha = $computed(() => provider === 'recaptcha');
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
enableHcaptcha = meta.enableHcaptcha;
|
||||
hcaptchaSiteKey = meta.hcaptchaSiteKey;
|
||||
hcaptchaSecretKey = meta.hcaptchaSecretKey;
|
||||
enableRecaptcha = meta.enableRecaptcha;
|
||||
recaptchaSiteKey = meta.recaptchaSiteKey;
|
||||
recaptchaSecretKey = meta.recaptchaSecretKey;
|
||||
|
||||
provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null;
|
||||
provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : null;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableHcaptcha,
|
||||
enableHcaptcha: provider === 'hcaptcha',
|
||||
hcaptchaSiteKey,
|
||||
hcaptchaSecretKey,
|
||||
enableRecaptcha,
|
||||
enableRecaptcha: provider === 'recaptcha',
|
||||
recaptchaSiteKey,
|
||||
recaptchaSecretKey,
|
||||
}).then(() => {
|
||||
|
|
|
@ -29,6 +29,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.database,
|
||||
icon: 'fas fa-database',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -122,6 +122,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.emailServer,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs" v-model:tab="tab"/></template>
|
||||
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
|
@ -292,7 +292,6 @@ const headerTabs = $computed(() => [{
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -110,7 +110,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.files,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ const router = useRouter();
|
|||
const indexInfo = {
|
||||
title: i18n.ts.controlPanel,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
};
|
||||
|
||||
|
@ -109,7 +108,7 @@ const menuDef = $computed(() => [{
|
|||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.ts.federation,
|
||||
to: '/admin/federation',
|
||||
to: '/about#federation',
|
||||
active: props.initialPage === 'federation',
|
||||
}, {
|
||||
icon: 'fas fa-clipboard-list',
|
||||
|
@ -201,7 +200,7 @@ const component = $computed(() => {
|
|||
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
|
||||
case 'users': return defineAsyncComponent(() => import('./users.vue'));
|
||||
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
|
||||
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
|
||||
//case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
|
||||
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
||||
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||
|
|
|
@ -47,6 +47,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.instanceBlocking,
|
||||
icon: 'fas fa-ban',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -53,6 +53,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.integration,
|
||||
icon: 'fas fa-share-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -73,7 +73,6 @@ 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';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
|
@ -145,6 +144,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.objectStorage,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -40,6 +40,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.other,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -45,7 +45,7 @@ Chart.register(
|
|||
);
|
||||
|
||||
const props = defineProps<{
|
||||
data: { name: string; value: number; color: string; }[];
|
||||
data: { name: string; value: number; color: string; onClick?: () => void }[];
|
||||
}>();
|
||||
|
||||
const chartEl = ref<HTMLCanvasElement>(null);
|
||||
|
@ -64,20 +64,26 @@ onMounted(() => {
|
|||
labels: props.data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: props.data.map(x => x.color),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: props.data.map(x => x.value),
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
onClick: (ev) => {
|
||||
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
|
||||
if (hit && props.data[hit.index].onClick) {
|
||||
props.data[hit.index].onClick();
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
|
|
|
@ -19,7 +19,7 @@ const props = defineProps<{
|
|||
user: misskey.entities.User;
|
||||
}>();
|
||||
|
||||
const chart = $ref(null);
|
||||
let chart = $ref(null);
|
||||
|
||||
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => {
|
||||
chart = res;
|
||||
|
|
|
@ -108,16 +108,27 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fedStats" class="container federationPies">
|
||||
<div class="container tagCloud">
|
||||
<div class="body">
|
||||
<MkTagCloud v-if="activeInstances">
|
||||
<li v-for="instance in activeInstances">
|
||||
<a @click.prevent="onInstanceClick(instance)">
|
||||
<img style="width: 32px;" :src="instance.iconUrl">
|
||||
</a>
|
||||
</li>
|
||||
</MkTagCloud>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="topSubInstancesForPie && topPubInstancesForPie" class="container federationPies">
|
||||
<div class="body">
|
||||
<div class="chart deliver">
|
||||
<div class="title">Sub</div>
|
||||
<XPie :data="fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowersCount }])"/>
|
||||
<XPie :data="topSubInstancesForPie"/>
|
||||
<div class="subTitle">Top 10</div>
|
||||
</div>
|
||||
<div class="chart inbox">
|
||||
<div class="title">Pub</div>
|
||||
<XPie :data="fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowingCount }])"/>
|
||||
<XPie :data="topPubInstancesForPie"/>
|
||||
<div class="subTitle">Top 10</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -154,8 +165,8 @@ import XFederation from './overview.federation.vue';
|
|||
import XQueueChart from './overview.queue-chart.vue';
|
||||
import XUser from './overview.user.vue';
|
||||
import XPie from './overview.pie.vue';
|
||||
import MkInstanceStats from '@/components/instance-stats.vue';
|
||||
import MkNumberDiff from '@/components/number-diff.vue';
|
||||
import MkTagCloud from '@/components/tag-cloud.vue';
|
||||
import { version, url } from '@/config';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
|
@ -189,7 +200,8 @@ const rootEl = $ref<HTMLElement>();
|
|||
const chartEl = $ref<HTMLCanvasElement>(null);
|
||||
let stats: any = $ref(null);
|
||||
let serverInfo: any = $ref(null);
|
||||
let fedStats: any = $ref(null);
|
||||
let topSubInstancesForPie: any = $ref(null);
|
||||
let topPubInstancesForPie: any = $ref(null);
|
||||
let usersComparedToThePrevDay: any = $ref(null);
|
||||
let notesComparedToThePrevDay: any = $ref(null);
|
||||
let federationPubActive = $ref<number | null>(null);
|
||||
|
@ -197,6 +209,7 @@ let federationPubActiveDiff = $ref<number | null>(null);
|
|||
let federationSubActive = $ref<number | null>(null);
|
||||
let federationSubActiveDiff = $ref<number | null>(null);
|
||||
let newUsers = $ref(null);
|
||||
let activeInstances = $shallowRef(null);
|
||||
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
|
||||
const now = new Date();
|
||||
let chartInstance: Chart = null;
|
||||
|
@ -363,6 +376,10 @@ async function renderChart() {
|
|||
});
|
||||
}
|
||||
|
||||
function onInstanceClick(i) {
|
||||
os.pageWindow(`/instance-info/${i.host}`);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
/*
|
||||
const magicGrid = new MagicGrid({
|
||||
|
@ -395,8 +412,23 @@ onMounted(async () => {
|
|||
federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
|
||||
});
|
||||
|
||||
os.apiGet('federation/stats').then(res => {
|
||||
fedStats = res;
|
||||
os.apiGet('federation/stats', { limit: 10 }).then(res => {
|
||||
topSubInstancesForPie = res.topSubInstances.map(x => ({
|
||||
name: x.host,
|
||||
color: x.themeColor,
|
||||
value: x.followersCount,
|
||||
onClick: () => {
|
||||
os.pageWindow(`/instance-info/${x.host}`);
|
||||
},
|
||||
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
|
||||
topPubInstancesForPie = res.topPubInstances.map(x => ({
|
||||
name: x.host,
|
||||
color: x.themeColor,
|
||||
value: x.followingCount,
|
||||
onClick: () => {
|
||||
os.pageWindow(`/instance-info/${x.host}`);
|
||||
},
|
||||
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
|
||||
});
|
||||
|
||||
os.api('admin/server-info').then(serverInfoResponse => {
|
||||
|
@ -410,6 +442,13 @@ onMounted(async () => {
|
|||
newUsers = res;
|
||||
});
|
||||
|
||||
os.api('federation/instances', {
|
||||
sort: '+lastCommunicatedAt',
|
||||
limit: 25,
|
||||
}).then(res => {
|
||||
activeInstances = res;
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
queueStatsConnection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
|
@ -429,7 +468,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.dashboard,
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -523,7 +561,7 @@ definePageMetadata({
|
|||
> .body {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -577,6 +615,14 @@ definePageMetadata({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tagCloud {
|
||||
> .body {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden; overflow: clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.proxyAccount,
|
||||
icon: 'fas fa-ghost',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -52,6 +52,5 @@ const headerTabs = $computed(() => [{
|
|||
definePageMetadata({
|
||||
title: i18n.ts.jobQueue,
|
||||
icon: 'fas fa-clipboard-list',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -78,7 +78,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.relays,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -14,6 +14,18 @@
|
|||
<XBotProtection/>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Log IP address</template>
|
||||
<template v-if="enableIpLogging" #suffix>Enabled</template>
|
||||
<template v-else #suffix>Disabled</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableIpLogging" class="_formBlock" @update:modelValue="save">
|
||||
<template #label>Enable</template>
|
||||
</FormSwitch>
|
||||
</div>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Summaly Proxy</template>
|
||||
|
||||
|
@ -51,17 +63,20 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
let summalyProxy: string = $ref('');
|
||||
let enableHcaptcha: boolean = $ref(false);
|
||||
let enableRecaptcha: boolean = $ref(false);
|
||||
let enableIpLogging: boolean = $ref(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
summalyProxy = meta.summalyProxy;
|
||||
enableHcaptcha = meta.enableHcaptcha;
|
||||
enableRecaptcha = meta.enableRecaptcha;
|
||||
enableIpLogging = meta.enableIpLogging;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy,
|
||||
enableIpLogging,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
@ -74,6 +89,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.security,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -258,6 +258,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -30,11 +30,11 @@
|
|||
</MkSelect>
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
|
||||
<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()">
|
||||
<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>
|
||||
|
@ -135,7 +135,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -47,7 +47,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<template>
|
||||
<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="tlEl" :key="antennaId"
|
||||
class="tl"
|
||||
src="antenna"
|
||||
:antenna="antennaId"
|
||||
:sound="true"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<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="tlEl" :key="antennaId"
|
||||
class="tl"
|
||||
src="antenna"
|
||||
:antenna="antennaId"
|
||||
:sound="true"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -21,7 +24,7 @@ import { scroll } from '@/scripts/scroll';
|
|||
import * as os from '@/os';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import i18n from '@/components/global/i18n';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -68,23 +71,21 @@ watch(() => props.antennaId, async () => {
|
|||
});
|
||||
}, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerActions = $computed(() => antenna ? [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: i18n.ts.jumpToSpecifiedDate,
|
||||
handler: timetravel,
|
||||
}, {
|
||||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.settings,
|
||||
handler: settings,
|
||||
}] : []);
|
||||
|
||||
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>
|
||||
|
||||
|
@ -109,7 +110,7 @@ definePageMetadata(computed(() => antenna ? {
|
|||
> .tl {
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
overflow: hidden; overflow: clip;
|
||||
}
|
||||
|
||||
&.min-width_800px {
|
||||
|
|
|
@ -111,11 +111,9 @@ 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>
|
||||
|
||||
|
|
|
@ -80,7 +80,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => channel ? {
|
||||
title: channel.name,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -75,6 +75,5 @@ const headerTabs = $computed(() => [{
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.channel,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
|
|
@ -102,7 +102,6 @@ const headerActions = $computed(() => clip && isOwned ? [{
|
|||
definePageMetadata(computed(() => clip ? {
|
||||
title: clip.name,
|
||||
icon: 'fas fa-paperclip',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: folder ? folder.name : i18n.ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
})));
|
||||
</script>
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
<template>
|
||||
<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 XCategory from './emojis.category.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const tab = ref('category');
|
||||
|
||||
function menu(ev) {
|
||||
os.popupMenu([{
|
||||
icon: 'fas fa-download',
|
||||
text: i18n.ts.export,
|
||||
action: async () => {
|
||||
os.api('export-custom-emojis', {
|
||||
})
|
||||
.then(() => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts.exportRequested,
|
||||
});
|
||||
}).catch((err) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message,
|
||||
});
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
30
packages/client/src/pages/explore.featured.vue
Normal file
30
packages/client/src/pages/explore.featured.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkTab v-model="tab" style="margin-bottom: var(--margin);">
|
||||
<option value="notes">{{ i18n.ts.notes }}</option>
|
||||
<option value="polls">{{ i18n.ts.poll }}</option>
|
||||
</MkTab>
|
||||
<XNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/>
|
||||
<XNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const paginationForNotes = {
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
const paginationForPolls = {
|
||||
endpoint: 'notes/polls/recommendation' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
let tab = $ref('notes');
|
||||
</script>
|
143
packages/client/src/pages/explore.users.vue
Normal file
143
packages/client/src/pages/explore.users.vue
Normal file
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<MkSpacer :content-max="1200">
|
||||
<div v-if="origin === 'local'">
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap" persist-key="explore-pinned-users">
|
||||
<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
|
||||
<XUserList :pagination="pinnedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-popular-users">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-updated-users">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-registered-users">
|
||||
<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsers"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
|
||||
|
||||
<div class="vxjfqztj">
|
||||
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
||||
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||
<XUserList :pagination="tagUsers"/>
|
||||
</MkFolder>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsersF"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import XUserList from '@/components/user-list.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
const props = defineProps<{
|
||||
origin: 'local' | 'remote';
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
let tagsEl = $ref<InstanceType<typeof MkFolder>>();
|
||||
let tagsLocal = $ref([]);
|
||||
let tagsRemote = $ref([]);
|
||||
|
||||
watch(() => props.tag, () => {
|
||||
if (tagsEl) tagsEl.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const tagUsers = $computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users' };
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
attachedToLocalUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsLocal = tags;
|
||||
});
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedRemoteUsers',
|
||||
attachedToRemoteUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsRemote = tags;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vxjfqztj {
|
||||
> * {
|
||||
margin-right: 16px;
|
||||
|
||||
&.local {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,90 +1,39 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1200">
|
||||
<div class="lznhrdub">
|
||||
<div v-if="tab === 'local'">
|
||||
<div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }">
|
||||
<header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header>
|
||||
<div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div>
|
||||
</div>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap" persist-key="explore-pinned-users">
|
||||
<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
|
||||
<XUserList :pagination="pinnedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-popular-users">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-updated-users">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-registered-users">
|
||||
<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsers"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="tab === 'remote'">
|
||||
<div v-if="tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: `url(/client-assets/fedi.jpg)` }">
|
||||
<header><span>{{ $ts.exploreFediverse }}</span></header>
|
||||
</div>
|
||||
|
||||
<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
|
||||
|
||||
<div class="vxjfqztj">
|
||||
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
||||
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||
<XUserList :pagination="tagUsers"/>
|
||||
</MkFolder>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsersF"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'">
|
||||
<div class="_isolated">
|
||||
<MkInput v-model="searchQuery" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.searchUser }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchOrigin">
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkRadios>
|
||||
</div>
|
||||
|
||||
<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
|
||||
</div>
|
||||
<div class="lznhrdub">
|
||||
<div v-if="tab === 'featured'">
|
||||
<XFeatured/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div v-else-if="tab === 'localUsers'">
|
||||
<XUsers origin="local"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'remoteUsers'">
|
||||
<XUsers origin="remote"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'">
|
||||
<div class="_isolated">
|
||||
<MkInput v-model="searchQuery" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.searchUser }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchOrigin">
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
</MkRadios>
|
||||
</div>
|
||||
|
||||
<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import XUserList from '@/components/user-list.vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import XFeatured from './explore.featured.vue';
|
||||
import XUsers from './explore.users.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkRadios from '@/components/form/radios.vue';
|
||||
|
@ -98,11 +47,8 @@ const props = defineProps<{
|
|||
tag?: string;
|
||||
}>();
|
||||
|
||||
let tab = $ref('local');
|
||||
let tab = $ref('featured');
|
||||
let tagsEl = $ref<InstanceType<typeof MkFolder>>();
|
||||
let tagsLocal = $ref([]);
|
||||
let tagsRemote = $ref([]);
|
||||
let stats = $ref(null);
|
||||
let searchQuery = $ref(null);
|
||||
let searchOrigin = $ref('combined');
|
||||
|
||||
|
@ -110,44 +56,6 @@ watch(() => props.tag, () => {
|
|||
if (tagsEl) tagsEl.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const tagUsers = $computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users' };
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const searchPagination = {
|
||||
endpoint: 'users/search' as const,
|
||||
limit: 10,
|
||||
|
@ -157,31 +65,19 @@ const searchPagination = {
|
|||
} : null),
|
||||
};
|
||||
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
attachedToLocalUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsLocal = tags;
|
||||
});
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedRemoteUsers',
|
||||
attachedToRemoteUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsRemote = tags;
|
||||
});
|
||||
os.api('stats').then(_stats => {
|
||||
stats = _stats;
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'local',
|
||||
title: i18n.ts.local,
|
||||
key: 'featured',
|
||||
icon: 'fas fa-bolt',
|
||||
title: i18n.ts.featured,
|
||||
}, {
|
||||
key: 'remote',
|
||||
key: 'localUsers',
|
||||
icon: 'fas fa-users',
|
||||
title: i18n.ts.users,
|
||||
}, {
|
||||
key: 'remoteUsers',
|
||||
icon: 'fas fa-users',
|
||||
title: i18n.ts.remote,
|
||||
}, {
|
||||
key: 'search',
|
||||
|
@ -191,49 +87,5 @@ const headerTabs = $computed(() => [{
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.explore,
|
||||
icon: 'fas fa-hashtag',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.localfedi7 {
|
||||
color: #fff;
|
||||
padding: 16px;
|
||||
height: 80px;
|
||||
background-position: 50%;
|
||||
background-size: cover;
|
||||
margin-bottom: var(--margin);
|
||||
|
||||
> * {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
padding: 6px 8px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
> header {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> div {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.vxjfqztj {
|
||||
> * {
|
||||
margin-right: 16px;
|
||||
|
||||
&.local {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -38,7 +38,6 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
|||
definePageMetadata({
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'fas fa-star',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes ref="notes" :pagination="pagination"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
|
@ -1,122 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1000">
|
||||
<div class="taeiyria">
|
||||
<div class="query">
|
||||
<MkInput v-model="host" :debounce="true" class="">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
<FormSplit style="margin-top: var(--margin);">
|
||||
<MkSelect v-model="state">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="federating">{{ $ts.federating }}</option>
|
||||
<option value="subscribing">{{ $ts.subscribing }}</option>
|
||||
<option value="publishing">{{ $ts.publishing }}</option>
|
||||
<option value="suspended">{{ $ts.suspended }}</option>
|
||||
<option value="blocked">{{ $ts.blocked }}</option>
|
||||
<option value="notResponding">{{ $ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkInstanceCardMini from '@/components/instance-card-mini.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let host = $ref('');
|
||||
let state = $ref('federating');
|
||||
let sort = $ref('+pubSub');
|
||||
const pagination = {
|
||||
endpoint: 'federation/instances' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: computed(() => ({
|
||||
sort: sort,
|
||||
host: host !== '' ? host : null,
|
||||
...(
|
||||
state === 'federating' ? { federating: true } :
|
||||
state === 'subscribing' ? { subscribing: true } :
|
||||
state === 'publishing' ? { publishing: true } :
|
||||
state === 'suspended' ? { suspended: true } :
|
||||
state === 'blocked' ? { blocked: true } :
|
||||
state === 'notResponding' ? { notResponding: true } :
|
||||
{}),
|
||||
})),
|
||||
};
|
||||
|
||||
function getStatus(instance) {
|
||||
if (instance.isSuspended) return 'Suspended';
|
||||
if (instance.isBlocked) return 'Blocked';
|
||||
if (instance.isNotResponding) return 'Error';
|
||||
return 'Alive';
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.federation,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.taeiyria {
|
||||
> .query {
|
||||
background: var(--bg);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dqokceoi {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
grid-gap: 12px;
|
||||
|
||||
> .instance:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -65,7 +65,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'fas fa-user-clock',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,30 +1,33 @@
|
|||
<template>
|
||||
<div>
|
||||
<FormSuspense :p="init">
|
||||
<FormInput v-model="title">
|
||||
<template #label>{{ $ts.title }}</template>
|
||||
</FormInput>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormInput v-model="title">
|
||||
<template #label>{{ $ts.title }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="description" :max="500">
|
||||
<template #label>{{ $ts.description }}</template>
|
||||
</FormTextarea>
|
||||
<FormTextarea v-model="description" :max="500">
|
||||
<template #label>{{ $ts.description }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormGroup>
|
||||
<div v-for="file in files" :key="file.id" class="_formGroup wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
|
||||
<div class="name">{{ file.name }}</div>
|
||||
<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
|
||||
<div class="">
|
||||
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
|
||||
<div class="name">{{ file.name }}</div>
|
||||
<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<FormButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
|
||||
</div>
|
||||
<FormButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
|
||||
<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
|
||||
|
||||
<FormButton v-if="postId" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton v-else primary @click="save"><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
|
||||
<FormButton v-if="postId" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton v-else primary @click="save"><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
|
||||
|
||||
<FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
|
||||
</FormSuspense>
|
||||
</div>
|
||||
<FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -33,7 +36,6 @@ import FormButton from '@/components/ui/button.vue';
|
|||
import FormInput from '@/components/form/input.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
|
@ -72,7 +74,7 @@ async function save() {
|
|||
fileIds: files.map(file => file.id),
|
||||
isSensitive: isSensitive,
|
||||
});
|
||||
mainRouter.push(`/gallery/${props.postId}`);
|
||||
router.push(`/gallery/${props.postId}`);
|
||||
} else {
|
||||
const created = await os.apiWithDialog('gallery/posts/create', {
|
||||
title: title,
|
||||
|
@ -93,7 +95,7 @@ async function del() {
|
|||
await os.apiWithDialog('gallery/posts/delete', {
|
||||
postId: props.postId,
|
||||
});
|
||||
mainRouter.push('/gallery');
|
||||
router.push('/gallery');
|
||||
}
|
||||
|
||||
watch(() => props.postId, () => {
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1400">
|
||||
<div class="_root">
|
||||
<MkTab v-if="$i" v-model="tab">
|
||||
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
|
||||
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
|
||||
<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
|
||||
</MkTab>
|
||||
|
||||
<div v-if="tab === 'explore'">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
|
||||
|
@ -60,6 +54,9 @@ import number from '@/filters/number';
|
|||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
|
@ -100,14 +97,31 @@ watch(() => props.tag, () => {
|
|||
if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.create,
|
||||
handler: () => {
|
||||
router.push('/gallery/new');
|
||||
},
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'explore',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
}, {
|
||||
key: 'liked',
|
||||
title: i18n.ts._gallery.liked,
|
||||
icon: 'fas fa-heart',
|
||||
}, {
|
||||
key: 'my',
|
||||
title: i18n.ts._gallery.my,
|
||||
icon: 'fas fa-edit',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,52 +1,57 @@
|
|||
<template>
|
||||
<div class="_root">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="post" class="rkxwuolj">
|
||||
<div class="files">
|
||||
<div v-for="file in post.files" :key="file.id" class="file">
|
||||
<img :src="file.url"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body _block">
|
||||
<div class="title">{{ post.title }}</div>
|
||||
<div class="description"><Mfm :text="post.description"/></div>
|
||||
<div class="info">
|
||||
<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<MkButton v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="$ts._gallery.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1000" :margin-min="16" :margin-max="32">
|
||||
<div class="_root">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="post" class="rkxwuolj">
|
||||
<div class="files">
|
||||
<div v-for="file in post.files" :key="file.id" class="file">
|
||||
<img :src="file.url"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="$ts.edit" v-click-anime class="_button" @click="edit"><i class="fas fa-pencil-alt fa-fw"></i></button>
|
||||
<button v-tooltip="$ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
|
||||
<button v-tooltip="$ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
|
||||
<div class="body _block">
|
||||
<div class="title">{{ post.title }}</div>
|
||||
<div class="description"><Mfm :text="post.description"/></div>
|
||||
<div class="info">
|
||||
<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<MkButton v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="$ts._gallery.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="$ts.edit" v-click-anime class="_button" @click="edit"><i class="fas fa-pencil-alt fa-fw"></i></button>
|
||||
<button v-tooltip="$ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
|
||||
<button v-tooltip="$ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="post.user" class="avatar"/>
|
||||
<div class="name">
|
||||
<MkUserName :user="post.user" style="display: block;"/>
|
||||
<MkAcct :user="post.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<div class="sdrarzaf">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkContainer>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="post.user" class="avatar"/>
|
||||
<div class="name">
|
||||
<MkUserName :user="post.user" style="display: block;"/>
|
||||
<MkAcct :user="post.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<div class="sdrarzaf">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkContainer>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -69,8 +74,8 @@ const props = defineProps<{
|
|||
postId: string;
|
||||
}>();
|
||||
|
||||
const post = $ref(null);
|
||||
const error = $ref(null);
|
||||
let post = $ref(null);
|
||||
let error = $ref(null);
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
|
@ -133,23 +138,17 @@ function edit() {
|
|||
|
||||
watch(() => props.postId, fetchPost, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => post ? {
|
||||
title: post.title,
|
||||
avatar: post.user,
|
||||
path: `/gallery/${post.id}`,
|
||||
share: {
|
||||
title: post.title,
|
||||
text: post.description,
|
||||
},
|
||||
actions: [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}],
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
@ -95,6 +95,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'users'" class="_formRoot">
|
||||
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${new Date(user.updatedAt).toLocaleString()}`" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'raw'" class="_formRoot">
|
||||
<MkObjectView tall :value="instance">
|
||||
</MkObjectView>
|
||||
|
@ -121,6 +128,8 @@ import bytes from '@/filters/bytes';
|
|||
import { iAmModerator } from '@/account';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkUserCardMini from '@/components/user-card-mini.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
host: string;
|
||||
|
@ -133,6 +142,17 @@ let instance = $ref<misskey.entities.Instance | null>(null);
|
|||
let suspended = $ref(false);
|
||||
let isBlocked = $ref(false);
|
||||
|
||||
const usersPagination = {
|
||||
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
sort: '+updatedAt',
|
||||
state: 'all',
|
||||
hostname: props.host,
|
||||
},
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
async function fetch() {
|
||||
instance = await os.api('federation/show-instance', {
|
||||
host: props.host,
|
||||
|
@ -182,16 +202,19 @@ const headerTabs = $computed(() => [{
|
|||
key: 'chart',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'fas fa-chart-simple',
|
||||
}, {
|
||||
key: 'users',
|
||||
title: i18n.ts.users,
|
||||
icon: 'fas fa-users',
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw data',
|
||||
title: 'Raw',
|
||||
icon: 'fas fa-code',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
title: props.host,
|
||||
icon: 'fas fa-server',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes :pagination="pagination"/>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.mentions,
|
||||
icon: 'fas fa-at',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
|
@ -1,30 +0,0 @@
|
|||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes :pagination="pagination"/>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
visibility: 'specified',
|
||||
},
|
||||
};
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.directNotes,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
|
@ -159,7 +159,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.messaging,
|
||||
icon: 'fas fa-comments',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue