* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wop

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* add notes

* wip

* wip

* wip

* wip

* sound

* wip

* add kick_gaba2

* wip
This commit is contained in:
syuilo 2020-08-18 22:44:21 +09:00 committed by GitHub
parent 122076e8ea
commit 9855405b89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 2191 additions and 184 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,141 @@
<template>
<button class="hdcaacmi _button"
:class="{ wait, active: isFollowing, full }"
@click="onClick"
:disabled="wait"
>
<template v-if="!wait">
<template v-if="isFollowing">
<span v-if="full">{{ $t('unfollow') }}</span><fa :icon="faMinus"/>
</template>
<template v-else>
<span v-if="full">{{ $t('follow') }}</span><fa :icon="faPlus"/>
</template>
</template>
<template v-else>
<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse fixed-width/>
</template>
</button>
</template>
<script lang="ts">
import Vue from 'vue';
import { faSpinner, faPlus, faMinus, } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
props: {
channel: {
type: Object,
required: true
},
full: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isFollowing: this.channel.isFollowing,
wait: false,
faSpinner, faPlus, faMinus,
};
},
methods: {
async onClick() {
this.wait = true;
try {
if (this.isFollowing) {
await this.$root.api('channels/unfollow', {
channelId: this.channel.id
});
this.isFollowing = false;
} else {
await this.$root.api('channels/follow', {
channelId: this.channel.id
});
this.isFollowing = true;
}
} catch (e) {
console.error(e);
} finally {
this.wait = false;
}
}
}
});
</script>
<style lang="scss" scoped>
.hdcaacmi {
position: relative;
display: inline-block;
font-weight: bold;
color: var(--accent);
background: transparent;
border: solid 1px var(--accent);
padding: 0;
height: 31px;
font-size: 16px;
border-radius: 32px;
background: #fff;
&.full {
padding: 0 8px 0 12px;
font-size: 14px;
}
&:not(.full) {
width: 31px;
}
&:focus {
&:after {
content: "";
pointer-events: none;
position: absolute;
top: -5px;
right: -5px;
bottom: -5px;
left: -5px;
border: 2px solid var(--focus);
border-radius: 32px;
}
}
&:hover {
//background: mix($primary, #fff, 20);
}
&:active {
//background: mix($primary, #fff, 40);
}
&.active {
color: #fff;
background: var(--accent);
&:hover {
background: var(--accentLighten);
border-color: var(--accentLighten);
}
&:active {
background: var(--accentDarken);
border-color: var(--accentDarken);
}
}
&.wait {
cursor: wait !important;
opacity: 0.7;
}
> span {
margin-right: 6px;
}
}
</style>

View file

@ -0,0 +1,144 @@
<template>
<router-link :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
<div class="banner" v-if="channel.bannerUrl" :style="`background-image: url('${channel.bannerUrl}')`">
<div class="fade"></div>
<div class="name"><fa :icon="faSatelliteDish"/> {{ channel.name }}</div>
<div class="status">
<div><fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.usersCount }}</b></i18n></div>
<div><fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.notesCount }}</b></i18n></div>
</div>
</div>
<article v-if="channel.description">
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
</article>
<footer>
<span>
{{ $t('updatedAt') }}: <mk-time :time="channel.lastNotedAt"/>
</span>
</footer>
</router-link>
</template>
<script lang="ts">
import Vue from 'vue';
import { faSatelliteDish, faUsers, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
props: {
channel: {
type: Object,
required: true
},
},
data() {
return {
faSatelliteDish, faUsers, faPencilAlt,
};
},
});
</script>
<style lang="scss" scoped>
.eftoefju {
display: block;
overflow: hidden;
width: 100%;
border: 1px solid var(--divider);
&:hover {
text-decoration: none;
}
> .banner {
position: relative;
width: 100%;
height: 200px;
background-position: center;
background-size: cover;
> .fade {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
}
> .name {
position: absolute;
top: 16px;
left: 16px;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 1.2em;
}
> .status {
position: absolute;
z-index: 1;
bottom: 16px;
right: 16px;
padding: 8px 12px;
font-size: 80%;
background: rgba(0, 0, 0, 0.7);
border-radius: 6px;
color: #fff;
}
}
> article {
padding: 16px;
> p {
margin: 0;
font-size: 1em;
}
}
> footer {
padding: 12px 16px;
border-top: solid 1px var(--divider);
> span {
opacity: 0.7;
font-size: 0.9em;
}
}
@media (max-width: 550px) {
font-size: 0.9em;
> .banner {
height: 80px;
> .status {
display: none;
}
}
> article {
padding: 12px;
}
> footer {
display: none;
}
}
@media (max-width: 500px) {
font-size: 0.8em;
> .banner {
height: 70px;
}
> article {
padding: 8px;
}
}
}
</style>

