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: "プレイヤーを開く" | enablePlayer: "プレイヤーを開く" | ||||||
| disablePlayer: "プレイヤーを閉じる" | disablePlayer: "プレイヤーを閉じる" | ||||||
| expandTweet: "ツイートを展開する" | expandTweet: "ツイートを展開する" | ||||||
|  | deck: "デッキ" | ||||||
|  | undeck: "デッキ解除" | ||||||
| 
 | 
 | ||||||
| _theme: | _theme: | ||||||
|   explore: "テーマを探す" |   explore: "テーマを探す" | ||||||
|  | @ -651,6 +653,7 @@ _widgets: | ||||||
|   rss: "RSSリーダー" |   rss: "RSSリーダー" | ||||||
|   activity: "アクティビティ" |   activity: "アクティビティ" | ||||||
|   photos: "フォト" |   photos: "フォト" | ||||||
|  |   digitalClock: "デジタル時計" | ||||||
| 
 | 
 | ||||||
| _cw: | _cw: | ||||||
|   hide: "隠す" |   hide: "隠す" | ||||||
|  | @ -1129,3 +1132,15 @@ _notification: | ||||||
|   yourFollowRequestAccepted: "フォローリクエストが承認されました" |   yourFollowRequestAccepted: "フォローリクエストが承認されました" | ||||||
|   youWereInvitedToGroup: "グループに招待されました" |   youWereInvitedToGroup: "グループに招待されました" | ||||||
| 
 | 
 | ||||||
|  | _deck: | ||||||
|  |   alwaysShowMainColumn: "常にメインカラムを表示" | ||||||
|  |   columnAlign: "カラムの寄せ" | ||||||
|  | 
 | ||||||
|  |   _columns: | ||||||
|  |     widgets: "ウィジェット" | ||||||
|  |     notifications: "通知" | ||||||
|  |     tl: "タイムライン" | ||||||
|  |     antenna: "アンテナ" | ||||||
|  |     list: "リスト" | ||||||
|  |     mentions: "あなた宛て" | ||||||
|  |     direct: "ダイレクト" | ||||||
|  |  | ||||||
|  | @ -29,47 +29,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</header> | 	</header> | ||||||
| 
 | 
 | ||||||
| 	<transition name="nav-back"> | 	<x-sidebar ref="nav"/> | ||||||
| 		<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> |  | ||||||
| 
 | 
 | ||||||
