Compare commits
20 commits
main
...
michcio-th
Author | SHA1 | Date | |
---|---|---|---|
Michcio | 96b3e93541 | ||
Michcio | fe6c1d5e86 | ||
Michcio | 27e3225525 | ||
Michcio | 0ad239488d | ||
Michcio | 53f4b7ad9d | ||
Michcio | d4a5e6f147 | ||
Michcio | 8d2aa96c89 | ||
Michcio | 8a8f6af36c | ||
Michcio | 147e2bea1c | ||
Michcio | 53dedb8f0b | ||
Michcio | 9fe21cd1bc | ||
Michcio | 91e1790528 | ||
Michcio | 3c5891af7b | ||
Michcio | 0ff228cace | ||
Michcio | 9e8e06310b | ||
Michał Sidor | 3836b685f6 | ||
Michał Sidor | a0a26835a4 | ||
Michcio | 0cd758117a | ||
Michcio | d1a0e522ec | ||
Michał Sidor | 5496733a24 |
|
@ -686,7 +686,6 @@ apply: "تطبيق"
|
|||
receiveAnnouncementFromInstance: "استلم إشعارات من هذا المثيل"
|
||||
emailNotification: "إشعارات البريد الكتروني"
|
||||
inChannelSearch: "ابحث عن قناة"
|
||||
useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند النقر بالزر الأيمن"
|
||||
typingUsers: "{users} يكتب(ون)..."
|
||||
jumpToSpecifiedDate: "انتقل إلى تاريخ محدد"
|
||||
showingPastTimeline: "أنت تستعرض حاليًا خيطًا زمنيًا قديمًا"
|
||||
|
|
|
@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "এই ইন্সট্যান্স থ
|
|||
emailNotification: "ইমেইল বিজ্ঞপ্তি"
|
||||
publish: "প্রকাশ"
|
||||
inChannelSearch: "চ্যানেলে খুঁজুন"
|
||||
useReactionPickerForContextMenu: "রাইট ক্লিকের মাধ্যমে রিঅ্যাকশন পিকার খুলুন"
|
||||
typingUsers: "{users} লেখছে"
|
||||
jumpToSpecifiedDate: "একটি নির্দিষ্ট তারিখে যান"
|
||||
showingPastTimeline: "অতীতের টাইমলাইন দেখানো হচ্ছে"
|
||||
|
|
|
@ -718,7 +718,6 @@ receiveAnnouncementFromInstance: "Benachrichtigungen von dieser Instanz empfange
|
|||
emailNotification: "Email-Benachrichtigungen"
|
||||
publish: "Veröffentlichen"
|
||||
inChannelSearch: "In Kanal suchen"
|
||||
useReactionPickerForContextMenu: "Reaktionsauswahl durch Rechtsklick öffnen"
|
||||
typingUsers: "{users} ist/sind am schreiben …"
|
||||
jumpToSpecifiedDate: "Zu bestimmtem Datum springen"
|
||||
showingPastTimeline: "Es wird eine alte Chronik angezeigt"
|
||||
|
|
|
@ -718,7 +718,6 @@ receiveAnnouncementFromInstance: "Receive notifications from this instance"
|
|||
emailNotification: "Email notifications"
|
||||
publish: "Publish"
|
||||
inChannelSearch: "Search in channel"
|
||||
useReactionPickerForContextMenu: "Open reaction picker on right-click"
|
||||
typingUsers: "{users} is/are typing..."
|
||||
jumpToSpecifiedDate: "Jump to specific date"
|
||||
showingPastTimeline: "Currently displaying an old timeline"
|
||||
|
|
|
@ -714,7 +714,6 @@ receiveAnnouncementFromInstance: "Recibir notificaciones de la instancia"
|
|||
emailNotification: "Notificaciones por correo electrónico"
|
||||
publish: "Publicar"
|
||||
inChannelSearch: "Buscar en el canal"
|
||||
useReactionPickerForContextMenu: "Haga clic con el botón derecho para abrir el menu de reacciones"
|
||||
typingUsers: "{users} está escribiendo"
|
||||
jumpToSpecifiedDate: "Saltar a una fecha específica"
|
||||
showingPastTimeline: "Mostrar líneas de tiempo antiguas"
|
||||
|
|
|
@ -712,7 +712,6 @@ receiveAnnouncementFromInstance: "Recevoir les messages d'information de l'insta
|
|||
emailNotification: "Notifications par mail"
|
||||
publish: "Public"
|
||||
inChannelSearch: "Chercher dans le canal"
|
||||
useReactionPickerForContextMenu: "Clic-droit pour ouvrir le panneau de réactions"
|
||||
typingUsers: "{users} est en train d'écrire"
|
||||
jumpToSpecifiedDate: "Se rendre à la date"
|
||||
showingPastTimeline: "Un fil ancien est affiché"
|
||||
|
|
|
@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "Terima pemberitahuan surel dari instansi ini"
|
|||
emailNotification: "Pemberitahuan surel"
|
||||
publish: "Terbitkan"
|
||||
inChannelSearch: "Cari di kanal"
|
||||
useReactionPickerForContextMenu: "Buka pemilih reaksi dengan klik-kanan"
|
||||
typingUsers: "{users} sedang mengetik..."
|
||||
jumpToSpecifiedDate: "Loncat ke tanggal spesifik"
|
||||
showingPastTimeline: "Sedang menampilkan linimasa lama"
|
||||
|
|
|
@ -706,7 +706,6 @@ receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza"
|
|||
emailNotification: "Eventi per notifiche via mail"
|
||||
publish: "Pubblico"
|
||||
inChannelSearch: "Cerca in canale"
|
||||
useReactionPickerForContextMenu: "Cliccare sul tasto destro per aprire il pannello di reazioni"
|
||||
typingUsers: "{users} sta(nno) scrivendo"
|
||||
jumpToSpecifiedDate: "Vai alla data "
|
||||
showingPastTimeline: "Stai visualizzando una vecchia timeline"
|
||||
|
|
|
@ -720,7 +720,6 @@ receiveAnnouncementFromInstance: "インスタンスからのお知らせを受
|
|||
emailNotification: "メール通知"
|
||||
publish: "公開"
|
||||
inChannelSearch: "チャンネル内検索"
|
||||
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開く"
|
||||
typingUsers: "{users}が入力中"
|
||||
jumpToSpecifiedDate: "特定の日付にジャンプ"
|
||||
showingPastTimeline: "過去のタイムラインを表示しています"
|
||||
|
|
|
@ -629,7 +629,6 @@ apply: "適用"
|
|||
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
|
||||
emailNotification: "メール通知"
|
||||
inChannelSearch: "チャンネル内検索"
|
||||
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開くようにする"
|
||||
typingUsers: "{users}が今書きよるで"
|
||||
jumpToSpecifiedDate: "特定の日付にジャンプ"
|
||||
showingPastTimeline: "過去のタイムラインを表示してるで"
|
||||
|
|
|
@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "이 인스턴스의 알림을 이메일로 수
|
|||
emailNotification: "메일 알림"
|
||||
publish: "게시"
|
||||
inChannelSearch: "채널에서 검색"
|
||||
useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기"
|
||||
typingUsers: "{users} 님이 입력하고 있어요.."
|
||||
jumpToSpecifiedDate: "특정 날짜로 이동"
|
||||
showingPastTimeline: "과거의 타임라인을 표시하고 있어요"
|
||||
|
|
|
@ -693,7 +693,6 @@ receiveAnnouncementFromInstance: "Otrzymuj powiadomienia e-mail z tej instancji"
|
|||
emailNotification: "Powiadomienia e-mail"
|
||||
publish: "Publikuj"
|
||||
inChannelSearch: "Szukaj na kanale"
|
||||
useReactionPickerForContextMenu: "Otwórz wybornik reakcji prawym kliknięciem"
|
||||
typingUsers: "{users} pisze(-ą)..."
|
||||
jumpToSpecifiedDate: "Przejdź do określonej daty"
|
||||
showingPastTimeline: "Obecnie wyświetla starą oś czasu"
|
||||
|
|
|
@ -715,7 +715,6 @@ receiveAnnouncementFromInstance: "Получать оповещения с ин
|
|||
emailNotification: "Уведомления по электронной почте"
|
||||
publish: "Опубликовать"
|
||||
inChannelSearch: "Поиск по каналу"
|
||||
useReactionPickerForContextMenu: "Открывать палитру реакций правой кнопкой"
|
||||
typingUsers: "Стук клавиш. Это {users}…"
|
||||
jumpToSpecifiedDate: "Перейти к заданной дате"
|
||||
showingPastTimeline: "Отображается старая лента"
|
||||
|
|
|
@ -716,7 +716,6 @@ receiveAnnouncementFromInstance: "Prijať notifikácie z tohoto servera"
|
|||
emailNotification: "Emailové upozornenia"
|
||||
publish: "Zverejniť"
|
||||
inChannelSearch: "Hľadať v kanáli"
|
||||
useReactionPickerForContextMenu: "Otvoriť výber reakcií na pravý klik"
|
||||
typingUsers: "{users} píše/u"
|
||||
jumpToSpecifiedDate: "Skočiť na konkrétny dátum"
|
||||
showingPastTimeline: "Práve vidíte starú časovú os"
|
||||
|
|
|
@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "Отримувати оповіщення з
|
|||
emailNotification: "Сповіщення електронною поштою"
|
||||
publish: "Опублікувати"
|
||||
inChannelSearch: "Пошук за каналом"
|
||||
useReactionPickerForContextMenu: "Відкривати палітру реакцій правою кнопкою"
|
||||
typingUsers: "Стук клавіш. Це {users}…"
|
||||
goBack: "Назад"
|
||||
info: "Інформація"
|
||||
|
|
|
@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "Nhận thông báo từ máy chủ này"
|
|||
emailNotification: "Thông báo email"
|
||||
publish: "Đăng"
|
||||
inChannelSearch: "Tìm trong kênh"
|
||||
useReactionPickerForContextMenu: "Nhấn chuột phải để mở bộ chọn biểu cảm"
|
||||
typingUsers: "{users} đang nhập…"
|
||||
jumpToSpecifiedDate: "Đến một ngày cụ thể"
|
||||
showingPastTimeline: "Hiện đang hiển thị dòng thời gian cũ"
|
||||
|
|
|
@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "从实例接收通知"
|
|||
emailNotification: "邮件通知"
|
||||
publish: "发布"
|
||||
inChannelSearch: "频道内搜索"
|
||||
useReactionPickerForContextMenu: "单击右键打开回应工具栏"
|
||||
typingUsers: "{users}正在输入"
|
||||
jumpToSpecifiedDate: "跳转到特定日期"
|
||||
showingPastTimeline: "显示过去的时间线"
|
||||
|
|
|
@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知"
|
|||
emailNotification: "郵件通知"
|
||||
publish: "發佈"
|
||||
inChannelSearch: "頻道内搜尋"
|
||||
useReactionPickerForContextMenu: "點擊右鍵開啟回應工具欄"
|
||||
typingUsers: "{users}輸入中..."
|
||||
jumpToSpecifiedDate: "跳轉到特定日期"
|
||||
showingPastTimeline: "顯示過往的時間線"
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.17.21",
|
||||
"@types/node": "^18.7.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": "5.1.1",
|
||||
|
@ -48,7 +49,6 @@
|
|||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.3.0",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"typescript": "4.8.3"
|
||||
},
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 200 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 118 KiB |
|
@ -63,6 +63,7 @@ export type Source = {
|
|||
|
||||
mediaProxy?: string;
|
||||
proxyRemoteFiles?: boolean;
|
||||
internalStoragePath?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -34,7 +34,7 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
|
|||
* @param user http-signature user
|
||||
* @param url URL to fetch
|
||||
*/
|
||||
export async function signedGet(url: string, user: { id: User['id'] }) {
|
||||
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const req = createSignedGet({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null) {
|
||||
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null, followingQuery: SelectQueryBuilder<any> | null) {
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
|
@ -14,6 +14,7 @@ export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User,
|
|||
q.andWhere(new Brackets(qb => { qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
||||
.orWhere('note.mentions && array[:meId]::varchar[]', { meId: me.id })
|
||||
.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.userId = :meId', { meId: me.id });
|
||||
|
@ -22,6 +23,12 @@ export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User,
|
|||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}));
|
||||
if (followingQuery !== null) {
|
||||
qb.orWhere(new Brackets(qb => { qb
|
||||
.where(`note.mentions && array(${ followingQuery.getQuery() })`)
|
||||
.setParameters(followingQuery.getParameters())
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, null);
|
||||
if (user) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -88,7 +88,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, followingQuery);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -81,7 +81,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, null);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMutedUserQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -80,7 +80,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateRepliesQuery(query, user, followingQuery);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
|
|
@ -38,11 +38,8 @@ export default class extends Channel {
|
|||
// Ignore notes from instances the user has muted
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
if (note.reply && note.mentions && !this.user!.showTimelineReplies) {
|
||||
if (!note.mentions.includes(this.user!.id) && !note.mentions.some((user: string) => this.following.has(user))) return;
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
|
|
|
@ -138,7 +138,7 @@ export const startServer = () => {
|
|||
return server;
|
||||
};
|
||||
|
||||
export default () => new Promise(resolve => {
|
||||
export default () => new Promise<void>(resolve => {
|
||||
const server = createServer();
|
||||
|
||||
initializeStreamingServer(server);
|
||||
|
|
|
@ -8,7 +8,7 @@ const _filename = fileURLToPath(import.meta.url);
|
|||
const _dirname = dirname(_filename);
|
||||
|
||||
export class InternalStorage {
|
||||
private static readonly path = Path.resolve(_dirname, '../../../../../files');
|
||||
private static readonly path = config.internalStoragePath || Path.resolve(_dirname, '../../../../../files');
|
||||
|
||||
public static resolvePath = (key: string) => Path.resolve(InternalStorage.path, key);
|
||||
|
||||
|
|
|
@ -94,7 +94,10 @@ export default class Logger {
|
|||
}
|
||||
}
|
||||
|
||||
public error(x: string | Error, data?: Record<string, any> = {}, important = false): void { // 実行を継続できない状況で使う
|
||||
public error(x: string | Error, data: Record<string, any> | null = {}, important = false): void { // 実行を継続できない状況で使う
|
||||
if (data === null) {
|
||||
data = {};
|
||||
}
|
||||
if (x instanceof Error) {
|
||||
data.e = x;
|
||||
this.log('error', x.toString(), data, important);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"@syuilo/aiscript": "0.11.1",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vue/compiler-sfc": "3.2.39",
|
||||
"@vueuse/core": "9.1.0",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.1",
|
||||
|
@ -100,7 +101,6 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.3.0",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-vue": "^9.1.1",
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1 }">
|
||||
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1, supercompact }">
|
||||
<div class="main">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="body">
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<XNoteHeader :supercompact="supercompact" class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
<MkNoteSubNoteContent class="text" :note="note"/>
|
||||
<MkNoteSubNoteContent class="text" :note="note" :supercompact="supercompact && note.cw == null" :compact-heads="compactHeads"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,11 +38,15 @@ const props = withDefaults(defineProps<{
|
|||
note: foundkey.entities.Note;
|
||||
conversation?: foundkey.entities.Note[] | null;
|
||||
|
||||
supercompact?: boolean;
|
||||
compactHeads?: boolean;
|
||||
// how many notes are in between this one and the note being viewed in detail
|
||||
depth?: number;
|
||||
}>(), {
|
||||
conversation: null,
|
||||
depth: 1,
|
||||
supercompact: false,
|
||||
compactHeads: false,
|
||||
});
|
||||
|
||||
let showContent = $ref(false);
|
||||
|
@ -67,6 +71,13 @@ const replies: foundkey.entities.Note[] = props.conversation?.filter(item => ite
|
|||
}
|
||||
}
|
||||
|
||||
&.supercompact {
|
||||
> .main > .avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
display: flex;
|
||||
|
||||
|
@ -76,7 +87,7 @@ const replies: foundkey.entities.Note[] = props.conversation?.filter(item => ite
|
|||
margin: 0 8px 0 0;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
border-radius: calc(8% / 38);
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
|
28
packages/client/src/components/NoteTooltip.vue
Normal file
28
packages/client/src/components/NoteTooltip.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<MkTooltip :showing="true" :target-element="targetElement" :max-width="400" @closed="emit('closed')">
|
||||
<div class="tooltip-note">
|
||||
<MkNoteSub v-for="note in notes" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'foundkey-js';
|
||||
import MkTooltip from './ui/tooltip.vue';
|
||||
import MkNoteSub from './MkNoteSub.vue';
|
||||
|
||||
defineProps<{
|
||||
notes: misskey.entities.Note[];
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip-note {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
|
||||
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }">
|
||||
<ol v-if="type === 'user'" ref="suggests" class="users">
|
||||
<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown">
|
||||
<img class="avatar" :src="user.avatarUrl"/>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
>
|
||||
|
@ -101,10 +100,6 @@ function onClick(ev: MouseEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
os.contextMenu(getMenu(), ev);
|
||||
}
|
||||
|
||||
function onDragstart(ev: DragEvent): void {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
@mouseover="onMouseover"
|
||||
@mouseout="onMouseout"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
|
@ -218,27 +217,6 @@ function deleteFolder() {
|
|||
});
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu([{
|
||||
text: i18n.ts.openInWindow,
|
||||
icon: 'fas fa-window-restore',
|
||||
action: () => {
|
||||
os.popup(defineAsyncComponent(() => import('./drive-window.vue')), {
|
||||
initialFolder: props.folder,
|
||||
}, {
|
||||
}, 'closed');
|
||||
},
|
||||
}, null, {
|
||||
text: i18n.ts.rename,
|
||||
icon: 'fas fa-i-cursor',
|
||||
action: rename,
|
||||
}, null, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'fas fa-trash-alt',
|
||||
danger: true,
|
||||
action: deleteFolder,
|
||||
}], ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="yfudmmck">
|
||||
<nav>
|
||||
<div class="path" @contextmenu.prevent.stop="() => {}">
|
||||
<div class="path">
|
||||
<XNavFolder
|
||||
:class="{ current: folder == null }"
|
||||
:parent-folder="folder"
|
||||
|
@ -33,7 +33,6 @@
|
|||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<div ref="contents" class="contents">
|
||||
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
|
||||
|
@ -601,10 +600,6 @@ function showMenu(ev: MouseEvent) {
|
|||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(getMenu(), ev);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
|
||||
nextTick(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
|
||||
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
@ -32,39 +32,6 @@ const active = $computed(() => {
|
|||
return resolved.route.name === router.currentRoute.value.name;
|
||||
});
|
||||
|
||||
function onContextmenu(ev) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString() !== '') return;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: props.to,
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(props.to);
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
router.push(props.to);
|
||||
},
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(props.to, '_blank');
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(`${url}${props.to}`);
|
||||
},
|
||||
}], ev);
|
||||
}
|
||||
|
||||
function nav() {
|
||||
if (props.behavior === 'browser') {
|
||||
location.href = props.to;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :custom-emojis="customEmojis" :is-note="isNote" class="havbbuyv" :class="{ nowrap }"/>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :custom-emojis="customEmojis" :is-note="isNote" class="havbbuyv" :class="{ nowrap }" :compact-heads="compactHeads"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -12,6 +12,7 @@ withDefaults(defineProps<{
|
|||
author?: any;
|
||||
customEmojis?: any;
|
||||
isNote?: boolean;
|
||||
compactHeads?: boolean;
|
||||
}>(), {
|
||||
plain: false,
|
||||
nowrap: false,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<component
|
||||
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.slice(local.length) : url" :rel="rel" :target="target"
|
||||
@contextmenu.stop="() => {}"
|
||||
>
|
||||
<template v-if="!self">
|
||||
<span class="schema">{{ schema }}//</span>
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
:alt="video.comment"
|
||||
preload="none"
|
||||
controls
|
||||
@contextmenu.stop
|
||||
>
|
||||
<source
|
||||
:src="video.url"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
|
||||
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
|
||||
<span class="main">
|
||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe, [$style.compact]: compact }]" :to="url" :style="{ background: bgCss }">
|
||||
<img :class="[$style.icon, { [$style.compact]: compact }]" :src="`/avatar/@${username}@${host}`" alt="">
|
||||
<span v-if="!compact" class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.mainHost">@{{ toUnicode(host) }}</span>
|
||||
</span>
|
||||
|
@ -25,6 +25,7 @@ import { defaultStore } from '@/store';
|
|||
const props = defineProps<{
|
||||
username: string;
|
||||
host: string;
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
|
||||
|
@ -49,6 +50,10 @@ useCssModule();
|
|||
border-radius: 999px;
|
||||
color: var(--mention);
|
||||
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.isMe {
|
||||
color: var(--mentionMe);
|
||||
}
|
||||
|
@ -61,6 +66,14 @@ useCssModule();
|
|||
margin: 0 0.2em 0 0;
|
||||
vertical-align: bottom;
|
||||
border-radius: 100%;
|
||||
|
||||
.compact > & {
|
||||
margin: 0;
|
||||
border: 0.1em solid var(--mention);
|
||||
}
|
||||
.compact.isMe > & {
|
||||
border: 0.1em solid var(--mentionMe);
|
||||
}
|
||||
}
|
||||
|
||||
.mainHost {
|
||||
|
|
|
@ -42,6 +42,11 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
compactHeads: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
|
@ -232,6 +237,7 @@ export default defineComponent({
|
|||
key: Math.random(),
|
||||
host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
|
||||
username: token.props.username,
|
||||
compact: this.compactHeads,
|
||||
})];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div class="header" @contextmenu="onContextmenu">
|
||||
<div class="header">
|
||||
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
|
||||
<span v-else style="display: inline-block; width: 20px"></span>
|
||||
<span v-if="pageMetadata?.value" class="title">
|
||||
|
@ -59,33 +59,6 @@ provide('shouldOmitHeaderTitle', true);
|
|||
provide('shouldHeaderThin', true);
|
||||
|
||||
const pageUrl = $computed(() => url + path);
|
||||
const contextmenu = $computed(() => {
|
||||
return [{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: expand,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.popout,
|
||||
action: popout,
|
||||
}, null, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(pageUrl, '_blank');
|
||||
modal.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(pageUrl);
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
function navigate(path, record = true) {
|
||||
if (record) history.push(router.getCurrentPath());
|
||||
|
@ -105,10 +78,6 @@ function popout() {
|
|||
_popout(path, rootEl);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(contextmenu, ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
<article class="article">
|
||||
<header class="header">
|
||||
<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
|
@ -230,24 +230,6 @@ function undoReact(note): void {
|
|||
});
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
|
||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), ev).then(focus);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), menuButton.value, {
|
||||
viaKeyboard,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<header class="kkwtjztg">
|
||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
<MkUserName v-if="!supercompact" :user="note.user"/>
|
||||
<div v-if="supercompact" class="username"><MkAcct :user="note.user"/></div>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" class="is-bot">bot</div>
|
||||
<div class="username"><MkAcct :user="note.user"/></div>
|
||||
<div v-if="!supercompact" class="username"><MkAcct :user="note.user"/></div>
|
||||
<div class="info">
|
||||
<MkA class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
|
@ -23,6 +24,7 @@ import { userPage } from '@/filters/user';
|
|||
defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
pinned?: boolean;
|
||||
supercompact?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :supercompact="compactParent" :compact-heads="compactHeads" class="reply-to"/>
|
||||
<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div>
|
||||
<div v-if="isRenote" class="renote">
|
||||
|
@ -30,7 +30,7 @@
|
|||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
<article class="article">
|
||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||
|
@ -42,8 +42,8 @@
|
|||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
||||
<div class="text">
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i ref="parentReply" class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis" :compact-heads="compactHeads"/>
|
||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
|
@ -70,7 +70,7 @@
|
|||
</div>
|
||||
<footer class="footer">
|
||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="button _button" @click="reply()">
|
||||
<button ref="replyButton" class="button _button" @click="reply()">
|
||||
<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
|
||||
<template v-else><i class="fas fa-reply"></i></template>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
|
@ -107,6 +107,7 @@ import * as foundkey from 'foundkey-js';
|
|||
import MkNoteSub from './MkNoteSub.vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XNoteSimple from './note-simple.vue';
|
||||
import NoteTooltip from './NoteTooltip.vue';
|
||||
import XReactionsViewer from './reactions-viewer.vue';
|
||||
import XMediaList from './media-list.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
|
@ -127,10 +128,13 @@ import { $i } from '@/account';
|
|||
import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
pinned?: boolean;
|
||||
compactParent?: boolean;
|
||||
compactHeads?: boolean;
|
||||
}>();
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
@ -157,9 +161,11 @@ const isRenote = (
|
|||
|
||||
const el = ref<HTMLElement>();
|
||||
const menuButton = ref<HTMLElement>();
|
||||
const replyButton = ref<HTMLElement>();
|
||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||
const renoteTime = ref<HTMLElement>();
|
||||
const reactButton = ref<HTMLElement>();
|
||||
const parentReply = ref<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as foundkey.entities.Note : note);
|
||||
const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
|
@ -192,6 +198,30 @@ useNoteCapture({
|
|||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
useTooltip(parentReply, async (showing) => {
|
||||
const parentNote = await os.api('notes/show', {
|
||||
noteId: appearNote.replyId,
|
||||
});
|
||||
|
||||
os.popup(NoteTooltip, {
|
||||
showing,
|
||||
notes: [parentNote],
|
||||
targetElement: parentReply.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
useTooltip(replyButton, async (showing) => {
|
||||
const replies = await os.api('notes/replies', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
os.popup(NoteTooltip, {
|
||||
showing,
|
||||
notes: replies,
|
||||
targetElement: replyButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
|
@ -224,24 +254,6 @@ function undoReact(): void {
|
|||
|
||||
const currentClipPage = inject<Ref<foundkey.entities.Clip> | null>('currentClipPage', null);
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
|
||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||
ev.preventDefault();
|
||||
react();
|
||||
} else {
|
||||
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
|
||||
}
|
||||
}
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
|
||||
viaKeyboard,
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<template #default="{ items: notes }">
|
||||
<div class="giivymft" :class="{ noGap }">
|
||||
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" class="notes">
|
||||
<XNote :key="note._featuredId_ || note.id" class="qtqtichx" :note="note"/>
|
||||
<XNote :key="note._featuredId_ || note.id" class="qtqtichx" :note="note" :compact-parent="true"/>
|
||||
</XList>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<template #default="{ items: notifications }">
|
||||
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
|
||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :compact-parent="true" :compact-heads="true" />
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
||||
</XList>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
:close-button="true"
|
||||
:buttons-left="buttonsLeft"
|
||||
:buttons-right="buttonsRight"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
|
@ -85,28 +84,6 @@ provideMetadataReceiver((info) => {
|
|||
provide('shouldOmitHeaderTitle', true);
|
||||
provide('shouldHeaderThin', true);
|
||||
|
||||
const contextmenu = $computed(() => ([{
|
||||
icon: 'fas fa-expand-alt',
|
||||
text: i18n.ts.showInPage,
|
||||
action: expand,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.popout,
|
||||
action: popout,
|
||||
}, {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(url + router.getCurrentPath(), '_blank');
|
||||
windowEl.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url + router.getCurrentPath());
|
||||
},
|
||||
}]));
|
||||
|
||||
function back() {
|
||||
history.pop();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div v-show="files.length != 0" class="skeikyzd">
|
||||
<XDraggable v-model="_files" class="files" item-key="id" animation="150" delay="100" delay-on-touch-only="true">
|
||||
<template #item="{element}">
|
||||
<div @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
|
||||
<div @click="showFileMenu(element, $event)">
|
||||
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
|
||||
<div v-if="element.isSensitive" class="sensitive">
|
||||
<i class="fas fa-exclamation-triangle icon"></i>
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
@click="toggleReaction()"
|
||||
>
|
||||
<MkEmoji class="icon" :emoji="reaction" :custom-emojis="note.emojis" :is-reaction="true" :normal="true"/>
|
||||
<span class="count">{{ count }}</span>
|
||||
<span v-if="users === undefined" class="count">{{ count }}</span>
|
||||
<span v-if="users !== undefined">
|
||||
<MkAvatar v-for="u in users" class="user" :style="{ height: '2em', width: '2em' }" :key="u.id" :user="u" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
@ -25,13 +28,14 @@ const props = defineProps<{
|
|||
count: number;
|
||||
isInitial: boolean;
|
||||
note: foundkey.entities.Note;
|
||||
users?: foundkey.entities.UserLite[];
|
||||
}>();
|
||||
|
||||
const buttonRef = ref<HTMLElement>();
|
||||
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
|
||||
const toggleReaction = () => {
|
||||
const toggleReaction = (): void => {
|
||||
if (!canToggle.value) return;
|
||||
|
||||
const oldReaction = props.note.myReaction;
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<template>
|
||||
<div class="tdflqwzn" :class="{ isMe }">
|
||||
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
|
||||
<div ref="targetEl" class="tdflqwzn" :class="{ isMe }">
|
||||
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :users="usersMap[reaction]" :note="note"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useIntersectionObserver } from '@vueuse/core';
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import XReaction from './reactions-viewer.reaction.vue';
|
||||
import { $i } from '@/account';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
|
@ -17,6 +19,39 @@ const props = defineProps<{
|
|||
const initialReactions = new Set(Object.keys(props.note.reactions));
|
||||
|
||||
const isMe = computed(() => $i && $i.id === props.note.userId);
|
||||
|
||||
const usersMap = ref({});
|
||||
const superloaded = ref(false);
|
||||
const targetEl = ref();
|
||||
|
||||
async function updateReactions(currentValue): Promise<void> {
|
||||
const reactions = await os.api('notes/reactions', {
|
||||
noteId: currentValue.id,
|
||||
limit: 100,
|
||||
});
|
||||
const users = {};
|
||||
for (const reaction of reactions) {
|
||||
if (users[reaction.type] === undefined) {
|
||||
users[reaction.type] = [];
|
||||
}
|
||||
users[reaction.type].push(reaction.user);
|
||||
}
|
||||
usersMap.value = users;
|
||||
superloaded.value = true;
|
||||
}
|
||||
|
||||
async function prepareFetch(note: foundkey.entities.Note): Promise<void> {
|
||||
superloaded.value = false;
|
||||
const { stop } = useIntersectionObserver(targetEl, ([{ isIntersecting }]) => {
|
||||
if (isIntersecting) {
|
||||
superloaded.value = true;
|
||||
stop();
|
||||
updateReactions(note);
|
||||
}
|
||||
});
|
||||
}
|
||||
onMounted(() => prepareFetch(props.note));
|
||||
watch(props.note, prepareFetch, { deep: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="wrmlmaau" :class="{ collapsed, isLong }">
|
||||
<div class="wrmlmaau" :class="{ collapsed, isLong, supercompact }">
|
||||
<div class="body">
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i ref="parentReply" class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis" :compact-heads="compactHeads"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
@ -15,7 +15,7 @@
|
|||
<XPoll :note="note"/>
|
||||
</details>
|
||||
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
|
||||
<span>{{ i18n.ts.showMore }}</span>
|
||||
<span v-if="!supercompact">{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
<button v-if="isLong && !collapsed" class="showLess _button" @click="collapsed = true">
|
||||
<span>{{ i18n.ts.showLess }}</span>
|
||||
|
@ -24,13 +24,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import XPoll from './poll.vue';
|
||||
import XMediaList from './media-list.vue';
|
||||
import NoteTooltip from './NoteTooltip.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: foundkey.entities.Note;
|
||||
supercompact?: boolean;
|
||||
compactHeads?: boolean;
|
||||
}>();
|
||||
|
||||
const isLong = (
|
||||
|
@ -39,7 +45,20 @@ const isLong = (
|
|||
(props.note.text.length > 500)
|
||||
)
|
||||
);
|
||||
const collapsed = $ref(props.note.cw == null && isLong);
|
||||
const collapsed = $ref(props.supercompact || (props.note.cw == null && isLong));
|
||||
|
||||
const parentReply = ref<HTMLElement>();
|
||||
useTooltip(parentReply, async (showing): Promise<void> => {
|
||||
const parentNote = await os.api('notes/show', {
|
||||
noteId: props.note.replyId,
|
||||
});
|
||||
|
||||
os.popup(NoteTooltip, {
|
||||
showing,
|
||||
notes: [parentNote],
|
||||
targetElement: parentReply.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -64,6 +83,10 @@ const collapsed = $ref(props.note.cw == null && isLong);
|
|||
max-height: 9em;
|
||||
overflow: hidden;
|
||||
|
||||
&.supercompact {
|
||||
max-height: 4.5em;
|
||||
}
|
||||
|
||||
> .fade {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
<template>
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" appear>
|
||||
<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
import MkMenu from './menu.vue';
|
||||
import { MenuItem } from './types/menu.vue';
|
||||
import contains from '@/scripts/contains';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
items: MenuItem[];
|
||||
ev: MouseEvent;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
let rootEl = $ref<HTMLDivElement>();
|
||||
|
||||
let zIndex = $ref<number>(os.claimZIndex('high'));
|
||||
|
||||
onMounted(() => {
|
||||
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
|
||||
const width = rootEl.offsetWidth;
|
||||
const height = rootEl.offsetHeight;
|
||||
|
||||
if (left + width - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - width + window.pageXOffset;
|
||||
}
|
||||
|
||||
if (top + height - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - height + window.pageYOffset;
|
||||
}
|
||||
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
|
||||
rootEl.style.top = `${top}px`;
|
||||
rootEl.style.left = `${left}px`;
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.addEventListener('mousedown', onMousedown);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.removeEventListener('mousedown', onMousedown);
|
||||
}
|
||||
});
|
||||
|
||||
function onMousedown(evt: Event) {
|
||||
if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nvlagfpb {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
</style>
|
|
@ -4,7 +4,6 @@
|
|||
class="rrevdjwt"
|
||||
:class="{ center: align === 'center', asDrawer }"
|
||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||
@contextmenu.self="e => e.preventDefault()"
|
||||
>
|
||||
<template v-for="(item, i) in items2">
|
||||
<div v-if="item === null" class="divider"></div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick"></div>
|
||||
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
|
||||
<slot :max-height="maxHeight" :type="type"></slot>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="emit('closed')">
|
||||
<div v-if="showing" ref="main" class="ebkgocck">
|
||||
<div class="body _shadow _narrow_" @mousedown="moveToTop" @keydown="onKeydown">
|
||||
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
|
||||
<div class="header" :class="{ mini }">
|
||||
<span class="left">
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
</span>
|
||||
|
@ -70,7 +70,6 @@ const props = withDefaults(defineProps<{
|
|||
closeButton?: boolean;
|
||||
mini?: boolean;
|
||||
front?: boolean;
|
||||
contextmenu?: MenuItem[];
|
||||
buttonsLeft?: any[];
|
||||
buttonsRight?: any[];
|
||||
}>(), {
|
||||
|
@ -79,7 +78,6 @@ const props = withDefaults(defineProps<{
|
|||
closeButton: true,
|
||||
mini: false,
|
||||
front: false,
|
||||
contextmenu: () => [] as MenuItem[],
|
||||
buttonsLeft: () => [],
|
||||
buttonsRight: () => [],
|
||||
});
|
||||
|
@ -121,12 +119,6 @@ function onKeydown(evt: KeyboardEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
if (props.contextmenu) {
|
||||
os.contextMenu(props.contextmenu, ev);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToTop(): void {
|
||||
main.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low').toString();
|
||||
}
|
||||
|
@ -139,9 +131,6 @@ function getClickPos(evt: MouseEvent | TouchEvent): [number, number] {
|
|||
}
|
||||
|
||||
function onHeaderMousedown(evt: MouseEvent | TouchEvent): void {
|
||||
// Right-click ignored as it is likely to have attempted to open a context menu
|
||||
if (evt instanceof MouseEvent && evt.button === 2) return;
|
||||
|
||||
if (!contains(main, document.activeElement)) main.focus();
|
||||
|
||||
const position = main.getBoundingClientRect();
|
||||
|
|
|
@ -171,7 +171,6 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
const app = createApp(
|
||||
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
defineAsyncComponent(() => import('@/ui/universal.vue')),
|
||||
);
|
||||
|
|
|
@ -169,13 +169,6 @@ export const menuDef = reactive({
|
|||
localStorage.setItem('ui', 'default');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.deck,
|
||||
active: ui === 'deck',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'deck');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.classic,
|
||||
active: ui === 'classic',
|
||||
|
|
|
@ -527,24 +527,6 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
|||
});
|
||||
}
|
||||
|
||||
export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
return new Promise((resolve) => {
|
||||
let dispose;
|
||||
popup(defineAsyncComponent(() => import('@/components/ui/context-menu.vue')), {
|
||||
items,
|
||||
ev,
|
||||
}, {
|
||||
closed: () => {
|
||||
resolve();
|
||||
dispose();
|
||||
},
|
||||
}).then(res => {
|
||||
dispose = res.dispose;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}) {
|
||||
return new Promise((resolve) => {
|
||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||
|
@ -564,8 +546,6 @@ export function post(props: Record<string, any> = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
export const deckGlobalEvents = new EventEmitter();
|
||||
|
||||
/*
|
||||
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
|
||||
|
||||
<FormRadios v-model="columnAlign" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._deck.columnAlign }}</template>
|
||||
<option value="left">{{ i18n.ts.left }}</option>
|
||||
<option value="center">{{ i18n.ts.center }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormRadios v-model="columnHeaderHeight" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
|
||||
<option :value="42">{{ i18n.ts.narrow }}</option>
|
||||
<option :value="45">{{ i18n.ts.medium }}</option>
|
||||
<option :value="48">{{ i18n.ts.wide }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormInput v-model="columnMargin" type="number" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._deck.columnMargin }}</template>
|
||||
<template #suffix>px</template>
|
||||
</FormInput>
|
||||
|
||||
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import { deckStore } from '@/ui/deck/deck-store';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
|
||||
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
|
||||
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
|
||||
const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
|
||||
const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
|
||||
const profile = computed(deckStore.makeGetterSetter('profile'));
|
||||
|
||||
watch(navWindow, async () => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
});
|
||||
|
||||
async function setProfile() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts._deck.profile,
|
||||
allowEmpty: false,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
profile.value = name;
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.deck,
|
||||
icon: 'fas fa-columns',
|
||||
});
|
||||
</script>
|
|
@ -26,7 +26,6 @@
|
|||
<template #label>{{ i18n.ts.behavior }}</template>
|
||||
<FormSwitch v-model="imageNewTab" class="_formBlock">{{ i18n.ts.openImageInNewTab }}</FormSwitch>
|
||||
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ i18n.ts.enableInfiniteScroll }}</FormSwitch>
|
||||
<FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch>
|
||||
|
||||
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
|
@ -90,8 +89,6 @@
|
|||
<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
|
||||
</FormRange>
|
||||
|
||||
<FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink>
|
||||
|
||||
<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -145,7 +142,6 @@ const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostF
|
|||
const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
|
||||
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
|
||||
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
|
||||
const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
|
||||
const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
|
||||
|
||||
watch(lang, () => {
|
||||
|
|
|
@ -217,7 +217,6 @@ const component = computed(() => {
|
|||
case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
|
||||
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
|
||||
case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
|
||||
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
|
||||
case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
|
||||
case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
|
||||
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
|
||||
|
|
|
@ -47,5 +47,9 @@ export const getNoteSummary = (note: foundkey.entities.Note): string => {
|
|||
}
|
||||
}
|
||||
|
||||
return summary.trim();
|
||||
summary = summary.trim();
|
||||
if (summary.length > 75) {
|
||||
summary = summary.substring(0, 70) + '…';
|
||||
}
|
||||
return summary;
|
||||
};
|
||||
|
|
|
@ -64,16 +64,8 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'deviceAccount',
|
||||
default: [
|
||||
'notifications',
|
||||
'favorites',
|
||||
'drive',
|
||||
'followRequests',
|
||||
'-',
|
||||
'explore',
|
||||
'announcements',
|
||||
'search',
|
||||
'-',
|
||||
'ui',
|
||||
],
|
||||
]
|
||||
},
|
||||
visibility: {
|
||||
where: 'deviceAccount',
|
||||
|
@ -95,9 +87,9 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
tl: {
|
||||
where: 'deviceAccount',
|
||||
default: {
|
||||
src: 'home' as 'home' | 'local' | 'social' | 'global',
|
||||
arg: null,
|
||||
},
|
||||
src: 'social' as 'home' | 'local' | 'social' | 'global',
|
||||
arg: null
|
||||
}
|
||||
},
|
||||
|
||||
overridedDeviceKind: {
|
||||
|
@ -164,17 +156,13 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
useReactionPickerForContextMenu: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
showGapBetweenNotesInTimeline: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
darkMode: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
default: true
|
||||
},
|
||||
instanceTicker: {
|
||||
where: 'device',
|
||||
|
@ -246,8 +234,8 @@ type Plugin = {
|
|||
/**
|
||||
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
|
||||
*/
|
||||
import lightTheme from '@/themes/l-light.json5';
|
||||
import darkTheme from '@/themes/d-dark.json5';
|
||||
import lightTheme from '@/themes/l-sushi.json5';
|
||||
import darkTheme from '@/themes/d-cherrybleu.json5';
|
||||
|
||||
export class ColdDeviceStorage {
|
||||
public static default = {
|
||||
|
@ -257,7 +245,7 @@ export class ColdDeviceStorage {
|
|||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
sound_masterVolume: 0.3,
|
||||
sound_note: { type: 'syuilo/down', volume: 1 },
|
||||
sound_note: { type: null, volume: 1 },
|
||||
sound_noteMy: { type: 'syuilo/up', volume: 1 },
|
||||
sound_notification: { type: 'syuilo/pope2', volume: 1 },
|
||||
sound_chat: { type: 'syuilo/pope1', volume: 1 },
|
||||
|
|
25
packages/client/src/themes/d-cherrybleu.json5
Normal file
25
packages/client/src/themes/d-cherrybleu.json5
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
base: 'dark',
|
||||
props: {
|
||||
accent: 'rgb(255, 89, 117)',
|
||||
actualBg: 'rgb(28, 28, 37)',
|
||||
bg: 'transparent',
|
||||
mg: ':lighten<10<@actualBg',
|
||||
fg: 'rgb(236, 239, 244)',
|
||||
panel: 'rgb(35, 35, 47)',
|
||||
renote: '@accent',
|
||||
link: '@accent',
|
||||
mention: '@accent',
|
||||
hashtag: '@accent',
|
||||
divider: 'rgb(63, 63, 80)',
|
||||
wallpaperOverlay: ':alpha<0.5<@actualBg',
|
||||
modalBg: ':alpha<0.5<@actualBg',
|
||||
shadow: ':alpha<0.3<@actualBg',
|
||||
buttonBg: '@mg',
|
||||
windowHeader: '@mg',
|
||||
},
|
||||
id: '32b376df-9f1c-4ffc-8a2a-8f8c4a8792ca',
|
||||
name: 'CherryBleu',
|
||||
author: '@michcio@fedi2.0x7f.one',
|
||||
desc: 'Cherry but with more transparencies',
|
||||
}
|
|
@ -10,7 +10,7 @@
|
|||
<XWidgets :place="'left'" @mounted="attachSticky(widgetsLeft)"/>
|
||||
</div>
|
||||
|
||||
<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
|
||||
<main class="main" :style="{ background: pageMetadata?.value?.bg }">
|
||||
<div class="content">
|
||||
<RouterView/>
|
||||
</div>
|
||||
|
@ -83,39 +83,6 @@ function attachSticky(el) {
|
|||
}, { passive: true });
|
||||
}
|
||||
|
||||
function top() {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = mainRouter.getCurrentPath();
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: fullView ? 'fas fa-compress' : 'fas fa-expand',
|
||||
text: fullView ? i18n.ts.quitFullView : i18n.ts.fullView,
|
||||
action: () => {
|
||||
fullView = !fullView;
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(path);
|
||||
},
|
||||
}], ev);
|
||||
}
|
||||
|
||||
if (window.innerWidth < 1024) {
|
||||
localStorage.setItem('ui', 'default');
|
||||
location.reload();
|
||||
|
|
|
@ -1,297 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
|
||||
@contextmenu.self.prevent="onContextmenu"
|
||||
>
|
||||
<XSidebar v-if="!isMobile"/>
|
||||
|
||||
<template v-for="ids in layout">
|
||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||
<section
|
||||
v-if="ids.length > 1"
|
||||
class="folder column"
|
||||
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
|
||||
>
|
||||
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
|
||||
</section>
|
||||
<DeckColumnCore
|
||||
v-else
|
||||
:ref="ids[0]"
|
||||
:key="ids[0]"
|
||||
class="column"
|
||||
:column="columns.find(c => c.id === ids[0])"
|
||||
:is-stacked="false"
|
||||
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
|
||||
@parent-focus="moveFocus(ids[0], $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div v-if="isMobile" class="buttons">
|
||||
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||
<button class="button home _button" @click="mainRouter.push('/')"><i class="fas fa-home"></i></button>
|
||||
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
|
||||
</div>
|
||||
|
||||
<transition :name="$store.state.animation ? 'menu-back' : ''">
|
||||
<div
|
||||
v-if="drawerMenuShowing"
|
||||
class="menu-back _modalBg"
|
||||
@click="drawerMenuShowing = false"
|
||||
@touchstart.passive="drawerMenuShowing = false"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<transition :name="$store.state.animation ? 'menu' : ''">
|
||||
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
|
||||
</transition>
|
||||
|
||||
<XCommon/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, ref, watch } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
|
||||
import DeckColumnCore from '@/ui/deck/column-core.vue';
|
||||
import XSidebar from '@/ui/_common_/sidebar.vue';
|
||||
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
|
||||
import { getScrollContainer } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import { menuDef } from '@/menu';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { mainRouter } from '@/router';
|
||||
|
||||
if (deckStore.state.navWindow) {
|
||||
mainRouter.navHook = (path) => {
|
||||
os.pageWindow(path);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
const isMobile = ref(window.innerWidth <= 500);
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= 500;
|
||||
});
|
||||
|
||||
const drawerMenuShowing = ref(false);
|
||||
|
||||
const route = 'TODO';
|
||||
watch(route, () => {
|
||||
drawerMenuShowing.value = false;
|
||||
});
|
||||
|
||||
const columns = deckStore.reactiveState.columns;
|
||||
const layout = deckStore.reactiveState.layout;
|
||||
const menuIndicated = computed(() => {
|
||||
if ($i == null) return false;
|
||||
for (const def in menuDef) {
|
||||
if (menuDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const addColumn = async (ev) => {
|
||||
const columns = [
|
||||
'main',
|
||||
'widgets',
|
||||
'notifications',
|
||||
'tl',
|
||||
'antenna',
|
||||
'list',
|
||||
'mentions',
|
||||
'direct',
|
||||
];
|
||||
|
||||
const { canceled, result: column } = await os.select({
|
||||
title: i18n.ts._deck.addColumn,
|
||||
items: columns.map(column => ({
|
||||
value: column, text: i18n.t('_deck._columns.' + column),
|
||||
})),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
addColumnToStore({
|
||||
type: column,
|
||||
id: uuid(),
|
||||
name: i18n.t('_deck._columns.' + column),
|
||||
width: 330,
|
||||
});
|
||||
};
|
||||
|
||||
const onContextmenu = (ev) => {
|
||||
os.contextMenu([{
|
||||
text: i18n.ts._deck.addColumn,
|
||||
action: addColumn,
|
||||
}], ev);
|
||||
};
|
||||
|
||||
provide('shouldSpacerMin', true);
|
||||
|
||||
document.documentElement.style.overflowY = 'hidden';
|
||||
document.documentElement.style.scrollBehavior = 'auto';
|
||||
window.addEventListener('wheel', (ev) => {
|
||||
if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
|
||||
document.documentElement.scrollLeft += ev.deltaY;
|
||||
}
|
||||
});
|
||||
loadDeck();
|
||||
|
||||
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
||||
// TODO??
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-enter-active,
|
||||
.menu-leave-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.menu-enter-from,
|
||||
.menu-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(-240px);
|
||||
}
|
||||
|
||||
.menu-back-enter-active,
|
||||
.menu-back-leave-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.menu-back-enter-from,
|
||||
.menu-back-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mk-deck {
|
||||
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||
|
||||
// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
display: flex;
|
||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
padding: var(--deckMargin);
|
||||
|
||||
&.center {
|
||||
> .column:first-of-type {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
> .column:last-of-type {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.isMobile {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
> .column {
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--deckMargin);
|
||||
|
||||
&.folder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: var(--deckMargin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .button {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
color: var(--fg);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
height: 60px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--X2);
|
||||
}
|
||||
|
||||
> .indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: var(--indicator);
|
||||
font-size: 16px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
|
||||
> * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .menu-back {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
> .menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
width: 240px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,63 +0,0 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn, Column } from './deck-store';
|
||||
import XTimeline from '@/components/timeline.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||
|
||||
onMounted(() => {
|
||||
if (props.column.antennaId == null) {
|
||||
setAntenna();
|
||||
}
|
||||
});
|
||||
|
||||
async function setAntenna() {
|
||||
const antennas = await os.api('antennas/list');
|
||||
const { canceled, result: antenna } = await os.select({
|
||||
title: i18n.ts.selectAntenna,
|
||||
items: antennas.map(x => ({
|
||||
value: x, text: x.name,
|
||||
})),
|
||||
default: props.column.antennaId,
|
||||
});
|
||||
if (canceled) return;
|
||||
updateColumn(props.column.id, {
|
||||
antennaId: antenna.id,
|
||||
});
|
||||
}
|
||||
/*
|
||||
function focus() {
|
||||
timeline.focus();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,43 +0,0 @@
|
|||
<template>
|
||||
<!-- TODO: リファクタの余地がありそう -->
|
||||
<div v-if="!column">たぶん見えちゃいけないやつ</div>
|
||||
<XMainColumn v-else-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XMainColumn from './main-column.vue';
|
||||
import XTlColumn from './tl-column.vue';
|
||||
import XAntennaColumn from './antenna-column.vue';
|
||||
import XListColumn from './list-column.vue';
|
||||
import XNotificationsColumn from './notifications-column.vue';
|
||||
import XWidgetsColumn from './widgets-column.vue';
|
||||
import XMentionsColumn from './mentions-column.vue';
|
||||
import XDirectColumn from './direct-column.vue';
|
||||
import { Column } from './deck-store';
|
||||
|
||||
defineProps<{
|
||||
column?: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
/*
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
focus() {
|
||||
this.$children[0].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
</script>
|
|
@ -1,382 +0,0 @@
|
|||
<template>
|
||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||
<section
|
||||
v-hotkey="keymap" class="dnpfarvg _panel _narrow_"
|
||||
:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
|
||||
:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
>
|
||||
<header
|
||||
:class="{ indicated }"
|
||||
draggable="true"
|
||||
@click="goTop"
|
||||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
@contextmenu.prevent.stop="onContextmenu"
|
||||
>
|
||||
<button v-if="isStacked && !isMainColumn" class="toggleActive _button" @click="toggleActive">
|
||||
<template v-if="active"><i class="fas fa-angle-up"></i></template>
|
||||
<template v-else><i class="fas fa-angle-down"></i></template>
|
||||
</button>
|
||||
<div class="action">
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
<span class="header"><slot name="header"></slot></span>
|
||||
<button v-if="func" v-tooltip="func.title" class="menu _button" @click.stop="func.handler"><i :class="func.icon || 'fas fa-cog'"></i></button>
|
||||
</header>
|
||||
<div v-show="active" ref="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type DeckFunc = {
|
||||
title: string;
|
||||
handler: (payload: MouseEvent) => void;
|
||||
icon?: string;
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
|
||||
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
|
||||
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
provide('shouldHeaderThin', true);
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
column: Column;
|
||||
isStacked?: boolean;
|
||||
func?: DeckFunc | null;
|
||||
naked?: boolean;
|
||||
indicated?: boolean;
|
||||
}>(), {
|
||||
isStacked: false,
|
||||
func: null,
|
||||
naked: false,
|
||||
indicated: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'change-active-state', v: boolean): void;
|
||||
}>();
|
||||
|
||||
let body = $ref<HTMLDivElement>();
|
||||
|
||||
let dragging = $ref(false);
|
||||
watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
|
||||
|
||||
let draghover = $ref(false);
|
||||
let dropready = $ref(false);
|
||||
|
||||
const isMainColumn = $computed(() => props.column.type === 'main');
|
||||
const active = $computed(() => props.column.active !== false);
|
||||
watch($$(active), v => emit('change-active-state', v));
|
||||
|
||||
const keymap = $computed(() => ({
|
||||
'shift+up': () => emit('parent-focus', 'up'),
|
||||
'shift+down': () => emit('parent-focus', 'down'),
|
||||
'shift+left': () => emit('parent-focus', 'left'),
|
||||
'shift+right': () => emit('parent-focus', 'right'),
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
os.deckGlobalEvents.on('column.dragStart', onOtherDragStart);
|
||||
os.deckGlobalEvents.on('column.dragEnd', onOtherDragEnd);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
os.deckGlobalEvents.off('column.dragStart', onOtherDragStart);
|
||||
os.deckGlobalEvents.off('column.dragEnd', onOtherDragEnd);
|
||||
});
|
||||
|
||||
function onOtherDragStart() {
|
||||
dropready = true;
|
||||
}
|
||||
|
||||
function onOtherDragEnd() {
|
||||
dropready = false;
|
||||
}
|
||||
|
||||
function toggleActive() {
|
||||
if (!props.isStacked) return;
|
||||
updateColumn(props.column.id, {
|
||||
active: !props.column.active,
|
||||
});
|
||||
}
|
||||
|
||||
function getMenu() {
|
||||
const items = [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
action: async () => {
|
||||
const { canceled, result } = await os.form(props.column.name, {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: i18n.ts.name,
|
||||
default: props.column.name,
|
||||
},
|
||||
width: {
|
||||
type: 'number',
|
||||
label: i18n.ts.width,
|
||||
default: props.column.width,
|
||||
},
|
||||
flexible: {
|
||||
type: 'boolean',
|
||||
label: i18n.ts.flexible,
|
||||
default: props.column.flexible,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
updateColumn(props.column.id, result);
|
||||
},
|
||||
}, null, {
|
||||
icon: 'fas fa-arrow-left',
|
||||
text: i18n.ts._deck.swapLeft,
|
||||
action: () => {
|
||||
swapLeftColumn(props.column.id);
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-arrow-right',
|
||||
text: i18n.ts._deck.swapRight,
|
||||
action: () => {
|
||||
swapRightColumn(props.column.id);
|
||||
},
|
||||
}, props.isStacked ? {
|
||||
icon: 'fas fa-arrow-up',
|
||||
text: i18n.ts._deck.swapUp,
|
||||
action: () => {
|
||||
swapUpColumn(props.column.id);
|
||||
},
|
||||
} : undefined, props.isStacked ? {
|
||||
icon: 'fas fa-arrow-down',
|
||||
text: i18n.ts._deck.swapDown,
|
||||
action: () => {
|
||||
swapDownColumn(props.column.id);
|
||||
},
|
||||
} : undefined, null, {
|
||||
icon: 'fas fa-window-restore',
|
||||
text: i18n.ts._deck.stackLeft,
|
||||
action: () => {
|
||||
stackLeftColumn(props.column.id);
|
||||
},
|
||||
}, props.isStacked ? {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts._deck.popRight,
|
||||
action: () => {
|
||||
popRightColumn(props.column.id);
|
||||
},
|
||||
} : undefined, null, {
|
||||
icon: 'fas fa-trash-alt',
|
||||
text: i18n.ts.remove,
|
||||
danger: true,
|
||||
action: () => {
|
||||
removeColumn(props.column.id);
|
||||
},
|
||||
}];
|
||||
return items;
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(getMenu(), ev);
|
||||
}
|
||||
|
||||
function goTop() {
|
||||
body.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
function onDragstart(ev) {
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
|
||||
|
||||
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
|
||||
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
|
||||
window.setTimeout(() => {
|
||||
dragging = true;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function onDragend(ev) {
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
function onDragover(ev) {
|
||||
// 自分自身がドラッグされている場合
|
||||
if (dragging) {
|
||||
// 自分自身にはドロップさせない
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
} else {
|
||||
const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_;
|
||||
|
||||
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||
|
||||
if (isDeckColumn) draghover = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onDragleave() {
|
||||
draghover = false;
|
||||
}
|
||||
|
||||
function onDrop(ev) {
|
||||
draghover = false;
|
||||
os.deckGlobalEvents.emit('column.dragEnd');
|
||||
|
||||
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
|
||||
if (id != null && id !== '') {
|
||||
swapColumn(props.column.id, id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dnpfarvg {
|
||||
--root-margin: 10px;
|
||||
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
contain: content;
|
||||
box-shadow: 0 0 8px 0 var(--shadow);
|
||||
|
||||
&.draghover {
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--focus);
|
||||
}
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
&.dropready {
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
flex-basis: var(--deckColumnHeaderHeight);
|
||||
min-height: var(--deckColumnHeaderHeight);
|
||||
|
||||
> header.indicated {
|
||||
box-shadow: 4px 0px var(--accent) inset;
|
||||
}
|
||||
}
|
||||
|
||||
&.naked {
|
||||
background: var(--bg) !important;
|
||||
-webkit-backdrop-filter: var(--blur, blur(10px));
|
||||
backdrop-filter: var(--blur, blur(10px));
|
||||
|
||||
> header {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
> button {
|
||||
color: var(--fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.paged {
|
||||
background: var(--bg) !important;
|
||||
}
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
line-height: var(--deckColumnHeaderHeight);
|
||||
height: var(--deckColumnHeaderHeight);
|
||||
padding: 0 16px;
|
||||
font-size: 0.9em;
|
||||
color: var(--panelHeaderFg);
|
||||
background: var(--panelHeaderBg);
|
||||
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||
cursor: pointer;
|
||||
|
||||
&, * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.indicated {
|
||||
box-shadow: 0 3px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
> .header {
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> span:only-of-type {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .toggleActive,
|
||||
> .action > ::v-deep(*),
|
||||
> .menu {
|
||||
z-index: 1;
|
||||
width: var(--deckColumnHeaderHeight);
|
||||
line-height: var(--deckColumnHeaderHeight);
|
||||
font-size: 16px;
|
||||
color: var(--faceTextButton);
|
||||
|
||||
&:hover {
|
||||
color: var(--faceTextButtonHover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--faceTextButtonActive);
|
||||
}
|
||||
}
|
||||
|
||||
> .toggleActive, > .action {
|
||||
margin-left: -16px;
|
||||
}
|
||||
|
||||
> .action {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> .action:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .menu {
|
||||
margin-left: auto;
|
||||
margin-right: -16px;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
height: calc(100% - var(--deckColumnHeaderHeight));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden; // Safari does not supports clip
|
||||
overflow-x: clip;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,304 +0,0 @@
|
|||
import { throttle } from 'throttle-debounce';
|
||||
import { markRaw } from 'vue';
|
||||
import { notificationTypes } from 'foundkey-js';
|
||||
import { Storage } from '../../pizzax';
|
||||
import { i18n } from '@/i18n';
|
||||
import { api } from '@/os';
|
||||
|
||||
type ColumnWidget = {
|
||||
name: string;
|
||||
id: string;
|
||||
data: Record<string, any>;
|
||||
};
|
||||
|
||||
export type Column = {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string | null;
|
||||
width: number;
|
||||
widgets?: ColumnWidget[];
|
||||
active?: boolean;
|
||||
flexible?: boolean;
|
||||
antennaId?: string;
|
||||
listId?: string;
|
||||
includingTypes?: typeof notificationTypes[number][];
|
||||
tl?: 'home' | 'local' | 'social' | 'global';
|
||||
};
|
||||
|
||||
function copy<T>(x: T): T {
|
||||
return JSON.parse(JSON.stringify(x));
|
||||
}
|
||||
|
||||
export const deckStore = markRaw(new Storage('deck', {
|
||||
profile: {
|
||||
where: 'deviceAccount',
|
||||
default: 'default',
|
||||
},
|
||||
columns: {
|
||||
where: 'deviceAccount',
|
||||
default: [] as Column[],
|
||||
},
|
||||
layout: {
|
||||
where: 'deviceAccount',
|
||||
default: [] as Column['id'][][],
|
||||
},
|
||||
columnAlign: {
|
||||
where: 'deviceAccount',
|
||||
default: 'left' as 'left' | 'right' | 'center',
|
||||
},
|
||||
alwaysShowMainColumn: {
|
||||
where: 'deviceAccount',
|
||||
default: true,
|
||||
},
|
||||
navWindow: {
|
||||
where: 'deviceAccount',
|
||||
default: true,
|
||||
},
|
||||
columnMargin: {
|
||||
where: 'deviceAccount',
|
||||
default: 16,
|
||||
},
|
||||
columnHeaderHeight: {
|
||||
where: 'deviceAccount',
|
||||
default: 42,
|
||||
},
|
||||
}));
|
||||
|
||||
export const loadDeck = async () => {
|
||||
let deck;
|
||||
|
||||
try {
|
||||
deck = await api('i/registry/get', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: deckStore.state.profile,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'NO_SUCH_KEY') {
|
||||
// 後方互換性のため
|
||||
if (deckStore.state.profile === 'default') {
|
||||
saveDeck();
|
||||
return;
|
||||
}
|
||||
|
||||
deckStore.set('columns', [{
|
||||
id: 'a',
|
||||
type: 'main',
|
||||
name: i18n.ts._deck._columns.main,
|
||||
width: 350,
|
||||
}, {
|
||||
id: 'b',
|
||||
type: 'notifications',
|
||||
name: i18n.ts._deck._columns.notifications,
|
||||
width: 330,
|
||||
}]);
|
||||
deckStore.set('layout', [['a'], ['b']]);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
deckStore.set('columns', deck.columns);
|
||||
deckStore.set('layout', deck.layout);
|
||||
};
|
||||
|
||||
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
|
||||
export const saveDeck = throttle(1000, () => {
|
||||
api('i/registry/set', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: deckStore.state.profile,
|
||||
value: {
|
||||
columns: deckStore.reactiveState.columns.value,
|
||||
layout: deckStore.reactiveState.layout.value,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export function addColumn(column: Column) {
|
||||
if (column.name === undefined) column.name = null;
|
||||
deckStore.push('columns', column);
|
||||
deckStore.push('layout', [column.id]);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function removeColumn(id: Column['id']) {
|
||||
deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
|
||||
deckStore.set('layout', deckStore.state.layout
|
||||
.map(ids => ids.filter(_id => _id !== id))
|
||||
.filter(ids => ids.length > 0));
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||
const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1);
|
||||
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
|
||||
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
||||
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
|
||||
const layout = copy(deckStore.state.layout);
|
||||
layout[aX][aY] = b;
|
||||
layout[bX][bY] = a;
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapLeftColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
deckStore.state.layout.some((ids, i) => {
|
||||
if (ids.includes(id)) {
|
||||
const left = deckStore.state.layout[i - 1];
|
||||
if (left) {
|
||||
layout[i - 1] = deckStore.state.layout[i];
|
||||
layout[i] = left;
|
||||
deckStore.set('layout', layout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapRightColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
deckStore.state.layout.some((ids, i) => {
|
||||
if (ids.includes(id)) {
|
||||
const right = deckStore.state.layout[i + 1];
|
||||
if (right) {
|
||||
layout[i + 1] = deckStore.state.layout[i];
|
||||
layout[i] = right;
|
||||
deckStore.set('layout', layout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapUpColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
const ids = copy(deckStore.state.layout[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const up = ids[i - 1];
|
||||
if (up) {
|
||||
ids[i - 1] = id;
|
||||
ids[i] = up;
|
||||
|
||||
layout[idsIndex] = ids;
|
||||
deckStore.set('layout', layout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapDownColumn(id: Column['id']) {
|
||||
const layout = copy(deckStore.state.layout);
|
||||
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
const ids = copy(deckStore.state.layout[idsIndex]);
|
||||
ids.some((x, i) => {
|
||||
if (x === id) {
|
||||
const down = ids[i + 1];
|
||||
if (down) {
|
||||
ids[i + 1] = id;
|
||||
ids[i] = down;
|
||||
|
||||
layout[idsIndex] = ids;
|
||||
deckStore.set('layout', layout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function stackLeftColumn(id: Column['id']) {
|
||||
let layout = copy(deckStore.state.layout);
|
||||
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||
layout[i - 1].push(id);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function popRightColumn(id: Column['id']) {
|
||||
let layout = copy(deckStore.state.layout);
|
||||
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||
const affected = layout[i];
|
||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||
layout.splice(i + 1, 0, [id]);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
deckStore.set('layout', layout);
|
||||
|
||||
const columns = copy(deckStore.state.columns);
|
||||
for (const column of columns) {
|
||||
if (affected.includes(column.id)) {
|
||||
column.active = true;
|
||||
}
|
||||
}
|
||||
deckStore.set('columns', columns);
|
||||
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets.unshift(widget);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = widgets;
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||
...w,
|
||||
data: widgetData,
|
||||
} : w);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||
const columns = copy(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const currentColumn = copy(deckStore.state.columns[columnIndex]);
|
||||
if (currentColumn == null) return;
|
||||
for (const [k, v] of Object.entries(column)) {
|
||||
currentColumn[k] = v;
|
||||
}
|
||||
columns[columnIndex] = currentColumn;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<XNotes :pagination="pagination"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XColumn from './column.vue';
|
||||
import { Column } from './deck-store';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
|
||||
defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
visibility: 'specified',
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,65 +0,0 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn, Column } from './deck-store';
|
||||
import XTimeline from '@/components/timeline.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||
|
||||
if (props.column.listId == null) {
|
||||
setList();
|
||||
}
|
||||
|
||||
async function setList() {
|
||||
const lists = await os.api('users/lists/list');
|
||||
const { canceled, result: list } = await os.select({
|
||||
title: i18n.ts.selectList,
|
||||
items: lists.map(x => ({
|
||||
value: x, text: x.name,
|
||||
})),
|
||||
default: props.column.listId,
|
||||
});
|
||||
if (canceled) return;
|
||||
updateColumn(props.column.id, {
|
||||
listId: list.id,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
function focus() {
|
||||
timeline.focus();
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
watch: {
|
||||
mediaOnly() {
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,68 +0,0 @@
|
|||
<template>
|
||||
<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<template v-if="pageMetadata?.value">
|
||||
<i :class="pageMetadata?.value.icon"></i>
|
||||
{{ pageMetadata?.value.title }}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<RouterView @contextmenu.stop="onContextmenu"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ComputedRef, provide } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { deckStore, Column } from '@/ui/deck/deck-store';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { mainRouter } from '@/router';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
|
||||
|
||||
defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||
|
||||
provide('router', mainRouter);
|
||||
provideMetadataReceiver((info) => {
|
||||
pageMetadata = info;
|
||||
});
|
||||
|
||||
/*
|
||||
function back() {
|
||||
history.back();
|
||||
}
|
||||
*/
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
if (!ev.target) return;
|
||||
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target as HTMLElement)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return;
|
||||
if (window.getSelection()?.toString() !== '') return;
|
||||
const path = mainRouter.currentRoute.value.path;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(path);
|
||||
},
|
||||
}], ev);
|
||||
}
|
||||
</script>
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<XNotes :pagination="pagination"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XColumn from './column.vue';
|
||||
import { Column } from './deck-store';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
|
||||
defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
};
|
||||
</script>
|
|
@ -1,37 +0,0 @@
|
|||
<template>
|
||||
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<XNotifications :include-types="column.includingTypes"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn , Column } from './deck-store';
|
||||
import XNotifications from '@/components/notifications.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
function func() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), {
|
||||
includingTypes: props.column.includingTypes,
|
||||
}, {
|
||||
done: async (res) => {
|
||||
const { includingTypes } = res;
|
||||
updateColumn(props.column.id, {
|
||||
includingTypes,
|
||||
});
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
</script>
|
|
@ -1,129 +0,0 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header>
|
||||
<i v-if="column.tl === 'home'" class="fas fa-home"></i>
|
||||
<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
|
||||
<i v-else-if="column.tl === 'social'" class="fas fa-share-alt"></i>
|
||||
<i v-else-if="column.tl === 'global'" class="fas fa-globe"></i>
|
||||
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<div v-if="disabled" class="iwaalbte">
|
||||
<p>
|
||||
<i class="fas fa-minus-circle"></i>
|
||||
{{ $t('disabled-timeline.title') }}
|
||||
</p>
|
||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||
</div>
|
||||
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { removeColumn, updateColumn, Column } from './deck-store';
|
||||
import XTimeline from '@/components/timeline.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let disabled = $ref(false);
|
||||
let indicated = $ref(false);
|
||||
let columnActive = $ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.column.tl == null) {
|
||||
setType();
|
||||
} else if ($i) {
|
||||
disabled = !$i.isModerator && !$i.isAdmin && (
|
||||
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
|
||||
instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
|
||||
}
|
||||
});
|
||||
|
||||
async function setType() {
|
||||
const { canceled, result: src } = await os.select({
|
||||
title: i18n.ts.timeline,
|
||||
items: [{
|
||||
value: 'home' as const, text: i18n.ts._timelines.home,
|
||||
}, {
|
||||
value: 'local' as const, text: i18n.ts._timelines.local,
|
||||
}, {
|
||||
value: 'social' as const, text: i18n.ts._timelines.social,
|
||||
}, {
|
||||
value: 'global' as const, text: i18n.ts._timelines.global,
|
||||
}],
|
||||
});
|
||||
if (canceled) {
|
||||
if (props.column.tl == null) {
|
||||
removeColumn(props.column.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
updateColumn(props.column.id, {
|
||||
tl: src,
|
||||
});
|
||||
}
|
||||
|
||||
function queueUpdated(q) {
|
||||
if (columnActive) {
|
||||
indicated = q !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
function onNote() {
|
||||
if (!columnActive) {
|
||||
indicated = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeActiveState(state) {
|
||||
columnActive = state;
|
||||
|
||||
if (columnActive) {
|
||||
indicated = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export default defineComponent({
|
||||
watch: {
|
||||
mediaOnly() {
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iwaalbte {
|
||||
text-align: center;
|
||||
|
||||
> p {
|
||||
margin: 16px;
|
||||
|
||||
&.desc {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,55 +0,0 @@
|
|||
<template>
|
||||
<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<div class="wtdtxvec">
|
||||
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||
</div>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XColumn from './column.vue';
|
||||
import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
||||
import XWidgets from '@/components/widgets.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let edit = $ref(false);
|
||||
|
||||
function addWidget(widget) {
|
||||
addColumnWidget(props.column.id, widget);
|
||||
}
|
||||
|
||||
function removeWidget(widget) {
|
||||
removeColumnWidget(props.column.id, widget);
|
||||
}
|
||||
|
||||
function updateWidget({ id, data }) {
|
||||
updateColumnWidget(props.column.id, id, data);
|
||||
}
|
||||
|
||||
function updateWidgets(widgets) {
|
||||
setColumnWidgets(props.column.id, widgets);
|
||||
}
|
||||
|
||||
function func() {
|
||||
edit = !edit;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wtdtxvec {
|
||||
--margin: 8px;
|
||||
--panelBorder: none;
|
||||
|
||||
padding: 0 var(--margin);
|
||||
}
|
||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||
<div class="dkgtipfy" :class="{ wallpaper }">
|
||||
<XSidebar v-if="!isMobile" class="sidebar"/>
|
||||
|
||||
<div class="contents" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
|
||||
<div class="contents" :style="{ background: pageMetadata?.value?.bg }">
|
||||
<main>
|
||||
<div class="content">
|
||||
<RouterView/>
|
||||
|
@ -131,29 +131,6 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
const onContextmenu = (ev) => {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
};
|
||||
if (isLink(ev.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection()?.toString() !== '') return;
|
||||
const path = mainRouter.getCurrentPath();
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'fas fa-window-maximize',
|
||||
text: i18n.ts.openInWindow,
|
||||
action: () => {
|
||||
os.pageWindow(path);
|
||||
},
|
||||
}], ev);
|
||||
};
|
||||
|
||||
const attachSticky = (el) => {
|
||||
const sticky = new StickySidebar(widgetsEl);
|
||||
window.addEventListener('scroll', () => {
|
||||
|
|
|
@ -47,10 +47,8 @@ export default defineConfig(({ command, mode }) => {
|
|||
|
||||
build: {
|
||||
target: [
|
||||
'chrome100',
|
||||
'firefox100',
|
||||
'firefox102',
|
||||
'safari15',
|
||||
'es2017',
|
||||
],
|
||||
manifest: 'manifest.json',
|
||||
rollupOptions: {
|
||||
|
|
Loading…
Reference in a new issue