Merge branch 'main' into feat/secure-fetch
This commit is contained in:
commit
66df12df0a
20 changed files with 495 additions and 624 deletions
|
@ -25,6 +25,7 @@ export const AppRepository = db.getRepository(App).extend({
|
|||
return {
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
callbackUrl: app.callbackUrl,
|
||||
permission: app.permission,
|
||||
...(opts.includeSecret ? { secret: app.secret } : {}),
|
||||
|
|
|
@ -5,11 +5,10 @@ import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js
|
|||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Channel } from '@/models/entities/channel.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH, HOUR } from '@/const.js';
|
||||
import { noteVisibilities } from '../../../../types.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import define from '../../define.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -78,13 +77,24 @@ export const meta = {
|
|||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
|
||||
},
|
||||
|
||||
lessRestrictiveVisibility: {
|
||||
message: 'The visibility cannot be less restrictive than the parent note.',
|
||||
code: 'LESS_RESTRICTIVE_VISIBILITY',
|
||||
id: 'c8ab7a7a-8852-41e2-8b24-079bbaceb585',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
visibility: { type: 'string', enum: noteVisibilities, default: 'public' },
|
||||
visibility: {
|
||||
description: 'The visibility of the new note. Must be the same or more restrictive than a replied to or quoted note.',
|
||||
type: 'string',
|
||||
enum: noteVisibilities,
|
||||
default: 'public',
|
||||
},
|
||||
visibleUserIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
|
@ -195,6 +205,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
}
|
||||
|
||||
// check that the visibility is not less restrictive
|
||||
if (noteVisibilities.indexOf(renote.visibility) > noteVisibilities.indexOf(ps.visibility)) {
|
||||
throw new ApiError(meta.errors.lessRestrictiveVisibility);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (renote.userId !== user.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
|
@ -219,6 +234,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
}
|
||||
|
||||
// check that the visibility is not less restrictive
|
||||
if (noteVisibilities.indexOf(reply.visibility) > noteVisibilities.indexOf(ps.visibility)) {
|
||||
throw new ApiError(meta.errors.lessRestrictiveVisibility);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (reply.userId !== user.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
|
|
|
@ -170,11 +170,6 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
|||
data.visibility = 'followers';
|
||||
}
|
||||
|
||||
// 返信対象がpublicではないならhomeにする
|
||||
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
|
||||
data.visibility = 'home';
|
||||
}
|
||||
|
||||
// ローカルのみをRenoteしたらローカルのみにする
|
||||
if (data.renote && data.renote.localOnly && data.channel == null) {
|
||||
data.localOnly = true;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
||||
|
||||
/**
|
||||
* Note visibilities, ordered from most to least open.
|
||||
*/
|
||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||
|
||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||
|
|
|
@ -26,150 +26,88 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
const emit = defineEmits<{
|
||||
(ev: 'change', v: any): void;
|
||||
(ev: 'keydown', v: KeyboardEvent): void;
|
||||
(ev: 'enter'): void;
|
||||
(ev: 'update:modelValue', v: string): void;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
autocomplete: {
|
||||
required: false,
|
||||
},
|
||||
spellcheck: {
|
||||
required: false,
|
||||
},
|
||||
code: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
tall: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
pre: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
debounce: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
manualSave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string;
|
||||
type?: string;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
pattern?: string;
|
||||
placeholder?: string;
|
||||
autofocus?: boolean;
|
||||
autocomplete?: boolean;
|
||||
spellcheck?: boolean;
|
||||
code?: boolean;
|
||||
tall?: boolean;
|
||||
pre?: boolean;
|
||||
debounce?: boolean;
|
||||
manualSave?: boolean;
|
||||
}>(), {
|
||||
autofocus: false,
|
||||
tall: false,
|
||||
pre: false,
|
||||
manualSave: false,
|
||||
});
|
||||
|
||||
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
|
||||
const { modelValue, autofocus } = toRefs(props);
|
||||
// modelValue is read only, so a separate ref is needed.
|
||||
const v = $ref(modelValue.value);
|
||||
|
||||
setup(props, context) {
|
||||
const { modelValue, autofocus } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
const invalid = ref(false);
|
||||
const filled = computed(() => v.value !== '' && v.value != null);
|
||||
const inputEl = ref(null);
|
||||
const focused = $ref(false);
|
||||
const changed = $ref(false);
|
||||
const invalid = $ref(false);
|
||||
const filled = computed(() => modelValue.value !== '' && modelValue.value != null);
|
||||
const inputEl = $ref(null);
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
changed.value = true;
|
||||
context.emit('change', ev);
|
||||
};
|
||||
const onKeydown = (ev: KeyboardEvent) => {
|
||||
context.emit('keydown', ev);
|
||||
const focus = () => inputEl.focus();
|
||||
const onInput = evt => {
|
||||
changed = true;
|
||||
emit('change', evt);
|
||||
};
|
||||
const onKeydown = (evt: KeyboardEvent) => {
|
||||
emit('keydown', evt);
|
||||
if (evt.code === 'Enter') {
|
||||
emit('enter');
|
||||
}
|
||||
};
|
||||
const updated = () => {
|
||||
changed = false;
|
||||
emit('update:modelValue', v);
|
||||
};
|
||||
const debouncedUpdated = debounce(1000, updated);
|
||||
|
||||
if (ev.code === 'Enter') {
|
||||
context.emit('enter');
|
||||
}
|
||||
};
|
||||
watch(modelValue, newValue => {
|
||||
if (!props.manualSave) {
|
||||
if (props.debounce) {
|
||||
debouncedUpdated();
|
||||
} else {
|
||||
updated();
|
||||
}
|
||||
}
|
||||
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
context.emit('update:modelValue', v.value);
|
||||
};
|
||||
invalid = inputEl.validity.badInput;
|
||||
});
|
||||
|
||||
const debouncedUpdated = debounce(1000, updated);
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
v.value = newValue;
|
||||
});
|
||||
|
||||
watch(v, newValue => {
|
||||
if (!props.manualSave) {
|
||||
if (props.debounce) {
|
||||
debouncedUpdated();
|
||||
} else {
|
||||
updated();
|
||||
}
|
||||
}
|
||||
|
||||
invalid.value = inputEl.value.validity.badInput;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (autofocus.value) {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
v,
|
||||
focused,
|
||||
invalid,
|
||||
changed,
|
||||
filled,
|
||||
inputEl,
|
||||
focus,
|
||||
onInput,
|
||||
onKeydown,
|
||||
updated,
|
||||
};
|
||||
},
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (props.autofocus) {
|
||||
inputEl.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="mjndxjcg">
|
||||
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
|
||||
<p><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
|
||||
<MkButton class="button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton>
|
||||
<MkButton v-if="!final" class="button" @click="retry">{{ i18n.ts.retry }}</MkButton>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
@ -11,6 +11,16 @@
|
|||
<script lang="ts" setup>
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'retry'): void;
|
||||
}>();
|
||||
const retry = emit.bind(null, 'retry');
|
||||
|
||||
defineProps<{
|
||||
// true if this operation can not be retried
|
||||
final?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="mk-google">
|
||||
<div class="mk-search">
|
||||
<input v-model="query" type="search" :placeholder="q">
|
||||
<button @click="search"><i class="fas fa-search"></i> {{ $ts.search }}</button>
|
||||
<button><i class="fas fa-search"></i> {{ $ts.search }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -13,14 +13,10 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const query = ref(props.q);
|
||||
|
||||
const search = () => {
|
||||
window.open(`https://www.google.com/search?q=${query.value}`, '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-google {
|
||||
.mk-search {
|
||||
display: flex;
|
||||
margin: 8px 0;
|
||||
|
|
@ -7,7 +7,7 @@ import MkEmoji from '@/components/global/emoji.vue';
|
|||
import { concat } from '@/scripts/array';
|
||||
import MkFormula from '@/components/formula.vue';
|
||||
import MkCode from '@/components/code.vue';
|
||||
import MkGoogle from '@/components/google.vue';
|
||||
import MkSearch from '@/components/mfm-search.vue';
|
||||
import MkSparkle from '@/components/sparkle.vue';
|
||||
import MkA from '@/components/global/a.vue';
|
||||
import { host } from '@/config';
|
||||
|
@ -306,7 +306,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
case 'search': {
|
||||
return [h(MkGoogle, {
|
||||
return [h(MkSearch, {
|
||||
key: Math.random(),
|
||||
q: token.props.query,
|
||||
})];
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
|
||||
<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">
|
||||
<div v-for="i in value.length" :key="i" class="element">
|
||||
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
|
||||
</div>
|
||||
</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">
|
||||
<div v-for="k in Object.keys(value)" :key="k" 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">
|
||||
|
@ -28,54 +28,38 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { reactive, defineProps } from 'vue';
|
||||
import number from '@/filters/number';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'XValue',
|
||||
const props = defineProps<{
|
||||
value: any;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
const collapsed = reactive({});
|
||||
|
||||
setup(props) {
|
||||
const collapsed = reactive({});
|
||||
if (isObject(props.value)) {
|
||||
for (const key in props.value) {
|
||||
collapsed[key] = collapsable(props.value[key]);
|
||||
}
|
||||
}
|
||||
|
||||
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 isObject(v): boolean {
|
||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||
}
|
||||
function isArray(v): boolean {
|
||||
return Array.isArray(v);
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
function collapsable(v): boolean {
|
||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||
}
|
||||
|
||||
return {
|
||||
number,
|
||||
collapsed,
|
||||
isObject,
|
||||
isArray,
|
||||
isEmpty,
|
||||
collapsable,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -27,90 +27,70 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkRadio from '@/components/form/radio.vue';
|
||||
import * as os from '@/os';
|
||||
import * as config from '@/config';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkSwitch,
|
||||
MkTextarea,
|
||||
MkRadio,
|
||||
},
|
||||
const text = ref('');
|
||||
const flag = ref(true);
|
||||
const radio = ref('misskey');
|
||||
const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
|
||||
|
||||
data() {
|
||||
return {
|
||||
text: '',
|
||||
flag: true,
|
||||
radio: 'misskey',
|
||||
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
|
||||
};
|
||||
},
|
||||
function openDialog(): void {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
title: 'Oh my Aichan',
|
||||
text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async openDialog() {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
title: 'Oh my Aichan',
|
||||
text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
});
|
||||
function openForm(): void {
|
||||
os.form('Example form', {
|
||||
foo: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
label: 'This is a boolean property',
|
||||
},
|
||||
|
||||
async openForm() {
|
||||
os.form('Example form', {
|
||||
foo: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
label: 'This is a boolean property'
|
||||
},
|
||||
bar: {
|
||||
type: 'number',
|
||||
default: 300,
|
||||
label: 'This is a number property'
|
||||
},
|
||||
baz: {
|
||||
type: 'string',
|
||||
default: 'Misskey makes you happy.',
|
||||
label: 'This is a string property'
|
||||
},
|
||||
});
|
||||
bar: {
|
||||
type: 'number',
|
||||
default: 300,
|
||||
label: 'This is a number property',
|
||||
},
|
||||
|
||||
async openDrive() {
|
||||
os.selectDriveFile();
|
||||
baz: {
|
||||
type: 'string',
|
||||
default: 'Misskey makes you happy.',
|
||||
label: 'This is a string property',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async selectUser() {
|
||||
os.selectUser();
|
||||
},
|
||||
function openDrive(): void {
|
||||
os.selectDriveFile(true);
|
||||
}
|
||||
|
||||
async openMenu(ev) {
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: 'Fruits'
|
||||
}, {
|
||||
text: 'Create some apples',
|
||||
action: () => {},
|
||||
}, {
|
||||
text: 'Read some oranges',
|
||||
action: () => {},
|
||||
}, {
|
||||
text: 'Update some melons',
|
||||
action: () => {},
|
||||
}, null, {
|
||||
text: 'Delete some bananas',
|
||||
danger: true,
|
||||
action: () => {},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
}
|
||||
});
|
||||
function openMenu(ev): void {
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: 'Fruits'
|
||||
}, {
|
||||
text: 'Create some apples',
|
||||
action: (): void => {},
|
||||
}, {
|
||||
text: 'Read some oranges',
|
||||
action: (): void => {},
|
||||
}, {
|
||||
text: 'Update some melons',
|
||||
action: (): void => {},
|
||||
}, null, {
|
||||
text: 'Delete some bananas',
|
||||
danger: true,
|
||||
action: (): void => {},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -31,7 +31,7 @@ type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
|
|||
const props = withDefaults(defineProps<{
|
||||
manualShowing?: boolean | null;
|
||||
anchor?: { x: string; y: string; };
|
||||
src?: HTMLElement;
|
||||
src?: HTMLElement | null;
|
||||
preferType?: ModalTypes | 'auto';
|
||||
zPriority?: 'low' | 'middle' | 'high';
|
||||
noOverlap?: boolean;
|
||||
|
@ -102,7 +102,7 @@ const MARGIN = 16;
|
|||
const align = () => {
|
||||
if (props.src == null) return;
|
||||
if (type === 'drawer') return;
|
||||
if (type == 'dialog') return;
|
||||
if (type === 'dialog') return;
|
||||
|
||||
if (content == null) return;
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
|
||||
</MkSelect>
|
||||
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
</header>
|
||||
<XDraggable
|
||||
v-model="widgets_"
|
||||
|
@ -30,73 +30,57 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { widgets as widgetDefs } from '@/widgets';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XDraggable: defineAsyncComponent(() => import('vuedraggable')),
|
||||
MkSelect,
|
||||
MkButton,
|
||||
},
|
||||
const XDraggable = defineAsyncComponent(() => import('vuedraggable'));
|
||||
|
||||
props: {
|
||||
widgets: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
type Widgets = any[];
|
||||
|
||||
emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
|
||||
const props = defineProps<{
|
||||
widgets: Widgets;
|
||||
edit: boolean;
|
||||
}>();
|
||||
|
||||
setup(props, context) {
|
||||
const widgetRefs = reactive({});
|
||||
const configWidget = (id: string) => {
|
||||
widgetRefs[id].configure();
|
||||
};
|
||||
const widgetAdderSelected = ref(null);
|
||||
const addWidget = () => {
|
||||
if (widgetAdderSelected.value == null) return;
|
||||
const emit = defineEmits<{
|
||||
(ev: 'updateWidgets', v: Widgets): void;
|
||||
(ev: 'addWidget', v: { name: string; id: string; data: Record<string, any>; }): void;
|
||||
(ev: 'removeWidget', v: { name: string; id: string; }): void;
|
||||
(ev: 'updateWidget', v: { id: string; data: Record<string, any>; }): void;
|
||||
(ev: 'exit'): void;
|
||||
}>();
|
||||
|
||||
context.emit('addWidget', {
|
||||
name: widgetAdderSelected.value,
|
||||
id: uuid(),
|
||||
data: {},
|
||||
});
|
||||
const widgetRefs = reactive({});
|
||||
const configWidget = (id: string) => {
|
||||
widgetRefs[id].configure();
|
||||
};
|
||||
const widgetAdderSelected = ref(null);
|
||||
const addWidget = () => {
|
||||
if (widgetAdderSelected.value == null) return;
|
||||
|
||||
widgetAdderSelected.value = null;
|
||||
};
|
||||
const removeWidget = (widget) => {
|
||||
context.emit('removeWidget', widget);
|
||||
};
|
||||
const updateWidget = (id, data) => {
|
||||
context.emit('updateWidget', { id, data });
|
||||
};
|
||||
const widgets_ = computed({
|
||||
get: () => props.widgets,
|
||||
set: (value) => {
|
||||
context.emit('updateWidgets', value);
|
||||
},
|
||||
});
|
||||
emit('addWidget', {
|
||||
name: widgetAdderSelected.value,
|
||||
id: uuid(),
|
||||
data: {},
|
||||
});
|
||||
|
||||
return {
|
||||
widgetRefs,
|
||||
configWidget,
|
||||
widgetAdderSelected,
|
||||
widgetDefs,
|
||||
addWidget,
|
||||
removeWidget,
|
||||
updateWidget,
|
||||
widgets_,
|
||||
};
|
||||
widgetAdderSelected.value = null;
|
||||
};
|
||||
const removeWidget = (widget) => {
|
||||
emit('removeWidget', widget);
|
||||
};
|
||||
const updateWidget = (id, data) => {
|
||||
emit('updateWidget', { id, data });
|
||||
};
|
||||
const widgets_ = computed({
|
||||
get: () => props.widgets,
|
||||
set: (value) => {
|
||||
emit('updateWidgets', value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -150,6 +150,7 @@ export const popups = ref([]) as Ref<{
|
|||
id: any;
|
||||
component: any;
|
||||
props: Record<string, any>;
|
||||
events: any[];
|
||||
}[]>;
|
||||
|
||||
const zIndexes = {
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
<template>
|
||||
<section class="_section">
|
||||
<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
|
||||
<div class="_title">{{ i18n.t('_auth.shareAccess', { name: app.name }) }}</div>
|
||||
<div class="_content">
|
||||
<h2>{{ app.name }}</h2>
|
||||
<p class="id">{{ app.id }}</p>
|
||||
<p class="description">{{ app.description }}</p>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<h2>{{ $ts._auth.permissionAsk }}</h2>
|
||||
<h2>{{ i18n.ts._auth.permissionAsk }}</h2>
|
||||
<ul>
|
||||
<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
<li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<MkButton inline @click="cancel">{{ $ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ $ts.accept }}</MkButton>
|
||||
<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from 'vue';
|
||||
import { } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'denied'): void;
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
<template>
|
||||
<div v-if="$i && fetching" class="">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<div v-else-if="$i">
|
||||
<div v-if="$i">
|
||||
<MkLoading v-if="state == 'fetching'"/>
|
||||
<XForm
|
||||
v-if="state == 'waiting'"
|
||||
v-else-if="state == 'waiting'"
|
||||
ref="form"
|
||||
class="form"
|
||||
:session="session"
|
||||
@denied="state = 'denied'"
|
||||
@accepted="accepted"
|
||||
/>
|
||||
<div v-if="state == 'denied'" class="denied">
|
||||
<h1>{{ $ts._auth.denied }}</h1>
|
||||
<div v-else-if="state == 'denied'" class="denied">
|
||||
<h1>{{ i18n.ts._auth.denied }}</h1>
|
||||
</div>
|
||||
<div v-if="state == 'accepted'" class="accepted">
|
||||
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : $ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
|
||||
<div v-else-if="state == 'accepted'" class="accepted">
|
||||
<h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
<div v-if="state == 'fetch-session-error'" class="error">
|
||||
<p>{{ $ts.somethingHappened }}</p>
|
||||
<div v-else-if="state == 'fetch-session-error'" class="error">
|
||||
<p>{{ i18n.ts.somethingHappened }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="signin">
|
||||
|
@ -28,64 +26,55 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import XForm from './auth.form.vue';
|
||||
import MkSignin from '@/components/signin.vue';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
import { query, appendQuery } from '@/scripts/url';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XForm,
|
||||
MkSignin,
|
||||
},
|
||||
props: ['token'],
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
session: null,
|
||||
fetching: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$i) return;
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
}>();
|
||||
|
||||
// Fetch session
|
||||
os.api('auth/session/show', {
|
||||
token: this.token,
|
||||
}).then(session => {
|
||||
this.session = session;
|
||||
this.fetching = false;
|
||||
let state: 'fetching' | 'waiting' | 'denied' | 'accepted' | 'fetch-session-error' = $ref('fetching');
|
||||
let session = $ref(null);
|
||||
|
||||
// 既に連携していた場合
|
||||
if (this.session.app.isAuthorized) {
|
||||
os.api('auth/accept', {
|
||||
token: this.session.token,
|
||||
}).then(() => {
|
||||
this.accepted();
|
||||
});
|
||||
} else {
|
||||
this.state = 'waiting';
|
||||
}
|
||||
}).catch(error => {
|
||||
this.state = 'fetch-session-error';
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
accepted() {
|
||||
this.state = 'accepted';
|
||||
if (this.session.app.callbackUrl) {
|
||||
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
|
||||
}
|
||||
}, onLogin(res) {
|
||||
login(res.i);
|
||||
},
|
||||
},
|
||||
onMounted(() => {
|
||||
if (!$i) return;
|
||||
|
||||
// Fetch session
|
||||
os.api('auth/session/show', {
|
||||
token: props.token,
|
||||
}).then(fetchedSession => {
|
||||
session = fetchedSession;
|
||||
|
||||
// 既に連携していた場合
|
||||
if (session.app.isAuthorized) {
|
||||
os.api('auth/accept', {
|
||||
token: session.token,
|
||||
}).then(() => {
|
||||
this.accepted();
|
||||
});
|
||||
} else {
|
||||
state = 'waiting';
|
||||
}
|
||||
}).catch(error => {
|
||||
state = 'fetch-session-error';
|
||||
});
|
||||
});
|
||||
|
||||
function accepted() {
|
||||
state = 'accepted';
|
||||
if (session.app.callbackUrl) {
|
||||
location.href = appendQuery(session.app.callbackUrl, query({ token: session.token }));
|
||||
}
|
||||
}
|
||||
|
||||
function onLogin(res) {
|
||||
login(res.i);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,65 +1,76 @@
|
|||
<template>
|
||||
<div class="mk-follow-page">
|
||||
</div>
|
||||
<!-- This page does not really have any content, it is mainly processing stuff -->
|
||||
<MkLoading v-if="state == 'loading'"/>
|
||||
<MkError v-if="state == 'error'" :final="finalError" @retry="doIt"/>
|
||||
<div v-if="state == 'done'">{{ i18n.ts.done }}</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
created() {
|
||||
const acct = new URL(location.href).searchParams.get('acct');
|
||||
if (acct == null) return;
|
||||
let state: 'loading' | 'error' | 'done' = $ref('loading');
|
||||
let finalError: boolean = $ref(false);
|
||||
|
||||
let promise;
|
||||
async function follow(user) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('followConfirm', { name: user.name || user.username }),
|
||||
});
|
||||
|
||||
if (acct.startsWith('https://')) {
|
||||
promise = os.api('ap/show', {
|
||||
uri: acct,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
this.follow(res.object);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.push(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise = os.api('users/show', Acct.parse(acct));
|
||||
promise.then(user => {
|
||||
this.follow(user);
|
||||
});
|
||||
}
|
||||
if (canceled) {
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
os.promiseDialog(promise, null, null, this.$ts.fetchingAsApObject);
|
||||
},
|
||||
os.apiWithDialog('following/create', {
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async follow(user) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: this.$t('followConfirm', { name: user.name || user.username }),
|
||||
});
|
||||
function doIt() {
|
||||
// this might be a retry
|
||||
state = 'loading';
|
||||
|
||||
if (canceled) {
|
||||
window.close();
|
||||
const acct = new URL(location.href).searchParams.get('acct');
|
||||
if (acct == null || acct.trim() === '') {
|
||||
finalError = true;
|
||||
state = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (acct.startsWith('https://')) {
|
||||
promise = os.api('ap/show', {
|
||||
uri: acct,
|
||||
}).then(res => {
|
||||
if (res.type === 'User') {
|
||||
follow(res.object);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.push(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
}).then(() => {
|
||||
finalError = true;
|
||||
state = 'error';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
os.apiWithDialog('following/create', {
|
||||
userId: user.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
state = 'done';
|
||||
});
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
} else {
|
||||
os.api('users/show', Acct.parse(acct))
|
||||
.then(user => follow(user))
|
||||
.then(() => state = 'done');
|
||||
}
|
||||
}
|
||||
|
||||
doIt();
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="civpbkhh">
|
||||
<div ref="scroll" class="scrollbox" v-bind:class="{ scroll: isScrolling }">
|
||||
<div v-for="note in notes" class="note">
|
||||
<div ref="scroll" class="scrollbox" :class="{ scroll: isScrolling }">
|
||||
<div v-for="note in notes" :key="note.id" class="note">
|
||||
<div class="content _panel">
|
||||
<div class="body">
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<XMediaList :media-list="note.files"/>
|
||||
</div>
|
||||
<div v-if="note.poll">
|
||||
<XPoll :note="note" :readOnly="true"/>
|
||||
<XPoll :note="note" :read-only="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<XReactionsViewer ref="reactionsViewer" :note="note"/>
|
||||
|
@ -21,37 +21,26 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onUpdated, ref, Ref } from 'vue';
|
||||
import { Note } from 'misskey-js/built/entities';
|
||||
import XReactionsViewer from '@/components/reactions-viewer.vue';
|
||||
import XMediaList from '@/components/media-list.vue';
|
||||
import XPoll from '@/components/poll.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XReactionsViewer,
|
||||
XMediaList,
|
||||
XPoll
|
||||
},
|
||||
const notes: Ref<Note[]> = ref([]);
|
||||
const isScrolling = ref(false);
|
||||
const scroll: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
data() {
|
||||
return {
|
||||
notes: [],
|
||||
isScrolling: false,
|
||||
};
|
||||
},
|
||||
os.api('notes/featured').then(newNotes => {
|
||||
notes.value = newNotes;
|
||||
});
|
||||
|
||||
created() {
|
||||
os.api('notes/featured').then(notes => {
|
||||
this.notes = notes;
|
||||
});
|
||||
},
|
||||
|
||||
updated() {
|
||||
if (this.$refs.scroll.clientHeight > window.innerHeight) {
|
||||
this.isScrolling = true;
|
||||
}
|
||||
onUpdated(() => {
|
||||
if (scroll.value && scroll.value.clientHeight > window.innerHeight) {
|
||||
isScrolling.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<component :is="popup.component"
|
||||
<component
|
||||
:is="popup.component"
|
||||
v-for="popup in popups"
|
||||
:key="popup.id"
|
||||
v-bind="popup.props"
|
||||
|
@ -15,56 +16,44 @@
|
|||
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { popup, popups, pendingApiRequestsCount } from '@/os';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, Ref, ref } from 'vue';
|
||||
import { swInject } from './sw-inject';
|
||||
import { popup as showPopup, popups, pendingApiRequestsCount } from '@/os';
|
||||
import { uploads } from '@/scripts/upload';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import { $i } from '@/account';
|
||||
import { swInject } from './sw-inject';
|
||||
import { stream } from '@/stream';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
|
||||
XUpload: defineAsyncComponent(() => import('./upload.vue')),
|
||||
},
|
||||
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
||||
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
||||
const dev: Ref<boolean> = ref(_DEV_);
|
||||
|
||||
setup() {
|
||||
const onNotification = notification => {
|
||||
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
||||
const onNotification = (notification: { type: string; id: any; }): void => {
|
||||
if ($i?.mutingNotificationTypes.includes(notification.type)) return;
|
||||
|
||||
if (document.visibilityState === 'visible') {
|
||||
stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
if (document.visibilityState === 'visible') {
|
||||
stream.send('readNotification', {
|
||||
id: notification.id,
|
||||
});
|
||||
|
||||
popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
|
||||
notification
|
||||
}, {}, 'closed');
|
||||
}
|
||||
showPopup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
|
||||
notification
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
sound.play('notification');
|
||||
};
|
||||
sound.play('notification');
|
||||
};
|
||||
|
||||
if ($i) {
|
||||
const connection = stream.useChannel('main', null, 'UI');
|
||||
connection.on('notification', onNotification);
|
||||
if ($i) {
|
||||
const connection = stream.useChannel('main', null, 'UI');
|
||||
connection.on('notification', onNotification);
|
||||
|
||||
//#region Listen message from SW
|
||||
if ('serviceWorker' in navigator) {
|
||||
swInject();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uploads,
|
||||
popups,
|
||||
pendingApiRequestsCount,
|
||||
dev: _DEV_,
|
||||
};
|
||||
},
|
||||
});
|
||||
//#region Listen message from SW
|
||||
if ('serviceWorker' in navigator) {
|
||||
swInject();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -3,17 +3,8 @@
|
|||
<XCommon/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import DesignA from './visitor/a.vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import DesignB from './visitor/b.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XCommon,
|
||||
DesignA,
|
||||
DesignB,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="sqxihjet">
|
||||
<div ref="header" class="sqxihjet">
|
||||
<div v-if="narrow === false" class="wide">
|
||||
<div class="content">
|
||||
<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
|
||||
|
@ -13,7 +13,9 @@
|
|||
<span v-if="info.title" class="text">{{ info.title }}</span>
|
||||
<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
|
||||
</div>
|
||||
<button v-if="info.action" class="_button action" @click.stop="info.action.handler"><!-- TODO --></button>
|
||||
<button v-if="info.action" class="_button action" @click.stop="info.action.handler">
|
||||
<!-- TODO -->
|
||||
</button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button class="_button search" @click="search()"><i class="fas fa-search icon"></i><span>{{ $ts.search }}</span></button>
|
||||
|
@ -22,8 +24,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="narrow === true" class="narrow">
|
||||
<button class="menu _button" @click="$parent.showMenu = true">
|
||||
<div v-else-if="narrow" class="narrow">
|
||||
<button class="menu _button" @click="showMenu = true">
|
||||
<i class="fas fa-bars icon"></i>
|
||||
</button>
|
||||
<div v-if="info" class="title">
|
||||
|
@ -39,47 +41,38 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, Ref } from 'vue';
|
||||
import XSigninDialog from '@/components/signin-dialog.vue';
|
||||
import XSignupDialog from '@/components/signup-dialog.vue';
|
||||
import * as os from '@/os';
|
||||
import { search } from '@/scripts/search';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
info: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
defineProps<{
|
||||
info: any;
|
||||
}>();
|
||||
|
||||
data() {
|
||||
return {
|
||||
narrow: null,
|
||||
showMenu: false,
|
||||
};
|
||||
},
|
||||
const narrow = ref(false);
|
||||
const showMenu = ref(false);
|
||||
const header: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
mounted() {
|
||||
this.narrow = this.$el.clientWidth < 1300;
|
||||
},
|
||||
|
||||
methods: {
|
||||
signin() {
|
||||
os.popup(XSigninDialog, {
|
||||
autoSet: true
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
signup() {
|
||||
os.popup(XSignupDialog, {
|
||||
autoSet: true
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
search
|
||||
onMounted(() => {
|
||||
if (header.value) {
|
||||
narrow.value = header.value.clientWidth < 1300;
|
||||
}
|
||||
});
|
||||
|
||||
function signin(): void {
|
||||
os.popup(XSigninDialog, {
|
||||
autoSet: true,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
function signup(): void {
|
||||
os.popup(XSignupDialog, {
|
||||
autoSet: true,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -94,14 +87,14 @@ export default defineComponent({
|
|||
backdrop-filter: var(--blur, blur(32px));
|
||||
background-color: var(--X16);
|
||||
|
||||
> .wide {
|
||||
> .content {
|
||||
>.wide {
|
||||
>.content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .link {
|
||||
>.link {
|
||||
$line: 3px;
|
||||
display: inline-block;
|
||||
padding: 0 16px;
|
||||
|
@ -109,7 +102,7 @@ export default defineComponent({
|
|||
border-top: solid $line transparent;
|
||||
border-bottom: solid $line transparent;
|
||||
|
||||
> .icon {
|
||||
>.icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
|
@ -118,8 +111,8 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
> .page {
|
||||
> .title {
|
||||
>.page {
|
||||
>.title {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
white-space: nowrap;
|
||||
|
@ -127,11 +120,11 @@ export default defineComponent({
|
|||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
|
||||
> .icon + .text {
|
||||
>.icon+.text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
>.avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
|
@ -151,76 +144,72 @@ export default defineComponent({
|
|||
box-shadow: 0 -2px 0 0 var(--accent) inset;
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
> .action {
|
||||
padding: 0 0 0 16px;
|
||||
}
|
||||
}
|
||||
el>.right {
|
||||
margin-left: auto;
|
||||
|
||||
> .right {
|
||||
margin-left: auto;
|
||||
>.search {
|
||||
background: var(--bg);
|
||||
border-radius: 999px;
|
||||
width: 230px;
|
||||
line-height: $height - 20px;
|
||||
margin-right: 16px;
|
||||
text-align: left;
|
||||
|
||||
> .search {
|
||||
background: var(--bg);
|
||||
border-radius: 999px;
|
||||
width: 230px;
|
||||
line-height: $height - 20px;
|
||||
margin-right: 16px;
|
||||
text-align: left;
|
||||
>* {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> * {
|
||||
opacity: 0.7;
|
||||
}
|
||||
>.icon {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .icon {
|
||||
padding: 0 16px;
|
||||
>.signup {
|
||||
border-radius: 999px;
|
||||
padding: 0 24px;
|
||||
line-height: $height - 20px;
|
||||
}
|
||||
|
||||
>.login {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .signup {
|
||||
border-radius: 999px;
|
||||
padding: 0 24px;
|
||||
line-height: $height - 20px;
|
||||
}
|
||||
|
||||
> .login {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .narrow {
|
||||
display: flex;
|
||||
|
||||
> .menu,
|
||||
> .action {
|
||||
width: $height;
|
||||
height: $height;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
> .icon + .text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
pointer-events: none;
|
||||
>.narrow {
|
||||
display: flex;
|
||||
|
||||
>.menu,
|
||||
>.action {
|
||||
width: $height;
|
||||
height: $height;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
>.title {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
>.icon+.text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
>.avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue