Merge pull request #3468 from Bnyro/custom-instances

feat: support for adding custom instances
This commit is contained in:
Bnyro 2024-03-15 22:54:41 +01:00 committed by GitHub
commit 89d9f30e41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 139 additions and 16 deletions

View File

@ -0,0 +1,79 @@
<template>
<ModalComponent @close="$emit('close')">
<h3 v-t="'titles.custom_instances'" class="my-4 font-bold" />
<hr />
<div class="text-center">
<div>
<div v-for="(customInstance, index) in customInstances" :key="customInstance.name">
<div class="flex items-center justify-between">
<span>{{ customInstance.name }} - {{ customInstance.api_url }}</span>
<span
class="i-fa6-solid:circle-minus cursor-pointer"
@click="removeInstance(customInstance, index)"
/>
</div>
<hr />
</div>
</div>
<form class="flex flex-col items-end gap-2">
<input v-model="name" class="input w-full" type="text" :placeholder="$t('preferences.instance_name')" />
<input
v-model="url"
class="input w-full"
type="text"
:placeholder="$t('preferences.api_url')"
@keyup.enter="addInstance"
/>
<button v-t="'actions.add'" class="btn w-min" @click.prevent="addInstance" />
</form>
</div>
</ModalComponent>
</template>
<script>
import ModalComponent from "./ModalComponent.vue";
export default {
components: { ModalComponent },
emits: ["close"],
data() {
return {
customInstances: [],
name: "",
url: "",
};
},
mounted() {
this.customInstances = this.getCustomInstances();
},
methods: {
async addInstance() {
const newInstance = {
name: this.name,
api_url: this.url,
};
if (!newInstance.name || !newInstance.api_url) {
return;
}
if (!this.isValidInstanceUrl(newInstance.api_url)) {
alert(this.$t("actions.invalid_url"));
return;
}
this.addCustomInstance(newInstance);
this.name = "";
this.url = "";
},
removeInstance(instance, index) {
this.customInstances.splice(index, 1);
this.removeCustomInstance(instance);
},
isValidInstanceUrl(str) {
var a = document.createElement("a");
a.href = str;
return a.host && a.host != window.location.host;
},
},
};
</script>

View File

