Deck (#6504)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
This commit is contained in:
		
							parent
							
								
									5b28d7bf90
								
							
						
					
					
						commit
						cf3fc97202
					
				
					 56 changed files with 2695 additions and 907 deletions
				
			
		|  | @ -519,6 +519,8 @@ fixedWidgetsPosition: "ウィジェットの位置を固定する" | |||
| enablePlayer: "プレイヤーを開く" | ||||
| disablePlayer: "プレイヤーを閉じる" | ||||
| expandTweet: "ツイートを展開する" | ||||
| deck: "デッキ" | ||||
| undeck: "デッキ解除" | ||||
| 
 | ||||
| _theme: | ||||
|   explore: "テーマを探す" | ||||
|  | @ -651,6 +653,7 @@ _widgets: | |||
|   rss: "RSSリーダー" | ||||
|   activity: "アクティビティ" | ||||
|   photos: "フォト" | ||||
|   digitalClock: "デジタル時計" | ||||
| 
 | ||||
| _cw: | ||||
|   hide: "隠す" | ||||
|  | @ -1129,3 +1132,15 @@ _notification: | |||
|   yourFollowRequestAccepted: "フォローリクエストが承認されました" | ||||
|   youWereInvitedToGroup: "グループに招待されました" | ||||
| 
 | ||||
| _deck: | ||||
|   alwaysShowMainColumn: "常にメインカラムを表示" | ||||
|   columnAlign: "カラムの寄せ" | ||||
| 
 | ||||
|   _columns: | ||||
|     widgets: "ウィジェット" | ||||
|     notifications: "通知" | ||||
|     tl: "タイムライン" | ||||
|     antenna: "アンテナ" | ||||
|     list: "リスト" | ||||
|     mentions: "あなた宛て" | ||||
|     direct: "ダイレクト" | ||||
|  |  | |||
|  | @ -29,47 +29,7 @@ | |||
| 		</div> | ||||
| 	</header> | ||||
| 
 | ||||
| 	<transition name="nav-back"> | ||||
| 		<div class="nav-back" | ||||
| 			v-if="showNav" | ||||
| 			@click="showNav = false" | ||||
| 			@touchstart="showNav = false" | ||||
| 		></div> | ||||
| 	</transition> | ||||
| 
 | ||||
| 	<transition name="nav"> | ||||
| 		<nav class="nav" ref="nav" v-show="showNav"> | ||||
| 			<div> | ||||
| 				<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn"> | ||||
| 					<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/> | ||||
| 				</button> | ||||
| 				<button class="item _button index active" @click="top()" v-if="$route.name === 'index'"> | ||||
| 					<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span> | ||||
| 				</button> | ||||
| 				<router-link class="item index" active-class="active" to="/" exact v-else> | ||||
| 					<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span> | ||||
| 				</router-link> | ||||
| 				<template v-for="item in menu"> | ||||
| 					<div v-if="item === '-'" class="divider"></div> | ||||
| 					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'router-link' : 'button'" class="item _button" :class="item" active-class="active" @click="() => { if (menuDef[item].action) menuDef[item].action() }" :to="menuDef[item].to"> | ||||
| 						<fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $t(menuDef[item].title) }}</span> | ||||
| 						<i v-if="menuDef[item].indicated"><fa :icon="faCircle"/></i> | ||||
| 					</component> | ||||
| 				</template> | ||||
| 				<div class="divider"></div> | ||||
| 				<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu"> | ||||
| 					<fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span> | ||||
| 				</button> | ||||
| 				<button class="item _button" @click="more"> | ||||
| 					<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span> | ||||
| 					<i v-if="otherNavItemIndicated"><fa :icon="faCircle"/></i> | ||||
| 				</button> | ||||
| 				<router-link class="item" active-class="active" to="/preferences"> | ||||
| 					<fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span> | ||||
| 				</router-link> | ||||
| 			</div> | ||||
| 		</nav> | ||||
| 	</transition> | ||||
| 	<x-sidebar ref="nav"/> | ||||
| 
 | ||||
| 	<div class="contents" ref="contents" :class="{ wallpaper }"> | ||||
| 		<main ref="main"> | ||||
|  | @ -103,20 +63,20 @@ | |||
| 								<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> | ||||
| 							</header> | ||||
| 							<div @click="widgetFunc(widget.id)"> | ||||
| 								<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/> | ||||
| 								<component class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</x-draggable> | ||||
| 				</div> | ||||
| 				<div class="container" v-else> | ||||
| 					<component class="_widget" v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/> | ||||
| 					<component v-for="widget in widgets[place]" class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="buttons"> | ||||
| 		<button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button> | ||||
| 		<button class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button> | ||||
| 		<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button> | ||||
| 		<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button> | ||||
| 		<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="$router.push('/my/notifications')"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button> | ||||
|  | @ -135,14 +95,17 @@ import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, | |||
| import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { ResizeObserver } from '@juggle/resize-observer'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import { host, instanceName } from './config'; | ||||
| import { host } from './config'; | ||||
| import { search } from './scripts/search'; | ||||
| import { StickySidebar } from './scripts/sticky-sidebar'; | ||||
| import { widgets } from './widgets'; | ||||
| import XSidebar from './components/sidebar.vue'; | ||||
| 
 | ||||
