no1b4me commited on
Commit
26c2f3f
·
verified ·
1 Parent(s): 98e5881

Upload 9 files

Browse files
Files changed (9) hide show
  1. .env +3 -0
  2. app.js +87 -0
  3. database.js +160 -0
  4. logger.js +70 -0
  5. package.json +31 -0
  6. public/css/style.css +465 -0
  7. public/index.html +285 -0
  8. public/js/SidebarLayout.js +145 -0
  9. public/js/main.js +753 -0
.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ MONGO_URI=mongodb+srv://mikmc55:[email protected]/?retryWrites=true&w=majority&appName=hy0
2
+ SESSION_SECRET=bhdsaububsb387444nxkj
3
+ PORT=3001
app.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ require('dotenv').config();
2
+
3
+ const express = require('express');
4
+ const mongoose = require('mongoose');
5
+ const session = require('express-session');
6
+ const path = require('path');
7
+ const fetch = require('node-fetch');
8
+ const cors = require('cors');
9
+ const logger = require('./logger');
10
+
11
+ const app = express();
12
+ const port = process.env.PORT || 3001;
13
+
14
+ // MongoDB connection setup
15
+ const mongoUri = "mongodb+srv://mikmc55:[email protected]/?retryWrites=true&w=majority&appName=hy0";
16
+ mongoose.connect(mongoUri, {
17
+ useNewUrlParser: true,
18
+ useUnifiedTopology: true,
19
+ })
20
+ .then(() => logger.info('Connected to MongoDB successfully'))
21
+ .catch(err => logger.error('Failed to connect to MongoDB:', err));
22
+
23
+ // Configuration and constants setup
24
+ const config = {
25
+ SESSION_SECRET: process.env.SESSION_SECRET || "bhdsaububsb387444nxkj"
26
+ };
27
+
28
+ const STREMIO_API = {
29
+ BASE_URL: "https://api.strem.io/api",
30
+ LOGIN: "/login",
31
+ REGISTER: "/register",
32
+ ADDON_COLLECTION_GET: "/addonCollectionGet",
33
+ ADDON_COLLECTION_SET: "/addonCollectionSet",
34
+ LOGOUT: "/logout"
35
+ };
36
+
37
+ const corsOptions = {
38
+ origin: "*",
39
+ methods: ["GET", "POST", "DELETE", "PUT", "PATCH"],
40
+ allowedHeaders: ["Content-Type", "Authorization"],
41
+ credentials: true,
42
+ maxAge: 86400
43
+ };
44
+
45
+ // Process error handlers
46
+ process.on("uncaughtException", (error) => {
47
+ logger.error("Uncaught Exception:", error);
48
+ process.exit(1);
49
+ });
50
+
51
+ process.on("unhandledRejection", (reason, promise) => {
52
+ logger.error("Unhandled Rejection at:", promise, "reason:", reason);
53
+ });
54
+
55
+ // Middleware setup
56
+ app.use(cors(corsOptions));
57
+ app.options("*", cors(corsOptions));
58
+ app.use(express.json({ limit: "50mb" }));
59
+ app.use(express.static("public"));
60
+ app.use(session({
61
+ secret: config.SESSION_SECRET,
62
+ resave: false,
63
+ saveUninitialized: true,
64
+ cookie: {
65
+ secure: false,
66
+ maxAge: 24 * 60 * 60 * 1000
67
+ }
68
+ }));
69
+
70
+ // Routes and logic remain unchanged
71
+ // Insert your existing routes and middleware here
72
+
73
+ // Server initialization
74
+ async function startServer() {
75
+ try {
76
+ app.listen(port, () => {
77
+ logger.info(`Server started on port ${port}`);
78
+ });
79
+ } catch (error) {
80
+ logger.error('Failed to initialize:', error);
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ startServer();
86
+
87
+ module.exports = app;
database.js ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const logger = require('./logger');
3
+
4
+ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/stremio-manager';
5
+
6
+ // Connection options
7
+ const options = {
8
+ useNewUrlParser: true,
9
+ useUnifiedTopology: true,
10
+ serverSelectionTimeoutMS: 5000,
11
+ socketTimeoutMS: 45000,
12
+ };
13
+
14
+ // Create connection
15
+ let db;
16
+
17
+ async function connectDB() {
18
+ try {
19
+ if (!db) {
20
+ db = await mongoose.connect(MONGODB_URI, options);
21
+ logger.info('MongoDB connected successfully');
22
+ }
23
+ return db;
24
+ } catch (error) {
25
+ logger.error('MongoDB connection error:', error);
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ // Initialize connection before starting server
31
+ async function initializeDB() {
32
+ await connectDB();
33
+ }
34
+
35
+ mongoose.connection.on('error', err => {
36
+ logger.error('MongoDB error:', err);
37
+ });
38
+
39
+ mongoose.connection.on('disconnected', () => {
40
+ logger.warn('MongoDB disconnected. Attempting to reconnect...');
41
+ connectDB();
42
+ });
43
+
44
+ // Define schemas
45
+ const userSchema = new mongoose.Schema({
46
+ email: { type: String, required: true, unique: true },
47
+ password: { type: String, required: true },
48
+ lastSync: Date,
49
+ managedBy: { type: String, required: true }
50
+ });
51
+
52
+ const addonSchema = new mongoose.Schema({
53
+ transportUrl: { type: String, required: true },
54
+ transportName: { type: String, default: 'http' },
55
+ manifest: {
56
+ id: String,
57
+ name: String,
58
+ version: String,
59
+ description: String,
60
+ logo: String,
61
+ icon: String
62
+ },
63
+ flags: { type: Map, of: Boolean },
64
+ userEmail: { type: String, required: true }
65
+ });
66
+
67
+ const catalogSchema = new mongoose.Schema({
68
+ userEmail: { type: String, required: true },
69
+ addons: [{
70
+ transportUrl: String,
71
+ transportName: String,
72
+ manifest: {
73
+ id: String,
74
+ name: String,
75
+ version: String,
76
+ description: String,
77
+ logo: String,
78
+ icon: String
79
+ },
80
+ flags: { type: Map, of: Boolean }
81
+ }]
82
+ });
83
+
84
+ // Create models
85
+ const User = mongoose.model('User', userSchema);
86
+ const Addon = mongoose.model('Addon', addonSchema);
87
+ const Catalog = mongoose.model('Catalog', catalogSchema);
88
+
89
+ // Database helper functions
90
+ async function ensureUserDatabases(email) {
91
+ const catalog = await Catalog.findOne({ userEmail: email });
92
+ if (!catalog) {
93
+ await Catalog.create({ userEmail: email, addons: [] });
94
+ }
95
+ }
96
+
97
+ async function readDatabase(email) {
98
+ const addons = await Addon.find({ userEmail: email });
99
+ return addons;
100
+ }
101
+
102
+ async function writeDatabase(email, data) {
103
+ await Addon.deleteMany({ userEmail: email });
104
+ if (data.length > 0) {
105
+ const addonsWithUser = data.map(addon => ({ ...addon, userEmail: email }));
106
+ await Addon.insertMany(addonsWithUser);
107
+ }
108
+ logger.debug('Database updated successfully');
109
+ }
110
+
111
+ async function readUsersDatabase(email) {
112
+ const users = await User.find({ managedBy: email });
113
+ return users.map(user => ({
114
+ email: user.email,
115
+ password: user.password,
116
+ lastSync: user.lastSync
117
+ }));
118
+ }
119
+
120
+ async function writeUsersDatabase(email, data) {
121
+ await User.deleteMany({ managedBy: email });
122
+ if (data.length > 0) {
123
+ const usersWithManager = data.map(user => ({
124
+ ...user,
125
+ managedBy: email
126
+ }));
127
+ await User.insertMany(usersWithManager);
128
+ }
129
+ logger.debug('Users database updated successfully');
130
+ }
131
+
132
+ async function writeCatalog(email, catalogs) {
133
+ await Catalog.findOneAndUpdate(
134
+ { userEmail: email },
135
+ { addons: catalogs },
136
+ { upsert: true }
137
+ );
138
+ }
139
+
140
+ async function readCatalog(email) {
141
+ const catalog = await Catalog.findOne({ userEmail: email });
142
+ return catalog ? catalog.addons : [];
143
+ }
144
+
145
+ module.exports = {
146
+ connectDB,
147
+ initializeDB,
148
+ ensureUserDatabases,
149
+ readDatabase,
150
+ writeDatabase,
151
+ readUsersDatabase,
152
+ writeUsersDatabase,
153
+ writeCatalog,
154
+ readCatalog,
155
+ models: {
156
+ User,
157
+ Addon,
158
+ Catalog
159
+ }
160
+ };
logger.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const winston = require('winston');
2
+ const DailyRotateFile = require('winston-daily-rotate-file');
3
+ const path = require('path');
4
+
5
+ // Define log format
6
+ const logFormat = winston.format.combine(
7
+ winston.format.timestamp({
8
+ format: 'YYYY-MM-DD HH:mm:ss'
9
+ }),
10
+ winston.format.errors({ stack: true }),
11
+ winston.format.splat(),
12
+ winston.format.json()
13
+ );
14
+
15
+ // Create logs directory if it doesn't exist
16
+ const logDir = 'logs';
17
+
18
+ // Configure daily rotate file transport
19
+ const fileRotateTransport = new DailyRotateFile({
20
+ filename: path.join(logDir, 'stremio-addon-manager-%DATE%.log'),
21
+ datePattern: 'YYYY-MM-DD',
22
+ maxSize: '20m',
23
+ maxFiles: '14d',
24
+ createSymlink: true,
25
+ symlinkName: 'current.log'
26
+ });
27
+
28
+ // Create logger instance
29
+ const logger = winston.createLogger({
30
+ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
31
+ format: logFormat,
32
+ transports: [
33
+ // Write all logs with level 'error' and below to error.log
34
+ new winston.transports.File({
35
+ filename: path.join(logDir, 'error.log'),
36
+ level: 'error',
37
+ format: winston.format.combine(
38
+ winston.format.timestamp(),
39
+ winston.format.json()
40
+ )
41
+ }),
42
+ // Write all logs with level 'info' and below to combined.log
43
+ fileRotateTransport
44
+ ]
45
+ });
46
+
47
+ // If we're not in production, also log to the console with colorized output
48
+ if (process.env.NODE_ENV !== 'production') {
49
+ logger.add(new winston.transports.Console({
50
+ format: winston.format.combine(
51
+ winston.format.colorize(),
52
+ winston.format.simple(),
53
+ winston.format.printf(({ level, message, timestamp, stack }) => {
54
+ if (stack) {
55
+ return `${timestamp} ${level}: ${message}\n${stack}`;
56
+ }
57
+ return `${timestamp} ${level}: ${message}`;
58
+ })
59
+ )
60
+ }));
61
+ }
62
+
63
+ // Create a stream object with a write function for Morgan
64
+ logger.stream = {
65
+ write: function(message) {
66
+ logger.info(message.trim());
67
+ }
68
+ };
69
+
70
+ module.exports = logger;
package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "stremio-addon-manager",
3
+ "version": "1.0.0",
4
+ "description": "A web application to manage Stremio addons",
5
+ "main": "app.js",
6
+ "scripts": {
7
+ "start": "node app.js",
8
+ "dev": "nodemon app.js",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [
12
+ "stremio",
13
+ "addon",
14
+ "manager"
15
+ ],
16
+ "author": "",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "cors": "^2.8.5",
20
+ "dotenv": "^16.4.7",
21
+ "express": "^4.18.2",
22
+ "express-session": "^1.17.3",
23
+ "mongoose": "^8.9.5",
24
+ "node-fetch": "^2.6.1",
25
+ "winston": "^3.17.0",
26
+ "winston-daily-rotate-file": "^5.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "nodemon": "^2.0.22"
30
+ }
31
+ }
public/css/style.css ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset and Base Styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ /* General Layout */
9
+ body {
10
+ font-family: Arial, sans-serif;
11
+ max-width: 1200px;
12
+ margin: 0 auto;
13
+ padding: 20px;
14
+ background-color: #111827;
15
+ line-height: 1.6;
16
+ color: #e5e7eb;
17
+ }
18
+
19
+ /* Container Styles */
20
+ .form-container {
21
+ background: #1f2937;
22
+ padding: 20px;
23
+ border-radius: 8px;
24
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
25
+ margin-bottom: 20px;
26
+ border: 1px solid #374151;
27
+ }
28
+
29
+ /* Form Elements */
30
+ .form-group {
31
+ margin-bottom: 15px;
32
+ }
33
+
34
+ label {
35
+ display: block;
36
+ margin-bottom: 5px;
37
+ font-weight: 500;
38
+ color: #e5e7eb;
39
+ }
40
+
41
+ input {
42
+ width: 100%;
43
+ padding: 8px 12px;
44
+ border: 1px solid #374151;
45
+ border-radius: 6px;
46
+ font-size: 14px;
47
+ background-color: #1f2937;
48
+ color: #e5e7eb;
49
+ transition: all 0.2s ease;
50
+ }
51
+
52
+ input:focus {
53
+ outline: none;
54
+ border-color: #3b82f6;
55
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
56
+ background-color: #262f3f;
57
+ }
58
+
59
+ input::placeholder {
60
+ color: #6b7280;
61
+ }
62
+
63
+ /* Button Styles */
64
+ button {
65
+ background-color: #3b82f6;
66
+ color: #ffffff;
67
+ padding: 8px 16px;
68
+ border: none;
69
+ border-radius: 6px;
70
+ cursor: pointer;
71
+ font-size: 14px;
72
+ font-weight: 500;
73
+ margin-right: 5px;
74
+ transition: all 0.2s ease;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ }
79
+
80
+ button:hover {
81
+ background-color: #2563eb;
82
+ transform: translateY(-1px);
83
+ }
84
+
85
+ button:active {
86
+ transform: translateY(0);
87
+ }
88
+
89
+ button:disabled {
90
+ background-color: #4b5563;
91
+ cursor: not-allowed;
92
+ opacity: 0.7;
93
+ }
94
+
95
+ .delete-btn {
96
+ background-color: #dc2626;
97
+ }
98
+
99
+ .delete-btn:hover {
100
+ background-color: #b91c1c;
101
+ }
102
+
103
+ .refresh-btn, .sync-btn {
104
+ background-color: #3b82f6;
105
+ }
106
+
107
+ .refresh-btn:hover, .sync-btn:hover {
108
+ background-color: #2563eb;
109
+ }
110
+
111
+ .install-btn {
112
+ display: inline-block;
113
+ text-decoration: none;
114
+ background-color: #059669;
115
+ color: white;
116
+ padding: 8px 16px;
117
+ border-radius: 6px;
118
+ transition: all 0.2s ease;
119
+ }
120
+
121
+ .install-btn:hover {
122
+ background-color: #047857;
123
+ }
124
+
125
+ /* Grid Layouts */
126
+ .addon-grid {
127
+ display: grid;
128
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
129
+ gap: 20px;
130
+ margin-top: 20px;
131
+ }
132
+
133
+ .users-grid {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 10px;
137
+ margin-top: 20px;
138
+ }
139
+
140
+ /* Card Styles */
141
+ .addon-item, .user-item {
142
+ background: #1f2937;
143
+ padding: 20px;
144
+ border-radius: 8px;
145
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
146
+ transition: all 0.2s ease;
147
+ border: 1px solid #374151;
148
+ }
149
+
150
+ .addon-item:hover, .user-item:hover {
151
+ transform: translateY(-2px);
152
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.3);
153
+ border-color: #4b5563;
154
+ }
155
+
156
+ /* Addon Specific Styles */
157
+ .addon-item img {
158
+ max-width: 100px;
159
+ height: auto;
160
+ margin-bottom: 10px;
161
+ border-radius: 6px;
162
+ }
163
+
164
+ .addon-header {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ align-items: center;
168
+ margin-bottom: 10px;
169
+ }
170
+
171
+ .addon-title {
172
+ margin: 0;
173
+ font-size: 1.2em;
174
+ color: #e5e7eb;
175
+ font-weight: 600;
176
+ }
177
+
178
+ .addon-version {
179
+ color: #9ca3af;
180
+ font-size: 0.9em;
181
+ }
182
+
183
+ .addon-description {
184
+ margin: 10px 0;
185
+ color: #d1d5db;
186
+ font-size: 0.95em;
187
+ line-height: 1.5;
188
+ }
189
+
190
+ .addon-url {
191
+ word-break: break-all;
192
+ color: #9ca3af;
193
+ font-size: 0.9em;
194
+ padding: 8px;
195
+ background: #262f3f;
196
+ border-radius: 4px;
197
+ border: 1px solid #374151;
198
+ }
199
+
200
+ /* Progress Bar Styles */
201
+ .sync-progress {
202
+ margin: 15px 0;
203
+ padding: 12px;
204
+ background: #262f3f;
205
+ border-radius: 6px;
206
+ border: 1px solid #374151;
207
+ }
208
+
209
+ .sync-status {
210
+ margin-bottom: 8px;
211
+ font-size: 14px;
212
+ color: #9ca3af;
213
+ }
214
+
215
+ .progress-bar {
216
+ width: 100%;
217
+ height: 8px;
218
+ background: #374151;
219
+ border-radius: 4px;
220
+ overflow: hidden;
221
+ }
222
+
223
+ .progress-fill {
224
+ height: 100%;
225
+ background: #3b82f6;
226
+ width: 0;
227
+ transition: width 0.3s ease;
228
+ }
229
+
230
+ /* Loading States */
231
+ .loading {
232
+ text-align: center;
233
+ padding: 20px;
234
+ color: #9ca3af;
235
+ }
236
+
237
+ .loading i {
238
+ color: #3b82f6;
239
+ }
240
+
241
+ /* Utility Classes */
242
+ .hidden {
243
+ display: none !important;
244
+ }
245
+
246
+ /* Button Groups */
247
+ .button-group {
248
+ display: flex;
249
+ gap: 10px;
250
+ align-items: center;
251
+ }
252
+
253
+ /* File Input Styling */
254
+ input[type="file"] {
255
+ display: none;
256
+ }
257
+
258
+ .file-input-label {
259
+ display: inline-block;
260
+ padding: 8px 16px;
261
+ background-color: #4b5563;
262
+ color: white;
263
+ border-radius: 6px;
264
+ cursor: pointer;
265
+ transition: all 0.2s ease;
266
+ }
267
+
268
+ .file-input-label:hover {
269
+ background-color: #374151;
270
+ }
271
+
272
+ /* Empty States */
273
+ .empty-state {
274
+ text-align: center;
275
+ padding: 40px 20px;
276
+ color: #9ca3af;
277
+ background: #1f2937;
278
+ border-radius: 8px;
279
+ border: 2px dashed #374151;
280
+ }
281
+
282
+ .empty-state i {
283
+ font-size: 48px;
284
+ margin-bottom: 16px;
285
+ color: #4b5563;
286
+ }
287
+
288
+ /* Error States */
289
+ .error {
290
+ background-color: #7f1d1d;
291
+ color: #fecaca;
292
+ padding: 12px;
293
+ border-radius: 6px;
294
+ margin: 10px 0;
295
+ border: 1px solid #dc2626;
296
+ }
297
+
298
+ /* Modal Styles */
299
+ .modal {
300
+ background: #1f2937;
301
+ border-radius: 8px;
302
+ padding: 20px;
303
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
304
+ border: 1px solid #374151;
305
+ }
306
+
307
+ .modal-header {
308
+ border-bottom: 1px solid #374151;
309
+ padding-bottom: 10px;
310
+ margin-bottom: 15px;
311
+ }
312
+
313
+ .modal-footer {
314
+ border-top: 1px solid #374151;
315
+ padding-top: 15px;
316
+ margin-top: 15px;
317
+ }
318
+
319
+ /* Table Styles */
320
+ table {
321
+ width: 100%;
322
+ border-collapse: separate;
323
+ border-spacing: 0;
324
+ margin: 1rem 0;
325
+ background: #1f2937;
326
+ border-radius: 6px;
327
+ overflow: hidden;
328
+ }
329
+
330
+ th, td {
331
+ padding: 12px;
332
+ text-align: left;
333
+ border-bottom: 1px solid #374151;
334
+ color: #e5e7eb;
335
+ }
336
+
337
+ th {
338
+ background-color: #262f3f;
339
+ font-weight: 600;
340
+ color: #e5e7eb;
341
+ }
342
+
343
+ tr:last-child td {
344
+ border-bottom: none;
345
+ }
346
+
347
+ tr:hover td {
348
+ background-color: #262f3f;
349
+ }
350
+
351
+ /* Sidebar Specific Styles */
352
+ .sidebar {
353
+ background-color: #1f2937;
354
+ border-right: 1px solid #374151;
355
+ }
356
+
357
+ .sidebar-item {
358
+ color: #e5e7eb;
359
+ padding: 10px 15px;
360
+ border-radius: 6px;
361
+ margin: 2px 0;
362
+ transition: all 0.2s ease;
363
+ }
364
+
365
+ .sidebar-item:hover {
366
+ background-color: #262f3f;
367
+ }
368
+
369
+ .sidebar-item.active {
370
+ background-color: #2563eb;
371
+ color: white;
372
+ }
373
+
374
+ /* Notification Styles */
375
+ .notification {
376
+ background-color: #1f2937;
377
+ border-left: 4px solid #3b82f6;
378
+ padding: 12px;
379
+ margin: 10px 0;
380
+ border-radius: 0 6px 6px 0;
381
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
382
+ }
383
+
384
+ .notification.success {
385
+ border-left-color: #059669;
386
+ }
387
+
388
+ .notification.error {
389
+ border-left-color: #dc2626;
390
+ }
391
+
392
+ .notification.warning {
393
+ border-left-color: #d97706;
394
+ }
395
+
396
+ /* Custom Scrollbar */
397
+ ::-webkit-scrollbar {
398
+ width: 8px;
399
+ height: 8px;
400
+ }
401
+
402
+ ::-webkit-scrollbar-track {
403
+ background: #1f2937;
404
+ }
405
+
406
+ ::-webkit-scrollbar-thumb {
407
+ background: #4b5563;
408
+ border-radius: 4px;
409
+ }
410
+
411
+ ::-webkit-scrollbar-thumb:hover {
412
+ background: #6b7280;
413
+ }
414
+
415
+ /* Media Queries */
416
+ @media (max-width: 768px) {
417
+ .addon-grid {
418
+ grid-template-columns: 1fr;
419
+ }
420
+
421
+ .button-group {
422
+ flex-direction: column;
423
+ width: 100%;
424
+ }
425
+
426
+ button {
427
+ width: 100%;
428
+ margin-bottom: 5px;
429
+ }
430
+
431
+ .form-container {
432
+ padding: 15px;
433
+ }
434
+ }
435
+
436
+ @media (max-width: 480px) {
437
+ body {
438
+ padding: 10px;
439
+ }
440
+
441
+ .addon-item, .user-item {
442
+ padding: 15px;
443
+ }
444
+
445
+ .notification {
446
+ padding: 10px;
447
+ }
448
+ }
449
+
450
+ /* Print Styles */
451
+ @media print {
452
+ body {
453
+ background: white;
454
+ color: black;
455
+ }
456
+
457
+ .form-container, .addon-item, .user-item {
458
+ box-shadow: none;
459
+ border: 1px solid #ddd;
460
+ }
461
+
462
+ .no-print {
463
+ display: none !important;
464
+ }
465
+ }
public/index.html ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Stremio Addon Manager</title>
7
+
8
+ <!-- React and ReactDOM -->
9
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
10
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
11
+
12
+ <!-- Lucide Icons -->
13
+ <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
14
+
15
+ <!-- Tailwind CSS -->
16
+ <script src="https://cdn.tailwindcss.com"></script>
17
+
18
+ <!-- Tailwind Config for Dark Mode -->
19
+ <script>
20
+ tailwind.config = {
21
+ darkMode: 'class',
22
+ theme: {
23
+ extend: {
24
+ colors: {
25
+ dark: {
26
+ bg: '#111827',
27
+ card: '#1f2937',
28
+ text: '#e5e7eb',
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ </script>
35
+
36
+ <!-- Custom CSS -->
37
+ <link rel="stylesheet" href="/css/style.css">
38
+ </head>
39
+ <body class="bg-gray-900 text-gray-200">
40
+ <!-- Login Section -->
41
+ <div id="loginForm" class="flex items-center justify-center min-h-screen bg-gray-900">
42
+ <div class="form-container w-full max-w-md bg-gray-800 shadow-xl">
43
+ <h2 class="text-2xl font-bold mb-6 text-center text-gray-100">Login with Stremio</h2>
44
+ <form id="loginFormElement" class="space-y-4">
45
+ <div class="form-group">
46
+ <label for="email" class="block text-sm font-medium text-gray-200">Email:</label>
47
+ <input type="email" id="email" required
48
+ class="mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md shadow-sm text-gray-200
49
+ focus:ring-blue-500 focus:border-blue-500">
50
+ </div>
51
+ <div class="form-group">
52
+ <label for="password" class="block text-sm font-medium text-gray-200">Password:</label>
53
+ <input type="password" id="password" required
54
+ class="mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md shadow-sm text-gray-200
55
+ focus:ring-blue-500 focus:border-blue-500">
56
+ </div>
57
+ <div class="button-group">
58
+ <button type="submit"
59
+ class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-blue-600
60
+ hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
61
+ Login with Stremio
62
+ </button>
63
+ </div>
64
+ </form>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Root for React Content -->
69
+ <div id="root" class="hidden"></div>
70
+ <!-- Main Content Sections -->
71
+ <div id="mainContent" class="hidden">
72
+ <!-- Dashboard Section -->
73
+ <div id="dashboardSection" class="section-content">
74
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
75
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700">
76
+ <div class="flex items-center justify-between">
77
+ <h3 class="text-lg font-medium text-gray-200">Total Users</h3>
78
+ <i data-lucide="users" class="w-6 h-6 text-blue-500"></i>
79
+ </div>
80
+ <p class="text-3xl font-bold mt-2 text-gray-100" id="totalUsers">0</p>
81
+ </div>
82
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700">
83
+ <div class="flex items-center justify-between">
84
+ <h3 class="text-lg font-medium text-gray-200">Active Addons</h3>
85
+ <i data-lucide="package" class="w-6 h-6 text-green-500"></i>
86
+ </div>
87
+ <p class="text-3xl font-bold mt-2 text-gray-100" id="activeAddons">0</p>
88
+ </div>
89
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700">
90
+ <div class="flex items-center justify-between">
91
+ <h3 class="text-lg font-medium text-gray-200">Last Sync</h3>
92
+ <i data-lucide="clock" class="w-6 h-6 text-purple-500"></i>
93
+ </div>
94
+ <p class="text-lg mt-2 text-gray-300" id="lastSync">Never</p>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Register Section -->
100
+ <div id="registerSection" class="section-content hidden">
101
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700">
102
+ <h2 class="text-xl font-semibold mb-4 text-gray-100">Register New Stremio Account</h2>
103
+ <form id="stremioRegisterForm" class="space-y-4">
104
+ <div class="form-group">
105
+ <label for="stremioRegEmail" class="block text-sm font-medium text-gray-200">Email:</label>
106
+ <input type="email" id="stremioRegEmail" required
107
+ class="mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-200">
108
+ </div>
109
+ <div class="form-group">
110
+ <label for="stremioRegPassword" class="block text-sm font-medium text-gray-200">Password:</label>
111
+ <input type="password" id="stremioRegPassword" required
112
+ class="mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-200">
113
+ </div>
114
+ <div class="form-group flex items-center">
115
+ <input type="checkbox" id="gdprConsent" required
116
+ class="h-4 w-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
117
+ <label for="gdprConsent" class="ml-2 block text-sm text-gray-200">
118
+ I agree to the Terms of Service and Privacy Policy
119
+ </label>
120
+ </div>
121
+ <button type="submit"
122
+ class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700">
123
+ Register New Stremio Account
124
+ </button>
125
+ </form>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- Addons Section -->
130
+ <div id="addonsSection" class="section-content hidden">
131
+ <!-- Add New Addon Form -->
132
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700 mb-6">
133
+ <h2 class="text-xl font-semibold mb-4 text-gray-100">Add New Addon</h2>
134
+ <form id="addonForm" class="space-y-4">
135
+ <div class="form-group">
136
+ <label for="manifestUrl" class="block text-sm font-medium text-gray-200">Manifest URL:</label>
137
+ <input type="url" id="manifestUrl" required
138
+ placeholder="https://example.com/manifest.json"
139
+ class="mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-200">
140
+ </div>
141
+ <button type="submit"
142
+ class="py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700">
143
+ Add Addon
144
+ </button>
145
+ </form>
146
+ </div>
147
+
148
+ <!-- Installed Addons -->
149
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700">
150
+ <div class="flex items-center justify-between mb-6">
151
+ <h2 class="text-xl font-semibold text-gray-100">Installed Addons</h2>
152
+ <div class="flex gap-2">
153
+ <button onclick="updateAddonList()"
154
+ class="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
155
+ <i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i>
156
+ Refresh
157
+ </button>
158
+ <button onclick="exportAddons()"
159
+ class="flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
160
+ <i data-lucide="download" class="w-4 h-4 mr-2"></i>
161
+ Export
162
+ </button>
163
+ <input type="file" id="importFile" accept=".json" class="hidden">
164
+ <button onclick="document.getElementById('importFile').click()"
165
+ class="flex items-center px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700">
166
+ <i data-lucide="upload" class="w-4 h-4 mr-2"></i>
167
+ Import
168
+ </button>
169
+ </div>
170
+ </div>
171
+ <div id="loadingState" class="loading hidden text-center py-8">
172
+ <i data-lucide="loader" class="w-8 h-8 mx-auto animate-spin text-blue-600"></i>
173
+ <p class="mt-2 text-gray-400">Loading addons...</p>
174
+ </div>
175
+ <div id="addons" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Users Section -->
180
+ <div id="usersSection" class="section-content hidden">
181
+ <!-- Add User Form -->
182
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700 mb-6">
183
+ <h2 class="text-xl font-semibold mb-4 text-gray-100">Add New User</h2>
184
+ <form id="userForm" class="space-y-4">
185
+ <div class="form-group">
186
+ <label for="userEmail" class="block text-sm font-medium text-gray-200">Stremio Email:</label>
187
+ <input type="email" id="userEmail" required
188
+ class="mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-200">
189
+ </div>
190
+ <div class="form-group">
191
+ <label for="userPassword" class="block text-sm font-medium text-gray-200">Stremio Password:</label>
192
+ <input type="password" id="userPassword" required
193
+ class="mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-gray-200">
194
+ </div>
195
+ <button type="submit"
196
+ class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700">
197
+ Add User
198
+ </button>
199
+ </form>
200
+ </div>
201
+
202
+ <!-- Users List -->
203
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700">
204
+ <div class="flex items-center justify-between mb-6">
205
+ <h2 class="text-xl font-semibold text-gray-100">Registered Users</h2>
206
+ <div class="flex gap-2">
207
+ <button onclick="syncAllUsersWithProgress()"
208
+ class="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
209
+ <i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i>
210
+ Sync All Users
211
+ </button>
212
+ <button onclick="updateUsersList()"
213
+ class="flex items-center px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700">
214
+ <i data-lucide="refresh" class="w-4 h-4 mr-2"></i>
215
+ Refresh
216
+ </button>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- Sync Progress Bar -->
221
+ <div id="syncProgress" class="hidden mb-6">
222
+ <div class="sync-status text-sm text-gray-400 mb-2"></div>
223
+ <div class="w-full bg-gray-700 rounded-full h-2">
224
+ <div class="progress-fill bg-blue-600 h-2 rounded-full transition-all duration-300"></div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Loading State -->
229
+ <div id="loadingUsers" class="loading hidden text-center py-8">
230
+ <i data-lucide="loader" class="w-8 h-8 mx-auto animate-spin text-blue-600"></i>
231
+ <p class="mt-2 text-gray-400">Loading users...</p>
232
+ </div>
233
+
234
+ <!-- Users List Container -->
235
+ <div id="usersList" class="divide-y divide-gray-700">
236
+ <!-- User items will be dynamically inserted here -->
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- Settings Section -->
242
+ <div id="settingsSection" class="section-content hidden">
243
+ <div class="bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700">
244
+ <h2 class="text-xl font-semibold mb-6 text-gray-100">Settings</h2>
245
+ <div class="space-y-4">
246
+ <div class="flex justify-between items-center py-4 border-t border-gray-700">
247
+ <div>
248
+ <h3 class="text-lg font-medium text-gray-200">Account</h3>
249
+ <p class="text-sm text-gray-400">Manage your account settings</p>
250
+ </div>
251
+ <button onclick="handleLogout()"
252
+ class="flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
253
+ <i data-lucide="log-out" class="w-4 h-4 mr-2"></i>
254
+ Logout
255
+ </button>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+
262
+ <!-- Scripts -->
263
+ <script src="/js/SidebarLayout.js"></script>
264
+ <script src="/js/main.js"></script>
265
+
266
+ <!-- Initialize Lucide Icons -->
267
+ <script>
268
+ document.addEventListener('DOMContentLoaded', function() {
269
+ lucide.createIcons();
270
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
271
+ document.documentElement.classList.add('dark');
272
+ }
273
+ });
274
+
275
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
276
+ if (e.matches) {
277
+ document.documentElement.classList.add('dark');
278
+ } else {
279
+ document.documentElement.classList.remove('dark');
280
+ }
281
+ });
282
+ </script>
283
+
284
+ </body>
285
+ </html>
public/js/SidebarLayout.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const SidebarLayout = () => {
2
+ const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
3
+ const [activeSection, setActiveSection] = React.useState('dashboard');
4
+
5
+ // Menu items configuration
6
+ const menuItems = [
7
+ { id: 'dashboard', label: 'Dashboard', iconName: 'layout-dashboard' },
8
+ { id: 'register', label: 'Register Stremio', iconName: 'user-plus' },
9
+ { id: 'addons', label: 'Add New Addon', iconName: 'package' },
10
+ { id: 'users', label: 'Manage Users', iconName: 'users' },
11
+ { id: 'settings', label: 'Settings', iconName: 'settings' }
12
+ ];
13
+
14
+ // Effect to move mainContent on first render
15
+ React.useEffect(() => {
16
+ const mainContent = document.getElementById('mainContent');
17
+ const sidebarMainContent = document.querySelector('#root .content-area');
18
+
19
+ if (mainContent && sidebarMainContent) {
20
+ // Move the content
21
+ mainContent.classList.remove('hidden');
22
+ sidebarMainContent.appendChild(mainContent);
23
+
24
+ // Show initial section
25
+ showSection('dashboard');
26
+ }
27
+ }, []);
28
+
29
+ // Function to handle section switching
30
+ const showSection = (sectionId) => {
31
+ setActiveSection(sectionId);
32
+
33
+ // Hide all sections
34
+ document.querySelectorAll('.section-content').forEach(section => {
35
+ section.classList.add('hidden');
36
+ });
37
+
38
+ // Show selected section
39
+ const selectedSection = document.getElementById(`${sectionId}Section`);
40
+ if (selectedSection) {
41
+ selectedSection.classList.remove('hidden');
42
+
43
+ // Update content based on section
44
+ switch (sectionId) {
45
+ case 'dashboard':
46
+ if (window.updateDashboardStats) {
47
+ window.updateDashboardStats();
48
+ }
49
+ break;
50
+ case 'users':
51
+ if (window.updateUsersList) {
52
+ window.updateUsersList();
53
+ }
54
+ break;
55
+ case 'addons':
56
+ if (window.updateAddonList) {
57
+ window.updateAddonList();
58
+ }
59
+ break;
60
+ }
61
+ }
62
+ };
63
+
64
+ return React.createElement('div', {
65
+ className: 'min-h-screen bg-gray-900'
66
+ }, [
67
+ // Top Bar
68
+ React.createElement('div', {
69
+ className: 'bg-gray-800 border-b border-gray-700 shadow-xl fixed top-0 left-0 right-0 h-16 z-30 flex items-center px-4'
70
+ }, [
71
+ React.createElement('button', {
72
+ key: 'toggle-button',
73
+ onClick: () => setIsSidebarOpen(!isSidebarOpen),
74
+ className: 'p-2 hover:bg-gray-700 rounded-lg text-gray-300'
75
+ }, [
76
+ React.createElement('i', {
77
+ key: 'toggle-icon',
78
+ 'data-lucide': isSidebarOpen ? 'panel-left-close' : 'panel-left-open',
79
+ className: 'w-6 h-6'
80
+ })
81
+ ]),
82
+ React.createElement('h1', {
83
+ key: 'title',
84
+ className: 'text-xl font-semibold ml-4 text-gray-100'
85
+ }, 'Stremio Addon Manager')
86
+ ]),
87
+
88
+ // Sidebar
89
+ React.createElement('div', {
90
+ className: `fixed left-0 top-16 h-[calc(100vh-4rem)] bg-gray-800 border-r border-gray-700 shadow-xl
91
+ transition-all duration-300 z-20 ${isSidebarOpen ? 'w-64' : 'w-20'}`
92
+ }, [
93
+ React.createElement('nav', {
94
+ key: 'nav',
95
+ className: 'p-4'
96
+ },
97
+ menuItems.map(item =>
98
+ React.createElement('button', {
99
+ key: item.id,
100
+ onClick: () => showSection(item.id),
101
+ className: `w-full flex items-center px-4 py-3 mb-2 rounded-lg transition-colors
102
+ ${activeSection === item.id
103
+ ? 'bg-blue-600/20 text-blue-400'
104
+ : 'text-gray-400 hover:bg-gray-700 hover:text-gray-200'}
105
+ ${!isSidebarOpen ? 'justify-center' : ''}`
106
+ }, [
107
+ React.createElement('i', {
108
+ key: 'icon',
109
+ 'data-lucide': item.iconName,
110
+ className: 'w-5 h-5'
111
+ }),
112
+ isSidebarOpen && React.createElement('span', {
113
+ key: 'label',
114
+ className: 'ml-3'
115
+ }, item.label)
116
+ ])
117
+ )
118
+ )
119
+ ]),
120
+
121
+ // Main Content Area
122
+ React.createElement('div', {
123
+ className: `pt-16 transition-all duration-300 ${isSidebarOpen ? 'ml-64' : 'ml-20'}`
124
+ }, [
125
+ React.createElement('div', {
126
+ key: 'content-wrapper',
127
+ className: 'p-6'
128
+ }, [
129
+ React.createElement('div', {
130
+ key: 'content-area',
131
+ className: 'max-w-6xl mx-auto content-area'
132
+ })
133
+ ])
134
+ ])
135
+ ]);
136
+ };
137
+
138
+ // Initialize icons after component renders
139
+ const initIcons = () => {
140
+ lucide.createIcons();
141
+ };
142
+
143
+ // Make the component and initialization function available globally
144
+ window.SidebarLayout = SidebarLayout;
145
+ window.initIcons = initIcons;
public/js/main.js ADDED
@@ -0,0 +1,753 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // React Component for Device Linking
2
+ const UserLinkComponent = ({ email, password }) => {
3
+ const [linkCode, setLinkCode] = React.useState('');
4
+ const [isLinking, setIsLinking] = React.useState(false);
5
+ const [linkStatus, setLinkStatus] = React.useState(null);
6
+
7
+ const handleLink = async () => {
8
+ if (linkCode.length !== 4) {
9
+ setLinkStatus({ type: 'error', message: 'Please enter a valid 4-digit code' });
10
+ return;
11
+ }
12
+
13
+ setIsLinking(true);
14
+ setLinkStatus(null);
15
+
16
+ try {
17
+ // Step 1: Login to Stremio API
18
+ const loginResponse = await fetch('https://api.strem.io/api/login', {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Content-Type': 'text/plain;charset=UTF-8',
22
+ 'Accept': '*/*',
23
+ 'Origin': 'https://web.stremio.com',
24
+ 'Referer': 'https://web.stremio.com/'
25
+ },
26
+ body: JSON.stringify({
27
+ type: "Login",
28
+ email: email,
29
+ password: password,
30
+ facebook: false
31
+ })
32
+ });
33
+
34
+ const loginData = await loginResponse.json();
35
+ if (!loginResponse.ok || !loginData.result?.authKey) {
36
+ throw new Error(loginData.error || 'Login failed');
37
+ }
38
+
39
+ const authKey = loginData.result.authKey;
40
+
41
+ // Step 2: Link the device
42
+ const linkResponse = await fetch(`https://link.stremio.com/api/write?code=${linkCode}`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
46
+ 'Accept': '*/*',
47
+ 'Origin': 'https://www.stremio.com',
48
+ 'Referer': 'https://www.stremio.com/'
49
+ },
50
+ body: JSON.stringify({ authKey })
51
+ });
52
+
53
+ const linkData = await linkResponse.json();
54
+ if (!linkResponse.ok || !linkData.success) {
55
+ throw new Error(linkData.error || 'Failed to link device');
56
+ }
57
+
58
+ setLinkStatus({ type: 'success', message: 'Device linked successfully! Please wait...' });
59
+
60
+ // Step 3: Wait for 1 minute before logging out
61
+ await new Promise(resolve => setTimeout(resolve, 60000));
62
+
63
+ // Step 4: Logout from Stremio API
64
+ await fetch('https://api.strem.io/api/logout', {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'text/plain;charset=UTF-8',
68
+ 'Accept': '*/*'
69
+ },
70
+ body: JSON.stringify({
71
+ type: "Logout",
72
+ authKey: authKey
73
+ })
74
+ });
75
+
76
+ setLinkStatus({ type: 'success', message: 'Device linked successfully!' });
77
+ } catch (error) {
78
+ setLinkStatus({ type: 'error', message: error.message });
79
+ } finally {
80
+ setIsLinking(false);
81
+ }
82
+ };
83
+
84
+ return React.createElement('div', {
85
+ className: 'mt-2 flex items-center gap-2'
86
+ }, [
87
+ React.createElement('input', {
88
+ key: 'input',
89
+ type: 'text',
90
+ maxLength: '4',
91
+ placeholder: 'Enter 4-digit code',
92
+ value: linkCode,
93
+ onChange: (e) => setLinkCode(e.target.value.toUpperCase()),
94
+ className: 'w-32 px-3 py-1 border border-gray-300 rounded-md text-center uppercase'
95
+ }),
96
+ React.createElement('button', {
97
+ key: 'button',
98
+ onClick: handleLink,
99
+ disabled: isLinking,
100
+ className: 'inline-flex items-center px-3 py-1 bg-purple-600 text-white rounded-md hover:bg-purple-700 disabled:opacity-50'
101
+ },
102
+ isLinking ? [
103
+ React.createElement('i', {
104
+ key: 'loader',
105
+ 'data-lucide': 'loader-2',
106
+ className: 'w-4 h-4 mr-2 animate-spin'
107
+ }),
108
+ 'Linking...'
109
+ ] : 'Link Device'
110
+ ),
111
+ linkStatus && React.createElement('div', {
112
+ key: 'status',
113
+ className: `ml-2 px-3 py-1 rounded-md ${linkStatus.type === 'success' ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'}`
114
+ }, linkStatus.message)
115
+ ].filter(Boolean));
116
+ };
117
+ // Event Listeners
118
+ document.addEventListener('DOMContentLoaded', () => {
119
+ const loginForm = document.getElementById('loginFormElement');
120
+ if (loginForm) loginForm.addEventListener('submit', handleLogin);
121
+
122
+ const stremioRegForm = document.getElementById('stremioRegisterForm');
123
+ if (stremioRegForm) stremioRegForm.addEventListener('submit', handleStremioRegister);
124
+
125
+ const userForm = document.getElementById('userForm');
126
+ if (userForm) userForm.addEventListener('submit', handleAddUser);
127
+
128
+ const addonForm = document.getElementById('addonForm');
129
+ if (addonForm) addonForm.addEventListener('submit', handleAddAddon);
130
+
131
+ const importFile = document.getElementById('importFile');
132
+ if (importFile) importFile.addEventListener('change', handleImportFile);
133
+
134
+ checkLoginStatus();
135
+ });
136
+
137
+ // Core Functions
138
+ window.handleLogin = async (event) => {
139
+ event.preventDefault();
140
+
141
+ const email = document.getElementById('email').value;
142
+ const password = document.getElementById('password').value;
143
+
144
+ try {
145
+ const response = await fetch('/api/login', {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ credentials: 'include',
149
+ body: JSON.stringify({ email, password })
150
+ });
151
+
152
+ const data = await response.json();
153
+
154
+ if (response.ok && data.success) {
155
+ localStorage.setItem('currentUserEmail', email);
156
+
157
+ // Fetch catalogs after login
158
+ const catalogs = await fetchAndUpdateCatalogs();
159
+ console.log('Fetched catalogs:', catalogs);
160
+
161
+ showMainContent();
162
+ updateAddonList();
163
+ updateUsersList();
164
+ updateDashboardStats();
165
+ } else {
166
+ throw new Error(data.error || 'Login failed');
167
+ }
168
+ } catch (error) {
169
+ console.error('Login error:', error);
170
+ alert('Login failed: ' + error.message);
171
+ }
172
+ };
173
+
174
+ window.handleLogout = async () => {
175
+ try {
176
+ const response = await fetch('/api/logout', {
177
+ method: 'POST',
178
+ credentials: 'include'
179
+ });
180
+
181
+ if (response.ok) {
182
+ localStorage.removeItem('mainUserCatalogs');
183
+ localStorage.removeItem('currentUserEmail');
184
+ window.location.reload();
185
+ }
186
+ } catch (error) {
187
+ console.error('Logout error:', error);
188
+ alert('Logout failed: ' + error.message);
189
+ }
190
+ };
191
+
192
+ async function fetchAndUpdateCatalogs() {
193
+ try {
194
+ const response = await fetch('/api/user/catalog', {
195
+ credentials: 'include'
196
+ });
197
+
198
+ if (!response.ok) {
199
+ throw new Error('Failed to fetch catalogs');
200
+ }
201
+
202
+ const data = await response.json();
203
+ console.log('Received catalog data:', data);
204
+
205
+ if (data.success && data.catalog) {
206
+ const currentUser = localStorage.getItem('currentUserEmail');
207
+ const catalogData = {
208
+ mainUser: currentUser,
209
+ catalogs: data.catalog
210
+ };
211
+ console.log('Storing catalog data:', catalogData);
212
+ localStorage.setItem('mainUserCatalogs', JSON.stringify(catalogData));
213
+ return data.catalog;
214
+ }
215
+ } catch (error) {
216
+ console.error('Error fetching catalogs:', error);
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ window.handleAddAddon = async (event) => {
222
+ event.preventDefault();
223
+
224
+ const url = document.getElementById('manifestUrl').value.trim();
225
+
226
+ try {
227
+ new URL(url);
228
+ } catch (e) {
229
+ alert('Please enter a valid URL');
230
+ return;
231
+ }
232
+
233
+ try {
234
+ const response = await fetch('/api/addons', {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json' },
237
+ credentials: 'include',
238
+ body: JSON.stringify({ url })
239
+ });
240
+
241
+ const data = await response.json();
242
+
243
+ if (response.ok) {
244
+ alert('Addon added successfully');
245
+ document.getElementById('addonForm').reset();
246
+
247
+ localStorage.setItem('mainUserCatalogs', JSON.stringify({
248
+ mainUser: localStorage.getItem('currentUserEmail'),
249
+ catalogs: data.addons
250
+ }));
251
+
252
+ await updateAddonList();
253
+ await updateDashboardStats();
254
+ } else {
255
+ throw new Error(data.error || 'Failed to add addon');
256
+ }
257
+ } catch (error) {
258
+ console.error('Error:', error);
259
+ alert('Error adding addon: ' + error.message);
260
+ }
261
+ };
262
+ // UI Management
263
+ function showMainContent() {
264
+ const loginForm = document.getElementById('loginForm');
265
+ const root = document.getElementById('root');
266
+
267
+ if (loginForm) loginForm.classList.add('hidden');
268
+ if (root) {
269
+ root.classList.remove('hidden');
270
+ ReactDOM.render(React.createElement(SidebarLayout), root, () => {
271
+ window.initIcons();
272
+ });
273
+ }
274
+ }
275
+
276
+ function showLoginForm() {
277
+ const loginForm = document.getElementById('loginForm');
278
+ const root = document.getElementById('root');
279
+
280
+ if (loginForm) loginForm.classList.remove('hidden');
281
+ if (root) root.classList.add('hidden');
282
+ }
283
+
284
+ async function checkLoginStatus() {
285
+ try {
286
+ const response = await fetch('/api/auth/status', {
287
+ credentials: 'include'
288
+ });
289
+
290
+ const data = await response.json();
291
+
292
+ if (data.isAuthenticated) {
293
+ showMainContent();
294
+ updateDashboardStats();
295
+ } else {
296
+ showLoginForm();
297
+ }
298
+ } catch (error) {
299
+ console.error('Error checking login status:', error);
300
+ showLoginForm();
301
+ }
302
+ }
303
+
304
+ // Dashboard Functions
305
+ async function updateDashboardStats() {
306
+ try {
307
+ const [usersResponse, addonsResponse] = await Promise.all([
308
+ fetch('/api/users', { credentials: 'include' }),
309
+ fetch('/api/addons', { credentials: 'include' })
310
+ ]);
311
+
312
+ const users = await usersResponse.json();
313
+ const addons = await addonsResponse.json();
314
+
315
+ document.getElementById('totalUsers').textContent = users.length;
316
+ document.getElementById('activeAddons').textContent = addons.length;
317
+
318
+ if (users.length > 0) {
319
+ const lastSyncDate = users
320
+ .map(user => user.lastSync)
321
+ .filter(date => date)
322
+ .sort((a, b) => new Date(b) - new Date(a))[0];
323
+
324
+ document.getElementById('lastSync').textContent = lastSyncDate
325
+ ? new Date(lastSyncDate).toLocaleString()
326
+ : 'Never';
327
+ }
328
+ } catch (error) {
329
+ console.error('Error updating dashboard:', error);
330
+ }
331
+ }
332
+
333
+ // Sync Functions
334
+ async function syncUser(email) {
335
+ try {
336
+ const mainUserCatalogs = JSON.parse(localStorage.getItem('mainUserCatalogs'));
337
+ if (!mainUserCatalogs?.catalogs) {
338
+ throw new Error('Main user catalogs not found');
339
+ }
340
+
341
+ const response = await fetch(`/api/users/${encodeURIComponent(email)}/sync`, {
342
+ method: 'POST',
343
+ headers: { 'Content-Type': 'application/json' },
344
+ credentials: 'include',
345
+ body: JSON.stringify({
346
+ addons: mainUserCatalogs.catalogs
347
+ })
348
+ });
349
+
350
+ if (!response.ok) {
351
+ throw new Error('Failed to sync user');
352
+ }
353
+
354
+ alert('User synced successfully with main user\'s catalogs');
355
+ updateUsersList();
356
+ updateDashboardStats();
357
+ } catch (error) {
358
+ console.error('Sync error:', error);
359
+ alert('Error syncing user: ' + error.message);
360
+ }
361
+ }
362
+
363
+ async function syncAllUsersWithProgress() {
364
+ const progressContainer = document.getElementById('syncProgress');
365
+ const progressBar = progressContainer.querySelector('.progress-fill');
366
+ const statusText = progressContainer.querySelector('.sync-status');
367
+
368
+ try {
369
+ const response = await fetch('/api/users', {
370
+ credentials: 'include'
371
+ });
372
+
373
+ if (!response.ok) {
374
+ throw new Error('Failed to fetch users');
375
+ }
376
+
377
+ const users = await response.json();
378
+
379
+ if (users.length === 0) {
380
+ alert('No users to sync');
381
+ return;
382
+ }
383
+
384
+ progressContainer.classList.remove('hidden');
385
+ let completed = 0;
386
+
387
+ for (const user of users) {
388
+ statusText.textContent = `Syncing ${user.email}... (${completed + 1}/${users.length})`;
389
+ progressBar.style.width = `${(completed / users.length) * 100}%`;
390
+
391
+ try {
392
+ await syncUser(user.email);
393
+ completed++;
394
+ } catch (error) {
395
+ console.error(`Failed to sync user ${user.email}:`, error);
396
+ statusText.textContent = `Error syncing ${user.email}. Continuing...`;
397
+ await new Promise(resolve => setTimeout(resolve, 2000));
398
+ }
399
+ }
400
+
401
+ progressBar.style.width = '100%';
402
+ statusText.textContent = `Completed syncing ${completed} out of ${users.length} users`;
403
+
404
+ setTimeout(() => {
405
+ progressContainer.classList.add('hidden');
406
+ progressBar.style.width = '0';
407
+ updateDashboardStats();
408
+ }, 3000);
409
+
410
+ } catch (error) {
411
+ console.error('Error syncing all users:', error);
412
+ alert('Error: ' + error.message);
413
+ progressContainer.classList.add('hidden');
414
+ }
415
+ }
416
+
417
+ // Registration Functions
418
+ async function handleStremioRegister(event) {
419
+ event.preventDefault();
420
+
421
+ const email = document.getElementById('stremioRegEmail').value;
422
+ const password = document.getElementById('stremioRegPassword').value;
423
+ const gdprConsent = document.getElementById('gdprConsent').checked;
424
+
425
+ if (!gdprConsent) {
426
+ alert('Please accept the Terms of Service and Privacy Policy');
427
+ return;
428
+ }
429
+
430
+ try {
431
+ const response = await fetch('/api/stremio/register', {
432
+ method: 'POST',
433
+ headers: { 'Content-Type': 'application/json' },
434
+ credentials: 'include',
435
+ body: JSON.stringify({
436
+ email,
437
+ password
438
+ })
439
+ });
440
+
441
+ const data = await response.json();
442
+
443
+ if (response.ok) {
444
+ alert('Successfully registered with Stremio! Now you can add this account to the managed accounts.');
445
+ document.getElementById('userEmail').value = email;
446
+ document.getElementById('userPassword').value = password;
447
+ document.getElementById('stremioRegisterForm').reset();
448
+ } else {
449
+ throw new Error(data.error || 'Registration failed');
450
+ }
451
+ } catch (error) {
452
+ console.error('Error:', error);
453
+ alert('Error registering with Stremio: ' + error.message);
454
+ }
455
+ }
456
+ async function updateUsersList() {
457
+ const loadingState = document.getElementById('loadingUsers');
458
+ const usersList = document.getElementById('usersList');
459
+
460
+ if (!usersList) return;
461
+
462
+ try {
463
+ if (loadingState) loadingState.classList.remove('hidden');
464
+ usersList.innerHTML = '';
465
+
466
+ const response = await fetch('/api/users', {
467
+ credentials: 'include'
468
+ });
469
+
470
+ if (!response.ok) {
471
+ throw new Error('Failed to fetch users');
472
+ }
473
+
474
+ const users = await response.json();
475
+
476
+ if (users.length === 0) {
477
+ usersList.innerHTML = `
478
+ <div class="text-center py-8 text-gray-500">
479
+ <i data-lucide="users" class="w-12 h-12 mx-auto mb-2"></i>
480
+ <p>No users registered</p>
481
+ </div>`;
482
+ window.initIcons();
483
+ return;
484
+ }
485
+
486
+ users.forEach(user => {
487
+ const div = document.createElement('div');
488
+ div.className = 'py-4 flex flex-col';
489
+
490
+ const lastSync = user.lastSync ? new Date(user.lastSync).toLocaleString() : 'Never';
491
+
492
+ div.innerHTML = `
493
+ <div class="flex items-center justify-between">
494
+ <div class="user-info-section">
495
+ <h3 class="font-medium">${user.email}</h3>
496
+ <div class="text-sm text-gray-500">Last Sync: ${lastSync}</div>
497
+ </div>
498
+ <div class="flex gap-2">
499
+ <button onclick="syncUser('${user.email}')"
500
+ class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg">
501
+ <i data-lucide="refresh-cw" class="w-5 h-5"></i>
502
+ </button>
503
+ <button onclick="deleteUser('${user.email}')"
504
+ class="p-2 text-red-600 hover:bg-red-50 rounded-lg">
505
+ <i data-lucide="trash-2" class="w-5 h-5"></i>
506
+ </button>
507
+ </div>
508
+ </div>`;
509
+
510
+ const linkContainer = document.createElement('div');
511
+ linkContainer.id = `link-container-${user.email.replace('@', '-at-')}`;
512
+ div.appendChild(linkContainer);
513
+
514
+ usersList.appendChild(div);
515
+
516
+ ReactDOM.render(
517
+ React.createElement(UserLinkComponent, {
518
+ email: user.email,
519
+ password: user.password
520
+ }),
521
+ linkContainer
522
+ );
523
+ });
524
+
525
+ window.initIcons();
526
+ } catch (error) {
527
+ console.error('Error:', error);
528
+ usersList.innerHTML = `
529
+ <div class="p-4 bg-red-50 text-red-600 rounded-lg">
530
+ <div class="flex items-center">
531
+ <i data-lucide="alert-circle" class="w-5 h-5 mr-2"></i>
532
+ Error loading users. Please try again.
533
+ </div>
534
+ </div>`;
535
+ window.initIcons();
536
+ } finally {
537
+ if (loadingState) loadingState.classList.add('hidden');
538
+ }
539
+ }
540
+ async function updateAddonList() {
541
+ const loadingState = document.getElementById('loadingState');
542
+ const addonsList = document.getElementById('addons');
543
+
544
+ if (!addonsList) return;
545
+
546
+ try {
547
+ if (loadingState) loadingState.classList.remove('hidden');
548
+ addonsList.innerHTML = '';
549
+
550
+ const response = await fetch('/api/addons', {
551
+ credentials: 'include'
552
+ });
553
+
554
+ if (!response.ok) {
555
+ throw new Error('Failed to fetch addons');
556
+ }
557
+
558
+ const addons = await response.json();
559
+
560
+ if (!Array.isArray(addons) || addons.length === 0) {
561
+ addonsList.innerHTML = `
562
+ <div class="col-span-full text-center py-8 text-gray-400 bg-gray-800 rounded-lg border border-gray-700">
563
+ <i data-lucide="package" class="w-12 h-12 mx-auto mb-2"></i>
564
+ <p>No addons installed</p>
565
+ </div>`;
566
+ window.initIcons();
567
+ return;
568
+ }
569
+
570
+ addons.forEach(addon => {
571
+ const div = document.createElement('div');
572
+ div.className = 'bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700 transition-all hover:shadow-xl';
573
+
574
+ let logo = addon.manifest.logo || addon.manifest.icon || '';
575
+
576
+ div.innerHTML = `
577
+ <div class="flex items-center justify-between mb-4">
578
+ ${logo ? `<img src="${logo}" alt="${addon.manifest.name} logo"
579
+ class="w-10 h-10 rounded bg-gray-700 p-1"
580
+ onerror="this.style.display='none'">` :
581
+ `<i data-lucide="package" class="w-10 h-10 text-gray-400"></i>`}
582
+ <button onclick="deleteAddon('${addon.manifest.id}')"
583
+ class="text-red-400 hover:bg-red-500/20 hover:text-red-300 p-2 rounded-lg transition-colors">
584
+ <i data-lucide="trash-2" class="w-5 h-5"></i>
585
+ </button>
586
+ </div>
587
+ <h3 class="font-semibold text-lg mb-1 text-gray-200">${addon.manifest.name}</h3>
588
+ <div class="text-sm text-gray-400 mb-2">Version: ${addon.manifest.version}</div>
589
+ <p class="text-gray-300 mb-4">${addon.manifest.description || 'No description available'}</p>
590
+ <div class="text-xs text-gray-400 break-all pt-4 border-t border-gray-700 bg-gray-800/50 rounded px-2 py-1">
591
+ URL: ${addon.transportUrl}
592
+ </div>`;
593
+ addonsList.appendChild(div);
594
+ });
595
+
596
+ window.initIcons();
597
+ } catch (error) {
598
+ console.error('Error:', error);
599
+ addonsList.innerHTML = `
600
+ <div class="col-span-full p-4 bg-red-900/20 text-red-400 rounded-lg border border-red-800">
601
+ <div class="flex items-center mb-2">
602
+ <i data-lucide="alert-circle" class="w-5 h-5 mr-2"></i>
603
+ <strong>Error loading addons</strong>
604
+ </div>
605
+ <p class="text-sm">${error.message}</p>
606
+ </div>`;
607
+ window.initIcons();
608
+ } finally {
609
+ if (loadingState) loadingState.classList.add('hidden');
610
+ }
611
+ }
612
+
613
+ async function handleAddUser(event) {
614
+ event.preventDefault();
615
+
616
+ const email = document.getElementById('userEmail').value;
617
+ const password = document.getElementById('userPassword').value;
618
+
619
+ try {
620
+ const response = await fetch('/api/users', {
621
+ method: 'POST',
622
+ headers: { 'Content-Type': 'application/json' },
623
+ credentials: 'include',
624
+ body: JSON.stringify({ email, password })
625
+ });
626
+
627
+ const data = await response.json();
628
+
629
+ if (response.ok) {
630
+ alert('User added successfully');
631
+ document.getElementById('userForm').reset();
632
+ updateUsersList();
633
+ updateDashboardStats();
634
+ } else {
635
+ alert(data.error || 'Failed to add user');
636
+ }
637
+ } catch (error) {
638
+ console.error('Error:', error);
639
+ alert('Error adding user: ' + error.message);
640
+ }
641
+ }
642
+
643
+ async function deleteUser(email) {
644
+ if (!confirm('Are you sure you want to delete this user?')) return;
645
+
646
+ try {
647
+ const response = await fetch(`/api/users/${encodeURIComponent(email)}`, {
648
+ method: 'DELETE',
649
+ credentials: 'include'
650
+ });
651
+
652
+ if (response.ok) {
653
+ alert('User deleted successfully');
654
+ updateUsersList();
655
+ updateDashboardStats();
656
+ } else {
657
+ const data = await response.json();
658
+ alert(data.error || 'Failed to delete user');
659
+ }
660
+ } catch (error) {
661
+ console.error('Error:', error);
662
+ alert('Error deleting user: ' + error.message);
663
+ }
664
+ }
665
+ async function handleImportFile(event) {
666
+ const file = event.target.files[0];
667
+ if (!file) return;
668
+
669
+ try {
670
+ const text = await file.text();
671
+ const response = await fetch('/api/import', {
672
+ method: 'POST',
673
+ headers: { 'Content-Type': 'application/json' },
674
+ credentials: 'include',
675
+ body: text
676
+ });
677
+
678
+ if (response.ok) {
679
+ const result = await response.json();
680
+ alert(`Import completed:\nSuccessful: ${result.results.success}\nFailed: ${result.results.failed}\nDuplicates: ${result.results.duplicates}`);
681
+
682
+ // Update catalogs after import
683
+ await fetchAndUpdateCatalogs();
684
+ updateAddonList();
685
+ updateDashboardStats();
686
+ } else {
687
+ const error = await response.json();
688
+ alert(error.error || 'Import failed');
689
+ }
690
+ } catch (error) {
691
+ console.error('Error:', error);
692
+ alert('Error importing addons: ' + error.message);
693
+ }
694
+ event.target.value = '';
695
+ }
696
+
697
+ async function exportAddons() {
698
+ try {
699
+ const response = await fetch('/api/export', {
700
+ credentials: 'include'
701
+ });
702
+
703
+ if (!response.ok) {
704
+ throw new Error('Failed to export addons');
705
+ }
706
+
707
+ const data = await response.json();
708
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
709
+ const url = URL.createObjectURL(blob);
710
+ const a = document.createElement('a');
711
+ a.href = url;
712
+ a.download = 'stremio-addons.json';
713
+ a.click();
714
+ URL.revokeObjectURL(url);
715
+ } catch (error) {
716
+ console.error('Error:', error);
717
+ alert('Error exporting addons: ' + error.message);
718
+ }
719
+ }
720
+
721
+ // Make the rest of functions globally available
722
+ window.updateUsersList = updateUsersList;
723
+ window.updateAddonList = updateAddonList;
724
+ window.handleImportFile = handleImportFile;
725
+ window.exportAddons = exportAddons;
726
+ window.syncAllUsersWithProgress = syncAllUsersWithProgress;
727
+ window.updateDashboardStats = updateDashboardStats;
728
+ window.handleStremioRegister = handleStremioRegister;
729
+ window.syncUser = syncUser;
730
+ window.deleteAddon = async (id) => {
731
+ if (!confirm('Are you sure you want to delete this addon?')) return;
732
+
733
+ try {
734
+ const response = await fetch(`/api/addons/${id}`, {
735
+ method: 'DELETE',
736
+ credentials: 'include'
737
+ });
738
+
739
+ if (response.ok) {
740
+ // Refresh catalogs and UI
741
+ await fetchAndUpdateCatalogs();
742
+ await updateAddonList();
743
+ await updateDashboardStats();
744
+ alert('Addon deleted successfully');
745
+ } else {
746
+ const data = await response.json();
747
+ throw new Error(data.error || 'Failed to delete addon');
748
+ }
749
+ } catch (error) {
750
+ console.error('Error:', error);
751
+ alert('Error deleting addon: ' + error.message);
752
+ }
753
+ };