View file

@ -57,6 +57,7 @@
<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
</div>
<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link>
</div>
<footer class="footer">
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
@ -96,7 +97,7 @@
<script lang="ts">
import Vue from 'vue';
import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import { parse } from '../../mfm/parse';
import { sum, unique } from '../../prelude/array';
@ -133,6 +134,12 @@ export default Vue.extend({
MkUrlPreview,
},
inject: {
inChannel: {
default: null
}
},
props: {
note: {
type: Object,
@ -159,7 +166,7 @@ export default Vue.extend({
isDeleted: false,
muted: false,
noteBody: this.$refs.noteBody,
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish
};
},
@ -954,6 +961,11 @@ export default Vue.extend({
}
}
}
> .channel {
opacity: 0.7;
font-size: 80%;
}
}
> .footer {

View file

@ -10,7 +10,7 @@
<div>
<span class="local-only" v-if="localOnly" v-text="$t('_visibility.localOnly')" />
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')">
<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')" :disabled="channel != null">
<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span>
<span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span>
@ -88,6 +88,10 @@ export default Vue.extend({
type: Object,
required: false
},
channel: {
type: Object,
required: false
},
mention: {
type: Object,
required: false
@ -140,30 +144,38 @@ export default Vue.extend({
},
computed: {
draftId(): string {
return this.renote
? `renote:${this.renote.id}`
: this.reply
? `reply:${this.reply.id}`
: 'note';
draftKey(): string {
let key = this.channel ? `channel:${this.channel.id}` : '';
if (this.renote) {
key += `renote:${this.renote.id}`;
} else if (this.reply) {
key += `reply:${this.reply.id}`;
} else {
key += 'note';
}
return key;
},
placeholder(): string {
const xs = [
this.$t('_postForm._placeholders.a'),
this.$t('_postForm._placeholders.b'),
this.$t('_postForm._placeholders.c'),
this.$t('_postForm._placeholders.d'),
this.$t('_postForm._placeholders.e'),
this.$t('_postForm._placeholders.f')
];
const x = xs[Math.floor(Math.random() * xs.length)];
return this.renote
? this.$t('_postForm.quotePlaceholder')
: this.reply
? this.$t('_postForm.replyPlaceholder')
: x;
if (this.renote) {
return this.$t('_postForm.quotePlaceholder');
} else if (this.reply) {
return this.$t('_postForm.replyPlaceholder');
} else if (this.channel) {
return this.$t('_postForm.channelPlaceholder');
} else {
const xs = [
this.$t('_postForm._placeholders.a'),
this.$t('_postForm._placeholders.b'),
this.$t('_postForm._placeholders.c'),
this.$t('_postForm._placeholders.d'),
this.$t('_postForm._placeholders.e'),
this.$t('_postForm._placeholders.f')
];
return xs[Math.floor(Math.random() * xs.length)];
}
},
submitText(): string {
@ -224,9 +236,11 @@ export default Vue.extend({
}
//
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : this.$store.state.settings.defaultNoteVisibility);
if (this.channel == null) {
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : this.$store.state.settings.defaultNoteVisibility);
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.localOnly : this.$store.state.settings.defaultNoteLocalOnly;
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.localOnly : this.$store.state.settings.defaultNoteLocalOnly;
}
//
if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
@ -266,7 +280,7 @@ export default Vue.extend({
this.$nextTick(() => {
// 稿
if (!this.instant && !this.mention) {
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
if (draft) {
this.text = draft.data.text;
this.useCw = draft.data.useCw;
@ -398,6 +412,10 @@ export default Vue.extend({
},
setVisibility() {
if (this.channel) {
// TODO: information dialog
return;
}
const w = this.$root.new(MkVisibilityChooser, {
source: this.$refs.visibilityButton,
currentVisibility: this.visibility,
@ -510,7 +528,7 @@ export default Vue.extend({
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftId] = {
data[this.draftKey] = {
updatedAt: new Date(),
data: {
text: this.text,
@ -529,7 +547,7 @@ export default Vue.extend({
deleteDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
delete data[this.draftId];
delete data[this.draftKey];
localStorage.setItem('drafts', JSON.stringify(data));
},
@ -540,6 +558,7 @@ export default Vue.extend({
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
channelId: this.channel ? this.channel.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
localOnly: this.localOnly,

View file

@ -24,6 +24,10 @@ export default Vue.extend({
type: String,
required: false
},
channel: {
type: String,
required: false
},
sound: {
type: Boolean,
required: false,
@ -31,6 +35,12 @@ export default Vue.extend({
}
},
provide() {
return {
inChannel: this.src === 'channel'
};
},
data() {
return {
connection: null,
@ -117,6 +127,15 @@ export default Vue.extend({
this.connection.on('note', prepend);
this.connection.on('userAdded', onUserAdded);
this.connection.on('userRemoved', onUserRemoved);
} else if (this.src == 'channel') {
endpoint = 'channels/timeline';
this.query = {
channelId: this.channel
};
this.connection = this.$root.stream.connectToChannel('channel', {
channelId: this.channel
});
this.connection.on('note', prepend);
}
this.pagination = {

View file

@ -350,6 +350,20 @@ os.init(async () => {
app.sound('antenna');
});
main.on('readAllChannels', () => {
store.dispatch('mergeMe', {
hasUnreadChannel: false
});
});
main.on('unreadChannel', () => {
store.dispatch('mergeMe', {
hasUnreadChannel: true
});
app.sound('channel');
});
main.on('readAllAnnouncements', () => {
store.dispatch('mergeMe', {
hasUnreadAnnouncement: false

View file

@ -0,0 +1,128 @@
<template>
<div>
<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
<portal to="title">{{ channelId ? $t('_channel.edit') : $t('_channel.create') }}</portal>
<div class="_card">
<div class="_content">
<mk-input v-model="name">{{ $t('name') }}</mk-input>
<mk-textarea v-model="description">{{ $t('description') }}</mk-textarea>
<div class="banner">
<mk-button v-if="bannerId == null" @click="setBannerImage"><fa :icon="faPlus"/> {{ $t('_channel.setBanner') }}</mk-button>
<div v-else-if="bannerUrl">
<img :src="bannerUrl" style="width: 100%;"/>
<mk-button @click="removeBannerImage()"><fa :icon="faTrashAlt"/> {{ $t('_channel.removeBanner') }}</mk-button>
</div>
</div>
</div>
<div class="_footer">
<mk-button @click="save()" primary><fa :icon="faSave"/> {{ channelId ? $t('save') : $t('create') }}</mk-button>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPlus, faSatelliteDish } from '@fortawesome/free-solid-svg-icons';
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import MkTextarea from '../components/ui/textarea.vue';
import MkButton from '../components/ui/button.vue';
import MkInput from '../components/ui/input.vue';
import { selectFile } from '../scripts/select-file';
export default Vue.extend({
components: {
MkTextarea, MkButton, MkInput,
},
props: {
channelId: {
type: String,
required: false
},
},
data() {
return {
channel: null,
name: null,
description: null,
bannerUrl: null,
bannerId: null,
faSave, faTrashAlt, faPlus,faSatelliteDish,
};
},
watch: {
async bannerId() {
if (this.bannerId == null) {
this.bannerUrl = null;
} else {
this.bannerUrl = (await this.$root.api('drive/files/show', {
fileId: this.bannerId,
})).url;
}
},
},
async created() {
if (this.channelId) {
this.channel = await this.$root.api('channels/show', {
channelId: this.channelId,
});
this.name = this.channel.name;
this.description = this.channel.description;
this.bannerId = this.channel.bannerId;
this.bannerUrl = this.channel.bannerUrl;
}
},
methods: {
save() {
const params = {
name: this.name,
description: this.description,
bannerId: this.bannerId,
};
if (this.channelId) {
params.channelId = this.channelId;
this.$root.api('channels/update', params)
.then(channel => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
} else {
this.$root.api('channels/create', params)
.then(channel => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$router.push(`/channels/${channel.id}`);
});
}
},
setBannerImage(e) {
selectFile(this, e.currentTarget || e.target, null, false).then(file => {
this.bannerId = file.id;
});
},
removeBannerImage() {
this.bannerId = null;
}
}
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,190 @@
<template>
<div v-if="channel">
<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
<portal to="title">{{ channel.name }}</portal>
<div class="wpgynlbz _panel _vMargin" :class="{ hide: !showBanner }">
<x-channel-follow-button :channel="channel" :full="true" class="subscribe"/>
<button class="_button toggle" @click="() => showBanner = !showBanner">
<template v-if="showBanner"><fa :icon="faAngleUp"/></template>
<template v-else><fa :icon="faAngleDown"/></template>
</button>
<div class="hideOverlay" v-if="!showBanner">
</div>
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
<div class="status">
<div><fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.usersCount }}</b></i18n></div>
<div><fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.notesCount }}</b></i18n></div>
</div>
<div class="fade"></div>
</div>
<div class="description" v-if="channel.description">
<mfm :text="channel.description" :is-note="false" :i="$store.state.i"/>
</div>
</div>
<x-post-form :channel="channel" class="post-form _panel _vMargin" fixed/>
<x-timeline class="_vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faSatelliteDish, faUsers, faPencilAlt, faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
import { } from '@fortawesome/free-regular-svg-icons';
import MkContainer from '../components/ui/container.vue';
import XPostForm from '../components/post-form.vue';
import XTimeline from '../components/timeline.vue';
import XChannelFollowButton from '../components/channel-follow-button.vue';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('channel') as string
};
},
components: {
MkContainer,
XPostForm,
XTimeline,
XChannelFollowButton
},
props: {
channelId: {
type: String,
required: true
}
},
data() {
return {
channel: null,
showBanner: true,
pagination: {
endpoint: 'channels/timeline',
limit: 10,
params: () => ({
channelId: this.channelId,
})
},
faSatelliteDish, faUsers, faPencilAlt, faAngleUp, faAngleDown,
};
},
watch: {
channelId: {
async handler() {
this.channel = await this.$root.api('channels/show', {
channelId: this.channelId,
});
},
immediate: true
}
},
created() {
},
});
</script>
<style lang="scss" scoped>
.wpgynlbz {
> .subscribe {
position: absolute;
z-index: 1;
top: 16px;
left: 16px;
}
> .toggle {
position: absolute;
z-index: 2;
top: 8px;
right: 8px;
font-size: 1.2em;
width: 48px;
height: 48px;
color: #fff;
background: rgba(0, 0, 0, 0.5);
border-radius: 100%;
> [data-icon] {
vertical-align: middle;
}
}
> .banner {
position: relative;
height: 200px;
background-position: center;
background-size: cover;
> .fade {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
}
> .status {
position: absolute;
z-index: 1;
bottom: 16px;
right: 16px;
padding: 8px 12px;
font-size: 80%;
background: rgba(0, 0, 0, 0.7);
border-radius: 6px;
color: #fff;
}
}
> .description {
padding: 16px;
}
> .hideOverlay {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
background: rgba(0, 0, 0, 0.3);
}
&.hide {
> .subscribe {
display: none;
}
> .toggle {
top: 0;
right: 0;
height: 100%;
background: transparent;
}
> .banner {
height: 42px;
filter: blur(8px);
> * {
display: none;
}
}
> .description {
display: none;
}
}
}
</style>

View file

@ -0,0 +1,86 @@
<template>
<div>
<portal to="icon"><fa :icon="faSatelliteDish"/></portal>
<portal to="title">{{ $t('channel') }}</portal>
<mk-tab v-model="tab" :items="[{ label: $t('_channel.featured'), value: 'featured', icon: faFireAlt }, { label: $t('_channel.following'), value: 'following', icon: faHeart }, { label: $t('_channel.owned'), value: 'owned', icon: faEdit }]"/>
<div class="grwlizim featured" v-if="tab === 'featured'">
<mk-pagination :pagination="featuredPagination" #default="{items}">
<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
</mk-pagination>
</div>
<div class="grwlizim following" v-if="tab === 'following'">
<mk-pagination :pagination="followingPagination" #default="{items}">
<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
</mk-pagination>
</div>
<div class="grwlizim owned" v-if="tab === 'owned'">
<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
<mk-pagination :pagination="ownedPagination" #default="{items}">
<mk-channel-preview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
</mk-pagination>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faSatelliteDish, faPlus, faEdit, faFireAlt } from '@fortawesome/free-solid-svg-icons';
import { faHeart } from '@fortawesome/free-regular-svg-icons';
import MkChannelPreview from '../components/channel-preview.vue';
import MkPagination from '../components/ui/pagination.vue';
import MkButton from '../components/ui/button.vue';
import MkTab from '../components/tab.vue';
export default Vue.extend({
components: {
MkChannelPreview, MkPagination, MkButton, MkTab
},
data() {
return {
tab: 'featured',
featuredPagination: {
endpoint: 'channels/featured',
limit: 5,
},
followingPagination: {
endpoint: 'channels/followed',
limit: 5,
},
ownedPagination: {
endpoint: 'channels/owned',
limit: 5,
},
faSatelliteDish, faPlus, faEdit, faHeart, faFireAlt
};
},
methods: {
create() {
this.$router.push(`/channels/new`);
}
}
});
</script>
<style lang="scss" scoped>
.grwlizim {
padding: 16px 0;
&.my .uveselbe:first-child {
margin-top: 16px;
}
.uveselbe:not(:last-child) {
margin-bottom: 8px;
}
@media (min-width: 500px) {
.uveselbe:not(:last-child) {
margin-bottom: 16px;
}
}
}
</style>

View file

@ -2,14 +2,15 @@
<div class="mk-home" v-hotkey.global="keymap">
<portal to="header" v-if="showTitle">
<button @click="choose" class="_button _kjvfvyph_">
<i><fa v-if="$store.state.i.hasUnreadAntenna" :icon="faCircle"/></i>
<i><fa v-if="$store.state.i.hasUnreadAntenna || $store.state.i.hasUnreadChannel" :icon="faCircle"/></i>
<fa v-if="src === 'home'" :icon="faHome"/>
<fa v-if="src === 'local'" :icon="faComments"/>
<fa v-if="src === 'social'" :icon="faShareAlt"/>
<fa v-if="src === 'global'" :icon="faGlobe"/>
<fa v-if="src === 'list'" :icon="faListUl"/>
<fa v-if="src === 'antenna'" :icon="faSatellite"/>
<span style="margin-left: 8px;">{{ src === 'list' ? list.name : src === 'antenna' ? antenna.name : $t('_timelines.' + src) }}</span>
<fa v-if="src === 'channel'" :icon="faSatelliteDish"/>
<span style="margin-left: 8px;">{{ src === 'list' ? list.name : src === 'antenna' ? antenna.name : src === 'channel' ? channel.name : $t('_timelines.' + src) }}</span>
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
</button>
</portal>
@ -19,13 +20,13 @@
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
<x-post-form class="post-form _panel" fixed v-if="$store.state.device.showFixedPostForm"/>
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list ? list.id : null" :antenna="antenna ? antenna.id : null" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" :src="src" :list="list ? list.id : null" :antenna="antenna ? antenna.id : null" :channel="channel ? channel.id : null" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faCircle } from '@fortawesome/free-solid-svg-icons';
import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faSatelliteDish, faCircle } from '@fortawesome/free-solid-svg-icons';
import { faComments } from '@fortawesome/free-regular-svg-icons';
import Progress from '../scripts/loading';
import XTimeline from '../components/timeline.vue';
@ -57,10 +58,11 @@ export default Vue.extend({
src: 'home',
list: null,
antenna: null,
channel: null,
menuOpened: false,
queue: 0,
width: 0,
faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faCircle
faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faSatelliteDish, faCircle
};
},
@ -79,16 +81,20 @@ export default Vue.extend({
watch: {
src() {
this.showNav = false;
this.saveSrc();
},
list(x) {
this.showNav = false;
this.saveSrc();
if (x != null) this.antenna = null;
if (x != null) this.channel = null;
},
antenna(x) {
this.showNav = false;
this.saveSrc();
if (x != null) this.list = null;
if (x != null) this.channel = null;
},
channel(x) {
this.showNav = false;
if (x != null) this.antenna = null;
if (x != null) this.list = null;
},
},
@ -99,6 +105,8 @@ export default Vue.extend({
this.list = this.$store.state.deviceUser.tl.arg;
} else if (this.src === 'antenna') {
this.antenna = this.$store.state.deviceUser.tl.arg;
} else if (this.src === 'channel') {
this.channel = this.$store.state.deviceUser.tl.arg;
}
},
@ -127,9 +135,10 @@ export default Vue.extend({
async choose(ev) {
if (this.meta == null) return;
this.menuOpened = true;
const [antennas, lists] = await Promise.all([
const [antennas, lists, channels] = await Promise.all([
this.$root.api('antennas/list'),
this.$root.api('users/lists/list')
this.$root.api('users/lists/list'),
this.$root.api('channels/followed'),
]);
const antennaItems = antennas.map(antenna => ({
text: antenna.name,
@ -137,7 +146,8 @@ export default Vue.extend({
indicate: antenna.hasUnreadNote,
action: () => {
this.antenna = antenna;
this.setSrc('antenna');
this.src = 'antenna';
this.saveSrc();
}
}));
const listItems = lists.map(list => ({
@ -145,27 +155,40 @@ export default Vue.extend({
icon: faListUl,
action: () => {
this.list = list;
this.setSrc('list');
this.src = 'list';
this.saveSrc();
}
}));
const channelItems = channels.map(channel => ({
text: channel.name,
icon: faSatelliteDish,
indicate: channel.hasUnreadNote,
action: () => {
// NOTE: 稿
//this.channel = channel;
//this.src = 'channel';
//this.saveSrc();
this.$router.push(`/channels/${channel.id}`);
}
}));
this.$root.menu({
items: [{
text: this.$t('_timelines.home'),
icon: faHome,
action: () => { this.setSrc('home') }
action: () => { this.src = 'home'; this.saveSrc(); }
}, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
text: this.$t('_timelines.local'),
icon: faComments,
action: () => { this.setSrc('local') }
action: () => { this.src = 'local'; this.saveSrc(); }
}, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
text: this.$t('_timelines.social'),
icon: faShareAlt,
action: () => { this.setSrc('social') }
action: () => { this.src = 'social'; this.saveSrc(); }
}, this.meta.disableGlobalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
text: this.$t('_timelines.global'),
icon: faGlobe,
action: () => { this.setSrc('global') }
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems],
action: () => { this.src = 'global'; this.saveSrc(); }
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems, channelItems.length > 0 ? null : undefined, ...channelItems],
fixed: true,
noCenter: true,
source: ev.currentTarget || ev.target
@ -174,14 +197,13 @@ export default Vue.extend({
});
},
setSrc(src) {
this.src = src;
},
saveSrc() {
this.$store.commit('deviceUser/setTl', {
src: this.src,
arg: this.src == 'list' ? this.list : this.antenna
arg:
this.src === 'list' ? this.list :
this.src === 'antenna' ? this.antenna :
this.channel
});
},

View file

@ -53,7 +53,7 @@ export default Vue.extend({
};
},
computed: {
draftId(): string {
draftKey(): string {
return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
},
canSend(): boolean {
@ -79,7 +79,7 @@ export default Vue.extend({
autosize(this.$refs.text);
// 稿
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftId];
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
if (draft) {
this.text = draft.data.text;
this.file = draft.data.file;
@ -199,7 +199,7 @@ export default Vue.extend({
saveDraft() {
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
data[this.draftId] = {
data[this.draftKey] = {
updatedAt: new Date(),
data: {
text: this.text,
@ -213,7 +213,7 @@ export default Vue.extend({
deleteDraft() {
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
delete data[this.draftId];
delete data[this.draftKey];
localStorage.setItem('message_drafts', JSON.stringify(data));
},

View file

@ -3,11 +3,11 @@
<portal to="icon"><fa :icon="faCog"/></portal>
<portal to="title">{{ $t('accountSettings') }}</portal>
<x-profile-setting/>
<x-privacy-setting/>
<x-reaction-setting/>
<x-profile-setting class="_vMargin"/>
<x-privacy-setting class="_vMargin"/>
<x-reaction-setting class="_vMargin"/>
<section class="_card">
<section class="_card _vMargin">
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
<div class="_content">
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
@ -24,14 +24,14 @@
</div>
</section>
<x-import-export/>
<x-drive/>
<x-mute-block/>
<x-word-mute/>
<x-security/>
<x-2fa/>
<x-integration/>
<x-api/>
<x-import-export class="_vMargin"/>
<x-drive class="_vMargin"/>
<x-mute-block class="_vMargin"/>
<x-word-mute class="_vMargin"/>
<x-security class="_vMargin"/>
<x-2fa class="_vMargin"/>
<x-integration class="_vMargin"/>
<x-api class="_vMargin"/>
<router-link class="_panel _buttonPrimary" to="/my/apps" style="margin: var(--margin) auto;">{{ $t('installedApps') }}</router-link>

View file

@ -5,11 +5,11 @@
<router-link v-if="$store.getters.isSignedIn" class="_panel _buttonPrimary" to="/my/settings" style="margin-bottom: var(--margin);">{{ $t('accountSettings') }}</router-link>
<x-theme/>
<x-theme class="_vMargin"/>
<x-sidebar/>
<x-sidebar class="_vMargin"/>
<x-plugins/>
<x-plugins class="_vMargin"/>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div>
@ -50,6 +50,11 @@
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxAntenna)" v-if="sfxAntenna"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</mk-select>
<mk-select v-model="sfxChannel">
<template #label>{{ $t('_sfx.channel') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChannel)" v-if="sfxChannel"><fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</mk-select>
</div>
</section>
@ -142,10 +147,14 @@ const sounds = [
'syuilo/pirori',
'syuilo/pirori-wet',
'syuilo/pirori-square-wet',
'syuilo/square-pico',
'syuilo/reverved',
'syuilo/ryukyu',
'aisha/1',
'aisha/2',
'aisha/3',
'noizenecio/kick_gaba',
'noizenecio/kick_gaba2',
];
export default Vue.extend({
@ -272,6 +281,11 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
},
sfxChannel: {
get() { return this.$store.state.device.sfxChannel; },
set(value) { this.$store.commit('device/set', { key: 'sfxChannel', value }); }
},
volumeIcon: {
get() {
return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;

View file

@ -29,6 +29,10 @@ export const router = new VueRouter({
{ path: '/explore', component: page('explore') },
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
{ path: '/search', component: page('search') },
{ path: '/channels', component: page('channels') },
{ path: '/channels/new', component: page('channel-editor') },
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
{ path: '/channels/:channelId', component: page('channel'), props: true },
{ path: '/my/notifications', component: page('notifications') },
{ path: '/my/favorites', component: page('favorites') },
{ path: '/my/messages', component: page('messages') },

View file

@ -1,7 +1,7 @@
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import * as nestedProperty from 'nested-property';
import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faSatelliteDish, faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
import { AiScript, utils, values } from '@syuilo/aiscript';
import { apiUrl, deckmode } from './config';
@ -90,6 +90,7 @@ export const defaultDeviceSettings = {
sfxChat: 'syuilo/pope1',
sfxChatBg: 'syuilo/waon',
sfxAntenna: 'syuilo/triple',
sfxChannel: 'syuilo/square-pico',
userData: {},
};
@ -213,6 +214,11 @@ export default () => new Vuex.Store({
get show() { return getters.isSignedIn; },
to: '/my/pages',
},
channels: {
title: 'channel',
icon: faSatelliteDish,
to: '/channels',
},
games: {
title: 'games',
icon: faGamepad,

View file

@ -72,5 +72,6 @@
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
},
}

View file

@ -72,5 +72,6 @@
X12: 'rgba(0, 0, 0, 0.1)',
X13: 'rgba(0, 0, 0, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
},
}