mirror of
https://github.com/Viren070/stremio-addon-manager.git
synced 2025-12-01 23:25:50 +01:00
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:
+148
-53
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user