Skip to content

Commit fcd374f

Browse files
committed
Thumbnails for images (list, detail) and videos (list).
* Configuration for relevant variables. * Separate thumbnail components wrapping v-img. * Use dweb.link as default IPFS gateway and our own gateway for thumbnails. * Auto scale thumbnails to container size. * Failure indicator for unavailable thumbnails/images.
1 parent fc2c6df commit fcd374f

File tree

8 files changed

+168
-28
lines changed

8 files changed

+168
-28
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ Developers: you will likely want to run `npm run serve` in one terminal and `npm
4949

5050
Ops will want to use `npm run build`, `npm run test` or `npm run test:coverage`, `npm run prettier:check`
5151

52-
## NSFW api
52+
## Configuration
5353

54-
This build uses an API to check nsfw content.
54+
The following environment variables may be used to configure the frontend.
5555

56-
The default API endpoint to is: https://api.ipfs-search.com/nsfw/
57-
. This can be overridden by injecting environment variable `VITE_NSFW_API`
56+
### Variables
5857

59-
The API call should be: `<VITE_NSFW_API><CID>`, so e.g.
60-
61-
`https://api.ipfs-search.com/nsfw/QmSZzv7ux1LGwpehVcCMQ9ec945X6qE4qyjKDhCVwY25iw`
62-
https://api.ipfs-search.com/v1/nsfw/classify/
58+
- `VITE_IPFS_GATEWAY`: Gateway for URL generation. Default: `https://dweb.link`
59+
- `VITE_NSFW_API`: Endpoint for [nsfw-server](https://github.com/ipfs-search/nsfw-server). Default: `https://api.ipfs-search.com/v1/nsfw/classify/`
60+
- `VITE_NYATS_API`: Endpoint for [nyats](https://github.com/ipfs-search/nyats) (Not Yet Another Thumbnail Server) API. Default: `https://api.ipfs-search.com/v1/thumbnail/`
61+
- `VITE_NYATS_IPFS_GATEWAY`: Gateway for nyats. Default: `https://gw.dwebsearch.com`
62+
- `VITE_NYATS_IPNS_ROOT`: Root for thumbnails over IPNS. Default: `/ipns/12D3KooWPVobDRG9Mdmact3ejSe6UAFP8cevmw35HHR1ZDwCozSo/`

package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"masonry-layout": "^4.2.2",
2626
"mime": "^3.0.0",
2727
"moment": "^2.29.4",
28+
"nyats-client": "^0.2.0-alpha.2",
2829
"pretty-bytes": "^6.0.0",
2930
"regenerator-runtime": "^0.13.9",
3031
"ts-node": "^10.8.0",
@@ -57,6 +58,7 @@
5758
"prettier": "2.7.1",
5859
"sass": "^1.53.0",
5960
"typescript": "^4.7.3",
61+
"url-join": "^5.0.0",
6062
"vite": "^3.0.2",
6163
"vite-plugin-vuetify": "^1.0.0-alpha.11",
6264
"vitest": "^0.13.1"

src/components/detailViewComponents/ImageDetail.vue

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
<script setup>
2+
import { mdiRobotDead } from "@mdi/js";
3+
24
import NsfwTooltip from "@/components/shared/nsfwTooltip.vue";
35
import { detailProps } from "@/composables/useDetail";
46
defineProps(detailProps);
5-
import getResourceURL from "@/helpers/resourceURL";
67
78
import GenericDetail from "@/components/detailViewComponents/GenericDetail.vue";
89
import { useBlurExplicit } from "@/composables/BlurExplicitImagesComposable";
910
const { blurExplicit } = useBlurExplicit();
11+
12+
import NyatsImg from "@/helpers/nyats/vuetify-img-cid.vue";
1013
</script>
1114

1215
<template>
1316
<generic-detail :file="file">
1417
<v-row>
1518
<v-col cols="12" md="10" offset-md="1" :class="{ blurExplicit: blurExplicit(file) }">
16-
<v-img
19+
<nyats-img
20+
:cid="file.hash"
21+
type="image"
1722
:data-nsfw-classification="JSON.stringify(file.nsfwClassification)"
1823
:data-nsfw="file.nsfw"
19-
:src="getResourceURL(file.hash)"
2024
max-height="400px"
2125
class="image-wrapper"
2226
:class="{ blurExplicit: blurExplicit(file) }"
@@ -26,8 +30,15 @@ const { blurExplicit } = useBlurExplicit();
2630
<v-progress-circular indeterminate color="ipfsPrimary" />
2731
</v-row>
2832
</template>
33+
34+
<template #failed>
35+
<v-row class="fill-height ma-0" align="center" justify="center">
36+
<v-icon color="grey" size="large" :icon="mdiRobotDead" />
37+
</v-row>
38+
</template>
39+
2940
<NsfwTooltip :file="file" />
30-
</v-img>
41+
</nyats-img>
3142
</v-col>
3243
</v-row>
3344
</generic-detail>

src/components/searchViewComponents/ImageList.vue

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
import NsfwTooltip from "@/components/shared/nsfwTooltip.vue";
33
import ListBase from "./BaseList.vue";
44
import HoverCard from "./subcomponents/HoverCard.vue";
5-
import { useFileListComposable, imports } from "@/composables/useFileListComposable";
5+
import { useFileListComposable } from "@/composables/useFileListComposable";
66
import { useBlurExplicit } from "@/composables/BlurExplicitImagesComposable";
77
import { Types } from "@/helpers/typeHelper";
88
import { useDisplay } from "vuetify";
9+
import { mdiRobotDead } from "@mdi/js";
10+
import NyatsImg from "@/helpers/nyats/vuetify-img-cid.vue";
911
1012
const fileType = Types.images;
1113
const { xs, smAndDown, mdAndDown } = useDisplay();
1214
1315
const { slicedHits } = useFileListComposable({ fileType });
14-
15-
const { getResourceURL } = imports;
16-
1716
const { blurExplicit } = useBlurExplicit();
1817
</script>
1918

@@ -30,8 +29,9 @@ const { blurExplicit } = useBlurExplicit();
3029
lg="2"
3130
>
3231
<hover-card :hit="hit" :index="index" :file-type="fileType">
33-
<v-img
34-
:src="getResourceURL(hit.hash)"
32+
<nyats-img
33+
:cid="hit.hash"
34+
type="image"
3535
aspect-ratio="1"
3636
:class="{ blurExplicit: blurExplicit(hit) }"
3737
:data-nsfw-classification="JSON.stringify(hit.nsfwClassification)"
@@ -43,8 +43,15 @@ const { blurExplicit } = useBlurExplicit();
4343
<v-progress-circular indeterminate color="ipfsPrimary" />
4444
</v-row>
4545
</template>
46+
47+
<template #failed>
48+
<v-row class="fill-height ma-0" align="center" justify="center">
49+
<v-icon color="grey" size="large" :icon="mdiRobotDead" />
50+
</v-row>
51+
</template>
52+
4653
<NsfwTooltip :file="hit" />
47-
</v-img>
54+
</nyats-img>
4855
</hover-card>
4956
</v-col>
5057
</v-row>

src/components/searchViewComponents/VideoList.vue

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import CardContent from "@/components/searchViewComponents/subcomponents/generic
66
import MediaCenterIcon from "@/components/searchViewComponents/subcomponents/MediaCenterIcon.vue";
77
import { mdiVideo } from "@mdi/js";
88
import { Types } from "@/helpers/typeHelper";
9-
import { picsum } from "@/helpers/picsum";
9+
import { mdiRobotDead, mdiTimerSand } from "@mdi/js";
10+
import NyatsImg from "@/helpers/nyats/vuetify-img-cid.vue";
1011
1112
const fileType = Types.video;
1213
@@ -19,15 +20,28 @@ const { slicedHits } = useFileListComposable({ fileType });
1920
<hover-card :hit="hit" :index="index" :file-type="fileType">
2021
<v-row>
2122
<v-col cols="12" sm="4" md="3" lg="2">
22-
<v-img
23+
<nyats-img
2324
class="ma-3 mr-sm-0"
2425
cover
2526
aspect-ratio="1.778"
26-
:src="hit.src || picsum({ seed: hit.hash })"
2727
gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
28+
:cid="hit.hash"
29+
type="video"
2830
>
2931
<media-center-icon :icon="mdiVideo" />
30-
</v-img>
32+
33+
<template #placeholder>
34+
<v-row class="fill-height ma-0" align="center" justify="center">
35+
<v-icon color="grey" size="large" :icon="mdiTimerSand" />
36+
</v-row>
37+
</template>
38+
39+
<template #failed>
40+
<v-row class="fill-height ma-0" align="center" justify="center">
41+
<v-icon color="grey" size="large" :icon="mdiRobotDead" />
42+
</v-row>
43+
</template>
44+
</nyats-img>
3145
</v-col>
3246
<v-col cols="12" sm="8" md="9" lg="10" class="py-sm-0 ml-sm-n6">
3347
<CardContent :hit="hit" />
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<template>
2+
<v-img ref="img" :src="thumbURL()" v-bind="$attrs" @error="thumbErr()">
3+
<template #placeholder>
4+
<slot v-if="generateErr" name="failed" />
5+
<slot v-else name="placeholder" />
6+
</template>
7+
8+
<template #failed> <slot name="failed" /> </template>
9+
10+
<template #default>
11+
<slot name="default" />
12+
</template>
13+
</v-img>
14+
</template>
15+
16+
<script>
17+
import { DefaultConfig, GetClient } from "nyats-client";
18+
19+
const config = {
20+
endpoint: import.meta.env.VITE_NYATS_API || DefaultConfig.endpoint,
21+
gatewayURL: import.meta.env.VITE_NYATS_IPFS_GATEWAY || DefaultConfig.gatewayURL,
22+
ipnsRoot: import.meta.env.VITE_NYATS_IPNS_ROOT || DefaultConfig.ipnsRoot,
23+
};
24+
25+
const { IPNSThumbnailURL, GenerateThumbnailURL } = GetClient(config);
26+
27+
export default {
28+
props: {
29+
cid: {
30+
type: String,
31+
required: true,
32+
},
33+
type: {
34+
type: String,
35+
required: false,
36+
default: null,
37+
},
38+
},
39+
data: () => ({
40+
ipnsErr: false,
41+
generateErr: false,
42+
width: null,
43+
height: null,
44+
}),
45+
mounted() {
46+
this.updateSize(this.$refs.img);
47+
},
48+
updated() {
49+
this.updateSize(this.$refs.img);
50+
},
51+
methods: {
52+
updateSize(el) {
53+
if (el) {
54+
this.height = el.$el.clientHeight;
55+
this.width = el.$el.clientHeight;
56+
}
57+
},
58+
thumbErr() {
59+
if (this.ipnsErr) {
60+
this.generateErr = true;
61+
} else {
62+
this.ipnsErr = true;
63+
}
64+
},
65+
thumbURL() {
66+
if (this.width && this.height) {
67+
if (this.ipnsErr) {
68+
return GenerateThumbnailURL(this.cid, this.width, this.height, this.type);
69+
}
70+
71+
return IPNSThumbnailURL(this.cid, this.width, this.height);
72+
}
73+
},
74+
},
75+
};
76+
</script>

src/helpers/resourceURL.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
const gatewayURL = "https://gateway.ipfs.io";
1+
const gatewayURL = import.meta.env.VITE_IPFS_GATEWAY || "https://dweb.link";
22

3-
function getResourceURL(hash) {
4-
return `${gatewayURL}/ipfs/${hash}`;
3+
export default function getResourceURL(hash) {
4+
const resourcePath = `/ipfs/${hash}`;
5+
return new URL(resourcePath, gatewayURL).toString();
56
}
6-
7-
export default getResourceURL;

0 commit comments

Comments
 (0)