@ -313,6 +313,10 @@
</select> </select>
</label> </label>
</template> </template>
<div class="pref">
<span v-t="'titles.custom_instances'" class="w-max" />
<button v-t="'actions.customize'" class="btn" @click="showCustomInstancesModal = true" />
</div>
<br /> <br />
<!-- options that are visible only when logged in --> <!-- options that are visible only when logged in -->
@ -359,7 +363,7 @@
<th v-t="'preferences.ssl_score'" /> <th v-t="'preferences.ssl_score'" />
</tr> </tr>
</thead> </thead>
<tbody v-for="instance in instances" :key="instance.name"> <tbody v-for="instance in publicInstances" :key="instance.name">
<tr> <tr>
<td v-text="instance.name" /> <td v-text="instance.name" />
<td v-text="instance.locations" /> <td v-text="instance.locations" />
@ -387,14 +391,23 @@
@close="showConfirmResetPrefsDialog = false" @close="showConfirmResetPrefsDialog = false"
@confirm="resetPreferences()" @confirm="resetPreferences()"
/> />
<CustomInstanceModal
v-if="showCustomInstancesModal"
@close="
showCustomInstancesModal = false;
fetchInstances();
"
/>
</template> </template>
<script> <script>
import CountryMap from "@/utils/CountryMaps/en.json"; import CountryMap from "@/utils/CountryMaps/en.json";
import ConfirmModal from "./ConfirmModal.vue"; import ConfirmModal from "./ConfirmModal.vue";
import CustomInstanceModal from "./CustomInstanceModal.vue";
export default { export default {
components: { components: {
ConfirmModal, ConfirmModal,
CustomInstanceModal,
}, },
data() { data() {
return { return {
@ -402,7 +415,8 @@ export default {
selectedInstance: null, selectedInstance: null,
authInstance: false, authInstance: false,
selectedAuthInstance: null, selectedAuthInstance: null,
instances: [], customInstances: [],
publicInstances: [],
sponsorBlock: true, sponsorBlock: true,
skipOptions: new Map([ skipOptions: new Map([
["sponsor", { value: "auto", label: "actions.skip_sponsors" }], ["sponsor", { value: "auto", label: "actions.skip_sponsors" }],
@ -496,25 +510,21 @@ export default {
prefetchLimit: 2, prefetchLimit: 2,
password: null, password: null,
showConfirmResetPrefsDialog: false, showConfirmResetPrefsDialog: false,
showCustomInstancesModal: false,
}; };
}, },
computed: {
instances() {
return [...this.publicInstances, ...this.customInstances];
},
},
activated() { activated() {
document.title = this.$t("titles.preferences") + " - Piped"; document.title = this.$t("titles.preferences") + " - Piped";
}, },
async mounted() { async mounted() {
if (Object.keys(this.$route.query).length > 0) this.$router.replace({ query: {} }); if (Object.keys(this.$route.query).length > 0) this.$router.replace({ query: {} });
this.fetchJson(import.meta.env.VITE_PIPED_INSTANCES).then(resp => { this.fetchInstances();
this.instances = resp;
if (!this.instances.some(instance => instance.api_url == this.apiUrl()))
this.instances.push({
name: "Custom Instance",
api_url: this.apiUrl(),
locations: "Unknown",
cdn: false,
uptime_30d: 100,
});
});
if (this.testLocalStorage) { if (this.testLocalStorage) {
this.selectedInstance = this.getPreferenceString("instance", import.meta.env.VITE_PIPED_API); this.selectedInstance = this.getPreferenceString("instance", import.meta.env.VITE_PIPED_API);
@ -633,6 +643,21 @@ export default {
if (shouldReload) window.location.reload(); if (shouldReload) window.location.reload();
} }
}, },
async fetchInstances() {
this.customInstances = this.getCustomInstances();
this.fetchJson(import.meta.env.VITE_PIPED_INSTANCES).then(resp => {
this.publicInstances = resp;
if (!this.publicInstances.some(instance => instance.api_url == this.apiUrl()))
this.publicInstances.push({
name: "Selected Instance",
api_url: this.apiUrl(),
locations: "Unknown",
cdn: false,
uptime_30d: 100,
});
});
},
sslScore(url) { sslScore(url) {
return "https://www.ssllabs.com/ssltest/analyze.html?d=" + new URL(url).host + "&latest"; return "https://www.ssllabs.com/ssltest/analyze.html?d=" + new URL(url).host + "&latest";
}, },

View File

@ -16,7 +16,8 @@
"albums": "Albums", "albums": "Albums",
"bookmarks": "Bookmarks", "bookmarks": "Bookmarks",
"channel_groups": "Channel groups", "channel_groups": "Channel groups",
"dearrow": "DeArrow" "dearrow": "DeArrow",
"custom_instances": "Custom instances"
}, },
"player": { "player": {
"watch_on": "View on {0}", "watch_on": "View on {0}",
@ -151,7 +152,10 @@
"generate_qrcode": "Generate QR Code", "generate_qrcode": "Generate QR Code",
"download_frame": "Download frame", "download_frame": "Download frame",
"add_to_group": "Add to group", "add_to_group": "Add to group",
"concurrent_prefetch_limit": "Concurrent Stream Prefetch Limit" "concurrent_prefetch_limit": "Concurrent Stream Prefetch Limit",
"customize": "Customize",
"invalid_url": "Invalid URL!",
"add": "Add"
}, },
"comment": { "comment": {
"pinned_by": "Pinned by {author}", "pinned_by": "Pinned by {author}",
@ -167,7 +171,8 @@
"version": "Version", "version": "Version",
"up_to_date": "Up to date?", "up_to_date": "Up to date?",
"ssl_score": "SSL Score", "ssl_score": "SSL Score",
"uptime_30d": "Uptime (30d)" "uptime_30d": "Uptime (30d)",
"api_url": "Api URL"
}, },
"login": { "login": {
"username": "Username", "username": "Username",

View File

@ -553,6 +553,20 @@ const mixin = {
return !resp.error; return !resp.error;
}, },
getCustomInstances() {
return JSON.parse(window.localStorage.getItem("customInstances")) ?? [];
},
addCustomInstance(instance) {
let customInstances = this.getCustomInstances();
customInstances.push(instance);
window.localStorage.setItem("customInstances", JSON.stringify(customInstances));
},
removeCustomInstance(instanceToDelete) {
let customInstances = this.getCustomInstances().filter(
instance => instance.api_url != instanceToDelete.api_url,
);
window.localStorage.setItem("customInstances", JSON.stringify(customInstances));
},
}, },
computed: { computed: {
authenticated(_this) { authenticated(_this) {