Edit addon manifest feature and responsive enhancements

- Added an "Edit Manifest" button next to existing addon management buttons
- Implemented a modal popup with the following features:
  - In classic mode, a dynamically generated form based on the addon's manifest allows editing of:
    - Name
    - Description
    - Logo URL
    - Background URL
    - Catalogs (rename or delete)
  - In advanced mode, the raw JSON of the manifest.json file is displayed for more advanced configuration
- Changes are saved only if the save button is pressed; clicking outside the modal will close it and discard any changes
- Updated synchronization logic to include changes made to the manifest.json

- Enhanced the responsive layout of the addon list for improved mobile experience
- Fixed mobile layout distortion caused by code block command (Summary.vue)
This commit is contained in:
redd-ravenn
2024-08-23 19:23:13 +02:00
parent 44815f942a
commit ec7d6ef707
4 changed files with 569 additions and 57 deletions
+148 -53
View File
@@ -1,57 +1,57 @@
<script setup>
const props = defineProps({
name: {
type: String,
required: true
},
idx: {
type: Number,
required: true
},
manifestURL: {
type: String,
required: true
},
logoURL: {
type: String,
required: false
},
isDeletable: {
type: Boolean,
required: false,
default: true
},
isConfigurable: {
type: Boolean,
required: false,
default: false
}
})
const emits = defineEmits(['delete-addon'])
const defaultLogo = 'https://icongr.am/feather/box.svg?size=48&color=ffffff'
function copyManifestURLToClipboard() {
navigator.clipboard.writeText(props.manifestURL).then(() => {
console.log('Text copied to clipboard')
}).catch((error) => {
console.error('Error copying text to clipboard', error)
const props = defineProps({
name: {
type: String,
required: true
},
idx: {
type: Number,
required: true
},
manifestURL: {
type: String,
required: true
},
logoURL: {
type: String,
required: false
},
isDeletable: {
type: Boolean,
required: false,
default: true
},
isConfigurable: {
type: Boolean,
required: false,
default: false
}
})
}
function openAddonConfigurationPage() {
const configureURL = props.manifestURL.replace("stremio://", "https://").replace("/manifest.json", "/configure");
window.open(configureURL);
}
function removeAddon() {
emits('delete-addon', props.idx)
}
const emits = defineEmits(['delete-addon', 'edit-manifest'])
const defaultLogo = 'https://icongr.am/feather/box.svg?size=48&color=ffffff'
function copyManifestURLToClipboard() {
navigator.clipboard.writeText(props.manifestURL).then(() => {
console.log('Text copied to clipboard')
}).catch((error) => {
console.error('Error copying text to clipboard', error)
})
}
function openAddonConfigurationPage() {
const configureURL = props.manifestURL.replace("stremio://", "https://").replace("/manifest.json", "/configure");
window.open(configureURL);
}
function removeAddon() {
emits('delete-addon', props.idx)
}
function openEditManifestModal() {
emits('edit-manifest', props.idx)
}
</script>
<template>
@@ -73,6 +73,9 @@ function removeAddon() {
@click="copyManifestURLToClipboard">
<img src="https://icongr.am/feather/clipboard.svg?size=12">
</button>
<button class="button icon-only edit-manifest" title="Edit manifest JSON" @click="openEditManifestModal">
<img src="https://icongr.am/feather/edit.svg?size=12">
</button>
<button class="button icon-only delete" title="Remove addon from list" :disabled="!isDeletable"
@click="removeAddon">
<img src="https://icongr.am/feather/trash-2.svg?size=12">
@@ -91,9 +94,9 @@ function removeAddon() {
border-radius: 5px;
padding: 10px 13px;
margin-bottom: 11px;
/* box-shadow: 0 2px 4px rgba(0,0,0,0.06); */
border: 1px solid #ccc;
justify-content: space-between;
flex-wrap: wrap;
}
.dark .sortable-list .item {
@@ -103,6 +106,7 @@ function removeAddon() {
.item .details {
display: flex;
align-items: center;
flex: 1;
}
.item .details img {
@@ -114,6 +118,97 @@ function removeAddon() {
object-position: center;
border-radius: 30%;
background-color: #262626;
}
.col {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
min-width: 200px;
}
.button {
border-radius: 4px;
cursor: pointer;
padding: 5px;
transition: background-color 0.3s;
}
.icon-only {
display: flex;
justify-content: center;
align-items: center;
}
.visit-url img,
.copy-url img,
.edit-manifest img,
.delete img {
width: 20px;
height: 20px;
}
@media (max-width: 768px) {
.sortable-list .item {
flex-direction: column;
align-items: center;
padding: 10px;
}
.item .details {
margin-bottom: 10px;
text-align: center;
}
.item .details img {
margin-right: 12px;
margin-bottom: 8px;
}
.col {
flex-direction: row;
gap: 8px;
justify-content: center;
width: 100%;
margin-top: 10px;
}
.button {
padding: 6px;
}
.uil-draggabledots {
position: absolute;
right: 10px;
bottom: 10px;
}
}
@media (max-width: 480px) {
.item .details {
flex-direction: column;
align-items: center;
text-align: center;
}
.item .details img {
margin-bottom: 6px;
}
.col {
flex-direction: row;
gap: 4px;
justify-content: center;
width: 100%;
}
.button {
padding: 4px;
}
.uil-draggabledots {
display: none;
}
}
</style>
+73 -3
View File
@@ -3,6 +3,7 @@ import { ref } from 'vue'
import draggable from 'vuedraggable'
import AddonItem from './AddonItem.vue'
import Authentication from './Authentication.vue'
import DynamicForm from './DynamicForm.vue'
const stremioAPIBase = "https://api.strem.io/api/"
const dragging = false
@@ -10,6 +11,10 @@ let stremioAuthKey = ref('');
let addons = ref([])
let loadAddonsButtonText = ref('Load Addons')
let isEditModalVisible = ref(false);
let currentManifest = ref({});
let currentEditIdx = ref(null);
function loadUserAddons() {
const key = stremioAuthKey.value
if (!key) {
@@ -97,7 +102,28 @@ function setAuthKey(authKey) {
console.log('AuthKey set to: ', stremioAuthKey.value)
}
function openEditModal(idx) {
isEditModalVisible.value = true;
currentEditIdx.value = idx;
currentManifest.value = { ...addons.value[idx].manifest };
document.body.classList.add('modal-open');
}
function closeEditModal() {
isEditModalVisible.value = false;
currentManifest.value = {};
currentEditIdx.value = null;
document.body.classList.remove('modal-open');
}
function saveManifestEdit(updatedManifest) {
try {
addons.value[currentEditIdx.value].manifest = updatedManifest;
closeEditModal();
} catch (e) {
alert('Failed to update manifest');
}
}
</script>
<template>
@@ -122,7 +148,8 @@ function setAuthKey(authKey) {
:logoURL="element.manifest.logo"
:isDeletable="!getNestedObjectProperty(element, 'flags.protected', false)"
:isConfigurable="getNestedObjectProperty(element, 'manifest.behaviorHints.configurable', false)"
@delete-addon="removeAddon" />
@delete-addon="removeAddon"
@edit-manifest="openEditModal" />
</template>
</draggable>
</fieldset>
@@ -133,12 +160,17 @@ function setAuthKey(authKey) {
</button>
</fieldset>
</form>
</section>
<div v-if="isEditModalVisible" class="modal" @click.self="closeEditModal">
<div class="modal-content">
<h3>Edit manifest</h3>
<DynamicForm :manifest="currentManifest" @update-manifest="saveManifestEdit" />
</div>
</div>
</template>
<style scoped>
/* sortable list/addon list specifics */
.sortable-list {
padding: 25px;
border-radius: 7px;
@@ -153,4 +185,42 @@ function setAuthKey(authKey) {
.item.dragging :where(.details, i) {
opacity: 0;
}
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
}
.modal-content {
background: #2e2e2e;
color: #e0e0e0;
width: 75vw;
max-width: 900px;
max-height: 90vh;
padding: 20px;
border-radius: 5px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.7);
overflow: hidden;
display: flex;
flex-direction: column;
}
button {
padding: 10px 20px;
border: none;
background-color: #ffa600;
color: white;
font-size: 16px;
cursor: pointer;
border-radius: 5px;
}
</style>
+321
View File
@@ -0,0 +1,321 @@
<template>
<form @submit.prevent="handleSubmit">
<div v-if="!isAdvancedMode">
<div class="form-group">
<label for="name">Name</label>
<input
id="name"
name="name"
type="text"
v-model="formModel.name"
/>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
name="description"
rows="4"
v-model="formModel.description"
></textarea>
</div>
<div class="form-group">
<label for="logo">Logo URL</label>
<input
id="logo"
name="logo"
type="text"
v-model="formModel.logo"
/>
</div>
<div class="form-group">
<label for="background">Background URL</label>
<input
id="background"
name="background"
type="text"
v-model="formModel.background"
/>
</div>
<div v-if="formModel.catalogs && formModel.catalogs.length > 0" class="form-group">
<label>Catalogs</label>
<div v-for="(catalog, index) in formModel.catalogs" :key="catalog.type" class="catalog-item">
<label :for="'catalog-' + catalog.type" class="catalog-type-label">
{{ catalog.type }}
</label>
<input
:id="'catalog-' + catalog.type"
type="text"
v-model="catalog.name"
placeholder="Catalog Name"
/>
<button type="button" class="delete-button" @click="removeCatalog(index)">
<img src="https://icongr.am/feather/trash-2.svg?size=16" alt="Delete Catalog" />
</button>
</div>
</div>
<div class="form-actions">
<button class="save-button" type="submit">Save</button>
<button type="button" class="switch-mode-button" @click="toggleEditMode">Advanced mode</button>
</div>
</div>
<div v-else>
<textarea v-model="jsonModel" rows="10" class="json-editor"></textarea>
<div class="form-actions">
<button class="save-button" type="button" @click="updateFromJson">Save</button>
<button type="button" class="switch-mode-button" @click="toggleEditMode">Classic mode</button>
</div>
</div>
</form>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits, onMounted } from 'vue'
const props = defineProps({
manifest: {
type: Object,
required: true
}
})
const emits = defineEmits(['update-manifest'])
const isAdvancedMode = ref(false);
const formModel = ref({
name: '',
description: '',
logo: '',
background: '',
catalogs: []
});
const jsonModel = ref('')
watch(() => props.manifest, (newManifest) => {
formModel.value = JSON.parse(JSON.stringify(newManifest));
jsonModel.value = JSON.stringify(newManifest, null, 2);
}, { immediate: true });
onMounted(() => {
calculateMaxLabelWidth();
});
function calculateMaxLabelWidth() {
const labels = document.querySelectorAll('.catalog-type-label');
let maxWidth = 0;
labels.forEach(label => {
maxWidth = Math.max(maxWidth, label.scrollWidth);
});
labels.forEach(label => {
label.style.width = `${maxWidth}px`;
});
}
function toggleEditMode() {
isAdvancedMode.value = !isAdvancedMode.value;
if (!isAdvancedMode.value) {
try {
formModel.value = JSON.parse(jsonModel.value);
calculateMaxLabelWidth();
} catch (e) {
alert('Invalid JSON format');
}
}
}
function handleSubmit() {
emits('update-manifest', formModel.value);
}
function removeCatalog(index) {
if (Array.isArray(formModel.value.catalogs)) {
formModel.value.catalogs.splice(index, 1);
}
}
function updateFromJson() {
try {
formModel.value = JSON.parse(jsonModel.value);
emits('update-manifest', formModel.value);
isAdvancedMode.value = false;
} catch (e) {
alert('Invalid JSON format');
}
}
</script>
<style scoped>
.save-button {
padding: 10px 20px;
border: none;
background-color: #14854f;
color: #ffffff;
font-size: 16px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s, transform 0.2s;
margin-right: 10px;
}
.save-button:hover {
background-color: #14854eef;
}
.save-button:active {
background-color: #14854eea;
transform: scale(0.98);
}
.save-button:disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.switch-mode-button {
padding: 10px 20px;
border: none;
background-color: #007bff;
color: #ffffff;
font-size: 16px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s, transform 0.2s;
}
.switch-mode-button:hover {
background-color: #0056b3;
}
.switch-mode-button:active {
background-color: #004494;
transform: scale(0.98);
}
.switch-mode-button:disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.delete-button {
padding: 10px 20px;
border: none;
background-color: #ac0415;
color: #00000091;
font-size: 16px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s, transform 0.2s;
}
.delete-button:hover {
background-color: #ac0415f3;
}
.delete-button:active {
background-color: #ff273de7;
transform: scale(0.98);
}
.delete-button:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
opacity: 0.6;
}
.delete-button img {
filter: brightness(0) invert(1);
}
form {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 0;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input, textarea {
width: 100%;
padding: 10px;
box-sizing: border-box;
}
textarea {
resize: vertical;
background-color: #131316;
color: #f5f5f5;
}
.catalog-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.catalog-type-label {
margin-right: 10px;
color: #f5f5f5;
font-weight: bold;
text-align: right;
white-space: nowrap;
}
.catalog-item input {
flex: 1;
margin-right: 10px;
box-sizing: border-box;
min-width: 150px;
}
.json-editor {
padding: 10px;
box-sizing: border-box;
margin-bottom: 20px;
}
.form-actions {
display: flex;
gap: 10px;
}
@media (max-width: 768px) {
.catalog-item {
flex-direction: column;
align-items: flex-start;
}
.catalog-type-label {
margin-right: 0;
margin-bottom: 5px;
text-align: left;
width: 100%;
max-width: 150px;
box-sizing: border-box;
}
.catalog-item input {
margin-right: 0;
margin-bottom: 10px;
width: 100%;
}
}
</style>
+27 -1
View File
@@ -32,7 +32,9 @@
<li>
Open the developer console <a href="#faq">(?)</a> and paste the
follow code snippet:
<code>JSON.parse(localStorage.getItem("profile")).auth.key</code>
<div class="code-container">
<code>JSON.parse(localStorage.getItem("profile")).auth.key</code>
</div>
</li>
<li>Take the output value and paste it into the form below.</li>
</ul>
@@ -46,3 +48,27 @@
</ol>
</section>
</template>
<style scoped>
.code-container {
overflow-x: auto;
padding: 10px;
border-radius: 4px;
}
code {
white-space: nowrap;
font-family: monospace;
font-size: 0.9em;
color: #b93334;
}
@media (max-width: 600px) {
.code-container {
padding: 5px;
}
code {
font-size: 0.8em;
}
}
</style>