diff --git a/src/client/app/common/views/components/drive-file-thumbnail.vue b/src/client/app/common/views/components/drive-file-thumbnail.vue
new file mode 100644
index 0000000000..c924aa434d
--- /dev/null
+++ b/src/client/app/common/views/components/drive-file-thumbnail.vue
@@ -0,0 +1,174 @@
+<template>
+<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`">
+	<img
+		:src="file.url"
+		:alt="file.name"
+		:title="file.name"
+		v-if="detail && is === 'image'"/>
+	<video
+		:src="file.url"
+		ref="volumectrl"
+		preload="metadata"
+		controls
+		v-else-if="detail && is === 'video'"/>
+	<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
+	<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
+	<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
+
+	<audio
+		:src="file.url"
+		ref="volumectrl"
+		preload="metadata"
+		controls
+		v-else-if="detail && is === 'audio'"/>
+	<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
+
+	<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
+	<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
+	<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
+	<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
+	<fa :icon="faFile" class="icon" v-else/>
+
+	<fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import anime from 'animejs';
+import i18n from '../../../i18n';
+import {
+	faFile,
+	faFileAlt,
+	faFileImage,
+	faMusic,
+	faFileVideo,
+	faFileCsv,
+	faFilePdf,
+	faFileArchive,
+	faFilm
+	} from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	props: {
+		file: {
+			type: Object,
+			required: true
+		},
+		fit: {
+			type: String,
+			required: true
+		},
+		detail: {
+			type: Boolean,
+			required: false,
+			default: false
+		}
+	},
+	data() {
+		return {
+			isContextmenuShowing: false,
+			isDragging: false,
+
+			faFile,
+			faFileAlt,
+			faFileImage,
+			faMusic,
+			faFileVideo,
+			faFileCsv,
+			faFilePdf,
+			faFileArchive,
+			faFilm
+		};
+	},
+	computed: {
+		is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
+			if (this.file.type.startsWith('image/')) return 'image';
+			if (this.file.type.startsWith('video/')) return 'video';
+			if (this.file.type === 'audio/midi') return 'midi';
+			if (this.file.type.startsWith('audio/')) return 'audio';
+			if (this.file.type.endsWith('/csv')) return 'csv';
+			if (this.file.type.endsWith('/pdf')) return 'pdf';
+			if (this.file.type.startsWith('text/')) return 'textfile';
+			if ([
+					"application/zip",
+					"application/x-cpio",
+					"application/x-bzip",
+					"application/x-bzip2",
+					"application/java-archive",
+					"application/x-rar-compressed",
+					"application/x-tar",
+					"application/gzip",
+					"application/x-7z-compressed"
+				].some(e => e === this.file.type)) return 'archive';
+			return 'unknown';
+		},
+		isThumbnailAvailable(): boolean {
+			return this.file.thumbnailUrl.endsWith('?thumbnail') ? (this.is === 'image' || this.is === 'video') : true;
+		},
+		background(): string {
+			return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
+				? `rgb(${this.file.properties.avgColor.join(',')})`
+				: 'transparent';
+		}
+	},
+	mounted() {
+		const audioTag = this.$refs.volumectrl as HTMLAudioElement;
+		if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
+	},
+	methods: {
+		onThumbnailLoaded() {
+			if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
+				anime({
+					targets: this.$refs.thumbnail,
+					backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
+					duration: 100,
+					easing: 'linear'
+				});
+			}
+		},
+		volumechange() {
+			const audioTag = this.$refs.volumectrl as HTMLAudioElement;
+			this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.zdjebgpv
+	display flex
+
+	> img,
+	> .icon
+		pointer-events none
+
+	> img
+		height 100%
+		width 100%
+		margin auto
+		object-fit cover
+
+	> .icon
+		height 65%
+		width 65%
+		margin auto
+
+	> video,
+	> audio
+		width 100%
+
+	> .icon-sub
+		position absolute
+		width 30%
+		height auto
+		margin 0
+		right 4%
+		bottom 4%
+
+	&.detail
+		> .icon
+			height 100px
+			margin 16px auto
+
+</style>
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index b9d202f555..975b1d6b52 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -21,9 +21,9 @@
 		<img src="/assets/label-red.svg"/>
 		<p>{{ $t('nsfw') }}</p>
 	</div>
-	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
-		<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded"/>
-	</div>
+
+	<x-file-thumbnail class="thumbnail" :file="file" fit="contain"/>
+
 	<p class="name">
 		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
@@ -34,15 +34,18 @@
 <script lang="ts">
 import Vue from 'vue';
 import i18n from '../../../i18n';
-import anime from 'animejs';
 import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 import updateAvatar from '../../api/update-avatar';
 import updateBanner from '../../api/update-banner';
 import { appendQuery } from '../../../../../prelude/url';
+import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
 
 export default Vue.extend({
 	i18n: i18n('desktop/views/components/drive.file.vue'),
 	props: ['file'],
+	components: {
+		XFileThumbnail
+	},
 	data() {
 		return {
 			isContextmenuShowing: false,
@@ -58,11 +61,6 @@ export default Vue.extend({
 		},
 		title(): string {
 			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
-		},
-		background(): string {
-			return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
-				? `rgb(${this.file.properties.avgColor.join(',')})`
-				: 'transparent';
 		}
 	},
 	methods: {
@@ -256,6 +254,9 @@ export default Vue.extend({
 		> .name
 			color var(--primaryForeground)
 
+		> .thumbnail
+			color var(--primaryForeground)
+
 	&[data-is-contextmenu-showing]
 		&:after
 			content ""
@@ -321,18 +322,7 @@ export default Vue.extend({
 		width 128px
 		height 128px
 		margin auto
-
-		> img
-			display block
-			position absolute
-			top 0
-			left 0
-			right 0
-			bottom 0
-			margin auto
-			max-width 128px
-			max-height 128px
-			pointer-events none
+		color var(--driveFileIcon)
 
 	> .name
 		display block
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index d7432437e0..be6d8594a0 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -1,11 +1,7 @@
 <template>
 <div class="pyvicwrksnfyhpfgkjwqknuururpaztw">
 	<div class="preview">
-		<img v-if="kind == 'image'" ref="img"
-			:src="file.url"
-			:alt="file.name"
-			:title="file.name"
-			:style="style">
+		<x-file-thumbnail class="preview" :file="file" fit="cover" :detail="true"/>
 		<template v-if="kind != 'image'"><fa icon="file"/></template>
 		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
 			<span class="size">
@@ -62,11 +58,16 @@ import Vue from 'vue';
 import i18n from '../../../i18n';
 import { gcd } from '../../../../../prelude/math';
 import { appendQuery } from '../../../../../prelude/url';
+import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
 
 export default Vue.extend({
 	i18n: i18n('mobile/views/components/drive.file-detail.vue'),
 	props: ['file'],
 
+	components: {
+		XFileThumbnail
+	},
+
 	data() {
 		return {
 			gcd,
@@ -147,8 +148,7 @@ export default Vue.extend({
 		padding 8px
 		background var(--bg)
 
-		> img
-			display block
+		> .preview
 			max-width 100%
 			max-height 300px
 			margin 0 auto
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index 28baa528c0..ff6dee2a72 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -1,7 +1,7 @@
 <template>
 <a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
 	<div class="container">
-		<div class="thumbnail" :style="thumbnail"></div>
+		<x-file-thumbnail class="thumbnail" :file="file" fit="cover"/>
 		<div class="body">
 			<p class="name">
 				<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
@@ -26,9 +26,14 @@
 <script lang="ts">
 import Vue from 'vue';
 import i18n from '../../../i18n';
+import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
+
 export default Vue.extend({
 	i18n: i18n('mobile/views/components/drive.file.vue'),
 	props: ['file'],
+	components: {
+		XFileThumbnail
+	},
 	data() {
 		return {
 			isSelected: false
@@ -37,12 +42,6 @@ export default Vue.extend({
 	computed: {
 		browser(): any {
 			return this.$parent;
-		},
-		thumbnail(): any {
-			return {
-				'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
-				'background-image': `url(${this.file.thumbnailUrl})`
-			};
 		}
 	},
 	created() {
@@ -74,9 +73,12 @@ export default Vue.extend({
 		pointer-events none
 
 	> .container
+		display grid
 		max-width 500px
 		margin 0 auto
 		padding 16px
+		grid-template-columns 64px 1fr
+		grid-column-gap 10px
 
 		&:after
 			content ""
@@ -84,18 +86,13 @@ export default Vue.extend({
 			clear both
 
 		> .thumbnail
-			display block
-			float left
 			width 64px
 			height 64px
-			background-size cover
-			background-position center center
+			color var(--driveFileIcon)
 
 		> .body
 			display block
-			float left
-			width calc(100% - 74px)
-			margin-left 10px
+			word-break break-all
 
 			> .name
 				display block
@@ -154,6 +151,6 @@ export default Vue.extend({
 		background var(--primary)
 
 		&, *
-			color #fff !important
+			color var(--primaryForeground) !important
 
 </style>
diff --git a/src/client/themes/dark.json5 b/src/client/themes/dark.json5
index 13c55999e5..5f44f8570e 100644
--- a/src/client/themes/dark.json5
+++ b/src/client/themes/dark.json5
@@ -153,6 +153,8 @@
 		messagingRoomMessageBg: '$secondary',
 		messagingRoomMessageFg: '#fff',
 
+		driveFileIcon: '$text',
+
 		formButtonBorder: 'rgba(255, 255, 255, 0.1)',
 		formButtonHoverBg: ':alpha<0.2<$primary',
 		formButtonHoverBorder: ':alpha<0.5<$primary',
diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5
index 65bd3b1216..4617f6aabe 100644
--- a/src/client/themes/light.json5
+++ b/src/client/themes/light.json5
@@ -153,6 +153,8 @@
 		messagingRoomMessageBg: '#eee',
 		messagingRoomMessageFg: '#333',
 
+		driveFileIcon: '$text',
+
 		formButtonBorder: 'rgba(0, 0, 0, 0.1)',
 		formButtonHoverBg: ':alpha<0.12<$primary',
 		formButtonHoverBorder: ':alpha<0.3<$primary',
@@ -179,7 +181,7 @@
 		desktopTimelineSrcHover: ':darken<7<$text',
 		desktopWindowTitle: '$text',
 		desktopWindowShadow: 'rgba(0, 0, 0, 0.2)',
-		desktopDriveBg: '#fff',
+		desktopDriveBg: '@bg',
 		desktopDriveFolderBg: ':lighten<31<$primary',
 		desktopDriveFolderHoverBg: ':lighten<27<$primary',
 		desktopDriveFolderActiveBg: ':lighten<25<$primary',