| 	<div class="contents" ref="contents" :class="{ wallpaper }"> | 	<div class="contents" ref="contents" :class="{ wallpaper }"> | ||||||
| 		<main ref="main"> | 		<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> | 								<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> | ||||||
| 							</header> | 							</header> | ||||||
| 							<div @click="widgetFunc(widget.id)"> | 							<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> | ||||||
| 						</div> | 						</div> | ||||||
| 					</x-draggable> | 					</x-draggable> | ||||||
| 				</div> | 				</div> | ||||||
| 				<div class="container" v-else> | 				<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> | ||||||
| 			</div> | 			</div> | ||||||
| 		</template> | 		</template> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<div class="buttons"> | 	<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-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-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> | 		<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 { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { ResizeObserver } from '@juggle/resize-observer'; | import { ResizeObserver } from '@juggle/resize-observer'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| import { host, instanceName } from './config'; | import { host } from './config'; | ||||||
| import { search } from './scripts/search'; | import { search } from './scripts/search'; | ||||||
| import { StickySidebar } from './scripts/sticky-sidebar'; | import { StickySidebar } from './scripts/sticky-sidebar'; | ||||||
|  | import { widgets } from './widgets'; | ||||||
|  | import XSidebar from './components/sidebar.vue'; | ||||||
| 
 | 
 | ||||||
| const DESKTOP_THRESHOLD = 1100; | const DESKTOP_THRESHOLD = 1100; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
|  | 		XSidebar, | ||||||
| 		XClock: () => import('./components/header-clock.vue').then(m => m.default), | 		XClock: () => import('./components/header-clock.vue').then(m => m.default), | ||||||
| 		MkButton: () => import('./components/ui/button.vue').then(m => m.default), | 		MkButton: () => import('./components/ui/button.vue').then(m => m.default), | ||||||
| 		XDraggable: () => import('vuedraggable'), | 		XDraggable: () => import('vuedraggable'), | ||||||
|  | @ -152,19 +115,14 @@ export default Vue.extend({ | ||||||
| 		return { | 		return { | ||||||
| 			host: host, | 			host: host, | ||||||
| 			pageKey: 0, | 			pageKey: 0, | ||||||
| 			showNav: false, |  | ||||||
| 			searching: false, | 			searching: false, | ||||||
| 			accounts: [], |  | ||||||
| 			lists: [], |  | ||||||
| 			connection: null, | 			connection: null, | ||||||
| 			searchQuery: '', | 			searchQuery: '', | ||||||
| 			searchWait: false, | 			searchWait: false, | ||||||
| 			widgetsEditMode: false, | 			widgetsEditMode: false, | ||||||
| 			menuDef: this.$store.getters.nav({ |  | ||||||
| 				search: this.search |  | ||||||
| 			}), |  | ||||||
| 			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, | 			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, | ||||||
| 			canBack: false, | 			canBack: false, | ||||||
|  | 			menuDef: this.$store.getters.nav({}), | ||||||
| 			wallpaper: localStorage.getItem('wallpaper') != null, | 			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 | 			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; | 			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 { | 		navIndicated(): boolean { | ||||||
| 			if (!this.$store.getters.isSignedIn) return false; | 			if (!this.$store.getters.isSignedIn) return false; | ||||||
| 			for (const def in this.menuDef) { | 			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; | 				if (this.menuDef[def].indicated) return true; | ||||||
| 			} | 			} | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	watch:{ | 	watch: { | ||||||
| 		$route(to, from) { | 		$route(to, from) { | ||||||
| 			this.pageKey++; | 			this.pageKey++; | ||||||
| 			this.showNav = false; |  | ||||||
| 			this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); | 			this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -245,6 +192,8 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	created() { | 	created() { | ||||||
|  | 		document.documentElement.style.overflowY = 'scroll'; | ||||||
|  | 
 | ||||||
| 		if (this.$store.getters.isSignedIn) { | 		if (this.$store.getters.isSignedIn) { | ||||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); | 			this.connection = this.$root.stream.useSharedConnection('main'); | ||||||
| 			this.connection.on('notification', this.onNotification); | 			this.connection.on('notification', this.onNotification); | ||||||
|  | @ -266,7 +215,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		const adjustTitlePosition = () => { | 		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) { | 			if (left >= 0) { | ||||||
| 				this.$refs.title.style.left = left + 'px'; | 				this.$refs.title.style.left = left + 'px'; | ||||||
| 			} | 			} | ||||||
|  | @ -293,6 +242,10 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
|  | 		showNav() { | ||||||
|  | 			this.$refs.nav.show(); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		attachSticky() { | 		attachSticky() { | ||||||
| 			if (!this.isDesktop) return; | 			if (!this.isDesktop) return; | ||||||
| 			if (this.$store.state.device.fixedWidgetsPosition) 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) { | 		async onNotification(notification) { | ||||||
| 			if (document.visibilityState === 'visible') { | 			if (document.visibilityState === 'visible') { | ||||||
| 				this.$root.stream.send('readNotification', { | 				this.$root.stream.send('readNotification', { | ||||||
|  | @ -540,8 +319,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		widgetFunc(id) { | 		widgetFunc(id) { | ||||||
| 			const w = this.$refs[id][0]; | 			this.$refs[id][0].setting(); | ||||||
| 			if (w.func) w.func(); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onWidgetSort() { | 		onWidgetSort() { | ||||||
|  | @ -549,18 +327,6 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async addWidget(place) { | 		async addWidget(place) { | ||||||
| 			const widgets = [ |  | ||||||
| 				'memo', |  | ||||||
| 				'notifications', |  | ||||||
| 				'timeline', |  | ||||||
| 				'calendar', |  | ||||||
| 				'rss', |  | ||||||
| 				'trends', |  | ||||||
| 				'clock', |  | ||||||
| 				'activity', |  | ||||||
| 				'photos', |  | ||||||
| 			]; |  | ||||||
| 
 |  | ||||||
| 			const { canceled, result: widget } = await this.$root.dialog({ | 			const { canceled, result: widget } = await this.$root.dialog({ | ||||||
| 				type: null, | 				type: null, | ||||||
| 				title: this.$t('chooseWidget'), | 				title: this.$t('chooseWidget'), | ||||||
|  | @ -594,36 +360,14 @@ export default Vue.extend({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <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 { | .mk-app { | ||||||
| 	$header-height: 60px; | 	$header-height: 60px; | ||||||
| 	$nav-width: 250px; | 	$nav-width: 250px; // TODO: どこかに集約したい | ||||||
| 	$nav-icon-only-width: 80px; | 	$nav-icon-only-width: 80px; // TODO: どこかに集約したい | ||||||
| 	$main-width: 670px; | 	$main-width: 670px; | ||||||
| 	$ui-font-size: 1em; | 	$ui-font-size: 1em; // TODO: どこかに集約したい | ||||||
| 	$nav-icon-only-threshold: 1279px; | 	$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい | ||||||
| 	$nav-hide-threshold: 650px; | 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい | ||||||
| 	$header-sub-hide-threshold: 1090px; | 	$header-sub-hide-threshold: 1090px; | ||||||
| 	$left-widgets-hide-threshold: 1600px; | 	$left-widgets-hide-threshold: 1600px; | ||||||
| 	$right-widgets-hide-threshold: 1090px; | 	$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 { | 	> .contents { | ||||||
| 		display: flex; | 		display: flex; | ||||||
| 		margin: 0 auto; | 		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 { | 	> img { | ||||||
| 		vertical-align: bottom; | 		vertical-align: bottom; | ||||||
| 		height: 150px; | 		height: 128px; | ||||||
| 		margin-bottom: 16px; | 		margin-bottom: 16px; | ||||||
| 		border-radius: 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> | <template> | ||||||
| <div class="mk-modal" v-hotkey.global="keymap"> | <div class="mk-modal" v-hotkey.global="keymap"> | ||||||
| 	<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear> | 	<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> | ||||||
| 	<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> | 	<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> | 	</transition> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -14,6 +14,11 @@ import Vue from 'vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	props: { | 	props: { | ||||||
|  | 		canClose: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  |  | ||||||
|  | @ -54,7 +54,6 @@ export default Vue.extend({ | ||||||
| 		margin: 0 .5em 0 0; | 		margin: 0 .5em 0 0; | ||||||
| 		padding: 0; | 		padding: 0; | ||||||
| 		overflow: hidden; | 		overflow: hidden; | ||||||
| 		color: var(--noteHeaderName); |  | ||||||
| 		font-size: 1em; | 		font-size: 1em; | ||||||
| 		font-weight: bold; | 		font-weight: bold; | ||||||
| 		text-decoration: none; | 		text-decoration: none; | ||||||
|  |  | ||||||
|  | @ -724,61 +724,6 @@ export default Vue.extend({ | ||||||
| 	transition: box-shadow 0.1s ease; | 	transition: box-shadow 0.1s ease; | ||||||
| 	overflow: hidden; | 	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 { | 	&:focus { | ||||||
| 		outline: none; | 		outline: none; | ||||||
| 		box-shadow: 0 0 0 3px var(--focus); | 		box-shadow: 0 0 0 3px var(--focus); | ||||||
|  | @ -797,10 +742,6 @@ export default Vue.extend({ | ||||||
| 		white-space: pre; | 		white-space: pre; | ||||||
| 		color: #d28a3f; | 		color: #d28a3f; | ||||||
| 
 | 
 | ||||||
| 		@media (max-width: 450px) { |  | ||||||
| 			padding: 8px 16px 0 16px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> [data-icon] { | 		> [data-icon] { | ||||||
| 			margin-right: 4px; | 			margin-right: 4px; | ||||||
| 		} | 		} | ||||||
|  | @ -985,5 +926,64 @@ export default Vue.extend({ | ||||||
| 	> .reply { | 	> .reply { | ||||||
| 		border-top: solid 1px var(--divider); | 		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> | </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 | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 		list: { | 		list: { | ||||||
|  | 			type: String, | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
| 		antenna: { | 		antenna: { | ||||||
|  | 			type: String, | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
| 		sound: { | 		sound: { | ||||||
|  | @ -53,6 +55,8 @@ export default Vue.extend({ | ||||||
| 			const _note = JSON.parse(JSON.stringify(note));	// deepcopy | 			const _note = JSON.parse(JSON.stringify(note));	// deepcopy | ||||||
| 			(this.$refs.tl as any).prepend(_note); | 			(this.$refs.tl as any).prepend(_note); | ||||||
| 
 | 
 | ||||||
|  | 			this.$emit('note'); | ||||||
|  | 
 | ||||||
| 			if (this.sound) { | 			if (this.sound) { | ||||||
| 				this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note'); | 				this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note'); | ||||||
| 			} | 			} | ||||||
|  | @ -77,10 +81,10 @@ export default Vue.extend({ | ||||||
| 		if (this.src == 'antenna') { | 		if (this.src == 'antenna') { | ||||||
| 			endpoint = 'antennas/notes'; | 			endpoint = 'antennas/notes'; | ||||||
| 			this.query = { | 			this.query = { | ||||||
| 				antennaId: this.antenna.id | 				antennaId: this.antenna | ||||||
| 			}; | 			}; | ||||||
| 			this.connection = this.$root.stream.connectToChannel('antenna', { | 			this.connection = this.$root.stream.connectToChannel('antenna', { | ||||||
| 				antennaId: this.antenna.id | 				antennaId: this.antenna | ||||||
| 			}); | 			}); | ||||||
| 			this.connection.on('note', prepend); | 			this.connection.on('note', prepend); | ||||||
| 		} else if (this.src == 'home') { | 		} else if (this.src == 'home') { | ||||||
|  | @ -106,10 +110,10 @@ export default Vue.extend({ | ||||||
| 		} else if (this.src == 'list') { | 		} else if (this.src == 'list') { | ||||||
| 			endpoint = 'notes/user-list-timeline'; | 			endpoint = 'notes/user-list-timeline'; | ||||||
| 			this.query = { | 			this.query = { | ||||||
| 				listId: this.list.id | 				listId: this.list | ||||||
| 			}; | 			}; | ||||||
| 			this.connection = this.$root.stream.connectToChannel('userList', { | 			this.connection = this.$root.stream.connectToChannel('userList', { | ||||||
| 				listId: this.list.id | 				listId: this.list | ||||||
| 			}); | 			}); | ||||||
| 			this.connection.on('note', prepend); | 			this.connection.on('note', prepend); | ||||||
| 			this.connection.on('userAdded', onUserAdded); | 			this.connection.on('userAdded', onUserAdded); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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"> | 	<header v-if="showHeader"> | ||||||
| 		<div class="title"><slot name="header"></slot></div> | 		<div class="title"><slot name="header"></slot></div> | ||||||
| 		<slot name="func"></slot> | 		<slot name="func"></slot> | ||||||
|  | @ -47,6 +47,11 @@ export default Vue.extend({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: true | 			default: true | ||||||
| 		}, | 		}, | ||||||
|  | 		scrollable: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | @ -107,10 +112,19 @@ export default Vue.extend({ | ||||||
| 		box-shadow: none !important; | 		box-shadow: none !important; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	&.scrollable { | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 
 | ||||||
|  | 		> div { | ||||||
|  | 			overflow: auto; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	> header { | 	> header { | ||||||
| 		position: relative; | 		position: relative; | ||||||
| 		box-shadow: 0 1px 0 0 var(--panelHeaderDivider); | 		box-shadow: 0 1px 0 0 var(--panelHeaderDivider); | ||||||
| 		z-index: 1; | 		z-index: 2; | ||||||
| 		background: var(--panelHeaderBg); | 		background: var(--panelHeaderBg); | ||||||
| 		color: var(--panelHeaderFg); | 		color: var(--panelHeaderFg); | ||||||
| 
 | 
 | ||||||
|  | @ -118,10 +132,6 @@ export default Vue.extend({ | ||||||
| 			margin: 0; | 			margin: 0; | ||||||
| 			padding: 12px 16px; | 			padding: 12px 16px; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				padding: 8px 10px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> [data-icon] { | 			> [data-icon] { | ||||||
| 				margin-right: 6px; | 				margin-right: 6px; | ||||||
| 			} | 			} | ||||||
|  | @ -141,5 +151,21 @@ export default Vue.extend({ | ||||||
| 			height: 100%; | 			height: 100%; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	&.max-width_500px { | ||||||
|  | 		> header { | ||||||
|  | 			> .title { | ||||||
|  | 				padding: 8px 10px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ._forceContainerFull_ .ukygtjoj { | ||||||
|  | 	> header { | ||||||
|  | 		> .title { | ||||||
|  | 			padding: 12px 16px !important; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ | ||||||
| 				:pattern="pattern" | 				:pattern="pattern" | ||||||
| 				:autocomplete="autocomplete" | 				:autocomplete="autocomplete" | ||||||
| 				:spellcheck="spellcheck" | 				:spellcheck="spellcheck" | ||||||
|  | 				:step="step" | ||||||
| 				@focus="focused = true" | 				@focus="focused = true" | ||||||
| 				@blur="focused = false" | 				@blur="focused = false" | ||||||
| 				@keydown="$emit('keydown', $event)" | 				@keydown="$emit('keydown', $event)" | ||||||
|  | @ -36,6 +37,7 @@ | ||||||
| 				:pattern="pattern" | 				:pattern="pattern" | ||||||
| 				:autocomplete="autocomplete" | 				:autocomplete="autocomplete" | ||||||
| 				:spellcheck="spellcheck" | 				:spellcheck="spellcheck" | ||||||
|  | 				:step="step" | ||||||
| 				@focus="focused = true" | 				@focus="focused = true" | ||||||
| 				@blur="focused = false" | 				@blur="focused = false" | ||||||
| 				@keydown="$emit('keydown', $event)" | 				@keydown="$emit('keydown', $event)" | ||||||
|  | @ -114,6 +116,9 @@ export default Vue.extend({ | ||||||
| 		spellcheck: { | 		spellcheck: { | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
|  | 		step: { | ||||||
|  | 			required: false | ||||||
|  | 		}, | ||||||
| 		debounce: { | 		debounce: { | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
|  | @ -164,7 +169,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 		v(v) { | 		v(v) { | ||||||
| 			if (this.type === 'number') { | 			if (this.type === 'number') { | ||||||
| 				this.$emit('input', parseInt(v, 10)); | 				this.$emit('input', parseFloat(v)); | ||||||
| 			} else { | 			} else { | ||||||
| 				this.$emit('input', v); | 				this.$emit('input', v); | ||||||
| 			} | 			} | ||||||
|  | @ -297,7 +302,7 @@ export default Vue.extend({ | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | ||||||
| 			transition-duration: 0.3s; | 			transition-duration: 0.3s; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			line-height: 32px; | 			line-height: 32px; | ||||||
| 			color: var(--inputLabel); | 			color: var(--inputLabel); | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
|  | @ -312,7 +317,7 @@ export default Vue.extend({ | ||||||
| 			top: -17px; | 			top: -17px; | ||||||
| 			left: 0 !important; | 			left: 0 !important; | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			line-height: 32px; | 			line-height: 32px; | ||||||
| 			color: var(--inputLabel); | 			color: var(--inputLabel); | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
|  | @ -343,7 +348,7 @@ export default Vue.extend({ | ||||||
| 			padding: 0; | 			padding: 0; | ||||||
| 			font: inherit; | 			font: inherit; | ||||||
| 			font-weight: normal; | 			font-weight: normal; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			line-height: $height; | 			line-height: $height; | ||||||
| 			color: var(--inputText); | 			color: var(--inputText); | ||||||
| 			background: transparent; | 			background: transparent; | ||||||
|  | @ -364,7 +369,7 @@ export default Vue.extend({ | ||||||
| 			position: absolute; | 			position: absolute; | ||||||
| 			z-index: 1; | 			z-index: 1; | ||||||
| 			top: 0; | 			top: 0; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			line-height: 32px; | 			line-height: 32px; | ||||||
| 			color: var(--inputLabel); | 			color: var(--inputLabel); | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
|  |  | ||||||
|  | @ -135,7 +135,7 @@ export default Vue.extend({ | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | ||||||
| 			transition-duration: 0.3s; | 			transition-duration: 0.3s; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			line-height: 32px; | 			line-height: 32px; | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 			//will-change transform | 			//will-change transform | ||||||
|  | @ -150,7 +150,7 @@ export default Vue.extend({ | ||||||
| 			padding: 0; | 			padding: 0; | ||||||
| 			font: inherit; | 			font: inherit; | ||||||
| 			font-weight: normal; | 			font-weight: normal; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			height: 32px; | 			height: 32px; | ||||||
| 			background: none; | 			background: none; | ||||||
| 			border: none; | 			border: none; | ||||||
|  | @ -170,7 +170,7 @@ export default Vue.extend({ | ||||||
| 			display: block; | 			display: block; | ||||||
| 			align-self: center; | 			align-self: center; | ||||||
| 			justify-self: center; | 			justify-self: center; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			line-height: 32px; | 			line-height: 32px; | ||||||
| 			color: rgba(#000, 0.54); | 			color: rgba(#000, 0.54); | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 	role="switch" | 	role="switch" | ||||||
| 	:aria-checked="checked" | 	:aria-checked="checked" | ||||||
| 	:aria-disabled="disabled" | 	:aria-disabled="disabled" | ||||||
| 	@click="toggle" | 	@click.prevent="toggle" | ||||||
| > | > | ||||||
| 	<input | 	<input | ||||||
| 		type="checkbox" | 		type="checkbox" | ||||||
|  |  | ||||||
|  | @ -133,7 +133,7 @@ export default Vue.extend({ | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | ||||||
| 			transition-duration: 0.3s; | 			transition-duration: 0.3s; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			line-height: 32px; | 			line-height: 32px; | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 			//will-change transform | 			//will-change transform | ||||||
|  | @ -151,7 +151,7 @@ export default Vue.extend({ | ||||||
| 			box-sizing: border-box; | 			box-sizing: border-box; | ||||||
| 			font: inherit; | 			font: inherit; | ||||||
| 			font-weight: normal; | 			font-weight: normal; | ||||||
| 			font-size: 16px; | 			font-size: 1em; | ||||||
| 			background: transparent; | 			background: transparent; | ||||||
| 			border: none; | 			border: none; | ||||||
| 			border-radius: 0; | 			border-radius: 0; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown" :style="{ width: `${width}px`, height: `${height}px` }"> | ||||||
| 		<div class="header"> | 		<div class="header"> | ||||||
| 			<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button> | 			<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button> | ||||||
|  | @ -57,6 +57,11 @@ export default Vue.extend({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: 400 | 			default: 400 | ||||||
| 		}, | 		}, | ||||||
|  | 		canClose: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
|  |  | ||||||
|  | @ -18,3 +18,4 @@ export const getLocale = async () => Object.fromEntries((await entries(clientDb. | ||||||
| export const version = _VERSION_; | export const version = _VERSION_; | ||||||
| export const env = _ENV_; | export const env = _ENV_; | ||||||
| export const instanceName = siteName === 'Misskey' ? null : siteName; | 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'; | import Vue from 'vue'; | ||||||
|  | @ -12,11 +12,13 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; | ||||||
| 
 | 
 | ||||||
| import VueHotkey from './scripts/hotkey'; | import VueHotkey from './scripts/hotkey'; | ||||||
| import App from './app.vue'; | import App from './app.vue'; | ||||||
|  | import Deck from './deck.vue'; | ||||||
| import MiOS from './mios'; | 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 PostFormDialog from './components/post-form-dialog.vue'; | ||||||
| import Dialog from './components/dialog.vue'; | import Dialog from './components/dialog.vue'; | ||||||
| import Menu from './components/menu.vue'; | import Menu from './components/menu.vue'; | ||||||
|  | import Form from './components/form-window.vue'; | ||||||
| import { router } from './router'; | import { router } from './router'; | ||||||
| import { applyTheme, lightTheme } from './scripts/theme'; | import { applyTheme, lightTheme } from './scripts/theme'; | ||||||
| import { isDeviceDarkmode } from './scripts/is-device-darkmode'; | 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
 | 				i18n // TODO: 消せないか考える SEE: https://github.com/syuilo/misskey/pull/6396#discussion_r429511030
 | ||||||
| 			}; | 			}; | ||||||
| 		}, | 		}, | ||||||
|  | 		// TODO: ここらへんのメソッド全部Vuexに移したい
 | ||||||
| 		methods: { | 		methods: { | ||||||
| 			api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }), | 			api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }), | ||||||
| 			signout: os.signout, | 			signout: os.signout, | ||||||
|  | @ -194,6 +197,13 @@ os.init(async () => { | ||||||
| 				}); | 				}); | ||||||
| 				return p; | 				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) { | 			post(opts, cb) { | ||||||
| 				if (!this.$store.getters.isSignedIn) return; | 				if (!this.$store.getters.isSignedIn) return; | ||||||
| 				const vm = this.new(PostFormDialog, opts); | 				const vm = this.new(PostFormDialog, opts); | ||||||
|  | @ -210,11 +220,9 @@ os.init(async () => { | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		router: router, | 		router: router, | ||||||
| 		render: createEl => createEl(App) | 		render: createEl => createEl(deckmode ? Deck : App) | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	os.app = app; |  | ||||||
| 
 |  | ||||||
| 	// マウント
 | 	// マウント
 | ||||||
| 	app.$mount('#app'); | 	app.$mount('#app'); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| // TODO: このファイル消したい
 | // TODO: このファイル消したい
 | ||||||
| 
 | 
 | ||||||
| import autobind from 'autobind-decorator'; | import autobind from 'autobind-decorator'; | ||||||
| import Vue from 'vue'; |  | ||||||
| import { EventEmitter } from 'eventemitter3'; | import { EventEmitter } from 'eventemitter3'; | ||||||
| 
 | 
 | ||||||
| import { apiUrl, version } from './config'; | import { apiUrl, version } from './config'; | ||||||
|  | @ -14,8 +13,6 @@ import store from './store'; | ||||||
|  * Misskey Operating System |  * Misskey Operating System | ||||||
|  */ |  */ | ||||||
| export default class MiOS extends EventEmitter { | export default class MiOS extends EventEmitter { | ||||||
| 	public app: Vue; |  | ||||||
| 
 |  | ||||||
| 	public store: ReturnType<typeof store>; | 	public store: ReturnType<typeof store>; | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
| 	<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/> | 	<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-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> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,14 +15,15 @@ | ||||||
| 
 | 
 | ||||||
| 		<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/> | 		<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"/> | 		<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> | 		<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"/> | 		<hr v-if="showPrev"/> | ||||||
| 		<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/> | 		<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/> | ||||||
| 	</div> | 	</div> | ||||||
|  | 
 | ||||||
|  | 	<div v-if="error"> | ||||||
|  | 		<mk-error @retry="fetch()"/> | ||||||
|  | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -51,6 +51,20 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</section> | 	</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"> | 	<section class="_card"> | ||||||
| 		<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div> | 		<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div> | ||||||
| 		<div class="_content"> | 		<div class="_content"> | ||||||
|  | @ -93,7 +107,7 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | 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 MkButton from '../../components/ui/button.vue'; | ||||||
| import MkSwitch from '../../components/ui/switch.vue'; | import MkSwitch from '../../components/ui/switch.vue'; | ||||||
| import MkSelect from '../../components/ui/select.vue'; | import MkSelect from '../../components/ui/select.vue'; | ||||||
|  | @ -145,7 +159,7 @@ export default Vue.extend({ | ||||||
| 			lang: localStorage.getItem('lang'), | 			lang: localStorage.getItem('lang'), | ||||||
| 			fontSize: localStorage.getItem('fontSize'), | 			fontSize: localStorage.getItem('fontSize'), | ||||||
| 			sounds, | 			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 }); } | 			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: { | 		sfxVolume: { | ||||||
| 			get() { return this.$store.state.device.sfxVolume; }, | 			get() { return this.$store.state.device.sfxVolume; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); } | 			set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <div class="kjeftjfm"> | <div class="kjeftjfm" v-size="[{ max: 500 }]"> | ||||||
| 	<div class="with"> | 	<div class="with"> | ||||||
| 		<button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button> | 		<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> | 		<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button> | ||||||
|  | @ -60,10 +60,6 @@ export default Vue.extend({ | ||||||
| 		display: flex; | 		display: flex; | ||||||
| 		margin-bottom: var(--margin); | 		margin-bottom: var(--margin); | ||||||
| 
 | 
 | ||||||
| 		@media (max-width: 500px) { |  | ||||||
| 			font-size: 80%; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> button { | 		> button { | ||||||
| 			flex: 1; | 			flex: 1; | ||||||
| 			padding: 11px 8px 8px 8px; | 			padding: 11px 8px 8px 8px; | ||||||
|  | @ -75,5 +71,11 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	&.max-width_500px { | ||||||
|  | 		> .with { | ||||||
|  | 			font-size: 80%; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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="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> | 	<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 MkRemoteCaution from '../../components/remote-caution.vue'; | ||||||
| import Progress from '../../scripts/loading'; | import Progress from '../../scripts/loading'; | ||||||
| import parseAcct from '../../../misc/acct/parse'; | import parseAcct from '../../../misc/acct/parse'; | ||||||
|  | import { getScrollPosition } from '../../scripts/scroll'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -168,12 +169,8 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		window.requestAnimationFrame(this.parallaxLoop); | 		window.requestAnimationFrame(this.parallaxLoop); | ||||||
| 		window.addEventListener('scroll', this.parallax, { passive: true }); |  | ||||||
| 		document.addEventListener('touchmove', this.parallax, { passive: true }); |  | ||||||
| 		this.$once('hook:beforeDestroy', () => { | 		this.$once('hook:beforeDestroy', () => { | ||||||
| 			window.cancelAnimationFrame(this.parallaxAnimationId); | 			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; | 			const banner = this.$refs.banner as any; | ||||||
| 			if (banner == null) return; | 			if (banner == null) return; | ||||||
| 
 | 
 | ||||||
| 			const top = window.scrollY; | 			const top = getScrollPosition(this.$el); | ||||||
| 
 | 
 | ||||||
| 			if (top < 0) return; | 			if (top < 0) return; | ||||||
| 
 | 
 | ||||||
|  | @ -219,7 +216,6 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mk-user-page { | .mk-user-page { | ||||||
| 
 |  | ||||||
| 	> .punished { | 	> .punished { | ||||||
| 		font-size: 0.8em; | 		font-size: 0.8em; | ||||||
| 		padding: 16px; | 		padding: 16px; | ||||||
|  | @ -237,10 +233,6 @@ export default Vue.extend({ | ||||||
| 			background-size: cover; | 			background-size: cover; | ||||||
| 			background-position: center; | 			background-position: center; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				height: 140px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .banner { | 			> .banner { | ||||||
| 				height: 100%; | 				height: 100%; | ||||||
| 				background-color: #4c5e6d; | 				background-color: #4c5e6d; | ||||||
|  | @ -257,10 +249,6 @@ export default Vue.extend({ | ||||||
| 				width: 100%; | 				width: 100%; | ||||||
| 				height: 78px; | 				height: 78px; | ||||||
| 				background: linear-gradient(transparent, rgba(#000, 0.7)); | 				background: linear-gradient(transparent, rgba(#000, 0.7)); | ||||||
| 
 |  | ||||||
| 				@media (max-width: 500px) { |  | ||||||
| 					display: none; |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			> .followed { | 			> .followed { | ||||||
|  | @ -308,10 +296,6 @@ export default Vue.extend({ | ||||||
| 				box-sizing: border-box; | 				box-sizing: border-box; | ||||||
| 				color: #fff; | 				color: #fff; | ||||||
| 
 | 
 | ||||||
| 				@media (max-width: 500px) { |  | ||||||
| 					display: none; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .name { | 				> .name { | ||||||
| 					display: block; | 					display: block; | ||||||
| 					margin: 0; | 					margin: 0; | ||||||
|  | @ -343,10 +327,6 @@ export default Vue.extend({ | ||||||
| 			font-weight: bold; | 			font-weight: bold; | ||||||
| 			border-bottom: solid 1px var(--divider); | 			border-bottom: solid 1px var(--divider); | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				display: block; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .bottom { | 			> .bottom { | ||||||
| 				> * { | 				> * { | ||||||
| 					display: inline-block; | 					display: inline-block; | ||||||
|  | @ -365,26 +345,12 @@ export default Vue.extend({ | ||||||
| 			width: 120px; | 			width: 120px; | ||||||
| 			height: 120px; | 			height: 120px; | ||||||
| 			box-shadow: 1px 1px 3px rgba(#000, 0.2); | 			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 { | 		> .description { | ||||||
| 			padding: 24px 24px 24px 154px; | 			padding: 24px 24px 24px 154px; | ||||||
| 			font-size: 0.95em; | 			font-size: 0.95em; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				padding: 16px; |  | ||||||
| 				text-align: center; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .empty { | 			> .empty { | ||||||
| 				margin: 0; | 				margin: 0; | ||||||
| 				opacity: 0.5; | 				opacity: 0.5; | ||||||
|  | @ -396,10 +362,6 @@ export default Vue.extend({ | ||||||
| 			font-size: 0.9em; | 			font-size: 0.9em; | ||||||
| 			border-top: solid 1px var(--divider); | 			border-top: solid 1px var(--divider); | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				padding: 16px; |  | ||||||
| 			} |  | ||||||
| 		 |  | ||||||
| 			> .field { | 			> .field { | ||||||
| 				display: flex; | 				display: flex; | ||||||
| 				padding: 0; | 				padding: 0; | ||||||
|  | @ -436,10 +398,6 @@ export default Vue.extend({ | ||||||
| 			padding: 24px; | 			padding: 24px; | ||||||
| 			border-top: solid 1px var(--divider); | 			border-top: solid 1px var(--divider); | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				padding: 16px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> a { | 			> a { | ||||||
| 				flex: 1; | 				flex: 1; | ||||||
| 				text-align: center; | 				text-align: center; | ||||||
|  | @ -473,5 +431,47 @@ export default Vue.extend({ | ||||||
| 	> .content { | 	> .content { | ||||||
| 		margin-bottom: var(--margin); | 		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> | </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, | 			moreFetching: false, | ||||||
| 			inited: false, | 			inited: false, | ||||||
| 			more: false, | 			more: false, | ||||||
| 			backed: false, | 			backed: false, // 遡り中か否か
 | ||||||
| 			isBackTop: false, | 			isBackTop: false, | ||||||
| 			ilObserver: new IntersectionObserver( | 			ilObserver: new IntersectionObserver( | ||||||
| 				(entries) => entries.some((entry) => entry.isIntersecting) | 				(entries) => entries.some((entry) => entry.isIntersecting) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| export function getScrollContainer(el: Element | null): Element | null { | export function getScrollContainer(el: Element | null): Element | null { | ||||||
| 	if (el == null || el.tagName === 'BODY') return null; | 	if (el == null || el.tagName === 'BODY') return null; | ||||||
| 	const style = window.getComputedStyle(el); | 	const overflow = window.getComputedStyle(el).getPropertyValue('overflow'); | ||||||
| 	if (style.getPropertyValue('overflow') === 'auto') { | 	if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる
 | ||||||
| 		return el; | 		return el; | ||||||
| 	} else { | 	} else { | ||||||
| 		return getScrollContainer(el.parentElement); | 		return getScrollContainer(el.parentElement); | ||||||
|  |  | ||||||
|  | @ -1,9 +1,10 @@ | ||||||
| import Vuex from 'vuex'; | import Vuex from 'vuex'; | ||||||
| import createPersistedState from 'vuex-persistedstate'; | import createPersistedState from 'vuex-persistedstate'; | ||||||
| import * as nestedProperty from 'nested-property'; | 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 { 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 = { | export const defaultSettings = { | ||||||
| 	tutorial: 0, | 	tutorial: 0, | ||||||
|  | @ -35,7 +36,13 @@ export const defaultDeviceUserSettings = { | ||||||
| 		'explore', | 		'explore', | ||||||
| 		'announcements', | 		'announcements', | ||||||
| 		'search', | 		'search', | ||||||
|  | 		'-', | ||||||
|  | 		'deck', | ||||||
| 	], | 	], | ||||||
|  | 	deck: { | ||||||
|  | 		columns: [], | ||||||
|  | 		layout: [], | ||||||
|  | 	}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const defaultDeviceSettings = { | export const defaultDeviceSettings = { | ||||||
|  | @ -50,6 +57,7 @@ export const defaultDeviceSettings = { | ||||||
| 	darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1', | 	darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1', | ||||||
| 	lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37', | 	lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37', | ||||||
| 	darkMode: false, | 	darkMode: false, | ||||||
|  | 	deckMode: false, | ||||||
| 	syncDeviceDarkMode: true, | 	syncDeviceDarkMode: true, | ||||||
| 	animation: true, | 	animation: true, | ||||||
| 	animatedMfm: true, | 	animatedMfm: true, | ||||||
|  | @ -60,6 +68,9 @@ export const defaultDeviceSettings = { | ||||||
| 	fixedWidgetsPosition: false, | 	fixedWidgetsPosition: false, | ||||||
| 	roomGraphicsQuality: 'medium', | 	roomGraphicsQuality: 'medium', | ||||||
| 	roomUseOrthographicCamera: true, | 	roomUseOrthographicCamera: true, | ||||||
|  | 	deckColumnAlign: 'left', | ||||||
|  | 	deckAlwaysShowMainColumn: true, | ||||||
|  | 	deckMainColumnPlace: 'left', | ||||||
| 	sfxVolume: 0.3, | 	sfxVolume: 0.3, | ||||||
| 	sfxNote: 'syuilo/down', | 	sfxNote: 'syuilo/down', | ||||||
| 	sfxNoteMy: 'syuilo/up', | 	sfxNoteMy: 'syuilo/up', | ||||||
|  | @ -197,6 +208,14 @@ export default () => new Vuex.Store({ | ||||||
| 				get show() { return getters.isSignedIn; }, | 				get show() { return getters.isSignedIn; }, | ||||||
| 				get to() { return `/@${state.i.username}/room`; }, | 				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; | 						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 { | :root { | ||||||
| 	--radius: 8px; | 	--radius: 8px; | ||||||
| 	--marginFull: 16px; | 	--marginFull: 16px; | ||||||
| 	--marginHalf: 8px; | 	--marginHalf: 10px; | ||||||
| 
 | 
 | ||||||
| 	--margin: var(--marginFull); | 	--margin: var(--marginFull); | ||||||
| 
 | 
 | ||||||
|  | @ -25,7 +25,6 @@ html { | ||||||
| 	background-position: center; | 	background-position: center; | ||||||
| 	color: var(--fg); | 	color: var(--fg); | ||||||
| 	overflow: auto; | 	overflow: auto; | ||||||
| 	overflow-y: scroll; |  | ||||||
| 
 | 
 | ||||||
| 	&, * { | 	&, * { | ||||||
| 		scrollbar-color: var(--scrollbarHandle) var(--panel); | 		scrollbar-color: var(--scrollbarHandle) var(--panel); | ||||||
|  | @ -278,13 +277,14 @@ hr { | ||||||
| 
 | 
 | ||||||
| ._panel { | ._panel { | ||||||
| 	position: relative; | 	position: relative; | ||||||
|  | 	z-index: 1; | ||||||
| 	background: var(--panel); | 	background: var(--panel); | ||||||
| 	border-radius: var(--radius); | 	border-radius: var(--radius); | ||||||
| 	box-shadow: 0 0 0 1px var(--panelBorder); | 	box-shadow: 0 0 0 1px var(--panelBorder); | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ._widget ._list_ ._panel { | ._close_ ._list_ > * { | ||||||
| 	box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider); | 	box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider); | ||||||
| 	border-radius: 0; | 	border-radius: 0; | ||||||
| 	margin: 0 !important; | 	margin: 0 !important; | ||||||
|  | @ -348,31 +348,6 @@ hr { | ||||||
| 		& + ._content { | 		& + ._content { | ||||||
| 			border-top: solid 1px var(--divider); | 			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 { | 	> ._footer { | ||||||
|  | @ -385,6 +360,21 @@ hr { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ._narrow_ ._card { | ||||||
|  | 	> ._title { | ||||||
|  | 		padding: 16px; | ||||||
|  | 		font-size: 1em; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> ._content { | ||||||
|  | 		padding: 16px; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> ._footer { | ||||||
|  | 		padding: 16px; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| ._fullinfo { | ._fullinfo { | ||||||
| 	padding: 64px 32px; | 	padding: 64px 32px; | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
|  |  | ||||||
|  | @ -26,8 +26,8 @@ | ||||||
| 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | ||||||
| 		panelBorder: 'rgba(0, 0, 0, 0)', | 		panelBorder: 'rgba(0, 0, 0, 0)', | ||||||
| 		shadow: 'rgba(0, 0, 0, 0.1)', | 		shadow: 'rgba(0, 0, 0, 0.1)', | ||||||
| 		header: 'rgba(20, 20, 20, 0.75)', | 		header: ':alpha<0.7<@bg', | ||||||
| 		navBg: '@panel', | 		navBg: '@bg', | ||||||
| 		navFg: '@fg', | 		navFg: '@fg', | ||||||
| 		navHoverFg: ':lighten<17<@fg', | 		navHoverFg: ':lighten<17<@fg', | ||||||
| 		navActive: '@accent', | 		navActive: '@accent', | ||||||
|  | @ -58,6 +58,7 @@ | ||||||
| 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', | 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', | ||||||
| 		badge: '#31b1ce', | 		badge: '#31b1ce', | ||||||
| 		messageBg: ':lighten<5<@bg', | 		messageBg: ':lighten<5<@bg', | ||||||
|  | 		deckColumnBorder: ':lighten<10<@panel', | ||||||
| 		X1: ':alpha<0<@bg', | 		X1: ':alpha<0<@bg', | ||||||
| 		X2: ':darken<2<@panel', | 		X2: ':darken<2<@panel', | ||||||
| 		X3: 'rgba(255, 255, 255, 0.05)', | 		X3: 'rgba(255, 255, 255, 0.05)', | ||||||
|  |  | ||||||
|  | @ -26,8 +26,8 @@ | ||||||
| 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | ||||||
| 		panelBorder: 'rgba(0, 0, 0, 0)', | 		panelBorder: 'rgba(0, 0, 0, 0)', | ||||||
| 		shadow: 'rgba(0, 0, 0, 0.1)', | 		shadow: 'rgba(0, 0, 0, 0.1)', | ||||||
| 		header: 'rgba(255, 255, 255, 0.75)', | 		header: ':alpha<0.7<@bg', | ||||||
| 		navBg: '@panel', | 		navBg: '@bg', | ||||||
| 		navFg: '@fg', | 		navFg: '@fg', | ||||||
| 		navHoverFg: ':darken<17<@fg', | 		navHoverFg: ':darken<17<@fg', | ||||||
| 		navActive: '@accent', | 		navActive: '@accent', | ||||||
|  | @ -58,6 +58,7 @@ | ||||||
| 		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', | 		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', | ||||||
| 		badge: '#31b1ce', | 		badge: '#31b1ce', | ||||||
| 		messageBg: '@panel', | 		messageBg: '@panel', | ||||||
|  | 		deckColumnBorder: ':darken<20<@panel', | ||||||
| 		X1: ':alpha<0<@bg', | 		X1: ':alpha<0<@bg', | ||||||
| 		X2: ':darken<2<@panel', | 		X2: ':darken<2<@panel', | ||||||
| 		X3: 'rgba(0, 0, 0, 0.05)', | 		X3: 'rgba(0, 0, 0, 0.05)', | ||||||
|  |  | ||||||
|  | @ -13,5 +13,6 @@ | ||||||
| 		panelHeaderDivider: '@divider', | 		panelHeaderDivider: '@divider', | ||||||
| 		panelBorder: '@divider', | 		panelBorder: '@divider', | ||||||
| 		messageBg: '#1d1d1d', | 		messageBg: '#1d1d1d', | ||||||
|  | 		deckColumnBorder: '@divider', | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,9 +10,11 @@ | ||||||
| 		accent: 'rgb(206, 147, 191)', | 		accent: 'rgb(206, 147, 191)', | ||||||
| 		bg: 'rgb(253, 242, 243)', | 		bg: 'rgb(253, 242, 243)', | ||||||
| 		fg: 'rgb(161, 139, 146)', | 		fg: 'rgb(161, 139, 146)', | ||||||
|  | 		divider: '#ece7e7', | ||||||
| 		renote: '@accent', | 		renote: '@accent', | ||||||
| 		link: '@accent', | 		link: '@accent', | ||||||
| 		mention: '@accent', | 		mention: '@accent', | ||||||
| 		hashtag: '@accent', | 		hashtag: '@accent', | ||||||
|  | 		panelHeaderDivider: '@divider', | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,5 +11,6 @@ | ||||||
| 		bg: 'rgb(220, 229, 232)', | 		bg: 'rgb(220, 229, 232)', | ||||||
| 		fg: 'rgb(139, 153, 161)', | 		fg: 'rgb(139, 153, 161)', | ||||||
| 		renote: '@accent', | 		renote: '@accent', | ||||||
|  | 		panelHeaderDivider: '@divider', | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,7 +8,11 @@ | ||||||
| 	base: 'light', | 	base: 'light', | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | 		bg: '#f2f2f2', | ||||||
|  | 		header: ':alpha<0.7<@bg', | ||||||
|  | 		navBg: '@bg', | ||||||
| 		panelHeaderDivider: '@divider', | 		panelHeaderDivider: '@divider', | ||||||
| 		messageBg: '#dedede', | 		messageBg: '#dedede', | ||||||
|  | 		deckColumnBorder: '#cccccc', | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,18 +1,16 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <mk-container :show-header="props.showHeader" :naked="props.transparent"> | ||||||
| 	<mk-container :show-header="props.design === 0" :naked="props.design === 2"> | 	<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template> | ||||||
| 		<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template> | 	<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template> | ||||||
| 		<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template> |  | ||||||
| 
 | 
 | ||||||
| 		<div> | 	<div> | ||||||
| 			<mk-loading v-if="fetching"/> | 		<mk-loading v-if="fetching"/> | ||||||
| 			<template v-else> | 		<template v-else> | ||||||
| 				<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/> | 			<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/> | ||||||
| 				<x-chart v-show="props.view === 1" :data="[].concat(activity)"/> | 			<x-chart v-show="props.view === 1" :data="[].concat(activity)"/> | ||||||
| 			</template> | 		</template> | ||||||
| 		</div> | 	</div> | ||||||
| 	</mk-container> | </mk-container> | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -25,8 +23,19 @@ import XChart from './activity.chart.vue'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'activity', | 	name: 'activity', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		design: 0, | 		showHeader: { | ||||||
| 		view: 0 | 			type: 'boolean', | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
|  | 		transparent: { | ||||||
|  | 			type: 'boolean', | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
|  | 		view: { | ||||||
|  | 			type: 'number', | ||||||
|  | 			default: 0, | ||||||
|  | 			hidden: true, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -57,14 +66,6 @@ export default define({ | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		func() { |  | ||||||
| 			if (this.props.design === 2) { |  | ||||||
| 				this.props.design = 0; |  | ||||||
| 			} else { |  | ||||||
| 				this.props.design++; |  | ||||||
| 			} |  | ||||||
| 			this.save(); |  | ||||||
| 		}, |  | ||||||
| 		toggleView() { | 		toggleView() { | ||||||
| 			if (this.props.view === 1) { | 			if (this.props.view === 1) { | ||||||
| 				this.props.view = 0; | 				this.props.view = 0; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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"> | 	<div class="calendar" :data-is-holiday="isHoliday"> | ||||||
| 		<p class="month-and-year"> | 		<p class="month-and-year"> | ||||||
| 			<span class="year">{{ $t('yearX', { year }) }}</span> | 			<span class="year">{{ $t('yearX', { year }) }}</span> | ||||||
|  | @ -37,7 +37,10 @@ import define from './define'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'calendar', | 	name: 'calendar', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		design: 0 | 		transparent: { | ||||||
|  | 			type: 'boolean', | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	data() { | 	data() { | ||||||
|  | @ -62,14 +65,6 @@ export default define({ | ||||||
| 		clearInterval(this.clock); | 		clearInterval(this.clock); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		func() { |  | ||||||
| 			if (this.props.design === 2) { |  | ||||||
| 				this.props.design = 0; |  | ||||||
| 			} else { |  | ||||||
| 				this.props.design++; |  | ||||||
| 			} |  | ||||||
| 			this.save(); |  | ||||||
| 		}, |  | ||||||
| 		tick() { | 		tick() { | ||||||
| 			const now = new Date(); | 			const now = new Date(); | ||||||
| 			const nd = now.getDate(); | 			const nd = now.getDate(); | ||||||
|  |  | ||||||
|  | @ -1,11 +1,9 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <mk-container :naked="props.transparent" :show-header="false"> | ||||||
| 	<mk-container :naked="props.style % 2 === 0" :show-header="false"> | 	<div class="vubelbmv"> | ||||||
| 		<div class="vubelbmv"> | 		<mk-analog-clock class="clock"/> | ||||||
| 			<mk-analog-clock class="clock" :smooth="props.style < 2"/> | 	</div> | ||||||
| 		</div> | </mk-container> | ||||||
| 	</mk-container> |  | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -16,19 +14,16 @@ import MkAnalogClock from '../components/analog-clock.vue'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'clock', | 	name: 'clock', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		style: 0 | 		transparent: { | ||||||
|  | 			type: 'boolean', | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkContainer, | 		MkContainer, | ||||||
| 		MkAnalogClock | 		MkAnalogClock | ||||||
| 	}, | 	}, | ||||||
| 	methods: { |  | ||||||
| 		func() { |  | ||||||
| 			this.props.style = (this.props.style + 1) % 4; |  | ||||||
| 			this.save(); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import Vue from 'vue'; | 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; | 	name: string; | ||||||
| 	props?: () => T; | 	props?: () => T; | ||||||
| }) { | }) { | ||||||
|  | @ -15,22 +16,22 @@ export default function <T extends object>(data: { | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		data() { | ||||||
|  | 			return { | ||||||
|  | 				bakedOldProps: null | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		computed: { | 		computed: { | ||||||
| 			id(): string { | 			id(): string { | ||||||
| 				return this.widget.id; | 				return this.widget.id; | ||||||
| 			}, | 			}, | ||||||
| 
 | 
 | ||||||
| 			props(): T { | 			props(): Record<string, any> { | ||||||
| 				return this.widget.data; | 				return this.widget.data; | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		data() { |  | ||||||
| 			return { |  | ||||||
| 				bakedOldProps: null |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		created() { | 		created() { | ||||||
| 			this.mergeProps(); | 			this.mergeProps(); | ||||||
| 
 | 
 | ||||||
|  | @ -45,11 +46,26 @@ export default function <T extends object>(data: { | ||||||
| 					const defaultProps = data.props(); | 					const defaultProps = data.props(); | ||||||
| 					for (const prop of Object.keys(defaultProps)) { | 					for (const prop of Object.keys(defaultProps)) { | ||||||
| 						if (this.props.hasOwnProperty(prop)) continue; | 						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() { | 			save() { | ||||||
| 				this.$store.commit('deviceUser/updateWidget', this.widget); | 				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-clock', () => import('./clock.vue').then(m => m.default)); | ||||||
| Vue.component('mkw-activity', () => import('./activity.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-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> | <template> | ||||||
| <div> | <mk-container :show-header="props.showHeader"> | ||||||
| 	<mk-container :show-header="!props.compact"> | 	<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template> | ||||||
| 		<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template> |  | ||||||
| 
 | 
 | ||||||
| 		<div class="otgbylcu"> | 	<div class="otgbylcu"> | ||||||
| 			<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> | 		<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea> | ||||||
| 			<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button> | 		<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button> | ||||||
| 		</div> | 	</div> | ||||||
| 	</mk-container> | </mk-container> | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -19,10 +17,12 @@ import define from './define'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'memo', | 	name: 'memo', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		compact: false | 		showHeader: { | ||||||
|  | 			type: 'boolean', | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	 |  | ||||||
| 	components: { | 	components: { | ||||||
| 		MkContainer | 		MkContainer | ||||||
| 	}, | 	}, | ||||||
|  | @ -45,11 +45,6 @@ export default define({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		func() { |  | ||||||
| 			this.props.compact = !this.props.compact; |  | ||||||
| 			this.save(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onChange() { | 		onChange() { | ||||||
| 			this.changed = true; | 			this.changed = true; | ||||||
| 			clearTimeout(this.timeoutId); | 			clearTimeout(this.timeoutId); | ||||||
|  |  | ||||||
|  | @ -1,13 +1,11 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mkw-notifications" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`"> | <mk-container :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true"> | ||||||
| 	<mk-container :show-header="!props.compact" class="container"> | 	<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template> | ||||||
| 		<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template> |  | ||||||
| 
 | 
 | ||||||
| 		<div> | 	<div> | ||||||
| 			<x-notifications/> | 		<x-notifications/> | ||||||
| 		</div> | 	</div> | ||||||
| 	</mk-container> | </mk-container> | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -16,17 +14,19 @@ import MkContainer from '../components/ui/container.vue'; | ||||||
| import XNotifications from '../components/notifications.vue'; | import XNotifications from '../components/notifications.vue'; | ||||||
| import define from './define'; | import define from './define'; | ||||||
| 
 | 
 | ||||||
| const basisSteps = [25, 50, 75, 100] |  | ||||||
| const previewHeights = [200, 300, 400, 500] |  | ||||||
| 
 |  | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'notifications', | 	name: 'notifications', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		compact: false, | 		showHeader: { | ||||||
| 		basisStep: 0 | 			type: 'boolean', | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
|  | 		height: { | ||||||
|  | 			type: 'number', | ||||||
|  | 			default: 300, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	 |  | ||||||
| 	components: { | 	components: { | ||||||
| 		MkContainer, | 		MkContainer, | ||||||
| 		XNotifications, | 		XNotifications, | ||||||
|  | @ -37,47 +37,5 @@ export default define({ | ||||||
| 			faBell | 			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> | </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> | <template> | ||||||
| <div> | <mk-container :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent"> | ||||||
| 	<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> | ||||||
| 		<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template> |  | ||||||
| 
 | 
 | ||||||
| 		<div class=""> | 	<div class=""> | ||||||
| 			<mk-loading v-if="fetching"/> | 		<mk-loading v-if="fetching"/> | ||||||
| 			<div v-else :class="$style.stream"> | 		<div v-else :class="$style.stream"> | ||||||
| 				<div v-for="(image, i) in images" :key="i" | 			<div v-for="(image, i) in images" :key="i" | ||||||
| 					:class="$style.img" | 				:class="$style.img" | ||||||
| 					:style="`background-image: url(${thumbnail(image)})`" | 				:style="`background-image: url(${thumbnail(image)})`" | ||||||
| 				></div> | 			></div> | ||||||
| 			</div> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</mk-container> | 	</div> | ||||||
| </div> | </mk-container> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -25,7 +23,14 @@ import { getStaticImageUrl } from '../scripts/get-static-image-url'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'photos', | 	name: 'photos', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		design: 0, | 		showHeader: { | ||||||
|  | 			type: 'boolean', | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
|  | 		transparent: { | ||||||
|  | 			type: 'boolean', | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	components: { | 	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 { | 		thumbnail(image: any): string { | ||||||
| 			return this.$store.state.device.disableShowingAnimatedImages | 			return this.$store.state.device.disableShowingAnimatedImages | ||||||
| 				? getStaticImageUrl(image.thumbnailUrl) | 				? getStaticImageUrl(image.thumbnailUrl) | ||||||
|  | @ -82,7 +78,7 @@ export default define({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" module> | <style lang="scss" module> | ||||||
| .root[data-melt] { | .root[data-transparent] { | ||||||
| 	.stream { | 	.stream { | ||||||
| 		padding: 0; | 		padding: 0; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,17 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <mk-container :show-header="props.showHeader"> | ||||||
| 	<mk-container :show-header="!props.compact"> | 	<template #header><fa :icon="faRssSquare"/>RSS</template> | ||||||
| 		<template #header><fa :icon="faRssSquare"/>RSS</template> | 	<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template> | ||||||
| 		<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template> |  | ||||||
| 
 | 
 | ||||||
| 		<div class="ekmkgxbj"> | 	<div class="ekmkgxbj"> | ||||||
| 			<mk-loading v-if="fetching"/> | 		<mk-loading v-if="fetching"/> | ||||||
| 			<div class="feed" v-else> | 		<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> | 			<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a> | ||||||
| 			</div> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</mk-container> | 	</div> | ||||||
| </div> | </mk-container> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -22,8 +20,14 @@ import define from './define'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'rss', | 	name: 'rss', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		compact: false, | 		showHeader: { | ||||||
| 		url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews' | 			type: 'boolean', | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
|  | 		url: { | ||||||
|  | 			type: 'string', | ||||||
|  | 			default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews', | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -40,15 +44,12 @@ export default define({ | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.fetch(); | 		this.fetch(); | ||||||
| 		this.clock = setInterval(this.fetch, 60000); | 		this.clock = setInterval(this.fetch, 60000); | ||||||
|  | 		this.$watch('props.url', this.fetch); | ||||||
| 	}, | 	}, | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
| 		clearInterval(this.clock); | 		clearInterval(this.clock); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		func() { |  | ||||||
| 			this.props.compact = !this.props.compact; |  | ||||||
| 			this.save(); |  | ||||||
| 		}, |  | ||||||
| 		fetch() { | 		fetch() { | ||||||
| 			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, { | 			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, { | ||||||
| 			}).then(res => { | 			}).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> | </script> | ||||||
|  |  | ||||||
|  | @ -1,24 +1,22 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mkw-timeline" :style="`flex-basis: calc(${basis}% - var(--margin)); height: ${previewHeight}px;`"> | <mk-container :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true"> | ||||||
| 	<mk-container :show-header="!props.compact" class="container"> | 	<template #header> | ||||||
| 		<template #header> | 		<button @click="choose" class="_button"> | ||||||
| 			<button @click="choose" class="_button"> | 			<fa v-if="props.src === 'home'" :icon="faHome"/> | ||||||
| 				<fa v-if="props.src === 'home'" :icon="faHome"/> | 			<fa v-if="props.src === 'local'" :icon="faComments"/> | ||||||
| 				<fa v-if="props.src === 'local'" :icon="faComments"/> | 			<fa v-if="props.src === 'social'" :icon="faShareAlt"/> | ||||||
| 				<fa v-if="props.src === 'social'" :icon="faShareAlt"/> | 			<fa v-if="props.src === 'global'" :icon="faGlobe"/> | ||||||
| 				<fa v-if="props.src === 'global'" :icon="faGlobe"/> | 			<fa v-if="props.src === 'list'" :icon="faListUl"/> | ||||||
| 				<fa v-if="props.src === 'list'" :icon="faListUl"/> | 			<fa v-if="props.src === 'antenna'" :icon="faSatellite"/> | ||||||
| 				<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> | ||||||
| 				<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;"/> | ||||||
| 				<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/> | 		</button> | ||||||
| 			</button> | 	</template> | ||||||
| 		</template> |  | ||||||
| 
 | 
 | ||||||
| 		<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" :antenna="props.antenna"/> | 		<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> | 	</div> | ||||||
| 	</mk-container> | </mk-container> | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -28,19 +26,25 @@ import MkContainer from '../components/ui/container.vue'; | ||||||
| import XTimeline from '../components/timeline.vue'; | import XTimeline from '../components/timeline.vue'; | ||||||
| import define from './define'; | import define from './define'; | ||||||
| 
 | 
 | ||||||
| const basisSteps = [25, 50, 75, 100] |  | ||||||
| const previewHeights = [200, 300, 400, 500] |  | ||||||
| 
 |  | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'timeline', | 	name: 'timeline', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		src: 'home', | 		showHeader: { | ||||||
| 		list: null, | 			type: 'boolean', | ||||||
| 		compact: false, | 			default: true, | ||||||
| 		basisStep: 0 | 		}, | ||||||
|  | 		src: { | ||||||
|  | 			type: 'string', | ||||||
|  | 			default: 'home', | ||||||
|  | 			hidden: true, | ||||||
|  | 		}, | ||||||
|  | 		list: { | ||||||
|  | 			type: 'object', | ||||||
|  | 			default: null, | ||||||
|  | 			hidden: true, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	 |  | ||||||
| 	components: { | 	components: { | ||||||
| 		MkContainer, | 		MkContainer, | ||||||
| 		XTimeline, | 		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: { | 	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) { | 		async choose(ev) { | ||||||
| 			this.menuOpened = true; | 			this.menuOpened = true; | ||||||
| 			const [antennas, lists] = await Promise.all([ | 			const [antennas, lists] = await Promise.all([ | ||||||
|  | @ -129,22 +112,3 @@ export default define({ | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </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> | <template> | ||||||
| <div> | <mk-container :show-header="props.showHeader"> | ||||||
| 	<mk-container :show-header="!props.compact"> | 	<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template> | ||||||
| 		<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template> |  | ||||||
| 
 | 
 | ||||||
| 		<div class="wbrkwala"> | 	<div class="wbrkwala"> | ||||||
| 			<mk-loading v-if="fetching"/> | 		<mk-loading v-if="fetching"/> | ||||||
| 			<transition-group tag="div" name="chart" class="tags" v-else> | 		<transition-group tag="div" name="chart" class="tags" v-else> | ||||||
| 				<div v-for="stat in stats" :key="stat.tag"> | 			<div v-for="stat in stats" :key="stat.tag"> | ||||||
| 					<div class="tag"> | 				<div class="tag"> | ||||||
| 						<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> | 					<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> | ||||||
| 						<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> | 					<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> | ||||||
| 					</div> |  | ||||||
| 					<x-chart class="chart" :src="stat.chart"/> |  | ||||||
| 				</div> | 				</div> | ||||||
| 			</transition-group> | 				<x-chart class="chart" :src="stat.chart"/> | ||||||
| 		</div> | 			</div> | ||||||
| 	</mk-container> | 		</transition-group> | ||||||
| </div> | 	</div> | ||||||
|  | </mk-container> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -28,7 +26,10 @@ import XChart from './trends.chart.vue'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'hashtags', | 	name: 'hashtags', | ||||||
| 	props: () => ({ | 	props: () => ({ | ||||||
| 		compact: false | 		showHeader: { | ||||||
|  | 			type: 'boolean', | ||||||
|  | 			default: true, | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| }).extend({ | }).extend({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -49,10 +50,6 @@ export default define({ | ||||||
| 		clearInterval(this.clock); | 		clearInterval(this.clock); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		func() { |  | ||||||
| 			this.props.compact = !this.props.compact; |  | ||||||
| 			this.save(); |  | ||||||
| 		}, |  | ||||||
| 		fetch() { | 		fetch() { | ||||||
| 			this.$root.api('hashtags/trend').then(stats => { | 			this.$root.api('hashtags/trend').then(stats => { | ||||||
| 				this.stats = stats; | 				this.stats = stats; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue