1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-12-03 09:18:58 +09:00

improve(friendly): 모바일 환경에서의 플로팅 버튼 디자인 업데이트, 데스크톱 모드에서 타임라인에 알림창 추가

This commit is contained in:
NoriDev 2022-09-02 17:35:03 +09:00
parent ceda332de7
commit d948860d3c
5 changed files with 246 additions and 34 deletions

View File

@ -0,0 +1,103 @@
<template>
<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :style="{ color }" :title="acct(user)" @click="onClick">
<img class="inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
</span>
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
<img class="inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
</MkA>
</template>
<script lang="ts" setup>
import { onMounted, watch } from 'vue';
import * as misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
import { acct, userPage } from '@/filters/user';
import MkUserOnlineIndicator from '@/components/user-online-indicator.vue';
import { defaultStore } from '@/store';
const props = withDefaults(defineProps<{
user: misskey.entities.User;
target?: string | null;
disableLink?: boolean;
disablePreview?: boolean;
showIndicator?: boolean;
}>(), {
target: null,
disableLink: false,
disablePreview: false,
showIndicator: false,
});
const emit = defineEmits<{
(ev: 'click', v: MouseEvent): void;
}>();
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl);
function onClick(ev: MouseEvent) {
emit('click', ev);
}
let color = $ref();
watch(() => props.user.avatarBlurhash, () => {
color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
}, {
immediate: true,
});
</script>
<style lang="scss" scoped>
@keyframes earwiggleleft {
from { transform: rotate(37.6deg) skew(30deg); }
25% { transform: rotate(10deg) skew(30deg); }
50% { transform: rotate(20deg) skew(30deg); }
75% { transform: rotate(0deg) skew(30deg); }
to { transform: rotate(37.6deg) skew(30deg); }
}
@keyframes earwiggleright {
from { transform: rotate(-37.6deg) skew(-30deg); }
30% { transform: rotate(-10deg) skew(-30deg); }
55% { transform: rotate(-20deg) skew(-30deg); }
75% { transform: rotate(0deg) skew(-30deg); }
to { transform: rotate(-37.6deg) skew(-30deg); }
}
.eiwwqkts {
position: relative;
display: inline-block;
vertical-align: bottom;
flex-shrink: 0;
// border-radius: 100%;
line-height: 16px;
> .inner {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
border-radius: 0 80% 0 0;
z-index: 1;
overflow: hidden;
object-fit: cover;
width: 100%;
height: 100%;
}
> .indicator {
position: absolute;
z-index: 1;
bottom: 0;
left: 0;
width: 20%;
height: 20%;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="hoawjimk">
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" :class="{ isFriendly }">
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length">
<template v-for="media in mediaList.filter(media => previewable(media))">
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
@ -25,8 +25,6 @@ import * as os from '@/os';
import { FILE_TYPE_BROWSERSAFE } from '@/const';
import { defaultStore } from '@/store';
const isFriendly = $ref(localStorage.getItem('ui') === 'friendly');
const props = defineProps<{
mediaList: misskey.entities.DriveFile[];
raw?: boolean;
@ -179,12 +177,6 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
}
}
}
.isFriendly {
@media (min-width: 1500px) {
width: 50%;
}
}
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="isFriendly">
<div v-if="tab === 'all' || tab === 'unread'">
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="unreadOnly"/>
</div>
<div v-else-if="tab === 'mentions'">
<XNotes :pagination="mentionsPagination"/>
</div>
<div v-else-if="tab === 'directNotes'">
<XNotes :pagination="directNotesPagination"/>
</div>
</MkSpacer>
<MkSpacer v-else :content-max="800">
<div v-if="tab === 'all' || tab === 'unread'">
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="unreadOnly"/>
</div>
<div v-else-if="tab === 'mentions'">
<XNotes :pagination="mentionsPagination"/>
</div>
<div v-else-if="tab === 'directNotes'">
<XNotes :pagination="directNotesPagination"/>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { notificationTypes } from 'misskey-js';
import XNotifications from '@/components/notifications.vue';
import XNotes from '@/components/notes.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const isFriendly = $ref(localStorage.getItem('ui') === 'friendly');
let tab = $ref('all');
let includeTypes = $ref<string[] | null>(null);
let unreadOnly = $computed(() => tab === 'unread');
const mentionsPagination = {
endpoint: 'notes/mentions' as const,
limit: 10,
};
const directNotesPagination = {
endpoint: 'notes/mentions' as const,
limit: 10,
params: {
visibility: 'specified',
},
};
function setFilter(ev) {
const typeItems = notificationTypes.map(t => ({
text: i18n.t(`_notification._types.${t}`),
active: includeTypes && includeTypes.includes(t),
action: () => {
includeTypes = [t];
},
}));
const items = includeTypes != null ? [{
icon: 'fas fa-times',
text: i18n.ts.clear,
action: () => {
includeTypes = null;
},
}, null, ...typeItems] : typeItems;
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
const headerActions = $computed(() => [tab === 'all' ? {
text: i18n.ts.filter,
icon: 'fas fa-filter',
highlighted: includeTypes != null,
handler: setFilter,
} : undefined, tab === 'all' ? {
text: i18n.ts.markAllAsRead,
icon: 'fas fa-check',
handler: () => {
os.apiWithDialog('notifications/mark-all-as-read');
},
} : undefined].filter(x => x !== undefined));
const headerTabs = $computed(() => [{
key: 'all',
title: i18n.ts.all,
}, {
key: 'unread',
title: i18n.ts.unread,
}, {
key: 'mentions',
title: i18n.ts.mentions,
icon: 'fas fa-at',
}, {
key: 'directNotes',
title: i18n.ts.directNotes,
icon: 'fas fa-envelope',
}]);
definePageMetadata(computed(() => ({
title: i18n.ts.notifications,
icon: 'fas fa-bell',
})));
</script>

View File

@ -5,7 +5,7 @@
<MkPageHeader v-else v-model:tab="src" :actions="headerActions" :tabs="headerTabs"/>
</template>
<MkSpacer v-if="isFriendly"> <!-- if notification view added, style="width: 70%" -->
<MkSpacer v-if="isFriendly">
<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
@ -23,12 +23,6 @@
</div>
</MkSpacer>
<!--
<MkContainer v-if="isFriendly" :scrollable="true">
<XNotifications :include-types="includeTypes" style="width: 30%; padding: 24px 24px 24px 0"/>
</MkContainer>
-->
<MkSpacer v-else :content-max="800">
<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
@ -60,9 +54,7 @@ import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { $i } from '@/account';
import { definePageMetadata } from '@/scripts/page-metadata';
import XNotifications from '@/components/notifications.vue';
import MkStickyContainerTL from '@/components/global/sticky-container-timeline.vue';
import MkContainer from '@/components/ui/container.vue';
const isFriendly = $ref(localStorage.getItem('ui') === 'friendly');
const DESKTOP_THRESHOLD = 1100;

View File

@ -12,11 +12,15 @@
</main>
</MkStickyContainer>
<div v-if="isDesktop" ref="widgetsEl" class="widgets tl-noti">
<XNotifications style="height: 100%;" @mounted="attachSticky"/>
</div>
<div v-if="isDesktop" ref="widgetsEl" class="widgets">
<XWidgets @mounted="attachSticky"/>
</div>
<button v-if="isMobile" class="navButton nav _button" @click="drawerMenuShowing = true"><MkAvatar class="avatar" v-if="!canBack" :user="$i" :disable-preview="true" :show-indicator="true"/></button>
<button v-if="isMobile" class="navButton nav _button" @click="drawerMenuShowing = true"><CPAvatar class="avatar" :user="$i" :disable-preview="true" :show-indicator="false"/></button>
<button v-if="isMobile" class="postButton post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
@ -77,9 +81,11 @@ import { Router } from '@/nirax';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
import { deviceKind } from '@/scripts/device-kind';
import CPAvatar from '@/components/global/CPAvatar-Friendly.vue';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/friendly/navbar.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XNotifications = defineAsyncComponent(() => import('@/pages/notifications.vue'));
localStorage.setItem('ui', 'friendly');
@ -228,7 +234,7 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
.dkgtipfy {
$ui-font-size: 1em; // TODO:
$widgets-hide-threshold: 1090px;
$float-button-size: 58px;
$float-button-size: 70px;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc(var(--vh, 1vh) * 100);
@ -245,7 +251,7 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
}
> .contents {
width: 100%;
width: 70%;
min-width: 0;
background: var(--bg);
}
@ -260,23 +266,31 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
}
}
> .tl-noti {
width: 30%;
padding: 0;
}
> .navButton {
display: block;
position: fixed;
z-index: 1000;
bottom: calc(70px + env(safe-area-inset-bottom));
left: 10px;
bottom: calc(40px + env(safe-area-inset-bottom));
left: -10px;
width: $float-button-size;
height: $float-button-size;
// box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px;
// background: var(--panel);
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 0 10px 6px rgba(0, 0, 0, 0.14), 0 0 18px 1px rgba(0, 0, 0, 0.12);
background: var(--panel);
// opacity: 0.7;
border-radius: 0 80% 0 0;
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
> .avatar {
width: $float-button-size;
height: $float-button-size;
width: 100%;
height: 100%;
vertical-align: middle;
opacity: 0.7;
border-radius: 0 80% 0 0;
}
}
@ -284,15 +298,17 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
display: block;
position: fixed;
z-index: 1000;
bottom: calc(70px + env(safe-area-inset-bottom));
right: 0;
bottom: calc(40px + env(safe-area-inset-bottom));
right: -10px;
width: $float-button-size;
height: $float-button-size;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px;
font-size: 18px;
background: var(--accent);
opacity: 0.7;
border-radius: 20%;
// opacity: 0.7;
border-radius: 80% 0 0 0;
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
i {
color: white;