| const DESKTOP_THRESHOLD = 1100; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSidebar, | ||||
| 		XClock: () => import('./components/header-clock.vue').then(m => m.default), | ||||
| 		MkButton: () => import('./components/ui/button.vue').then(m => m.default), | ||||
| 		XDraggable: () => import('vuedraggable'), | ||||
|  | @ -152,19 +115,14 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			host: host, | ||||
| 			pageKey: 0, | ||||
| 			showNav: false, | ||||
| 			searching: false, | ||||
| 			accounts: [], | ||||
| 			lists: [], | ||||
| 			connection: null, | ||||
| 			searchQuery: '', | ||||
| 			searchWait: false, | ||||
| 			widgetsEditMode: false, | ||||
| 			menuDef: this.$store.getters.nav({ | ||||
| 				search: this.search | ||||
| 			}), | ||||
| 			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, | ||||
| 			canBack: false, | ||||
| 			menuDef: this.$store.getters.nav({}), | ||||
| 			wallpaper: localStorage.getItem('wallpaper') != null, | ||||
| 			faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram | ||||
| 		}; | ||||
|  | @ -210,30 +168,19 @@ export default Vue.extend({ | |||
| 			return this.$store.state.deviceUser.menu; | ||||
| 		}, | ||||
| 
 | ||||
| 		otherNavItemIndicated(): boolean { | ||||
| 			if (!this.$store.getters.isSignedIn) return false; | ||||
| 			for (const def in this.menuDef) { | ||||
| 				if (this.menu.includes(def)) continue; | ||||
| 				if (this.menuDef[def].indicated) return true; | ||||
| 			} | ||||
| 			return false; | ||||
| 		}, | ||||
| 
 | ||||
| 		navIndicated(): boolean { | ||||
| 			if (!this.$store.getters.isSignedIn) return false; | ||||
| 			for (const def in this.menuDef) { | ||||
| 				if (def === 'timeline') continue; | ||||
| 				if (def === 'notifications') continue; | ||||
| 				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから | ||||
| 				if (this.menuDef[def].indicated) return true; | ||||
| 			} | ||||
| 			return false; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch:{ | ||||
| 	watch: { | ||||
| 		$route(to, from) { | ||||
| 			this.pageKey++; | ||||
| 			this.showNav = false; | ||||
| 			this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -245,6 +192,8 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		document.documentElement.style.overflowY = 'scroll'; | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('notification', this.onNotification); | ||||
|  | @ -266,7 +215,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	mounted() { | ||||
| 		const adjustTitlePosition = () => { | ||||
| 			const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth; | ||||
| 			const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.$el.offsetWidth; | ||||
| 			if (left >= 0) { | ||||
| 				this.$refs.title.style.left = left + 'px'; | ||||
| 			} | ||||
|  | @ -293,6 +242,10 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		showNav() { | ||||
| 			this.$refs.nav.show(); | ||||
| 		}, | ||||
| 
 | ||||
| 		attachSticky() { | ||||
| 			if (!this.isDesktop) return; | ||||
| 			if (this.$store.state.device.fixedWidgetsPosition) return; | ||||
|  | @ -351,180 +304,6 @@ export default Vue.extend({ | |||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		async openAccountMenu(ev) { | ||||
| 			const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id); | ||||
| 
 | ||||
| 			const accountItems = accounts.map(account => ({ | ||||
| 				type: 'user', | ||||
| 				user: account, | ||||
| 				action: () => { this.switchAccount(account); } | ||||
| 			})); | ||||
| 
 | ||||
| 			this.$root.menu({ | ||||
| 				items: [...[{ | ||||
| 					type: 'link', | ||||
| 					text: this.$t('profile'), | ||||
| 					to: `/@${ this.$store.state.i.username }`, | ||||
| 					avatar: this.$store.state.i, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('accountSettings'), | ||||
| 					to: '/my/settings', | ||||
| 					icon: faCog, | ||||
| 				}, null, ...accountItems, { | ||||
| 					icon: faPlus, | ||||
| 					text: this.$t('addAcount'), | ||||
| 					action: () => { | ||||
| 						this.$root.menu({ | ||||
| 							items: [{ | ||||
| 								text: this.$t('existingAcount'), | ||||
| 								action: () => { this.addAcount(); }, | ||||
| 							}, { | ||||
| 								text: this.$t('createAccount'), | ||||
| 								action: () => { this.createAccount(); }, | ||||
| 							}], | ||||
| 							align: 'left', | ||||
| 							fixed: true, | ||||
| 							width: 240, | ||||
| 							source: ev.currentTarget || ev.target, | ||||
| 						}); | ||||
| 					}, | ||||
| 				}]], | ||||
| 				align: 'left', | ||||
| 				fixed: true, | ||||
| 				width: 240, | ||||
| 				source: ev.currentTarget || ev.target, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		oepnInstanceMenu(ev) { | ||||
| 			this.$root.menu({ | ||||
| 				items: [{ | ||||
| 					type: 'link', | ||||
| 					text: this.$t('dashboard'), | ||||
| 					to: '/instance', | ||||
| 					icon: faTachometerAlt, | ||||
| 				}, null, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('settings'), | ||||
| 					to: '/instance/settings', | ||||
| 					icon: faCog, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('customEmojis'), | ||||
| 					to: '/instance/emojis', | ||||
| 					icon: faLaugh, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('users'), | ||||
| 					to: '/instance/users', | ||||
| 					icon: faUsers, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('files'), | ||||
| 					to: '/instance/files', | ||||
| 					icon: faCloud, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('jobQueue'), | ||||
| 					to: '/instance/queue', | ||||
| 					icon: faExchangeAlt, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('federation'), | ||||
| 					to: '/instance/federation', | ||||
| 					icon: faGlobe, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('relays'), | ||||
| 					to: '/instance/relays', | ||||
| 					icon: faProjectDiagram, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('announcements'), | ||||
| 					to: '/instance/announcements', | ||||
| 					icon: faBroadcastTower, | ||||
| 				}], | ||||
| 				align: 'left', | ||||
| 				fixed: true, | ||||
| 				width: 200, | ||||
| 				source: ev.currentTarget || ev.target, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		more(ev) { | ||||
| 			const items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({ | ||||
| 				type: def.to ? 'link' : 'button', | ||||
| 				text: this.$t(def.title), | ||||
| 				icon: def.icon, | ||||
| 				to: def.to, | ||||
| 				action: def.action, | ||||
| 				indicate: def.indicated, | ||||
| 			})); | ||||
| 			this.$root.menu({ | ||||
| 				items: [...items, null, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('help'), | ||||
| 					to: '/docs', | ||||
| 					icon: faQuestionCircle, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('aboutX', { x: instanceName || host }), | ||||
| 					to: '/about', | ||||
| 					icon: faInfoCircle, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('aboutMisskey'), | ||||
| 					to: '/about-misskey', | ||||
| 					icon: faInfoCircle, | ||||
| 				}], | ||||
| 				align: 'left', | ||||
| 				fixed: true, | ||||
| 				width: 200, | ||||
| 				source: ev.currentTarget || ev.target, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async addAcount() { | ||||
| 			this.$root.new(await import('./components/signin-dialog.vue').then(m => m.default)).$once('login', res => { | ||||
| 				this.$store.dispatch('addAcount', res); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					iconOnly: true, autoClose: true | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async createAccount() { | ||||
| 			this.$root.new(await import('./components/signup-dialog.vue').then(m => m.default)).$once('signup', res => { | ||||
| 				this.$store.dispatch('addAcount', res); | ||||
| 				this.switchAccountWithToken(res.i); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async switchAccount(account: any) { | ||||
| 			const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token; | ||||
| 			this.switchAccountWithToken(token); | ||||
| 		}, | ||||
| 
 | ||||
| 		switchAccountWithToken(token: string) { | ||||
| 			this.$root.dialog({ | ||||
| 				type: 'waiting', | ||||
| 				iconOnly: true | ||||
| 			}); | ||||
| 
 | ||||
| 			this.$root.api('i', {}, token).then((i: any) => { | ||||
| 				this.$store.dispatch('switchAccount', { | ||||
| 					...i, | ||||
| 					token: token | ||||
| 				}).then(() => { | ||||
| 					this.$nextTick(() => { | ||||
| 						location.reload(); | ||||
| 					}); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async onNotification(notification) { | ||||
| 			if (document.visibilityState === 'visible') { | ||||
| 				this.$root.stream.send('readNotification', { | ||||
|  | @ -540,8 +319,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		widgetFunc(id) { | ||||
| 			const w = this.$refs[id][0]; | ||||
| 			if (w.func) w.func(); | ||||
| 			this.$refs[id][0].setting(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onWidgetSort() { | ||||
|  | @ -549,18 +327,6 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		async addWidget(place) { | ||||
| 			const widgets = [ | ||||
| 				'memo', | ||||
| 				'notifications', | ||||
| 				'timeline', | ||||
| 				'calendar', | ||||
| 				'rss', | ||||
| 				'trends', | ||||
| 				'clock', | ||||
| 				'activity', | ||||
| 				'photos', | ||||
| 			]; | ||||
| 
 | ||||
| 			const { canceled, result: widget } = await this.$root.dialog({ | ||||
| 				type: null, | ||||
| 				title: this.$t('chooseWidget'), | ||||
|  | @ -594,36 +360,14 @@ export default Vue.extend({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .nav-enter-active, | ||||
| .nav-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); | ||||
| } | ||||
| .nav-enter, | ||||
| .nav-leave-active { | ||||
| 	opacity: 0; | ||||
| 	transform: translateX(-240px); | ||||
| } | ||||
| 
 | ||||
| .nav-back-enter-active, | ||||
| .nav-back-leave-active { | ||||
| 	opacity: 1; | ||||
| 	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); | ||||
| } | ||||
| .nav-back-enter, | ||||
| .nav-back-leave-active { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .mk-app { | ||||
| 	$header-height: 60px; | ||||
| 	$nav-width: 250px; | ||||
| 	$nav-icon-only-width: 80px; | ||||
| 	$nav-width: 250px; // TODO: どこかに集約したい | ||||
| 	$nav-icon-only-width: 80px; // TODO: どこかに集約したい | ||||
| 	$main-width: 670px; | ||||
| 	$ui-font-size: 1em; | ||||
| 	$nav-icon-only-threshold: 1279px; | ||||
| 	$nav-hide-threshold: 650px; | ||||
| 	$ui-font-size: 1em; // TODO: どこかに集約したい | ||||
| 	$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい | ||||
| 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい | ||||
| 	$header-sub-hide-threshold: 1090px; | ||||
| 	$left-widgets-hide-threshold: 1600px; | ||||
| 	$right-widgets-hide-threshold: 1090px; | ||||
|  | @ -780,176 +524,6 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .nav-back { | ||||
| 		position: fixed; | ||||
| 		top: 0; | ||||
| 		left: 0; | ||||
| 		z-index: 1001; | ||||
| 		width: 100%; | ||||
| 		height: 100%; | ||||
| 		background: var(--modalBg); | ||||
| 	} | ||||
| 
 | ||||
| 	> .nav { | ||||
| 		$avatar-size: 32px; | ||||
| 		$avatar-margin: ($header-height - $avatar-size) / 2; | ||||
| 
 | ||||
| 		flex: 0 0 $nav-width; | ||||
| 		width: $nav-width; | ||||
| 		box-sizing: border-box; | ||||
| 
 | ||||
| 		@media (max-width: $nav-icon-only-threshold) { | ||||
| 			flex: 0 0 $nav-icon-only-width; | ||||
| 			width: $nav-icon-only-width; | ||||
| 		} | ||||
| 
 | ||||
| 		@media (max-width: $nav-hide-threshold) { | ||||
| 			position: fixed; | ||||
| 			top: 0; | ||||
| 			left: 0; | ||||
| 			z-index: 1001; | ||||
| 		} | ||||
| 
 | ||||
| 		@media (min-width: $nav-hide-threshold + 1px) { | ||||
| 			display: block !important; | ||||
| 		} | ||||
| 
 | ||||
| 		> div { | ||||
| 			position: fixed; | ||||
| 			top: 0; | ||||
| 			left: 0; | ||||
| 			z-index: 1001; | ||||
| 			width: $nav-width; | ||||
| 			height: 100vh; | ||||
| 			box-sizing: border-box; | ||||
| 			overflow: auto; | ||||
| 			background: var(--navBg); | ||||
| 			border-right: solid 1px var(--divider); | ||||
| 
 | ||||
| 			> .divider { | ||||
| 				margin: 16px 0; | ||||
| 				border-top: solid 1px var(--divider); | ||||
| 			} | ||||
| 
 | ||||
| 			@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { | ||||
| 				width: $nav-icon-only-width; | ||||
| 
 | ||||
| 				> .divider { | ||||
| 					margin: 8px auto; | ||||
| 					width: calc(100% - 32px); | ||||
| 				} | ||||
| 
 | ||||
| 				> .item { | ||||
| 					&:first-child { | ||||
| 						margin-bottom: 8px; | ||||
| 					} | ||||
| 
 | ||||
| 					&:last-child { | ||||
| 						margin-top: 8px; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .item { | ||||
| 				position: relative; | ||||
| 				display: block; | ||||
| 				padding-left: 32px; | ||||
| 				font-size: $ui-font-size; | ||||
| 				line-height: 3.2rem; | ||||
| 				text-overflow: ellipsis; | ||||
| 				overflow: hidden; | ||||
| 				white-space: nowrap; | ||||
| 				width: 100%; | ||||
| 				text-align: left; | ||||
| 				box-sizing: border-box; | ||||
| 				color: var(--navFg); | ||||
| 
 | ||||
| 				> [data-icon] { | ||||
| 					width: ($header-height - ($avatar-margin * 2)); | ||||
| 				} | ||||
| 
 | ||||
| 				> [data-icon], | ||||
| 				> .avatar { | ||||
| 					margin-right: $avatar-margin; | ||||
| 				} | ||||
| 
 | ||||
| 				> .avatar { | ||||
| 					width: $avatar-size; | ||||
| 					height: $avatar-size; | ||||
| 					vertical-align: middle; | ||||
| 				} | ||||
| 
 | ||||
| 				> i { | ||||
| 					position: absolute; | ||||
| 					top: 0; | ||||
| 					left: 20px; | ||||
| 					color: var(--navIndicator); | ||||
| 					font-size: 8px; | ||||
| 					animation: blink 1s infinite; | ||||
| 				} | ||||
| 
 | ||||
| 				&:hover { | ||||
| 					text-decoration: none; | ||||
| 					color: var(--navHoverFg); | ||||
| 				} | ||||
| 
 | ||||
| 				&.active { | ||||
| 					color: var(--navActive); | ||||
| 				} | ||||
| 
 | ||||
| 				&:first-child, &:last-child { | ||||
| 					position: sticky; | ||||
| 					z-index: 1; | ||||
| 					padding-top: 8px; | ||||
| 					padding-bottom: 8px; | ||||
| 					background: var(--X14); | ||||
| 					-webkit-backdrop-filter: blur(8px); | ||||
| 					backdrop-filter: blur(8px); | ||||
| 				} | ||||
| 
 | ||||
| 				&:first-child { | ||||
| 					top: 0; | ||||
| 					margin-bottom: 16px; | ||||
| 					border-bottom: solid 1px var(--divider); | ||||
| 				} | ||||
| 
 | ||||
| 				&:last-child { | ||||
| 					bottom: 0; | ||||
| 					margin-top: 16px; | ||||
| 					border-top: solid 1px var(--divider); | ||||
| 				} | ||||
| 
 | ||||
| 				@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { | ||||
| 					padding-left: 0; | ||||
| 					width: 100%; | ||||
| 					text-align: center; | ||||
| 					font-size: $ui-font-size * 1.2; | ||||
| 					line-height: 3.7rem; | ||||
| 
 | ||||
| 					> [data-icon], | ||||
| 					> .avatar { | ||||
| 						margin-right: 0; | ||||
| 					} | ||||
| 
 | ||||
| 					> i { | ||||
| 						left: 10px; | ||||
| 					} | ||||
| 
 | ||||
| 					> .text { | ||||
| 						display: none; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			@media (max-width: $nav-hide-threshold) { | ||||
| 				> .index, | ||||
| 				> .notifications { | ||||
| 					display: none; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .contents { | ||||
| 		display: flex; | ||||
| 		margin: 0 auto; | ||||
|  |  | |||
							
								
								
									
										80
									
								
								src/client/components/deck/antenna-column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/client/components/deck/antenna-column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | |||
| <template> | ||||
| <x-column :menu="menu" :column="column" :is-stacked="isStacked"> | ||||
| 	<template #header> | ||||
| 		<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons'; | ||||
| import XColumn from './column.vue'; | ||||
| import XTimeline from '../timeline.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XTimeline, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			menu: null, | ||||
| 			faSatellite | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		mediaOnly() { | ||||
| 			(this.$refs.timeline as any).reload(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.menu = [{ | ||||
| 			icon: faCog, | ||||
| 			text: this.$t('antenna'), | ||||
| 			action: async () => { | ||||
| 				const antennas = await this.$root.api('antennas/list'); | ||||
| 				this.$root.dialog({ | ||||
| 					title: this.$t('antenna'), | ||||
| 					type: null, | ||||
| 					select: { | ||||
| 						items: antennas.map(x => ({ | ||||
| 							value: x, text: x.name | ||||
| 						})) | ||||
| 					}, | ||||
| 					showCancelButton: true | ||||
| 				}).then(({ canceled, result: antenna }) => { | ||||
| 					if (canceled) return; | ||||
| 					this.column.antennaId = antenna.id; | ||||
| 					this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||
| 				}); | ||||
| 			} | ||||
| 		}]; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
							
								
								
									
										50
									
								
								src/client/components/deck/column-core.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/client/components/deck/column-core.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | |||
| <template> | ||||
| <!-- TODO: リファクタの余地がありそう --> | ||||
| <x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> --> | ||||
| <x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from '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'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XTlColumn, | ||||
| 		XAntennaColumn, | ||||
| 		XListColumn, | ||||
| 		XNotificationsColumn, | ||||
| 		XWidgetsColumn, | ||||
| 		XMentionsColumn, | ||||
| 		XDirectColumn | ||||
| 	}, | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		focus() { | ||||
| 			this.$children[0].focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										426
									
								
								src/client/components/deck/column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										426
									
								
								src/client/components/deck/column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,426 @@ | |||
| <template> | ||||
| <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> | ||||
| <section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }" | ||||
| 	@dragover.prevent.stop="onDragover" | ||||
| 	@dragleave="onDragleave" | ||||
| 	@drop.prevent.stop="onDrop" | ||||
| 	v-hotkey="keymap" | ||||
| 	:style="{ width: `${width}px` }" | ||||
| > | ||||
| 	<header :class="{ indicated }" | ||||
| 		draggable="true" | ||||
| 		@click="goTop" | ||||
| 		@dragstart="onDragstart" | ||||
| 		@dragend="onDragend" | ||||
| 		@contextmenu.prevent.stop="onContextmenu" | ||||
| 	> | ||||
| 		<button class="toggleActive _button" @click="toggleActive" v-if="isStacked"> | ||||
| 			<template v-if="active"><fa :icon="faAngleUp"/></template> | ||||
| 			<template v-else><fa :icon="faAngleDown"/></template> | ||||
| 		</button> | ||||
| 		<div class="action"> | ||||
| 			<slot name="action"></slot> | ||||
| 		</div> | ||||
| 		<span class="header"><slot name="header"></slot></span> | ||||
| 		<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button> | ||||
| 		<button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button> | ||||
| 	</header> | ||||
| 	<div ref="body" v-show="active"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| </section> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: false, | ||||
| 			default: null | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		menu: { | ||||
| 			type: Array, | ||||
| 			required: false, | ||||
| 			default: null | ||||
| 		}, | ||||
| 		naked: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		indicated: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			active: true, | ||||
| 			dragging: false, | ||||
| 			draghover: false, | ||||
| 			dropready: false, | ||||
| 			faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		isMainColumn(): boolean { | ||||
| 			return this.column == null; | ||||
| 		}, | ||||
| 
 | ||||
| 		width(): number { | ||||
| 			return this.isMainColumn ? 350 : this.column.width; | ||||
| 		}, | ||||
| 
 | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				'shift+up': () => this.$parent.$emit('parentFocus', 'up'), | ||||
| 				'shift+down': () => this.$parent.$emit('parentFocus', 'down'), | ||||
| 				'shift+left': () => this.$parent.$emit('parentFocus', 'left'), | ||||
| 				'shift+right': () => this.$parent.$emit('parentFocus', 'right'), | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		active(v) { | ||||
| 			this.$emit('change-active-state', v); | ||||
| 		}, | ||||
| 
 | ||||
| 		dragging(v) { | ||||
| 			this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd'); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		if (!this.isMainColumn) { | ||||
| 			this.$root.$on('deck.column.dragStart', this.onOtherDragStart); | ||||
| 			this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
| 		if (!this.isMainColumn) { | ||||
| 			this.$root.$off('deck.column.dragStart', this.onOtherDragStart); | ||||
| 			this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		onOtherDragStart() { | ||||
| 			this.dropready = true; | ||||
| 		}, | ||||
| 
 | ||||
| 		onOtherDragEnd() { | ||||
| 			this.dropready = false; | ||||
| 		}, | ||||
| 
 | ||||
| 		toggleActive() { | ||||
| 			if (!this.isStacked) return; | ||||
| 			this.active = !this.active; | ||||
| 		}, | ||||
| 
 | ||||
| 		getMenu() { | ||||
| 			const items = [{ | ||||
| 				icon: faPencilAlt, | ||||
| 				text: this.$t('rename'), | ||||
| 				action: () => { | ||||
| 					this.$root.dialog({ | ||||
| 						title: this.$t('rename'), | ||||
| 						input: { | ||||
| 							default: this.column.name, | ||||
| 							allowEmpty: false | ||||
| 						} | ||||
| 					}).then(({ canceled, result: name }) => { | ||||
| 						if (canceled) return; | ||||
| 						this.$store.commit('deviceUser/renameDeckColumn', { id: this.column.id, name }); | ||||
| 					}); | ||||
| 				} | ||||
| 			}, null, { | ||||
| 				icon: faArrowLeft, | ||||
| 				text: this.$t('swap-left'), | ||||
| 				action: () => { | ||||
| 					this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}, { | ||||
| 				icon: faArrowRight, | ||||
| 				text: this.$t('swap-right'), | ||||
| 				action: () => { | ||||
| 					this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}, this.isStacked ? { | ||||
| 				icon: faArrowUp, | ||||
| 				text: this.$t('swap-up'), | ||||
| 				action: () => { | ||||
| 					this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			} : undefined, this.isStacked ? { | ||||
| 				icon: faArrowDown, | ||||
| 				text: this.$t('swap-down'), | ||||
| 				action: () => { | ||||
| 					this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			} : undefined, null, { | ||||
| 				icon: faWindowRestore, | ||||
| 				text: this.$t('stack-left'), | ||||
| 				action: () => { | ||||
| 					this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}, this.isStacked ? { | ||||
| 				icon: faWindowMaximize, | ||||
| 				text: this.$t('pop-right'), | ||||
| 				action: () => { | ||||
| 					this.$store.commit('deviceUser/popRightDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			} : undefined, null, { | ||||
| 				icon: faTrashAlt, | ||||
| 				text: this.$t('remove'), | ||||
| 				action: () => { | ||||
| 					this.$store.commit('deviceUser/removeDeckColumn', this.column.id); | ||||
| 				} | ||||
| 			}]; | ||||
| 
 | ||||
| 			if (this.menu) { | ||||
| 				for (const i of this.menu.reverse()) { | ||||
| 					items.unshift(i); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return items; | ||||
| 		}, | ||||
| 
 | ||||
| 		onContextmenu(e) { | ||||
| 			if (this.isMainColumn) return; | ||||
| 			this.showMenu(); | ||||
| 		}, | ||||
| 
 | ||||
| 		showMenu() { | ||||
| 			this.$root.menu({ | ||||
| 				items: this.getMenu(), | ||||
| 				source: this.$refs.menu, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		close() { | ||||
| 			this.$router.push('/'); | ||||
| 		}, | ||||
| 
 | ||||
| 		goTop() { | ||||
| 			this.$refs.body.scrollTo({ | ||||
| 				top: 0, | ||||
| 				behavior: 'smooth' | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		onDragstart(e) { | ||||
| 			// メインカラムはドラッグさせない | ||||
| 			if (this.isMainColumn) { | ||||
| 				e.preventDefault(); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			e.dataTransfer.effectAllowed = 'move'; | ||||
| 			e.dataTransfer.setData('mk-deck-column', this.column.id); | ||||
| 			this.dragging = true; | ||||
| 		}, | ||||
| 
 | ||||
| 		onDragend(e) { | ||||
| 			this.dragging = false; | ||||
| 		}, | ||||
| 
 | ||||
| 		onDragover(e) { | ||||
| 			// メインカラムにはドロップさせない | ||||
| 			if (this.isMainColumn) { | ||||
| 				e.dataTransfer.dropEffect = 'none'; | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			// 自分自身がドラッグされている場合 | ||||
| 			if (this.dragging) { | ||||
| 				// 自分自身にはドロップさせない | ||||
| 				e.dataTransfer.dropEffect = 'none'; | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; | ||||
| 
 | ||||
| 			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; | ||||
| 
 | ||||
| 			if (!this.dragging && isDeckColumn) this.draghover = true; | ||||
| 		}, | ||||
| 
 | ||||
| 		onDragleave() { | ||||
| 			this.draghover = false; | ||||
| 		}, | ||||
| 
 | ||||
| 		onDrop(e) { | ||||
| 			this.draghover = false; | ||||
| 			this.$root.$emit('deck.column.dragEnd'); | ||||
| 
 | ||||
| 			const id = e.dataTransfer.getData('mk-deck-column'); | ||||
| 			if (id != null && id != '') { | ||||
| 				this.$store.commit('deviceUser/swapDeckColumn', { | ||||
| 					a: this.column.id, | ||||
| 					b: id | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .dnpfarvg { | ||||
| 	$header-height: 42px; | ||||
| 
 | ||||
| 	height: 100%; | ||||
| 	overflow: hidden; | ||||
| 	box-shadow: 0 0 0 1px var(--deckColumnBorder); | ||||
| 
 | ||||
| 	&.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: $header-height; | ||||
| 		min-height: $header-height; | ||||
| 
 | ||||
| 		> header.indicated { | ||||
| 			box-shadow: 4px 0px var(--accent) inset; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.naked { | ||||
| 		//background: var(--deckAcrylicColumnBg); | ||||
| 		background: transparent !important; | ||||
| 
 | ||||
| 		> header { | ||||
| 			background: transparent; | ||||
| 			box-shadow: none; | ||||
| 
 | ||||
| 			> button { | ||||
| 				color: var(--fg); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.paged { | ||||
| 		> div { | ||||
| 			background: var(--bg); | ||||
| 			padding: var(--margin); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> header { | ||||
| 		position: relative; | ||||
| 		display: flex; | ||||
| 		z-index: 2; | ||||
| 		line-height: $header-height; | ||||
| 		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 > *, | ||||
| 		> .menu, | ||||
| 		> .close { | ||||
| 			z-index: 1; | ||||
| 			width: $header-height; | ||||
| 			line-height: $header-height; | ||||
| 			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, | ||||
| 		> .close { | ||||
| 			margin-left: auto; | ||||
| 			margin-right: -16px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> div { | ||||
| 		height: calc(100% - #{$header-height}); | ||||
| 		overflow: auto; | ||||
| 		overflow-x: hidden; | ||||
| 		-webkit-overflow-scrolling: touch; | ||||
| 		box-sizing: border-box; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										39
									
								
								src/client/components/deck/direct-column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/client/components/deck/direct-column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| <template> | ||||
| <x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> | ||||
| 	<template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||
| 
 | ||||
| 	<x-direct/> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; | ||||
| import XColumn from './column.vue'; | ||||
| import XDirect from '../../pages/messages.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XDirect | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			menu: null, | ||||
| 			faEnvelope | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										87
									
								
								src/client/components/deck/list-column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/client/components/deck/list-column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | |||
| <template> | ||||
| <x-column :menu="menu" :column="column" :is-stacked="isStacked"> | ||||
| 	<template #header> | ||||
| 		<fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons'; | ||||
| import XColumn from './column.vue'; | ||||
| import XTimeline from '../timeline.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XTimeline, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			faListUl | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		mediaOnly() { | ||||
| 			(this.$refs.timeline as any).reload(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.menu = [{ | ||||
| 			icon: faCog, | ||||
| 			text: this.$t('list'), | ||||
| 			action: this.setList | ||||
| 		}]; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		if (this.column.listId == null) { | ||||
| 			this.setList(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async setList() { | ||||
| 			const lists = await this.$root.api('users/lists/list'); | ||||
| 			const { canceled, result: list } = await this.$root.dialog({ | ||||
| 				title: this.$t('list'), | ||||
| 				type: null, | ||||
| 				select: { | ||||
| 					items: lists.map(x => ({ | ||||
| 						value: x, text: x.name | ||||
| 					})), | ||||
| 					default: this.column.listId | ||||
| 				}, | ||||
| 				showCancelButton: true | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			Vue.set(this.column, 'listId', list.id); | ||||
| 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
							
								
								
									
										39
									
								
								src/client/components/deck/mentions-column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/client/components/deck/mentions-column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| <template> | ||||
| <x-column :column="column" :is-stacked="isStacked" :menu="menu"> | ||||
| 	<template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||
| 
 | ||||
| 	<x-mentions/> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faAt } from '@fortawesome/free-solid-svg-icons'; | ||||
| import XColumn from './column.vue'; | ||||
| import XMentions from '../../pages/mentions.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XMentions | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			menu: null, | ||||
| 			faAt | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										69
									
								
								src/client/components/deck/notifications-column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/client/components/deck/notifications-column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,69 @@ | |||
| <template> | ||||
| <x-column :column="column" :is-stacked="isStacked" :menu="menu"> | ||||
| 	<template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||
| 
 | ||||
| 	<x-notifications/> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faCog } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faBell } from '@fortawesome/free-regular-svg-icons'; | ||||
| import XColumn from './column.vue'; | ||||
| import XNotifications from '../notifications.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XNotifications | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			menu: null, | ||||
| 			faBell | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		if (this.column.notificationType == null) { | ||||
| 			this.column.notificationType = 'all'; | ||||
| 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||
| 		} | ||||
| 
 | ||||
| 		this.menu = [{ | ||||
| 			icon: faCog, | ||||
| 			text: this.$t('@.notification-type'), | ||||
| 			action: () => { | ||||
| 				this.$root.dialog({ | ||||
| 					title: this.$t('@.notification-type'), | ||||
| 					type: null, | ||||
| 					select: { | ||||
| 						items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ | ||||
| 							value: x, text: this.$t('@.notification-types.' + x) | ||||
| 						})) | ||||
| 						default: this.column.notificationType, | ||||
| 					}, | ||||
| 					showCancelButton: true | ||||
| 				}).then(({ canceled, result: type }) => { | ||||
| 					if (canceled) return; | ||||
| 					this.column.notificationType = type; | ||||
| 					this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||
| 				}); | ||||
| 			} | ||||
| 		}]; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										141
									
								
								src/client/components/deck/tl-column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/client/components/deck/tl-column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,141 @@ | |||
| <template> | ||||
| <x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState"> | ||||
| 	<template #header> | ||||
| 		<fa v-if="column.tl === 'home'" :icon="faHome"/> | ||||
| 		<fa v-else-if="column.tl === 'local'" :icon="faComments"/> | ||||
| 		<fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/> | ||||
| 		<fa v-else-if="column.tl === 'global'" :icon="faGlobe"/> | ||||
| 		<span style="margin-left: 8px;">{{ column.name }}</span> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<div class="iwaalbte" v-if="disabled"> | ||||
| 		<p> | ||||
| 			<fa :icon="faMinusCircle"/> | ||||
| 			{{ $t('disabled-timeline.title') }} | ||||
| 		</p> | ||||
| 		<p class="desc">{{ $t('disabled-timeline.description') }}</p> | ||||
| 	</div> | ||||
| 	<x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons'; | ||||
| import XColumn from './column.vue'; | ||||
| import XTimeline from '../timeline.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XTimeline, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			menu: null, | ||||
| 			disabled: false, | ||||
| 			indicated: false, | ||||
| 			columnActive: true, | ||||
| 			faMinusCircle, faHome, faComments, faShareAlt, faGlobe, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		mediaOnly() { | ||||
| 			(this.$refs.timeline as any).reload(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.menu = [{ | ||||
| 			icon: faCog, | ||||
| 			text: this.$t('timeline'), | ||||
| 			action: this.setType | ||||
| 		}]; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		if (this.column.tl == null) { | ||||
| 			this.setType(); | ||||
| 		} else { | ||||
| 			this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( | ||||
| 				this.$store.state.instance.meta.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) || | ||||
| 				this.$store.state.instance.meta.disableGlobalTimeline && ['global'].includes(this.column.tl)); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async setType() { | ||||
| 			const { canceled, result: src } = await this.$root.dialog({ | ||||
| 				title: this.$t('timeline'), | ||||
| 				type: null, | ||||
| 				select: { | ||||
| 					items: [{ | ||||
| 						value: 'home', text: this.$t('_timelines.home') | ||||
| 					}, { | ||||
| 						value: 'local', text: this.$t('_timelines.local') | ||||
| 					}, { | ||||
| 						value: 'social', text: this.$t('_timelines.social') | ||||
| 					}, { | ||||
| 						value: 'global', text: this.$t('_timelines.global') | ||||
| 					}] | ||||
| 				}, | ||||
| 				showCancelButton: true | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			Vue.set(this.column, 'tl', src); | ||||
| 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||
| 		}, | ||||
| 
 | ||||
| 		queueUpdated(q) { | ||||
| 			if (this.columnActive) { | ||||
| 				this.indicated = q !== 0; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onNote() { | ||||
| 			if (!this.columnActive) { | ||||
| 				this.indicated = true; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onChangeActiveState(state) { | ||||
| 			this.columnActive = state; | ||||
| 
 | ||||
| 			if (this.columnActive) { | ||||
| 				this.indicated = false; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.timeline as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .iwaalbte { | ||||
| 	text-align: center; | ||||
| 
 | ||||
| 	> p { | ||||
| 		margin: 16px; | ||||
| 
 | ||||
| 		&.desc { | ||||
| 			font-size: 14px; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										151
									
								
								src/client/components/deck/widgets-column.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/client/components/deck/widgets-column.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,151 @@ | |||
| <template> | ||||
| <x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked"> | ||||
| 	<template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||
| 
 | ||||
| 	<div class="wtdtxvec"> | ||||
| 		<template v-if="edit"> | ||||
| 			<header> | ||||
| 				<select v-model="widgetAdderSelected" @change="addWidget"> | ||||
| 					<option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option> | ||||
| 				</select> | ||||
| 			</header> | ||||
| 			<x-draggable | ||||
| 				:list="column.widgets" | ||||
| 				animation="150" | ||||
| 				@sort="onWidgetSort" | ||||
| 			> | ||||
| 				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)"> | ||||
| 					<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> | ||||
| 					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/> | ||||
| 				</div> | ||||
| 			</x-draggable> | ||||
| 		</template> | ||||
| 		<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import * as XDraggable from 'vuedraggable'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import { faWindowMaximize, faTimes } from '@fortawesome/free-solid-svg-icons'; | ||||
| import XColumn from './column.vue'; | ||||
| import { widgets } from '../../widgets'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XDraggable, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		column: { | ||||
| 			type: Object, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		isStacked: { | ||||
| 			type: Boolean, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			edit: false, | ||||
| 			menu: null, | ||||
| 			widgetAdderSelected: null, | ||||
| 			widgets, | ||||
| 			faWindowMaximize, faTimes | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.menu = [{ | ||||
| 			icon: 'cog', | ||||
| 			text: this.$t('edit'), | ||||
| 			action: () => { | ||||
| 				this.edit = !this.edit; | ||||
| 			} | ||||
| 		}]; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		widgetFunc(id) { | ||||
| 			this.$refs[id][0].setting(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onWidgetSort() { | ||||
| 			this.saveWidgets(); | ||||
| 		}, | ||||
| 
 | ||||
| 		addWidget() { | ||||
| 			this.$store.commit('deviceUser/addDeckWidget', { | ||||
| 				id: this.column.id, | ||||
| 				widget: { | ||||
| 					name: this.widgetAdderSelected, | ||||
| 					id: uuid(), | ||||
| 					data: {} | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			this.widgetAdderSelected = null; | ||||
| 		}, | ||||
| 
 | ||||
| 		removeWidget(widget) { | ||||
| 			this.$store.commit('deviceUser/removeDeckWidget', { | ||||
| 				id: this.column.id, | ||||
| 				widget | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		saveWidgets() { | ||||
| 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .wtdtxvec { | ||||
| 	padding-top: 1px; // ウィジェットのbox-shadowを利用した1px borderを隠さないようにするため | ||||
| 
 | ||||
| 	> header { | ||||
| 		padding: 16px; | ||||
| 
 | ||||
| 		> * { | ||||
| 			width: 100%; | ||||
| 			padding: 4px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .widget, .customize-container { | ||||
| 		margin: 8px; | ||||
| 
 | ||||
| 		&:first-of-type { | ||||
| 			margin-top: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.customize-container { | ||||
| 		position: relative; | ||||
| 		cursor: move; | ||||
| 
 | ||||
| 		> *:not(.remove) { | ||||
| 			pointer-events: none; | ||||
| 		} | ||||
| 
 | ||||
| 		> .remove { | ||||
| 			position: absolute; | ||||
| 			z-index: 2; | ||||
| 			top: 8px; | ||||
| 			right: 8px; | ||||
| 			width: 32px; | ||||
| 			height: 32px; | ||||
| 			color: #fff; | ||||
| 			background: rgba(#000, 0.7); | ||||
| 			border-radius: 4px; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -40,7 +40,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	> img { | ||||
| 		vertical-align: bottom; | ||||
| 		height: 150px; | ||||
| 		height: 128px; | ||||
| 		margin-bottom: 16px; | ||||
| 		border-radius: 16px; | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										71
									
								
								src/client/components/form-window.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/client/components/form-window.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| <template> | ||||
| <x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false"> | ||||
| 	<template #header> | ||||
| 		{{ title }} | ||||
| 	</template> | ||||
| 	<div class="xkpnjxcv"> | ||||
| 		<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> | ||||
| 			<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"><span v-text="form[item].label || item"></span></mk-input> | ||||
| 			<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text"><span v-text="form[item].label || item"></span></mk-input> | ||||
| 			<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-textarea> | ||||
| 			<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-switch> | ||||
| 		</label> | ||||
| 	</div> | ||||
| </x-window> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import XWindow from './window.vue'; | ||||
| import MkInput from './ui/input.vue'; | ||||
| import MkTextarea from './ui/textarea.vue'; | ||||
| import MkSwitch from './ui/switch.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XWindow, | ||||
| 		MkInput, | ||||
| 		MkTextarea, | ||||
| 		MkSwitch, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		title: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		form: { | ||||
| 			type: Object, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			values: {} | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		for (const item in this.form) { | ||||
| 			Vue.set(this.values, item, this.form[item].default || null); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		ok() { | ||||
| 			this.$emit('ok', this.values); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .xkpnjxcv { | ||||
| 	> label { | ||||
| 		display: block; | ||||
| 		padding: 16px 24px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -1,10 +1,10 @@ | |||
| <template> | ||||
| <div class="mk-modal" v-hotkey.global="keymap"> | ||||
| 	<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear> | ||||
| 		<div class="bg" ref="bg" v-if="show" @click="close()"></div> | ||||
| 		<div class="bg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div> | ||||
| 	</transition> | ||||
| 	<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> | ||||
| 		<div class="content" ref="content" v-if="show" @click.self="close()"><slot></slot></div> | ||||
| 		<div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div> | ||||
| 	</transition> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -14,6 +14,11 @@ import Vue from 'vue'; | |||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: { | ||||
| 		canClose: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
|  |  | |||
|  | @ -54,7 +54,6 @@ export default Vue.extend({ | |||
| 		margin: 0 .5em 0 0; | ||||
| 		padding: 0; | ||||
| 		overflow: hidden; | ||||
| 		color: var(--noteHeaderName); | ||||
| 		font-size: 1em; | ||||
| 		font-weight: bold; | ||||
| 		text-decoration: none; | ||||
|  |  | |||
|  | @ -724,61 +724,6 @@ export default Vue.extend({ | |||
| 	transition: box-shadow 0.1s ease; | ||||
| 	overflow: hidden; | ||||
| 
 | ||||
| 	&.max-width_500px { | ||||
| 		font-size: 0.9em; | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_450px { | ||||
| 		> .renote { | ||||
| 			padding: 8px 16px 0 16px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .article { | ||||
| 			padding: 14px 16px 9px; | ||||
| 
 | ||||
| 			> .avatar { | ||||
| 				margin: 0 10px 8px 0; | ||||
| 				width: 50px; | ||||
| 				height: 50px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_350px { | ||||
| 		> .article { | ||||
| 			> .main { | ||||
| 				> .footer { | ||||
| 					> .button { | ||||
| 						&:not(:last-child) { | ||||
| 							margin-right: 18px; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_300px { | ||||
| 		font-size: 0.825em; | ||||
| 
 | ||||
| 		> .article { | ||||
| 			> .avatar { | ||||
| 				width: 44px; | ||||
| 				height: 44px; | ||||
| 			} | ||||
| 
 | ||||
| 			> .main { | ||||
| 				> .footer { | ||||
| 					> .button { | ||||
| 						&:not(:last-child) { | ||||
| 							margin-right: 12px; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&:focus { | ||||
| 		outline: none; | ||||
| 		box-shadow: 0 0 0 3px var(--focus); | ||||
|  | @ -797,10 +742,6 @@ export default Vue.extend({ | |||
| 		white-space: pre; | ||||
| 		color: #d28a3f; | ||||
| 
 | ||||
| 		@media (max-width: 450px) { | ||||
| 			padding: 8px 16px 0 16px; | ||||
| 		} | ||||
| 
 | ||||
| 		> [data-icon] { | ||||
| 			margin-right: 4px; | ||||
| 		} | ||||
|  | @ -985,5 +926,64 @@ export default Vue.extend({ | |||
| 	> .reply { | ||||
| 		border-top: solid 1px var(--divider); | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_500px { | ||||
| 		font-size: 0.9em; | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_450px { | ||||
| 		> .renote { | ||||
| 			padding: 8px 16px 0 16px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .info { | ||||
| 			padding: 8px 16px 0 16px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .article { | ||||
| 			padding: 14px 16px 9px; | ||||
| 
 | ||||
| 			> .avatar { | ||||
| 				margin: 0 10px 8px 0; | ||||
| 				width: 50px; | ||||
| 				height: 50px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_350px { | ||||
| 		> .article { | ||||
| 			> .main { | ||||
| 				> .footer { | ||||
| 					> .button { | ||||
| 						&:not(:last-child) { | ||||
| 							margin-right: 18px; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_300px { | ||||
| 		font-size: 0.825em; | ||||
| 
 | ||||
| 		> .article { | ||||
| 			> .avatar { | ||||
| 				width: 44px; | ||||
| 				height: 44px; | ||||
| 			} | ||||
| 
 | ||||
| 			> .main { | ||||
| 				> .footer { | ||||
| 					> .button { | ||||
| 						&:not(:last-child) { | ||||
| 							margin-right: 12px; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										488
									
								
								src/client/components/sidebar.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										488
									
								
								src/client/components/sidebar.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,488 @@ | |||
| <template> | ||||
| <div class="mvcprjjd"> | ||||
| 	<transition name="nav-back"> | ||||
| 		<div class="nav-back" | ||||
| 			v-if="showing" | ||||
| 			@click="showing = false" | ||||
| 			@touchstart="showing = false" | ||||
| 		></div> | ||||
| 	</transition> | ||||
| 
 | ||||
| 	<transition name="nav"> | ||||
| 		<nav class="nav" v-show="showing"> | ||||
| 			<div> | ||||
| 				<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn"> | ||||
| 					<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/> | ||||
| 				</button> | ||||
| 				<button class="item _button index active" @click="top()" v-if="$route.name === 'index'"> | ||||
| 					<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span> | ||||
| 				</button> | ||||
| 				<router-link class="item index" active-class="active" to="/" exact v-else> | ||||
| 					<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span> | ||||
| 				</router-link> | ||||
| 				<template v-for="item in menu"> | ||||
| 					<div v-if="item === '-'" class="divider"></div> | ||||
| 					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'router-link' : 'button'" class="item _button" :class="item" active-class="active" @click="() => { if (menuDef[item].action) menuDef[item].action() }" :to="menuDef[item].to"> | ||||
| 						<fa :icon="menuDef[item].icon" fixed-width/><span class="text">{{ $t(menuDef[item].title) }}</span> | ||||
| 						<i v-if="menuDef[item].indicated"><fa :icon="faCircle"/></i> | ||||
| 					</component> | ||||
| 				</template> | ||||
| 				<div class="divider"></div> | ||||
| 				<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu"> | ||||
| 					<fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span> | ||||
| 				</button> | ||||
| 				<button class="item _button" @click="more"> | ||||
| 					<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span> | ||||
| 					<i v-if="otherNavItemIndicated"><fa :icon="faCircle"/></i> | ||||
| 				</button> | ||||
| 				<router-link class="item" active-class="active" to="/preferences"> | ||||
| 					<fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span> | ||||
| 				</router-link> | ||||
| 			</div> | ||||
| 		</nav> | ||||
| 	</transition> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { host, instanceName } from '../config'; | ||||
| import { search } from '../scripts/search'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			host: host, | ||||
| 			showing: false, | ||||
| 			searching: false, | ||||
| 			accounts: [], | ||||
| 			connection: null, | ||||
| 			menuDef: this.$store.getters.nav({ | ||||
| 				search: this.search | ||||
| 			}), | ||||
| 			faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		menu(): string[] { | ||||
| 			return this.$store.state.deviceUser.menu; | ||||
| 		}, | ||||
| 
 | ||||
| 		otherNavItemIndicated(): boolean { | ||||
| 			if (!this.$store.getters.isSignedIn) return false; | ||||
| 			for (const def in this.menuDef) { | ||||
| 				if (this.menu.includes(def)) continue; | ||||
| 				if (this.menuDef[def].indicated) return true; | ||||
| 			} | ||||
| 			return false; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		$route(to, from) { | ||||
| 			this.showing = false; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		show() { | ||||
| 			this.showing = true; | ||||
| 		}, | ||||
| 
 | ||||
| 		search() { | ||||
| 			if (this.searching) return; | ||||
| 
 | ||||
| 			this.$root.dialog({ | ||||
| 				title: this.$t('search'), | ||||
| 				input: true | ||||
| 			}).then(async ({ canceled, result: query }) => { | ||||
| 				if (canceled || query == null || query === '') return; | ||||
| 
 | ||||
| 				this.searching = true; | ||||
| 				search(this, query).finally(() => { | ||||
| 					this.searching = false; | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async openAccountMenu(ev) { | ||||
| 			const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id); | ||||
| 
 | ||||
| 			const accountItems = accounts.map(account => ({ | ||||
| 				type: 'user', | ||||
| 				user: account, | ||||
| 				action: () => { this.switchAccount(account); } | ||||
| 			})); | ||||
| 
 | ||||
| 			this.$root.menu({ | ||||
| 				items: [...[{ | ||||
| 					type: 'link', | ||||
| 					text: this.$t('profile'), | ||||
| 					to: `/@${ this.$store.state.i.username }`, | ||||
| 					avatar: this.$store.state.i, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('accountSettings'), | ||||
| 					to: '/my/settings', | ||||
| 					icon: faCog, | ||||
| 				}, null, ...accountItems, { | ||||
| 					icon: faPlus, | ||||
| 					text: this.$t('addAcount'), | ||||
| 					action: () => { | ||||
| 						this.$root.menu({ | ||||
| 							items: [{ | ||||
| 								text: this.$t('existingAcount'), | ||||
| 								action: () => { this.addAcount(); }, | ||||
| 							}, { | ||||
| 								text: this.$t('createAccount'), | ||||
| 								action: () => { this.createAccount(); }, | ||||
| 							}], | ||||
| 							align: 'left', | ||||
| 							fixed: true, | ||||
| 							width: 240, | ||||
| 							source: ev.currentTarget || ev.target, | ||||
| 						}); | ||||
| 					}, | ||||
| 				}]], | ||||
| 				align: 'left', | ||||
| 				fixed: true, | ||||
| 				width: 240, | ||||
| 				source: ev.currentTarget || ev.target, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		oepnInstanceMenu(ev) { | ||||
| 			this.$root.menu({ | ||||
| 				items: [{ | ||||
| 					type: 'link', | ||||
| 					text: this.$t('dashboard'), | ||||
| 					to: '/instance', | ||||
| 					icon: faTachometerAlt, | ||||
| 				}, null, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('settings'), | ||||
| 					to: '/instance/settings', | ||||
| 					icon: faCog, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('customEmojis'), | ||||
| 					to: '/instance/emojis', | ||||
| 					icon: faLaugh, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('users'), | ||||
| 					to: '/instance/users', | ||||
| 					icon: faUsers, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('files'), | ||||
| 					to: '/instance/files', | ||||
| 					icon: faCloud, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('jobQueue'), | ||||
| 					to: '/instance/queue', | ||||
| 					icon: faExchangeAlt, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('federation'), | ||||
| 					to: '/instance/federation', | ||||
| 					icon: faGlobe, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('relays'), | ||||
| 					to: '/instance/relays', | ||||
| 					icon: faProjectDiagram, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('announcements'), | ||||
| 					to: '/instance/announcements', | ||||
| 					icon: faBroadcastTower, | ||||
| 				}], | ||||
| 				align: 'left', | ||||
| 				fixed: true, | ||||
| 				width: 200, | ||||
| 				source: ev.currentTarget || ev.target, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		more(ev) { | ||||
| 			const items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({ | ||||
| 				type: def.to ? 'link' : 'button', | ||||
| 				text: this.$t(def.title), | ||||
| 				icon: def.icon, | ||||
| 				to: def.to, | ||||
| 				action: def.action, | ||||
| 				indicate: def.indicated, | ||||
| 			})); | ||||
| 			this.$root.menu({ | ||||
| 				items: [...items, null, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('help'), | ||||
| 					to: '/docs', | ||||
| 					icon: faQuestionCircle, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('aboutX', { x: instanceName || host }), | ||||
| 					to: '/about', | ||||
| 					icon: faInfoCircle, | ||||
| 				}, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('aboutMisskey'), | ||||
| 					to: '/about-misskey', | ||||
| 					icon: faInfoCircle, | ||||
| 				}], | ||||
| 				align: 'left', | ||||
| 				fixed: true, | ||||
| 				width: 200, | ||||
| 				source: ev.currentTarget || ev.target, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async addAcount() { | ||||
| 			this.$root.new(await import('./signin-dialog.vue').then(m => m.default)).$once('login', res => { | ||||
| 				this.$store.dispatch('addAcount', res); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					iconOnly: true, autoClose: true | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async createAccount() { | ||||
| 			this.$root.new(await import('./signup-dialog.vue').then(m => m.default)).$once('signup', res => { | ||||
| 				this.$store.dispatch('addAcount', res); | ||||
| 				this.switchAccountWithToken(res.i); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async switchAccount(account: any) { | ||||
| 			const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token; | ||||
| 			this.switchAccountWithToken(token); | ||||
| 		}, | ||||
| 
 | ||||
| 		switchAccountWithToken(token: string) { | ||||
| 			this.$root.dialog({ | ||||
| 				type: 'waiting', | ||||
| 				iconOnly: true | ||||
| 			}); | ||||
| 
 | ||||
| 			this.$root.api('i', {}, token).then((i: any) => { | ||||
| 				this.$store.dispatch('switchAccount', { | ||||
| 					...i, | ||||
| 					token: token | ||||
| 				}).then(() => { | ||||
| 					this.$nextTick(() => { | ||||
| 						location.reload(); | ||||
| 					}); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .nav-enter-active, | ||||
| .nav-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); | ||||
| } | ||||
| .nav-enter, | ||||
| .nav-leave-active { | ||||
| 	opacity: 0; | ||||
| 	transform: translateX(-240px); | ||||
| } | ||||
| 
 | ||||
| .nav-back-enter-active, | ||||
| .nav-back-leave-active { | ||||
| 	opacity: 1; | ||||
| 	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); | ||||
| } | ||||
| .nav-back-enter, | ||||
| .nav-back-leave-active { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .mvcprjjd { | ||||
| 	$ui-font-size: 1em; // TODO: どこかに集約したい | ||||
| 	$nav-width: 250px; // TODO: どこかに集約したい | ||||
| 	$nav-icon-only-width: 80px; // TODO: どこかに集約したい | ||||
| 	$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい | ||||
| 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい | ||||
| 
 | ||||
| 	> .nav-back { | ||||
| 		position: fixed; | ||||
| 		top: 0; | ||||
| 		left: 0; | ||||
| 		z-index: 1001; | ||||
| 		width: 100%; | ||||
| 		height: 100%; | ||||
| 		background: var(--modalBg); | ||||
| 	} | ||||
| 
 | ||||
| 	> .nav { | ||||
| 		$avatar-size: 32px; | ||||
| 		$avatar-margin: 8px; | ||||
| 
 | ||||
| 		flex: 0 0 $nav-width; | ||||
| 		width: $nav-width; | ||||
| 		box-sizing: border-box; | ||||
| 
 | ||||
| 		@media (max-width: $nav-icon-only-threshold) { | ||||
| 			flex: 0 0 $nav-icon-only-width; | ||||
| 			width: $nav-icon-only-width; | ||||
| 		} | ||||
| 
 | ||||
| 		@media (max-width: $nav-hide-threshold) { | ||||
| 			position: fixed; | ||||
| 			top: 0; | ||||
| 			left: 0; | ||||
| 			z-index: 1001; | ||||
| 		} | ||||
| 
 | ||||
| 		@media (min-width: $nav-hide-threshold + 1px) { | ||||
| 			display: block !important; | ||||
| 		} | ||||
| 
 | ||||
| 		> div { | ||||
| 			position: fixed; | ||||
| 			top: 0; | ||||
| 			left: 0; | ||||
| 			z-index: 1001; | ||||
| 			width: $nav-width; | ||||
| 			height: 100vh; | ||||
| 			box-sizing: border-box; | ||||
| 			overflow: auto; | ||||
| 			background: var(--navBg); | ||||
| 			border-right: solid 1px var(--divider); | ||||
| 
 | ||||
| 			> .divider { | ||||
| 				margin: 16px 0; | ||||
| 				border-top: solid 1px var(--divider); | ||||
| 			} | ||||
| 
 | ||||
| 			@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { | ||||
| 				width: $nav-icon-only-width; | ||||
| 
 | ||||
| 				> .divider { | ||||
| 					margin: 8px auto; | ||||
| 					width: calc(100% - 32px); | ||||
| 				} | ||||
| 
 | ||||
| 				> .item { | ||||
| 					&:first-child { | ||||
| 						margin-bottom: 8px; | ||||
| 					} | ||||
| 
 | ||||
| 					&:last-child { | ||||
| 						margin-top: 8px; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .item { | ||||
| 				position: relative; | ||||
| 				display: block; | ||||
| 				padding-left: 32px; | ||||
| 				font-size: $ui-font-size; | ||||
| 				line-height: 3.2rem; | ||||
| 				text-overflow: ellipsis; | ||||
| 				overflow: hidden; | ||||
| 				white-space: nowrap; | ||||
| 				width: 100%; | ||||
| 				text-align: left; | ||||
| 				box-sizing: border-box; | ||||
| 				color: var(--navFg); | ||||
| 
 | ||||
| 				> [data-icon] { | ||||
| 					width: 32px; | ||||
| 				} | ||||
| 
 | ||||
| 				> [data-icon], | ||||
| 				> .avatar { | ||||
| 					margin-right: $avatar-margin; | ||||
| 				} | ||||
| 
 | ||||
| 				> .avatar { | ||||
| 					width: $avatar-size; | ||||
| 					height: $avatar-size; | ||||
| 					vertical-align: middle; | ||||
| 				} | ||||
| 
 | ||||
| 				> i { | ||||
| 					position: absolute; | ||||
| 					top: 0; | ||||
| 					left: 20px; | ||||
| 					color: var(--navIndicator); | ||||
| 					font-size: 8px; | ||||
| 					animation: blink 1s infinite; | ||||
| 				} | ||||
| 
 | ||||
| 				&:hover { | ||||
| 					text-decoration: none; | ||||
| 					color: var(--navHoverFg); | ||||
| 				} | ||||
| 
 | ||||
| 				&.active { | ||||
| 					color: var(--navActive); | ||||
| 				} | ||||
| 
 | ||||
| 				&:first-child, &:last-child { | ||||
| 					position: sticky; | ||||
| 					z-index: 1; | ||||
| 					padding-top: 8px; | ||||
| 					padding-bottom: 8px; | ||||
| 					background: var(--X14); | ||||
| 					-webkit-backdrop-filter: blur(8px); | ||||
| 					backdrop-filter: blur(8px); | ||||
| 				} | ||||
| 
 | ||||
| 				&:first-child { | ||||
| 					top: 0; | ||||
| 					margin-bottom: 16px; | ||||
| 					border-bottom: solid 1px var(--divider); | ||||
| 				} | ||||
| 
 | ||||
| 				&:last-child { | ||||
| 					bottom: 0; | ||||
| 					margin-top: 16px; | ||||
| 					border-top: solid 1px var(--divider); | ||||
| 				} | ||||
| 
 | ||||
| 				@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { | ||||
| 					padding-left: 0; | ||||
| 					width: 100%; | ||||
| 					text-align: center; | ||||
| 					font-size: $ui-font-size * 1.2; | ||||
| 					line-height: 3.7rem; | ||||
| 
 | ||||
| 					> [data-icon], | ||||
| 					> .avatar { | ||||
| 						margin-right: 0; | ||||
| 					} | ||||
| 
 | ||||
| 					> i { | ||||
| 						left: 10px; | ||||
| 					} | ||||
| 
 | ||||
| 					> .text { | ||||
| 						display: none; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			@media (max-width: $nav-hide-threshold) { | ||||
| 				> .index, | ||||
| 				> .notifications { | ||||
| 					display: none; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -17,9 +17,11 @@ export default Vue.extend({ | |||
| 			required: true | ||||
| 		}, | ||||
| 		list: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		antenna: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		sound: { | ||||
|  | @ -53,6 +55,8 @@ export default Vue.extend({ | |||
| 			const _note = JSON.parse(JSON.stringify(note));	// deepcopy | ||||
| 			(this.$refs.tl as any).prepend(_note); | ||||
| 
 | ||||
| 			this.$emit('note'); | ||||
| 
 | ||||
| 			if (this.sound) { | ||||
| 				this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note'); | ||||
| 			} | ||||
|  | @ -77,10 +81,10 @@ export default Vue.extend({ | |||
| 		if (this.src == 'antenna') { | ||||
| 			endpoint = 'antennas/notes'; | ||||
| 			this.query = { | ||||
| 				antennaId: this.antenna.id | ||||
| 				antennaId: this.antenna | ||||
| 			}; | ||||
| 			this.connection = this.$root.stream.connectToChannel('antenna', { | ||||
| 				antennaId: this.antenna.id | ||||
| 				antennaId: this.antenna | ||||
| 			}); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'home') { | ||||
|  | @ -106,10 +110,10 @@ export default Vue.extend({ | |||
| 		} else if (this.src == 'list') { | ||||
| 			endpoint = 'notes/user-list-timeline'; | ||||
| 			this.query = { | ||||
| 				listId: this.list.id | ||||
| 				listId: this.list | ||||
| 			}; | ||||
| 			this.connection = this.$root.stream.connectToChannel('userList', { | ||||
| 				listId: this.list.id | ||||
| 				listId: this.list | ||||
| 			}); | ||||
| 			this.connection.on('note', prepend); | ||||
| 			this.connection.on('userAdded', onUserAdded); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader }"> | ||||
| <div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable }" v-size="[{ max: 500 }]"> | ||||
| 	<header v-if="showHeader"> | ||||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<slot name="func"></slot> | ||||
|  | @ -47,6 +47,11 @@ export default Vue.extend({ | |||
| 			required: false, | ||||
| 			default: true | ||||
| 		}, | ||||
| 		scrollable: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
|  | @ -107,10 +112,19 @@ export default Vue.extend({ | |||
| 		box-shadow: none !important; | ||||
| 	} | ||||
| 
 | ||||
| 	&.scrollable { | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 
 | ||||
| 		> div { | ||||
| 			overflow: auto; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> header { | ||||
| 		position: relative; | ||||
| 		box-shadow: 0 1px 0 0 var(--panelHeaderDivider); | ||||
| 		z-index: 1; | ||||
| 		z-index: 2; | ||||
| 		background: var(--panelHeaderBg); | ||||
| 		color: var(--panelHeaderFg); | ||||
| 
 | ||||
|  | @ -118,10 +132,6 @@ export default Vue.extend({ | |||
| 			margin: 0; | ||||
| 			padding: 12px 16px; | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				padding: 8px 10px; | ||||
| 			} | ||||
| 
 | ||||
| 			> [data-icon] { | ||||
| 				margin-right: 6px; | ||||
| 			} | ||||
|  | @ -141,5 +151,21 @@ export default Vue.extend({ | |||
| 			height: 100%; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_500px { | ||||
| 		> header { | ||||
| 			> .title { | ||||
| 				padding: 8px 10px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._forceContainerFull_ .ukygtjoj { | ||||
| 	> header { | ||||
| 		> .title { | ||||
| 			padding: 12px 16px !important; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ | |||
| 				:pattern="pattern" | ||||
| 				:autocomplete="autocomplete" | ||||
| 				:spellcheck="spellcheck" | ||||
| 				:step="step" | ||||
| 				@focus="focused = true" | ||||
| 				@blur="focused = false" | ||||
| 				@keydown="$emit('keydown', $event)" | ||||
|  | @ -36,6 +37,7 @@ | |||
| 				:pattern="pattern" | ||||
| 				:autocomplete="autocomplete" | ||||
| 				:spellcheck="spellcheck" | ||||
| 				:step="step" | ||||
| 				@focus="focused = true" | ||||
| 				@blur="focused = false" | ||||
| 				@keydown="$emit('keydown', $event)" | ||||
|  | @ -114,6 +116,9 @@ export default Vue.extend({ | |||
| 		spellcheck: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		step: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		debounce: { | ||||
| 			required: false | ||||
| 		}, | ||||
|  | @ -164,7 +169,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 		v(v) { | ||||
| 			if (this.type === 'number') { | ||||
| 				this.$emit('input', parseInt(v, 10)); | ||||
| 				this.$emit('input', parseFloat(v)); | ||||
| 			} else { | ||||
| 				this.$emit('input', v); | ||||
| 			} | ||||
|  | @ -297,7 +302,7 @@ export default Vue.extend({ | |||
| 			pointer-events: none; | ||||
| 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | ||||
| 			transition-duration: 0.3s; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			line-height: 32px; | ||||
| 			color: var(--inputLabel); | ||||
| 			pointer-events: none; | ||||
|  | @ -312,7 +317,7 @@ export default Vue.extend({ | |||
| 			top: -17px; | ||||
| 			left: 0 !important; | ||||
| 			pointer-events: none; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			line-height: 32px; | ||||
| 			color: var(--inputLabel); | ||||
| 			pointer-events: none; | ||||
|  | @ -343,7 +348,7 @@ export default Vue.extend({ | |||
| 			padding: 0; | ||||
| 			font: inherit; | ||||
| 			font-weight: normal; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			line-height: $height; | ||||
| 			color: var(--inputText); | ||||
| 			background: transparent; | ||||
|  | @ -364,7 +369,7 @@ export default Vue.extend({ | |||
| 			position: absolute; | ||||
| 			z-index: 1; | ||||
| 			top: 0; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			line-height: 32px; | ||||
| 			color: var(--inputLabel); | ||||
| 			pointer-events: none; | ||||
|  |  | |||
|  | @ -135,7 +135,7 @@ export default Vue.extend({ | |||
| 			pointer-events: none; | ||||
| 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | ||||
| 			transition-duration: 0.3s; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			line-height: 32px; | ||||
| 			pointer-events: none; | ||||
| 			//will-change transform | ||||
|  | @ -150,7 +150,7 @@ export default Vue.extend({ | |||
| 			padding: 0; | ||||
| 			font: inherit; | ||||
| 			font-weight: normal; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			height: 32px; | ||||
| 			background: none; | ||||
| 			border: none; | ||||
|  | @ -170,7 +170,7 @@ export default Vue.extend({ | |||
| 			display: block; | ||||
| 			align-self: center; | ||||
| 			justify-self: center; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			line-height: 32px; | ||||
| 			color: rgba(#000, 0.54); | ||||
| 			pointer-events: none; | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 	role="switch" | ||||
| 	:aria-checked="checked" | ||||
| 	:aria-disabled="disabled" | ||||
| 	@click="toggle" | ||||
| 	@click.prevent="toggle" | ||||
| > | ||||
| 	<input | ||||
| 		type="checkbox" | ||||
|  |  | |||
|  | @ -133,7 +133,7 @@ export default Vue.extend({ | |||
| 			pointer-events: none; | ||||
| 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | ||||
| 			transition-duration: 0.3s; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			line-height: 32px; | ||||
| 			pointer-events: none; | ||||
| 			//will-change transform | ||||
|  | @ -151,7 +151,7 @@ export default Vue.extend({ | |||
| 			box-sizing: border-box; | ||||
| 			font: inherit; | ||||
| 			font-weight: normal; | ||||
| 			font-size: 16px; | ||||
| 			font-size: 1em; | ||||
| 			background: transparent; | ||||
| 			border: none; | ||||
| 			border-radius: 0; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }"> | ||||
| <x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }" :can-close="canClose"> | ||||
| 	<div class="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown" :style="{ width: `${width}px`, height: `${height}px` }"> | ||||
| 		<div class="header"> | ||||
| 			<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button> | ||||
|  | @ -57,6 +57,11 @@ export default Vue.extend({ | |||
| 			required: false, | ||||
| 			default: 400 | ||||
| 		}, | ||||
| 		canClose: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
|  |  | |||
|  | @ -18,3 +18,4 @@ export const getLocale = async () => Object.fromEntries((await entries(clientDb. | |||
| export const version = _VERSION_; | ||||
| export const env = _ENV_; | ||||
| export const instanceName = siteName === 'Misskey' ? null : siteName; | ||||
| export const deckmode = localStorage.getItem('deckmode') === 'true'; | ||||
|  |  | |||
							
								
								
									
										312
									
								
								src/client/deck.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								src/client/deck.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,312 @@ | |||
| <template> | ||||
| <div class="mk-deck" :class="`${$store.state.device.deckColumnAlign}`" v-hotkey.global="keymap"> | ||||
| 	<x-sidebar ref="nav"/> | ||||
| 
 | ||||
| 	<!-- TODO: deckMainColumnPlace を見て位置変える --> | ||||
| 	<deck-column class="column" v-if="$store.state.device.deckAlwaysShowMainColumn || $route.name !== 'index'"> | ||||
| 		<template #action> | ||||
| 			<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button> | ||||
| 		</template> | ||||
| 
 | ||||
| 		<template #header> | ||||
| 			<div class="iwnjqeul"> | ||||
| 				<div class="default"> | ||||
| 					<portal-target name="avatar" slim/> | ||||
| 					<span class="title"><portal-target name="icon" slim/><portal-target name="title" slim/></span> | ||||
| 				</div> | ||||
| 				<div class="custom"> | ||||
| 					<portal-target name="header" slim/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 
 | ||||
| 		<router-view></router-view> | ||||
| 	</deck-column> | ||||
| 
 | ||||
| 	<template v-for="ids in layout"> | ||||
| 		<div v-if="ids.length > 1" class="folder column"> | ||||
| 			<deck-column-core v-for="id, i in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/> | ||||
| 		</div> | ||||
| 		<deck-column-core v-else class="column" :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id === ids[0])" @parent-focus="moveFocus(ids[0], $event)"/> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<button @click="addColumn" class="_button add"><fa :icon="faPlus"/></button> | ||||
| 
 | ||||
| 	<button v-if="$store.getters.isSignedIn" class="nav _button" @click="showNav()"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button> | ||||
| 	<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> | ||||
| 
 | ||||
| 	<stream-indicator v-if="$store.getters.isSignedIn"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faPlus, faPencilAlt, faChevronLeft, faBars, faCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import {  } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import { host } from './config'; | ||||
| import { search } from './scripts/search'; | ||||
| import DeckColumnCore from './components/deck/column-core.vue'; | ||||
| import DeckColumn from './components/deck/column.vue'; | ||||
| import XSidebar from './components/sidebar.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XSidebar, | ||||
| 		DeckColumn, | ||||
| 		DeckColumnCore, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			host: host, | ||||
| 			pageKey: 0, | ||||
| 			searching: false, | ||||
| 			connection: null, | ||||
| 			searchQuery: '', | ||||
| 			searchWait: false, | ||||
| 			canBack: false, | ||||
| 			menuDef: this.$store.getters.nav({}), | ||||
| 			wallpaper: localStorage.getItem('wallpaper') != null, | ||||
| 			faPlus, faPencilAlt, faChevronLeft, faBars, faCircle | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		deck() { | ||||
| 			return this.$store.state.deviceUser.deck; | ||||
| 		}, | ||||
| 		columns(): any[] { | ||||
| 			return this.deck.columns; | ||||
| 		}, | ||||
| 		layout(): any[] { | ||||
| 			return this.deck.layout; | ||||
| 		}, | ||||
| 		navIndicated(): boolean { | ||||
| 			if (!this.$store.getters.isSignedIn) return false; | ||||
| 			for (const def in this.menuDef) { | ||||
| 				if (this.menuDef[def].indicated) return true; | ||||
| 			} | ||||
| 			return false; | ||||
| 		}, | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				'p': this.post, | ||||
| 				'n': this.post, | ||||
| 				's': this.search, | ||||
| 				'h|/': this.help | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		$route(to, from) { | ||||
| 			this.pageKey++; | ||||
| 			this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		document.documentElement.style.overflowY = 'hidden'; | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn) { | ||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); | ||||
| 			this.connection.on('notification', this.onNotification); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		showNav() { | ||||
| 			this.$refs.nav.show(); | ||||
| 		}, | ||||
| 
 | ||||
| 		help() { | ||||
| 			this.$router.push('/docs/keyboard-shortcut'); | ||||
| 		}, | ||||
| 
 | ||||
| 		back() { | ||||
| 			if (this.canBack) window.history.back(); | ||||
| 		}, | ||||
| 
 | ||||
| 		post() { | ||||
| 			this.$root.post(); | ||||
| 		}, | ||||
| 
 | ||||
| 		search() { | ||||
| 			if (this.searching) return; | ||||
| 
 | ||||
| 			this.$root.dialog({ | ||||
| 				title: this.$t('search'), | ||||
| 				input: true | ||||
| 			}).then(async ({ canceled, result: query }) => { | ||||
| 				if (canceled || query == null || query === '') return; | ||||
| 
 | ||||
| 				this.searching = true; | ||||
| 				search(this, query).finally(() => { | ||||
| 					this.searching = false; | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async onNotification(notification) { | ||||
| 			if (document.visibilityState === 'visible') { | ||||
| 				this.$root.stream.send('readNotification', { | ||||
| 					id: notification.id | ||||
| 				}); | ||||
| 
 | ||||
| 				this.$root.new(await import('./components/toast.vue').then(m => m.default), { | ||||
| 					notification | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			this.$root.sound('notification'); | ||||
| 		}, | ||||
| 
 | ||||
| 		async addColumn(ev) { | ||||
| 			const columns = [ | ||||
| 				'widgets', | ||||
| 				'notifications', | ||||
| 				'tl', | ||||
| 				'antenna', | ||||
| 				'list', | ||||
| 				'mentions', | ||||
| 				'direct', | ||||
| 			]; | ||||
| 
 | ||||
| 			const { canceled, result: column } = await this.$root.dialog({ | ||||
| 				title: this.$t('_deck.addColumn'), | ||||
| 				type: null, | ||||
| 				select: { | ||||
| 					items: columns.map(column => ({ | ||||
| 						value: column, text: this.$t('_deck._columns.' + column) | ||||
| 					})) | ||||
| 				}, | ||||
| 				showCancelButton: true | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			this.$store.commit('deviceUser/addDeckColumn', { | ||||
| 				type: column, | ||||
| 				id: uuid(), | ||||
| 				name: this.$t('_deck._columns.' + column), | ||||
| 				width: 330, | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-deck { | ||||
| 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい | ||||
| 
 | ||||
| 	// TODO: この値を設定で変えられるようにする? | ||||
| 	$columnMargin: 12px; | ||||
| 
 | ||||
| 	$deckMargin: 12px; | ||||
| 
 | ||||
| 	--margin: var(--marginHalf); | ||||
| 
 | ||||
| 	display: flex; | ||||
| 	height: 100vh; | ||||
| 	box-sizing: border-box; | ||||
| 	flex: 1; | ||||
| 	padding: $deckMargin 0 $deckMargin $deckMargin; | ||||
| 
 | ||||
| 	&.center { | ||||
| 		> .column:first-of-type { | ||||
| 			margin-left: auto; | ||||
| 		} | ||||
| 
 | ||||
| 		> .add { | ||||
| 			margin-right: auto; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .column { | ||||
| 		flex-shrink: 0; | ||||
| 		margin-right: $columnMargin; | ||||
| 
 | ||||
| 		&.folder { | ||||
| 			display: flex; | ||||
| 			flex-direction: column; | ||||
| 
 | ||||
| 			> *:not(:last-child) { | ||||
| 				margin-bottom: $columnMargin; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .post, | ||||
| 	> .nav { | ||||
| 		position: fixed; | ||||
| 		z-index: 1000; | ||||
| 		bottom: 32px; | ||||
| 		width: 64px; | ||||
| 		height: 64px; | ||||
| 		border-radius: 100%; | ||||
| 		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); | ||||
| 		font-size: 22px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .post { | ||||
| 		right: 32px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .nav { | ||||
| 		left: 32px; | ||||
| 		background: var(--panel); | ||||
| 		color: var(--fg); | ||||
| 
 | ||||
| 		@media (min-width: ($nav-hide-threshold + 1px)) { | ||||
| 			display: none; | ||||
| 		} | ||||
| 
 | ||||
| 		&:hover { | ||||
| 			background: var(--X2); | ||||
| 		} | ||||
| 
 | ||||
| 		> i { | ||||
| 			position: absolute; | ||||
| 			top: 0; | ||||
| 			left: 0; | ||||
| 			color: var(--indicator); | ||||
| 			font-size: 16px; | ||||
| 			animation: blink 1s infinite; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .iwnjqeul { | ||||
| 	$header-height: 42px; // TODO: column.vueのそれを参照するようにしたい(出来るのか?) | ||||
| 
 | ||||
| 	> .default { | ||||
| 		> .avatar { | ||||
| 			$size: 28px; | ||||
| 			display: inline-block; | ||||
| 			width: $size; | ||||
| 			height: $size; | ||||
| 			vertical-align: bottom; | ||||
| 			margin: (($header-height - $size) / 2) 8px (($header-height - $size) / 2) 0; | ||||
| 		} | ||||
| 
 | ||||
| 		> .title { | ||||
| 			display: inline-block; | ||||
| 			margin: 0; | ||||
| 			line-height: $header-height; | ||||
| 
 | ||||
| 			> [data-icon] { | ||||
| 				margin-right: 8px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .custom { | ||||
| 		position: absolute; | ||||
| 		top: 0; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -1,5 +1,5 @@ | |||
| /** | ||||
|  * App entry point | ||||
|  * Client entry point | ||||
|  */ | ||||
| 
 | ||||
| import Vue from 'vue'; | ||||
|  | @ -12,11 +12,13 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; | |||
| 
 | ||||
| import VueHotkey from './scripts/hotkey'; | ||||
| import App from './app.vue'; | ||||
| import Deck from './deck.vue'; | ||||
| import MiOS from './mios'; | ||||
| import { version, langs, instanceName, getLocale } from './config'; | ||||
| import { version, langs, instanceName, getLocale, deckmode } from './config'; | ||||
| import PostFormDialog from './components/post-form-dialog.vue'; | ||||
| import Dialog from './components/dialog.vue'; | ||||
| import Menu from './components/menu.vue'; | ||||
| import Form from './components/form-window.vue'; | ||||
| import { router } from './router'; | ||||
| import { applyTheme, lightTheme } from './scripts/theme'; | ||||
| import { isDeviceDarkmode } from './scripts/is-device-darkmode'; | ||||
|  | @ -165,6 +167,7 @@ os.init(async () => { | |||
| 				i18n // TODO: 消せないか考える SEE: https://github.com/syuilo/misskey/pull/6396#discussion_r429511030
 | ||||
| 			}; | ||||
| 		}, | ||||
| 		// TODO: ここらへんのメソッド全部Vuexに移したい
 | ||||
| 		methods: { | ||||
| 			api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }), | ||||
| 			signout: os.signout, | ||||
|  | @ -194,6 +197,13 @@ os.init(async () => { | |||
| 				}); | ||||
| 				return p; | ||||
| 			}, | ||||
| 			form(title, form) { | ||||
| 				const vm = this.new(Form, { title, form }); | ||||
| 				return new Promise((res) => { | ||||
| 					vm.$once('ok', result => res({ canceled: false, result })); | ||||
| 					vm.$once('cancel', () => res({ canceled: true })); | ||||
| 				}); | ||||
| 			}, | ||||
| 			post(opts, cb) { | ||||
| 				if (!this.$store.getters.isSignedIn) return; | ||||
| 				const vm = this.new(PostFormDialog, opts); | ||||
|  | @ -210,11 +220,9 @@ os.init(async () => { | |||
| 			} | ||||
| 		}, | ||||
| 		router: router, | ||||
| 		render: createEl => createEl(App) | ||||
| 		render: createEl => createEl(deckmode ? Deck : App) | ||||
| 	}); | ||||
| 
 | ||||
| 	os.app = app; | ||||
| 
 | ||||
| 	// マウント
 | ||||
| 	app.$mount('#app'); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| // TODO: このファイル消したい
 | ||||
| 
 | ||||
| import autobind from 'autobind-decorator'; | ||||
| import Vue from 'vue'; | ||||
| import { EventEmitter } from 'eventemitter3'; | ||||
| 
 | ||||
| import { apiUrl, version } from './config'; | ||||
|  | @ -14,8 +13,6 @@ import store from './store'; | |||
|  * Misskey Operating System | ||||
|  */ | ||||
| export default class MiOS extends EventEmitter { | ||||
| 	public app: Vue; | ||||
| 
 | ||||
| 	public store: ReturnType<typeof store>; | ||||
| 
 | ||||
| 	/** | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
| 	<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/> | ||||
| 
 | ||||
| 	<x-post-form class="post-form _panel" fixed v-if="$store.state.device.showFixedPostForm"/> | ||||
| 	<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/> | ||||
| 	<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list ? list.id : null" :antenna="antenna ? antenna.id : null" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,14 +15,15 @@ | |||
| 
 | ||||
| 		<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/> | ||||
| 		<x-note :note="note" :key="note.id" :detail="true"/> | ||||
| 		<div v-if="error"> | ||||
| 			<mk-error @retry="fetch()"/> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button> | ||||
| 		<hr v-if="showPrev"/> | ||||
| 		<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div v-if="error"> | ||||
| 		<mk-error @retry="fetch()"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,6 +51,20 @@ | |||
| 		</div> | ||||
| 	</section> | ||||
| 
 | ||||
| 	<section class="_card"> | ||||
| 		<div class="_title"><fa :icon="faColumns"/> {{ $t('deck') }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<mk-switch v-model="deckAlwaysShowMainColumn"> | ||||
| 				{{ $t('_deck.alwaysShowMainColumn') }} | ||||
| 			</mk-switch> | ||||
| 		</div> | ||||
| 		<div class="_content"> | ||||
| 			<div>{{ $t('_deck.columnAlign') }}</div> | ||||
| 			<mk-radio v-model="deckColumnAlign" value="left">{{ $t('left') }}</mk-radio> | ||||
| 			<mk-radio v-model="deckColumnAlign" value="center">{{ $t('center') }}</mk-radio> | ||||
| 		</div> | ||||
| 	</section> | ||||
| 
 | ||||
| 	<section class="_card"> | ||||
| 		<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div> | ||||
| 		<div class="_content"> | ||||
|  | @ -93,7 +107,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute, faColumns } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkButton from '../../components/ui/button.vue'; | ||||
| import MkSwitch from '../../components/ui/switch.vue'; | ||||
| import MkSelect from '../../components/ui/select.vue'; | ||||
|  | @ -145,7 +159,7 @@ export default Vue.extend({ | |||
| 			lang: localStorage.getItem('lang'), | ||||
| 			fontSize: localStorage.getItem('fontSize'), | ||||
| 			sounds, | ||||
| 			faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute | ||||
| 			faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute, faColumns | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -195,6 +209,16 @@ export default Vue.extend({ | |||
| 			set(value) { this.$store.commit('device/set', { key: 'fixedWidgetsPosition', value }); } | ||||
| 		}, | ||||
| 
 | ||||
| 		deckAlwaysShowMainColumn: { | ||||
| 			get() { return this.$store.state.device.deckAlwaysShowMainColumn; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); } | ||||
| 		}, | ||||
| 
 | ||||
| 		deckColumnAlign: { | ||||
| 			get() { return this.$store.state.device.deckColumnAlign; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); } | ||||
| 		}, | ||||
| 
 | ||||
| 		sfxVolume: { | ||||
| 			get() { return this.$store.state.device.sfxVolume; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="kjeftjfm"> | ||||
| <div class="kjeftjfm" v-size="[{ max: 500 }]"> | ||||
| 	<div class="with"> | ||||
| 		<button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button> | ||||
| 		<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button> | ||||
|  | @ -60,10 +60,6 @@ export default Vue.extend({ | |||
| 		display: flex; | ||||
| 		margin-bottom: var(--margin); | ||||
| 
 | ||||
| 		@media (max-width: 500px) { | ||||
| 			font-size: 80%; | ||||
| 		} | ||||
| 
 | ||||
| 		> button { | ||||
| 			flex: 1; | ||||
| 			padding: 11px 8px 8px 8px; | ||||
|  | @ -75,5 +71,11 @@ export default Vue.extend({ | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_500px { | ||||
| 		> .with { | ||||
| 			font-size: 80%; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="mk-user-page" v-if="user"> | ||||
| <div class="mk-user-page" v-if="user" v-size="[{ max: 500 }]"> | ||||
| 	<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal> | ||||
| 	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> | ||||
| 
 | ||||
|  | @ -118,6 +118,7 @@ import MkContainer from '../../components/ui/container.vue'; | |||
| import MkRemoteCaution from '../../components/remote-caution.vue'; | ||||
| import Progress from '../../scripts/loading'; | ||||
| import parseAcct from '../../../misc/acct/parse'; | ||||
| import { getScrollPosition } from '../../scripts/scroll'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	components: { | ||||
|  | @ -168,12 +169,8 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	mounted() { | ||||
| 		window.requestAnimationFrame(this.parallaxLoop); | ||||
| 		window.addEventListener('scroll', this.parallax, { passive: true }); | ||||
| 		document.addEventListener('touchmove', this.parallax, { passive: true }); | ||||
| 		this.$once('hook:beforeDestroy', () => { | ||||
| 			window.cancelAnimationFrame(this.parallaxAnimationId); | ||||
| 			window.removeEventListener('scroll', this.parallax); | ||||
| 			document.removeEventListener('touchmove', this.parallax); | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -205,7 +202,7 @@ export default Vue.extend({ | |||
| 			const banner = this.$refs.banner as any; | ||||
| 			if (banner == null) return; | ||||
| 
 | ||||
| 			const top = window.scrollY; | ||||
| 			const top = getScrollPosition(this.$el); | ||||
| 
 | ||||
| 			if (top < 0) return; | ||||
| 
 | ||||
|  | @ -219,7 +216,6 @@ export default Vue.extend({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-user-page { | ||||
| 
 | ||||
| 	> .punished { | ||||
| 		font-size: 0.8em; | ||||
| 		padding: 16px; | ||||
|  | @ -237,10 +233,6 @@ export default Vue.extend({ | |||
| 			background-size: cover; | ||||
| 			background-position: center; | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				height: 140px; | ||||
| 			} | ||||
| 
 | ||||
| 			> .banner { | ||||
| 				height: 100%; | ||||
| 				background-color: #4c5e6d; | ||||
|  | @ -257,10 +249,6 @@ export default Vue.extend({ | |||
| 				width: 100%; | ||||
| 				height: 78px; | ||||
| 				background: linear-gradient(transparent, rgba(#000, 0.7)); | ||||
| 
 | ||||
| 				@media (max-width: 500px) { | ||||
| 					display: none; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .followed { | ||||
|  | @ -308,10 +296,6 @@ export default Vue.extend({ | |||
| 				box-sizing: border-box; | ||||
| 				color: #fff; | ||||
| 
 | ||||
| 				@media (max-width: 500px) { | ||||
| 					display: none; | ||||
| 				} | ||||
| 
 | ||||
| 				> .name { | ||||
| 					display: block; | ||||
| 					margin: 0; | ||||
|  | @ -343,10 +327,6 @@ export default Vue.extend({ | |||
| 			font-weight: bold; | ||||
| 			border-bottom: solid 1px var(--divider); | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				display: block; | ||||
| 			} | ||||
| 
 | ||||
| 			> .bottom { | ||||
| 				> * { | ||||
| 					display: inline-block; | ||||
|  | @ -365,26 +345,12 @@ export default Vue.extend({ | |||
| 			width: 120px; | ||||
| 			height: 120px; | ||||
| 			box-shadow: 1px 1px 3px rgba(#000, 0.2); | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				top: 90px; | ||||
| 				left: 0; | ||||
| 				right: 0; | ||||
| 				width: 92px; | ||||
| 				height: 92px; | ||||
| 				margin: auto; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .description { | ||||
| 			padding: 24px 24px 24px 154px; | ||||
| 			font-size: 0.95em; | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				padding: 16px; | ||||
| 				text-align: center; | ||||
| 			} | ||||
| 
 | ||||
| 			> .empty { | ||||
| 				margin: 0; | ||||
| 				opacity: 0.5; | ||||
|  | @ -396,10 +362,6 @@ export default Vue.extend({ | |||
| 			font-size: 0.9em; | ||||
| 			border-top: solid 1px var(--divider); | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				padding: 16px; | ||||
| 			} | ||||
| 		 | ||||
| 			> .field { | ||||
| 				display: flex; | ||||
| 				padding: 0; | ||||
|  | @ -436,10 +398,6 @@ export default Vue.extend({ | |||
| 			padding: 24px; | ||||
| 			border-top: solid 1px var(--divider); | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				padding: 16px; | ||||
| 			} | ||||
| 
 | ||||
| 			> a { | ||||
| 				flex: 1; | ||||
| 				text-align: center; | ||||
|  | @ -473,5 +431,47 @@ export default Vue.extend({ | |||
| 	> .content { | ||||
| 		margin-bottom: var(--margin); | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_500px { | ||||
| 		> .profile { | ||||
| 			> .banner-container { | ||||
| 				height: 140px; | ||||
| 
 | ||||
| 				> .fade { | ||||
| 					display: none; | ||||
| 				} | ||||
| 
 | ||||
| 				> .title { | ||||
| 					display: none; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .title { | ||||
| 				display: block; | ||||
| 			} | ||||
| 
 | ||||
| 			> .avatar { | ||||
| 				top: 90px; | ||||
| 				left: 0; | ||||
| 				right: 0; | ||||
| 				width: 92px; | ||||
| 				height: 92px; | ||||
| 				margin: auto; | ||||
| 			} | ||||
| 
 | ||||
| 			> .description { | ||||
| 				padding: 16px; | ||||
| 				text-align: center; | ||||
| 			} | ||||
| 
 | ||||
| 			> .fields { | ||||
| 				padding: 16px; | ||||
| 			} | ||||
| 
 | ||||
| 			> .status { | ||||
| 				padding: 16px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										26
									
								
								src/client/scripts/form.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/client/scripts/form.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| export type FormItem = { | ||||
| 	label?: string; | ||||
| 	type: 'string'; | ||||
| 	default: string | null; | ||||
| 	hidden?: boolean; | ||||
| 	multiline?: boolean; | ||||
| } | { | ||||
| 	label?: string; | ||||
| 	type: 'number'; | ||||
| 	default: number | null; | ||||
| 	hidden?: boolean; | ||||
| 	step?: number; | ||||
| } | { | ||||
| 	label?: string; | ||||
| 	type: 'boolean'; | ||||
| 	default: boolean | null; | ||||
| 	hidden?: boolean; | ||||
| } | { | ||||
| 	label?: string; | ||||
| 	type: 'enum'; | ||||
| 	default: string | null; | ||||
| 	hidden?: boolean; | ||||
| 	enum: string[]; | ||||
| }; | ||||
| 
 | ||||
| export type Form = Record<string, FormItem>; | ||||
|  | @ -13,7 +13,7 @@ export default (opts) => ({ | |||
| 			moreFetching: false, | ||||
| 			inited: false, | ||||
| 			more: false, | ||||
| 			backed: false, | ||||
| 			backed: false, // 遡り中か否か
 | ||||
| 			isBackTop: false, | ||||
| 			ilObserver: new IntersectionObserver( | ||||
| 				(entries) => entries.some((entry) => entry.isIntersecting) | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| export function getScrollContainer(el: Element | null): Element | null { | ||||
| 	if (el == null || el.tagName === 'BODY') return null; | ||||
| 	const style = window.getComputedStyle(el); | ||||
| 	if (style.getPropertyValue('overflow') === 'auto') { | ||||
| 	const overflow = window.getComputedStyle(el).getPropertyValue('overflow'); | ||||
| 	if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる
 | ||||
| 		return el; | ||||
| 	} else { | ||||
| 		return getScrollContainer(el.parentElement); | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| import Vuex from 'vuex'; | ||||
| import createPersistedState from 'vuex-persistedstate'; | ||||
| import * as nestedProperty from 'nested-property'; | ||||
| import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { apiUrl } from './config'; | ||||
| import { apiUrl, deckmode } from './config'; | ||||
| import { erase } from '../prelude/array'; | ||||
| 
 | ||||
| export const defaultSettings = { | ||||
| 	tutorial: 0, | ||||
|  | @ -35,7 +36,13 @@ export const defaultDeviceUserSettings = { | |||
| 		'explore', | ||||
| 		'announcements', | ||||
| 		'search', | ||||
| 		'-', | ||||
| 		'deck', | ||||
| 	], | ||||
| 	deck: { | ||||
| 		columns: [], | ||||
| 		layout: [], | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| export const defaultDeviceSettings = { | ||||
|  | @ -50,6 +57,7 @@ export const defaultDeviceSettings = { | |||
| 	darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1', | ||||
| 	lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37', | ||||
| 	darkMode: false, | ||||
| 	deckMode: false, | ||||
| 	syncDeviceDarkMode: true, | ||||
| 	animation: true, | ||||
| 	animatedMfm: true, | ||||
|  | @ -60,6 +68,9 @@ export const defaultDeviceSettings = { | |||
| 	fixedWidgetsPosition: false, | ||||
| 	roomGraphicsQuality: 'medium', | ||||
| 	roomUseOrthographicCamera: true, | ||||
| 	deckColumnAlign: 'left', | ||||
| 	deckAlwaysShowMainColumn: true, | ||||
| 	deckMainColumnPlace: 'left', | ||||
| 	sfxVolume: 0.3, | ||||
| 	sfxNote: 'syuilo/down', | ||||
| 	sfxNoteMy: 'syuilo/up', | ||||
|  | @ -197,6 +208,14 @@ export default () => new Vuex.Store({ | |||
| 				get show() { return getters.isSignedIn; }, | ||||
| 				get to() { return `/@${state.i.username}/room`; }, | ||||
| 			}, | ||||
| 			deck: { | ||||
| 				title: deckmode ? 'undeck' : 'deck', | ||||
| 				icon: faColumns, | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('deckmode', (!deckmode).toString()); | ||||
| 					location.reload(); | ||||
| 				}, | ||||
| 			}, | ||||
| 		}), | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -399,6 +418,137 @@ export default () => new Vuex.Store({ | |||
| 						w.data = x.data; | ||||
| 					} | ||||
| 				}, | ||||
| 
 | ||||
| 				//#region Deck
 | ||||
| 				addDeckColumn(state, column) { | ||||
| 					if (column.name == undefined) column.name = null; | ||||
| 					state.deck.columns.push(column); | ||||
| 					state.deck.layout.push([column.id]); | ||||
| 				}, | ||||
| 		 | ||||
| 				removeDeckColumn(state, id) { | ||||
| 					state.deck.columns = state.deck.columns.filter(c => c.id != id); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); | ||||
| 					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); | ||||
| 				}, | ||||
| 		 | ||||
| 				swapDeckColumn(state, x) { | ||||
| 					const a = x.a; | ||||
| 					const b = x.b; | ||||
| 					const aX = state.deck.layout.findIndex(ids => ids.indexOf(a) != -1); | ||||
| 					const aY = state.deck.layout[aX].findIndex(id => id == a); | ||||
| 					const bX = state.deck.layout.findIndex(ids => ids.indexOf(b) != -1); | ||||
| 					const bY = state.deck.layout[bX].findIndex(id => id == b); | ||||
| 					state.deck.layout[aX][aY] = b; | ||||
| 					state.deck.layout[bX][bY] = a; | ||||
| 				}, | ||||
| 		 | ||||
| 				swapLeftDeckColumn(state, id) { | ||||
| 					state.deck.layout.some((ids, i) => { | ||||
| 						if (ids.indexOf(id) != -1) { | ||||
| 							const left = state.deck.layout[i - 1]; | ||||
| 							if (left) { | ||||
| 								// https://vuejs.org/v2/guide/list.html#Caveats
 | ||||
| 								//state.deck.layout[i - 1] = state.deck.layout[i];
 | ||||
| 								//state.deck.layout[i] = left;
 | ||||
| 								state.deck.layout.splice(i - 1, 1, state.deck.layout[i]); | ||||
| 								state.deck.layout.splice(i, 1, left); | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
| 		 | ||||
| 				swapRightDeckColumn(state, id) { | ||||
| 					state.deck.layout.some((ids, i) => { | ||||
| 						if (ids.indexOf(id) != -1) { | ||||
| 							const right = state.deck.layout[i + 1]; | ||||
| 							if (right) { | ||||
| 								// https://vuejs.org/v2/guide/list.html#Caveats
 | ||||
| 								//state.deck.layout[i + 1] = state.deck.layout[i];
 | ||||
| 								//state.deck.layout[i] = right;
 | ||||
| 								state.deck.layout.splice(i + 1, 1, state.deck.layout[i]); | ||||
| 								state.deck.layout.splice(i, 1, right); | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
| 		 | ||||
| 				swapUpDeckColumn(state, id) { | ||||
| 					const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1); | ||||
| 					ids.some((x, i) => { | ||||
| 						if (x == id) { | ||||
| 							const up = ids[i - 1]; | ||||
| 							if (up) { | ||||
| 								// https://vuejs.org/v2/guide/list.html#Caveats
 | ||||
| 								//ids[i - 1] = id;
 | ||||
| 								//ids[i] = up;
 | ||||
| 								ids.splice(i - 1, 1, id); | ||||
| 								ids.splice(i, 1, up); | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
| 		 | ||||
| 				swapDownDeckColumn(state, id) { | ||||
| 					const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1); | ||||
| 					ids.some((x, i) => { | ||||
| 						if (x == id) { | ||||
| 							const down = ids[i + 1]; | ||||
| 							if (down) { | ||||
| 								// https://vuejs.org/v2/guide/list.html#Caveats
 | ||||
| 								//ids[i + 1] = id;
 | ||||
| 								//ids[i] = down;
 | ||||
| 								ids.splice(i + 1, 1, id); | ||||
| 								ids.splice(i, 1, down); | ||||
| 							} | ||||
| 							return true; | ||||
| 						} | ||||
| 					}); | ||||
| 				}, | ||||
| 		 | ||||
| 				stackLeftDeckColumn(state, id) { | ||||
| 					const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); | ||||
| 					const left = state.deck.layout[i - 1]; | ||||
| 					if (left) state.deck.layout[i - 1].push(id); | ||||
| 					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); | ||||
| 				}, | ||||
| 		 | ||||
| 				popRightDeckColumn(state, id) { | ||||
| 					const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1); | ||||
| 					state.deck.layout = state.deck.layout.map(ids => erase(id, ids)); | ||||
| 					state.deck.layout.splice(i + 1, 0, [id]); | ||||
| 					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0); | ||||
| 				}, | ||||
| 		 | ||||
| 				addDeckWidget(state, x) { | ||||
| 					const column = state.deck.columns.find(c => c.id == x.id); | ||||
| 					if (column == null) return; | ||||
| 					if (column.widgets == null) column.widgets = []; | ||||
| 					column.widgets.unshift(x.widget); | ||||
| 				}, | ||||
| 		 | ||||
| 				removeDeckWidget(state, x) { | ||||
| 					const column = state.deck.columns.find(c => c.id == x.id); | ||||
| 					if (column == null) return; | ||||
| 					column.widgets = column.widgets.filter(w => w.id != x.widget.id); | ||||
| 				}, | ||||
| 		 | ||||
| 				renameDeckColumn(state, x) { | ||||
| 					const column = state.deck.columns.find(c => c.id == x.id); | ||||
| 					if (column == null) return; | ||||
| 					column.name = x.name; | ||||
| 				}, | ||||
| 		 | ||||
| 				updateDeckColumn(state, x) { | ||||
| 					let column = state.deck.columns.find(c => c.id == x.id); | ||||
| 					if (column == null) return; | ||||
| 					column = x; | ||||
| 				}, | ||||
| 				//#endregion
 | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| :root { | ||||
| 	--radius: 8px; | ||||
| 	--marginFull: 16px; | ||||
| 	--marginHalf: 8px; | ||||
| 	--marginHalf: 10px; | ||||
| 
 | ||||
| 	--margin: var(--marginFull); | ||||
| 
 | ||||
|  | @ -25,7 +25,6 @@ html { | |||
| 	background-position: center; | ||||
| 	color: var(--fg); | ||||
| 	overflow: auto; | ||||
| 	overflow-y: scroll; | ||||
| 
 | ||||
| 	&, * { | ||||
| 		scrollbar-color: var(--scrollbarHandle) var(--panel); | ||||
|  | @ -278,13 +277,14 @@ hr { | |||
| 
 | ||||
| ._panel { | ||||
| 	position: relative; | ||||
| 	z-index: 1; | ||||
| 	background: var(--panel); | ||||
| 	border-radius: var(--radius); | ||||
| 	box-shadow: 0 0 0 1px var(--panelBorder); | ||||
| 	overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| ._widget ._list_ ._panel { | ||||
| ._close_ ._list_ > * { | ||||
| 	box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider); | ||||
| 	border-radius: 0; | ||||
| 	margin: 0 !important; | ||||
|  | @ -348,31 +348,6 @@ hr { | |||
| 		& + ._content { | ||||
| 			border-top: solid 1px var(--divider); | ||||
| 		} | ||||
| 
 | ||||
| 		&._list { | ||||
| 			padding: 16px; | ||||
| 
 | ||||
| 			@media (max-width: 500px) { | ||||
| 				padding: 8px; | ||||
| 			} | ||||
| 
 | ||||
| 			._listItem { | ||||
| 				padding: 8px 16px; | ||||
| 				border-radius: var(--radius); | ||||
| 
 | ||||
| 				@media (max-width: 500px) { | ||||
| 					padding: 8px; | ||||
| 				} | ||||
| 
 | ||||
| 				&:hover { | ||||
| 					background: var(--listItemHoverBg); | ||||
| 				} | ||||
| 
 | ||||
| 				> * { | ||||
| 					pointer-events: none; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> ._footer { | ||||
|  | @ -385,6 +360,21 @@ hr { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._narrow_ ._card { | ||||
| 	> ._title { | ||||
| 		padding: 16px; | ||||
| 		font-size: 1em; | ||||
| 	} | ||||
| 
 | ||||
| 	> ._content { | ||||
| 		padding: 16px; | ||||
| 	} | ||||
| 
 | ||||
| 	> ._footer { | ||||
| 		padding: 16px; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._fullinfo { | ||||
| 	padding: 64px 32px; | ||||
| 	text-align: center; | ||||
|  |  | |||
|  | @ -26,8 +26,8 @@ | |||
| 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | ||||
| 		panelBorder: 'rgba(0, 0, 0, 0)', | ||||
| 		shadow: 'rgba(0, 0, 0, 0.1)', | ||||
| 		header: 'rgba(20, 20, 20, 0.75)', | ||||
| 		navBg: '@panel', | ||||
| 		header: ':alpha<0.7<@bg', | ||||
| 		navBg: '@bg', | ||||
| 		navFg: '@fg', | ||||
| 		navHoverFg: ':lighten<17<@fg', | ||||
| 		navActive: '@accent', | ||||
|  | @ -58,6 +58,7 @@ | |||
| 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', | ||||
| 		badge: '#31b1ce', | ||||
| 		messageBg: ':lighten<5<@bg', | ||||
| 		deckColumnBorder: ':lighten<10<@panel', | ||||
| 		X1: ':alpha<0<@bg', | ||||
| 		X2: ':darken<2<@panel', | ||||
| 		X3: 'rgba(255, 255, 255, 0.05)', | ||||
|  |  | |||
|  | @ -26,8 +26,8 @@ | |||
| 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | ||||
| 		panelBorder: 'rgba(0, 0, 0, 0)', | ||||
| 		shadow: 'rgba(0, 0, 0, 0.1)', | ||||
| 		header: 'rgba(255, 255, 255, 0.75)', | ||||
| 		navBg: '@panel', | ||||
| 		header: ':alpha<0.7<@bg', | ||||
| 		navBg: '@bg', | ||||
| 		navFg: '@fg', | ||||
| 		navHoverFg: ':darken<17<@fg', | ||||
| 		navActive: '@accent', | ||||
|  | @ -58,6 +58,7 @@ | |||
| 		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', | ||||
| 		badge: '#31b1ce', | ||||
| 		messageBg: '@panel', | ||||
| 		deckColumnBorder: ':darken<20<@panel', | ||||
| 		X1: ':alpha<0<@bg', | ||||
| 		X2: ':darken<2<@panel', | ||||
| 		X3: 'rgba(0, 0, 0, 0.05)', | ||||
|  |  | |||
|  | @ -13,5 +13,6 @@ | |||
| 		panelHeaderDivider: '@divider', | ||||
| 		panelBorder: '@divider', | ||||
| 		messageBg: '#1d1d1d', | ||||
| 		deckColumnBorder: '@divider', | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
|  | @ -10,9 +10,11 @@ | |||
| 		accent: 'rgb(206, 147, 191)', | ||||
| 		bg: 'rgb(253, 242, 243)', | ||||
| 		fg: 'rgb(161, 139, 146)', | ||||
| 		divider: '#ece7e7', | ||||
| 		renote: '@accent', | ||||
| 		link: '@accent', | ||||
| 		mention: '@accent', | ||||
| 		hashtag: '@accent', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
|  | @ -11,5 +11,6 @@ | |||
| 		bg: 'rgb(220, 229, 232)', | ||||
| 		fg: 'rgb(139, 153, 161)', | ||||
| 		renote: '@accent', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
|  | @ -8,7 +8,11 @@ | |||
| 	base: 'light', | ||||
| 
 | ||||
| 	props: { | ||||
| 		bg: '#f2f2f2', | ||||
| 		header: ':alpha<0.7<@bg', | ||||
| 		navBg: '@bg', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 		messageBg: '#dedede', | ||||
| 		deckColumnBorder: '#cccccc', | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
|  | @ -1,18 +1,16 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<mk-container :show-header="props.design === 0" :naked="props.design === 2"> | ||||
| 		<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template> | ||||
| 		<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template> | ||||
| <mk-container :show-header="props.showHeader" :naked="props.transparent"> | ||||
| 	<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template> | ||||
| 	<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<mk-loading v-if="fetching"/> | ||||
| 			<template v-else> | ||||
| 				<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/> | ||||
| 				<x-chart v-show="props.view === 1" :data="[].concat(activity)"/> | ||||
| 			</template> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| 	<div> | ||||
| 		<mk-loading v-if="fetching"/> | ||||
| 		<template v-else> | ||||
| 			<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/> | ||||
| 			<x-chart v-show="props.view === 1" :data="[].concat(activity)"/> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -25,8 +23,19 @@ import XChart from './activity.chart.vue'; | |||
| export default define({ | ||||
| 	name: 'activity', | ||||
| 	props: () => ({ | ||||
| 		design: 0, | ||||
| 		view: 0 | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		view: { | ||||
| 			type: 'number', | ||||
| 			default: 0, | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	components: { | ||||
|  | @ -57,14 +66,6 @@ export default define({ | |||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			if (this.props.design === 2) { | ||||
| 				this.props.design = 0; | ||||
| 			} else { | ||||
| 				this.props.design++; | ||||
| 			} | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 		toggleView() { | ||||
| 			if (this.props.view === 1) { | ||||
| 				this.props.view = 0; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="mkw-calendar" :class="{ _panel: props.design === 0 }"> | ||||
| <div class="mkw-calendar" :class="{ _panel: !props.transparent }"> | ||||
| 	<div class="calendar" :data-is-holiday="isHoliday"> | ||||
| 		<p class="month-and-year"> | ||||
| 			<span class="year">{{ $t('yearX', { year }) }}</span> | ||||
|  | @ -37,7 +37,10 @@ import define from './define'; | |||
| export default define({ | ||||
| 	name: 'calendar', | ||||
| 	props: () => ({ | ||||
| 		design: 0 | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	data() { | ||||
|  | @ -62,14 +65,6 @@ export default define({ | |||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			if (this.props.design === 2) { | ||||
| 				this.props.design = 0; | ||||
| 			} else { | ||||
| 				this.props.design++; | ||||
| 			} | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 		tick() { | ||||
| 			const now = new Date(); | ||||
| 			const nd = now.getDate(); | ||||
|  |  | |||
|  | @ -1,11 +1,9 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<mk-container :naked="props.style % 2 === 0" :show-header="false"> | ||||
| 		<div class="vubelbmv"> | ||||
| 			<mk-analog-clock class="clock" :smooth="props.style < 2"/> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| <mk-container :naked="props.transparent" :show-header="false"> | ||||
| 	<div class="vubelbmv"> | ||||
| 		<mk-analog-clock class="clock"/> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -16,19 +14,16 @@ import MkAnalogClock from '../components/analog-clock.vue'; | |||
| export default define({ | ||||
| 	name: 'clock', | ||||
| 	props: () => ({ | ||||
| 		style: 0 | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		MkAnalogClock | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			this.props.style = (this.props.style + 1) % 4; | ||||
| 			this.save(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import Vue from 'vue'; | ||||
| import { Form } from '../scripts/form'; | ||||
| 
 | ||||
| export default function <T extends object>(data: { | ||||
| export default function <T extends Form>(data: { | ||||
| 	name: string; | ||||
| 	props?: () => T; | ||||
| }) { | ||||
|  | @ -15,22 +16,22 @@ export default function <T extends object>(data: { | |||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		data() { | ||||
| 			return { | ||||
| 				bakedOldProps: null | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		computed: { | ||||
| 			id(): string { | ||||
| 				return this.widget.id; | ||||
| 			}, | ||||
| 
 | ||||
| 			props(): T { | ||||
| 			props(): Record<string, any> { | ||||
| 				return this.widget.data; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		data() { | ||||
| 			return { | ||||
| 				bakedOldProps: null | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		created() { | ||||
| 			this.mergeProps(); | ||||
| 
 | ||||
|  | @ -45,11 +46,26 @@ export default function <T extends object>(data: { | |||
| 					const defaultProps = data.props(); | ||||
| 					for (const prop of Object.keys(defaultProps)) { | ||||
| 						if (this.props.hasOwnProperty(prop)) continue; | ||||
| 						Vue.set(this.props, prop, defaultProps[prop]); | ||||
| 						Vue.set(this.props, prop, defaultProps[prop].default); | ||||
| 					} | ||||
| 				} | ||||
| 			}, | ||||
| 
 | ||||
| 			async setting() { | ||||
| 				const form = data.props(); | ||||
| 				for (const item of Object.keys(form)) { | ||||
| 					form[item].default = this.props[item]; | ||||
| 				} | ||||
| 				const { canceled, result } = await this.$root.form(data.name, form); | ||||
| 				if (canceled) return; | ||||
| 
 | ||||
| 				for (const key of Object.keys(result)) { | ||||
| 					Vue.set(this.props, key, result[key]); | ||||
| 				} | ||||
| 
 | ||||
| 				this.save(); | ||||
| 			}, | ||||
| 
 | ||||
| 			save() { | ||||
| 				this.$store.commit('deviceUser/updateWidget', this.widget); | ||||
| 			} | ||||
|  |  | |||
							
								
								
									
										75
									
								
								src/client/widgets/digital-clock.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/client/widgets/digital-clock.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| <template> | ||||
| <div class="mkw-digitalClock" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }"> | ||||
| 	<span> | ||||
| 		<span v-text="hh"></span> | ||||
| 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> | ||||
| 		<span v-text="mm"></span> | ||||
| 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> | ||||
| 		<span v-text="ss"></span> | ||||
| 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }" v-if="props.showMs">:</span> | ||||
| 		<span v-text="ms" v-if="props.showMs"></span> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import define from './define'; | ||||
| 
 | ||||
| export default define({ | ||||
| 	name: 'digitalClock', | ||||
| 	props: () => ({ | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		fontSize: { | ||||
| 			type: 'number', | ||||
| 			default: 1.5, | ||||
| 			step: 0.1, | ||||
| 		}, | ||||
| 		showMs: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			clock: null, | ||||
| 			hh: null, | ||||
| 			mm: null, | ||||
| 			ss: null, | ||||
| 			ms: null, | ||||
| 			showColon: true, | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.tick(); | ||||
| 		this.$watch('props.showMs', () => { | ||||
| 			if (this.clock) clearInterval(this.clock); | ||||
| 			this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000); | ||||
| 		}, { immediate: true }); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		tick() { | ||||
| 			const now = new Date(); | ||||
| 			this.hh = now.getHours().toString().padStart(2, '0'); | ||||
| 			this.mm = now.getMinutes().toString().padStart(2, '0'); | ||||
| 			this.ss = now.getSeconds().toString().padStart(2, '0'); | ||||
| 			this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0'); | ||||
| 			this.showColon = now.getSeconds() % 2 === 0; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mkw-digitalClock { | ||||
| 	padding: 16px 0; | ||||
| 	font-family: Lucida Console, Courier, monospace; | ||||
| 	text-align: center; | ||||
| } | ||||
| </style> | ||||
|  | @ -10,3 +10,17 @@ Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default)); | |||
| Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default)); | ||||
| Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default)); | ||||
| Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default)); | ||||
| Vue.component('mkw-digitalClock', () => import('./digital-clock.vue').then(m => m.default)); | ||||
| 
 | ||||
| export const widgets = [ | ||||
| 	'memo', | ||||
| 	'notifications', | ||||
| 	'timeline', | ||||
| 	'calendar', | ||||
| 	'rss', | ||||
| 	'trends', | ||||
| 	'clock', | ||||
| 	'activity', | ||||
| 	'photos', | ||||
| 	'digitalClock', | ||||
| ]; | ||||
|  |  | |||
|  | @ -1,14 +1,12 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<mk-container :show-header="!props.compact"> | ||||
| 		<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template> | ||||
| <mk-container :show-header="props.showHeader"> | ||||
| 	<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template> | ||||
| 
 | ||||
| 		<div class="otgbylcu"> | ||||
| 			<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> | ||||
| 			<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| 	<div class="otgbylcu"> | ||||
| 		<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> | ||||
| 		<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -19,10 +17,12 @@ import define from './define'; | |||
| export default define({ | ||||
| 	name: 'memo', | ||||
| 	props: () => ({ | ||||
| 		compact: false | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	 | ||||
| 	components: { | ||||
| 		MkContainer | ||||
| 	}, | ||||
|  | @ -45,11 +45,6 @@ export default define({ | |||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			this.props.compact = !this.props.compact; | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onChange() { | ||||
| 			this.changed = true; | ||||
| 			clearTimeout(this.timeoutId); | ||||
|  |  | |||
|  | @ -1,13 +1,11 @@ | |||
| <template> | ||||
| <div class="mkw-notifications" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`"> | ||||
| 	<mk-container :show-header="!props.compact" class="container"> | ||||
| 		<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template> | ||||
| <mk-container :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true"> | ||||
| 	<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<x-notifications/> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| 	<div> | ||||
| 		<x-notifications/> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -16,17 +14,19 @@ import MkContainer from '../components/ui/container.vue'; | |||
| import XNotifications from '../components/notifications.vue'; | ||||
| import define from './define'; | ||||
| 
 | ||||
| const basisSteps = [25, 50, 75, 100] | ||||
| const previewHeights = [200, 300, 400, 500] | ||||
| 
 | ||||
| export default define({ | ||||
| 	name: 'notifications', | ||||
| 	props: () => ({ | ||||
| 		compact: false, | ||||
| 		basisStep: 0 | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		height: { | ||||
| 			type: 'number', | ||||
| 			default: 300, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	 | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XNotifications, | ||||
|  | @ -37,47 +37,5 @@ export default define({ | |||
| 			faBell | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		basis(): number { | ||||
| 			return basisSteps[this.props.basisStep] || 25 | ||||
| 		}, | ||||
| 
 | ||||
| 		previewHeight(): number { | ||||
| 			return previewHeights[this.props.basisStep] || 200 | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			if (this.props.basisStep === basisSteps.length - 1) { | ||||
| 				this.props.basisStep = 0 | ||||
| 				this.props.compact = !this.props.compact; | ||||
| 			} else { | ||||
| 				this.props.basisStep += 1 | ||||
| 			} | ||||
| 
 | ||||
| 			this.save(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss"> | ||||
| .mkw-notifications { | ||||
| 	flex-grow: 1; | ||||
| 	flex-shrink: 0; | ||||
| 	min-height: 0; // https://www.gwtcenter.com/min-height-required-on-firefox-flexbox | ||||
| 
 | ||||
| 	.container { | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		height: 100%; | ||||
| 
 | ||||
| 		> div { | ||||
| 			overflow: auto; | ||||
| 			flex-grow: 1; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,19 +1,17 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design === 2"> | ||||
| 		<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template> | ||||
| <mk-container :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent"> | ||||
| 	<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template> | ||||
| 
 | ||||
| 		<div class=""> | ||||
| 			<mk-loading v-if="fetching"/> | ||||
| 			<div v-else :class="$style.stream"> | ||||
| 				<div v-for="(image, i) in images" :key="i" | ||||
| 					:class="$style.img" | ||||
| 					:style="`background-image: url(${thumbnail(image)})`" | ||||
| 				></div> | ||||
| 			</div> | ||||
| 	<div class=""> | ||||
| 		<mk-loading v-if="fetching"/> | ||||
| 		<div v-else :class="$style.stream"> | ||||
| 			<div v-for="(image, i) in images" :key="i" | ||||
| 				:class="$style.img" | ||||
| 				:style="`background-image: url(${thumbnail(image)})`" | ||||
| 			></div> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -25,7 +23,14 @@ import { getStaticImageUrl } from '../scripts/get-static-image-url'; | |||
| export default define({ | ||||
| 	name: 'photos', | ||||
| 	props: () => ({ | ||||
| 		design: 0, | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		transparent: { | ||||
| 			type: 'boolean', | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	components: { | ||||
|  | @ -63,15 +68,6 @@ export default define({ | |||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		func() { | ||||
| 			if (this.props.design === 2) { | ||||
| 				this.props.design = 0; | ||||
| 			} else { | ||||
| 				this.props.design++; | ||||
| 			} | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 
 | ||||
| 		thumbnail(image: any): string { | ||||
| 			return this.$store.state.device.disableShowingAnimatedImages | ||||
| 				? getStaticImageUrl(image.thumbnailUrl) | ||||
|  | @ -82,7 +78,7 @@ export default define({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root[data-melt] { | ||||
| .root[data-transparent] { | ||||
| 	.stream { | ||||
| 		padding: 0; | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,17 +1,15 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<mk-container :show-header="!props.compact"> | ||||
| 		<template #header><fa :icon="faRssSquare"/>RSS</template> | ||||
| 		<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template> | ||||
| <mk-container :show-header="props.showHeader"> | ||||
| 	<template #header><fa :icon="faRssSquare"/>RSS</template> | ||||
| 	<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template> | ||||
| 
 | ||||
| 		<div class="ekmkgxbj"> | ||||
| 			<mk-loading v-if="fetching"/> | ||||
| 			<div class="feed" v-else> | ||||
| 				<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> | ||||
| 			</div> | ||||
| 	<div class="ekmkgxbj"> | ||||
| 		<mk-loading v-if="fetching"/> | ||||
| 		<div class="feed" v-else> | ||||
| 			<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -22,8 +20,14 @@ import define from './define'; | |||
| export default define({ | ||||
| 	name: 'rss', | ||||
| 	props: () => ({ | ||||
| 		compact: false, | ||||
| 		url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews' | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		url: { | ||||
| 			type: 'string', | ||||
| 			default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	components: { | ||||
|  | @ -40,15 +44,12 @@ export default define({ | |||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 		this.clock = setInterval(this.fetch, 60000); | ||||
| 		this.$watch('props.url', this.fetch); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			this.props.compact = !this.props.compact; | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 		fetch() { | ||||
| 			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, { | ||||
| 			}).then(res => { | ||||
|  | @ -58,20 +59,6 @@ export default define({ | |||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 		setting() { | ||||
| 			this.$root.dialog({ | ||||
| 				title: 'URL', | ||||
| 				input: { | ||||
| 					type: 'url', | ||||
| 					default: this.props.url | ||||
| 				} | ||||
| 			}).then(({ canceled, result: url }) => { | ||||
| 				if (canceled) return; | ||||
| 				this.props.url = url; | ||||
| 				this.save(); | ||||
| 				this.fetch(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,24 +1,22 @@ | |||
| <template> | ||||
| <div class="mkw-timeline" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`"> | ||||
| 	<mk-container :show-header="!props.compact" class="container"> | ||||
| 		<template #header> | ||||
| 			<button @click="choose" class="_button"> | ||||
| 				<fa v-if="props.src === 'home'" :icon="faHome"/> | ||||
| 				<fa v-if="props.src === 'local'" :icon="faComments"/> | ||||
| 				<fa v-if="props.src === 'social'" :icon="faShareAlt"/> | ||||
| 				<fa v-if="props.src === 'global'" :icon="faGlobe"/> | ||||
| 				<fa v-if="props.src === 'list'" :icon="faListUl"/> | ||||
| 				<fa v-if="props.src === 'antenna'" :icon="faSatellite"/> | ||||
| 				<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span> | ||||
| 				<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/> | ||||
| 			</button> | ||||
| 		</template> | ||||
| <mk-container :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true"> | ||||
| 	<template #header> | ||||
| 		<button @click="choose" class="_button"> | ||||
| 			<fa v-if="props.src === 'home'" :icon="faHome"/> | ||||
| 			<fa v-if="props.src === 'local'" :icon="faComments"/> | ||||
| 			<fa v-if="props.src === 'social'" :icon="faShareAlt"/> | ||||
| 			<fa v-if="props.src === 'global'" :icon="faGlobe"/> | ||||
| 			<fa v-if="props.src === 'list'" :icon="faListUl"/> | ||||
| 			<fa v-if="props.src === 'antenna'" :icon="faSatellite"/> | ||||
| 			<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span> | ||||
| 			<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/> | ||||
| 		</button> | ||||
| 	</template> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| 	<div> | ||||
| 		<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -28,19 +26,25 @@ import MkContainer from '../components/ui/container.vue'; | |||
| import XTimeline from '../components/timeline.vue'; | ||||
| import define from './define'; | ||||
| 
 | ||||
| const basisSteps = [25, 50, 75, 100] | ||||
| const previewHeights = [200, 300, 400, 500] | ||||
| 
 | ||||
| export default define({ | ||||
| 	name: 'timeline', | ||||
| 	props: () => ({ | ||||
| 		src: 'home', | ||||
| 		list: null, | ||||
| 		compact: false, | ||||
| 		basisStep: 0 | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		src: { | ||||
| 			type: 'string', | ||||
| 			default: 'home', | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 		list: { | ||||
| 			type: 'object', | ||||
| 			default: null, | ||||
| 			hidden: true, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	 | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XTimeline, | ||||
|  | @ -53,28 +57,7 @@ export default define({ | |||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		basis(): number { | ||||
| 			return basisSteps[this.props.basisStep] || 25 | ||||
| 		}, | ||||
| 
 | ||||
| 		previewHeight(): number { | ||||
| 			return previewHeights[this.props.basisStep] || 200 | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			if (this.props.basisStep === basisSteps.length - 1) { | ||||
| 				this.props.basisStep = 0 | ||||
| 				this.props.compact = !this.props.compact; | ||||
| 			} else { | ||||
| 				this.props.basisStep += 1 | ||||
| 			} | ||||
| 
 | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 
 | ||||
| 		async choose(ev) { | ||||
| 			this.menuOpened = true; | ||||
| 			const [antennas, lists] = await Promise.all([ | ||||
|  | @ -129,22 +112,3 @@ export default define({ | |||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss"> | ||||
| .mkw-timeline { | ||||
| 	flex-grow: 1; | ||||
| 	flex-shrink: 0; | ||||
| 	min-height: 0; // https://www.gwtcenter.com/min-height-required-on-firefox-flexbox | ||||
| 
 | ||||
| 	.container { | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		height: 100%; | ||||
| 
 | ||||
| 		> div { | ||||
| 			overflow: auto; | ||||
| 			flex-grow: 1; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,22 +1,20 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<mk-container :show-header="!props.compact"> | ||||
| 		<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template> | ||||
| <mk-container :show-header="props.showHeader"> | ||||
| 	<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template> | ||||
| 
 | ||||
| 		<div class="wbrkwala"> | ||||
| 			<mk-loading v-if="fetching"/> | ||||
| 			<transition-group tag="div" name="chart" class="tags" v-else> | ||||
| 				<div v-for="stat in stats" :key="stat.tag"> | ||||
| 					<div class="tag"> | ||||
| 						<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> | ||||
| 						<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> | ||||
| 					</div> | ||||
| 					<x-chart class="chart" :src="stat.chart"/> | ||||
| 	<div class="wbrkwala"> | ||||
| 		<mk-loading v-if="fetching"/> | ||||
| 		<transition-group tag="div" name="chart" class="tags" v-else> | ||||
| 			<div v-for="stat in stats" :key="stat.tag"> | ||||
| 				<div class="tag"> | ||||
| 					<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> | ||||
| 					<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> | ||||
| 				</div> | ||||
| 			</transition-group> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| </div> | ||||
| 				<x-chart class="chart" :src="stat.chart"/> | ||||
| 			</div> | ||||
| 		</transition-group> | ||||
| 	</div> | ||||
| </mk-container> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -28,7 +26,10 @@ import XChart from './trends.chart.vue'; | |||
| export default define({ | ||||
| 	name: 'hashtags', | ||||
| 	props: () => ({ | ||||
| 		compact: false | ||||
| 		showHeader: { | ||||
| 			type: 'boolean', | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}) | ||||
| }).extend({ | ||||
| 	components: { | ||||
|  | @ -49,10 +50,6 @@ export default define({ | |||
| 		clearInterval(this.clock); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		func() { | ||||
| 			this.props.compact = !this.props.compact; | ||||
| 			this.save(); | ||||
| 		}, | ||||
| 		fetch() { | ||||
| 			this.$root.api('hashtags/trend').then(stats => { | ||||
| 				this.stats = stats; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue