-
-
-
-
-
+
+
- Streamers
-
+
+
+
+
-
+
+
+
+ Streamers
+
+
+
+
+
-
+
-
+
-
+
-
- - -
-- Add a streamer below to start tracking their status. -
-
+
+
+
-
+
+ +
++ Add a streamer below to start tracking their status. +
+
-
-
-
+
diff --git a/extension/scripts/pop-up.js b/extension/scripts/pop-up.js
index 4f982b2..e9f3db1 100644
--- a/extension/scripts/pop-up.js
+++ b/extension/scripts/pop-up.js
@@ -267,4 +267,133 @@ document.addEventListener('DOMContentLoaded', () => {
}
);
});
+
+ /**
+ * Downloads data as a JSON file
+ */
+ const downloadJson = (data, filename = 'twitch_streamers.json') => {
+ const json = JSON.stringify(data, null, 2);
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+
+ // Cleanup
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ /**
+ * Parses import content into an array of streamer usernames
+ */
+ const parseImportContent = (text) => {
+ // Try JSON first
+ try {
+ const parsed = JSON.parse(text);
+ if (Array.isArray(parsed)) {
+ return parsed
+ .map(String)
+ .map((s) => s.trim())
+ .filter(Boolean);
+ }
+ } catch {
+ // Not JSON, fall through
+ }
+
+ // Fallback: split by newlines or commas
+ return text
+ .split(/[\r\n,]+/)
+ .map((s) => s.trim())
+ .filter(Boolean);
+ };
+
+ /**
+ * Loads current streamers from storage
+ */
+ const loadStreamers = () => {
+ return new Promise((resolve) => {
+ chrome.storage.sync.get('twitchStreams', (storage) => {
+ const list = Array.isArray(storage.twitchStreams)
+ ? storage.twitchStreams
+ : [];
+ resolve(list);
+ });
+ });
+ };
+
+ /**
+ * Saves streamers to storage and refreshes status
+ */
+ const saveAndRefresh = (streamers) => {
+ chrome.storage.sync.set({ twitchStreams: streamers }, () => {
+ fetchStreamerStatus({ twitchStreams: streamers });
+ });
+ };
+
+ /**
+ * Merges new streamers with existing ones (deduplicated)
+ */
+ const mergeStreamers = (existing, incoming) => {
+ return Array.from(new Set([...existing, ...incoming]));
+ };
+
+ // === Export Functionality ===
+ const exportBtn = document.getElementById('exportBtn');
+ if (exportBtn) {
+ exportBtn.addEventListener('click', async () => {
+ try {
+ const streamers = await loadStreamers();
+ downloadJson(streamers);
+ } catch (err) {
+ alert('Failed to export streamers.');
+ console.error('Export error:', err);
+ }
+ });
+ }
+
+ // === Import Functionality ===
+ const importBtn = document.getElementById('importBtn');
+ const importFile = document.getElementById('importFile');
+
+ if (importBtn && importFile) {
+ importBtn.addEventListener('click', () => importFile.click());
+
+ importFile.addEventListener('change', async (evt) => {
+ const file = evt.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+
+ reader.onload = async () => {
+ try {
+ const text = reader.result;
+ if (typeof text !== 'string') throw new Error('Invalid file content');
+
+ const newStreamers = parseImportContent(text);
+ if (!newStreamers.length) throw new Error('No valid usernames found');
+
+ const existing = await loadStreamers();
+ const merged = mergeStreamers(existing, newStreamers);
+
+ saveAndRefresh(merged);
+ evt.target.value = ''; // Allow re-import of same file
+ } catch (err) {
+ alert(
+ 'Import failed. Please provide a JSON array or a list of usernames separated by commas or newlines.'
+ );
+ console.error('Import error:', err);
+ }
+ };
+
+ reader.onerror = () => {
+ alert('Failed to read the file.');
+ };
+
+ reader.readAsText(file);
+ });
+ }
});
diff --git a/extension/styles/main.css b/extension/styles/main.css
index 3e86612..a5ed753 100644
--- a/extension/styles/main.css
+++ b/extension/styles/main.css
@@ -46,6 +46,55 @@ body {
height: 60px;
}
+.banner .logo {
+ height: 36px;
+ width: auto;
+ margin-right: 10px;
+ align-self: center;
+}
+
+.header-left .heading {
+ margin: 0;
+ line-height: 60px;
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.header-btns {
+ display: flex;
+ align-items: center;
+}
+
+.header-btns .btn {
+ margin-left: 8px;
+ padding: 8px 16px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ color: #fff;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-transform: uppercase;
+ letter-spacing: 0.031em;
+}
+
+.header-btns .btn:hover {
+ background: rgba(255, 255, 255, 0.2);
+ border-color: rgba(255, 255, 255, 0.5);
+ transform: translateY(-1px);
+}
+
+.header-btns .btn:active {
+ transform: translateY(0);
+ background: rgba(255, 255, 255, 0.15);
+}
+
.footer {
border-top: 1px solid #ccc;
padding-top: 15px;
@@ -100,7 +149,7 @@ li {
}
.spacing {
- padding: 0px 15px;
+ padding: 0 15px;
}
.spacing .col-xs-3 {
@@ -109,4 +158,4 @@ li {
.no-padding {
padding: 0;
-}
+}
\ No newline at end of file
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+