Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
custom: ["https://buymemomo.com/timilsinabimal"]
ko_fi: TimilsinaBimal
github: ["TimilsinaBimal"]
2 changes: 1 addition & 1 deletion app/api/endpoints/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def get_config_id(catalog) -> str | None:


async def _manifest_handler(response: Response, token: str):
response.headers["Cache-Control"] = "public, max-age=86400"
response.headers["Cache-Control"] = "no-cache"

if not token:
raise HTTPException(status_code=401, detail="Missing token. Please reconfigure the addon.")
Expand Down
6 changes: 3 additions & 3 deletions app/services/row_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def get_cname(code):
f"Genre: {get_gname(g_id)} + Keyword: {normalize_keyword(kw_name)}"
)
if not title:
title = f"{get_gname(g_id)} {normalize_keyword(kw_name)}"
title = f"{normalize_keyword(kw_name)} {get_gname(g_id)}"
# keyword and genre can have same name sometimes, remove if so
title = " ".join(dict.fromkeys(title.split()))

Expand All @@ -129,7 +129,7 @@ def get_cname(code):
if c_adj:
title = await gemini_service.generate_content_async(f"Genre: {get_gname(g_id)} + Country: {c_adj}")
if not title:
title = f"{get_gname(g_id)} {c_adj}"
title = f"{c_adj} {get_gname(g_id)}"
rows.append(
RowDefinition(
title=title,
Expand All @@ -152,7 +152,7 @@ def get_cname(code):
decade_str = str(decade_start)[2:] + "s" # "90s"
title = await gemini_service.generate_content_async(f"Genre: {get_gname(g_id)} + Era: {decade_str}")
if not title:
title = f"{get_gname(g_id)} {decade_str}"
title = f"{decade_str} {get_gname(g_id)}"
rows.append(
RowDefinition(
title=title,
Expand Down
86 changes: 51 additions & 35 deletions app/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@

/* Announcement link styling to ensure visibility */
#announcement-content a {
color: #60a5fa; /* blue-400 */
color: #60a5fa;
/* blue-400 */
text-decoration: underline;
}

Expand All @@ -89,13 +90,20 @@

<!-- Mobile Header -->
<div class="md:hidden p-4 bg-slate-900 border-b border-slate-800 flex items-center gap-3">
<button id="mobileNavToggle" aria-label="Open navigation" class="p-2 rounded-md hover:bg-slate-800/50">
<svg class="w-6 h-6 text-slate-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<img src="/app/static/logo.png" alt="Watchly" class="w-8 h-8 rounded-lg">
<h1 class="font-bold text-lg text-white">Watchly</h1>

</div>
<!-- Mobile Nav Backdrop -->
<div id="mobileNavBackdrop" class="fixed inset-0 bg-black/50 z-30 hidden md:hidden"></div>
<!-- Sidebar Navigation -->
<aside
class="w-full md:w-72 bg-slate-900/50 backdrop-blur-xl border-b md:border-b-0 md:border-r border-slate-800 flex flex-col flex-shrink-0 relative z-20">
<aside id="mainSidebar"
class="fixed inset-y-0 left-0 z-40 w-72 transform -translate-x-full transition-transform duration-300 ease-out bg-slate-900/50 backdrop-blur-xl border-b md:border-b-0 md:border-r border-slate-800 flex flex-col flex-shrink-0 md:relative md:translate-x-0 md:flex">
<div class="p-6 md:p-8 flex-col items-start gap-4 hidden md:flex">
<div class="flex items-center gap-3">
<img src="/app/static/logo.png" alt="Watchly" class="w-10 h-10 rounded-xl shadow-lg shadow-blue-500/10">
Expand All @@ -106,7 +114,7 @@ <h1 class="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r fro


<nav
class="flex-grow px-4 md:px-6 py-4 space-y-1 overflow-x-auto md:overflow-visible flex md:block whitespace-nowrap md:whitespace-normal gap-2 md:gap-0">
class="flex-grow px-4 md:px-6 py-4 space-y-1 md:overflow-visible flex flex-col md:block whitespace-normal gap-2">

<!-- User Profile Section (Hidden by default, shown after login) -->
<div id="user-profile" class="hidden mb-6 p-4 bg-slate-800/50 border border-slate-700/50 rounded-xl w-full">
Expand Down Expand Up @@ -194,13 +202,16 @@ <h1 class="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r fro
<div class="p-4 md:p-6 mt-auto space-y-4">
<!-- Ko-fi Support Button -->
<button type="button" id="kofiBtn"
class="block w-full bg-gradient-to-r from-pink-600 to-rose-600 hover:from-pink-500 hover:to-rose-500 text-white font-medium py-3 px-4 rounded-xl transition-all shadow-lg shadow-pink-900/20 hover:shadow-xl hover:shadow-pink-900/30 group text-left">
<div class="flex items-center justify-center gap-2">
<svg class="w-5 h-5 group-hover:scale-110 transition-transform" viewBox="0 0 24 24" fill="currentColor">
<path
d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-1.246 1.453-4.011 3.976-4.011 3.976s-.121.119-.31.023c-.076-.057-.108-.09-.108-.09-.443-.441-3.368-3.049-4.034-3.954-.709-.965-1.041-2.7-.091-3.71.951-1.01 3.005-1.086 4.363.407 0 0 1.565-1.782 3.468-.963 1.904.82 1.832 3.011.723 4.311zm6.173.478c-.928.116-1.682.028-1.682.028V7.284h1.77s1.971.551 1.971 2.638c0 1.913-.985 2.667-2.059 3.015z" />
class="block w-full bg-gradient-to-r from-amber-700 to-amber-900 hover:from-amber-600 hover:to-amber-800 text-white font-medium py-3 px-4 rounded-xl transition-all shadow-lg shadow-amber-900/40 hover:shadow-xl group text-left"
aria-label="Buy me Coffee">
<div class="flex items-center justify-center gap-3">
<!-- Coffee cup icon -->
<svg class="w-5 h-5 group-hover:scale-110 transition-transform" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7h14v3a5 5 0 01-5 5H8a5 5 0 01-5-5V7z" fill="#fff" opacity="0.9"/>
<path d="M19 8.5a3 3 0 010 6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 4v1M9 4v1M12 4v1" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Support on Ko-fi</span>
<span>Buy me Coffee</span>
</div>
</button>

Expand Down Expand Up @@ -240,20 +251,21 @@ <h1 class="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r fro
Personalized Recommendation Engine for Stremio
</p>
<p class="text-sm text-slate-400 max-w-2xl mx-auto">
Discover movies and series tailored to your unique taste, powered by your Stremio library and watch history.
Discover movies and series tailored to your unique taste, powered by your Stremio library
and watch history.
</p>
<!-- Announcement (fetched from API) -->
<div id="announcement"
class="hidden max-w-2xl mx-auto mt-6 p-3 rounded-xl bg-slate-800/50 border border-slate-700/50 text-sm text-slate-200 flex items-start gap-3"
role="region" aria-live="polite">
<div class="flex-shrink-0 text-blue-400 mt-0.5">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div id="announcement-content" class="flex-grow text-slate-200"></div>
</div>
<!-- Announcement (fetched from API) -->
<div id="announcement"
class="hidden max-w-2xl mx-auto mt-6 p-3 rounded-xl bg-slate-800/50 border border-slate-700/50 text-sm text-slate-200 flex items-start gap-3"
role="region" aria-live="polite">
<div class="flex-shrink-0 text-blue-400 mt-0.5">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div id="announcement-content" class="flex-grow text-slate-200"></div>
</div>
</div>

<!-- Features Grid -->
Expand All @@ -272,7 +284,7 @@ <h3 class="text-base font-bold text-white mb-1">Smart Recommendations</h3>
<p class="text-xs text-slate-400 leading-relaxed">
AI-powered suggestions based on your watch history
</p>
</div>
</div>

<!-- Feature 2 -->
<div
Expand Down Expand Up @@ -305,7 +317,7 @@ <h3 class="text-base font-bold text-white mb-1">Genre Filtering</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Exclude genres you don't like
</p>
</div>
</div>
<!-- Feature 4 -->
<div
class="group bg-gradient-to-br from-slate-900/50 to-slate-800/30 border border-slate-700/50 hover:border-green-500/50 rounded-xl p-4 transition-all hover:shadow-lg hover:shadow-green-900/10">
Expand All @@ -321,7 +333,7 @@ <h3 class="text-base font-bold text-white mb-1">Multi-Language</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Recommendations in your preferred language
</p>
</div>
</div>

<!-- Feature 5 -->
<div
Expand All @@ -338,7 +350,7 @@ <h3 class="text-base font-bold text-white mb-1">RPDB Integration</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Enhanced posters with ratings
</p>
</div>
</div>

<!-- Feature 6 -->
<div
Expand All @@ -355,8 +367,8 @@ <h3 class="text-base font-bold text-white mb-1">Based on Your Loves</h3>
<p class="text-xs text-slate-400 leading-relaxed">
Recommendations from content you loved
</p>
</div>
</div>
</div>
</div>
<!-- CTA Section -->
<div class="text-center space-y-6">
<div class="max-w-2xl mx-auto">
Expand All @@ -383,11 +395,12 @@ <h3 class="text-base font-bold text-white mb-1">Based on Your Loves</h3>
<span class="font-medium">View Source Code</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14">
</path>
</svg>
</a>
</div>
</div>
</div>
</section>

<!-- SECTION 1: LOGIN -->
Expand Down Expand Up @@ -540,7 +553,8 @@ <h2 class="text-3xl font-bold text-white mb-2">Save & Install</h2>
</div>
<div class="flex-grow">
<h3 class="text-sm font-semibold text-red-400 mb-1">Danger Zone</h3>
<p class="text-xs text-slate-400 mb-4">Permanently delete your settings and account data. This action
<p class="text-xs text-slate-400 mb-4">Permanently delete your settings and account
data. This action
cannot be undone.</p>
<button type="button" id="deleteAccountBtn"
class="w-full bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 hover:border-red-500/50 text-red-400 hover:text-red-300 font-medium py-3 px-4 rounded-lg transition-all flex items-center justify-center gap-2 group">
Expand All @@ -550,10 +564,10 @@ <h3 class="text-sm font-semibold text-red-400 mb-1">Danger Zone</h3>
</path>
</svg>
<span>Delete Account</span>
</button>
</div>
</div>
</button>
</div>
</div>
</div>
</div>

<!-- Error Message -->
Expand Down Expand Up @@ -609,6 +623,8 @@ <h3 class="text-3xl font-bold text-white mb-2">You're all set!</h3>
</div>
</main>

<!-- BuyMeMoMo modal removed — button now redirects directly -->

<!-- Ko-fi Modal -->
<div id="kofi-modal" class="fixed inset-0 z-50 hidden">
<!-- Backdrop -->
Expand Down
103 changes: 62 additions & 41 deletions app/static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ document.addEventListener('DOMContentLoaded', () => {
initializeWelcomeFlow();

initializeNavigation();
// By default, ensure logged-out users see only Welcome/Login and not configure/install/catalogs
lockNavigationForLoggedOut();
initializeCatalogList();
initializeLanguageSelect();
initializeMobileNav();
initializeGenreLists();
initializeFormSubmission();
initializeSuccessActions();
Expand All @@ -83,12 +86,19 @@ document.addEventListener('DOMContentLoaded', () => {
// Welcome Flow Logic
function initializeWelcomeFlow() {
// Single "Get Started" button leads to Stremio login
if (btnGetStarted) {
btnGetStarted.addEventListener('click', () => {
navItems.login.classList.remove('disabled');
switchSection('login');
});
}
if (!btnGetStarted) return;

// Support mobile taps reliably while avoiding double-fire (touch -> click)
let touched = false;
const handleGetStarted = (e) => {
if (e.type === 'click' && touched) return;
if (e.type === 'touchstart') touched = true;
navItems.login.classList.remove('disabled');
switchSection('login');
};

btnGetStarted.addEventListener('click', handleGetStarted);
btnGetStarted.addEventListener('touchstart', handleGetStarted, { passive: true });
}


Expand All @@ -107,6 +117,46 @@ function unlockNavigation() {
Object.values(navItems).forEach(el => el.classList.remove('disabled'));
}

function lockNavigationForLoggedOut() {
// Ensure welcome and login remain accessible; disable only config/catalogs/install
if (navItems.welcome) navItems.welcome.classList.remove('disabled');
if (navItems.login) navItems.login.classList.remove('disabled');
if (navItems.config) navItems.config.classList.add('disabled');
if (navItems.catalogs) navItems.catalogs.classList.add('disabled');
if (navItems.install) navItems.install.classList.add('disabled');
}

function initializeMobileNav() {
const mobileToggle = document.getElementById('mobileNavToggle');
const sidebar = document.getElementById('mainSidebar');
const backdrop = document.getElementById('mobileNavBackdrop');
if (!mobileToggle || !sidebar || !backdrop) return;

const openNav = () => {
sidebar.classList.remove('-translate-x-full');
sidebar.classList.add('translate-x-0');
backdrop.classList.remove('hidden');
document.body.classList.add('overflow-hidden');
};
const closeNav = () => {
sidebar.classList.remove('translate-x-0');
sidebar.classList.add('-translate-x-full');
backdrop.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
};

mobileToggle.addEventListener('click', (e) => { e.preventDefault(); openNav(); });
backdrop.addEventListener('click', closeNav);

// Auto-close when a nav item is selected (mobile)
Object.values(navItems).forEach(n => {
if (!n) return;
n.addEventListener('click', () => {
if (!sidebar.classList.contains('hidden')) closeNav();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The condition to check if the mobile navigation is open is incorrect. The sidebar's visibility is controlled by transform classes (-translate-x-full and translate-x-0), not the hidden class. This condition will always evaluate to true on mobile, causing closeNav() to be called unnecessarily on every nav item click, even if the sidebar is already closed.

A more correct check would be to see if the sidebar is currently translated into view.

Suggested change
if (!sidebar.classList.contains('hidden')) closeNav();
if (sidebar.classList.contains('translate-x-0')) closeNav();

});
});
}

function switchSection(sectionKey) {
// Hide all sections
Object.values(sections).forEach(el => {
Expand Down Expand Up @@ -137,8 +187,9 @@ function resetApp() {
switchSection('welcome');

// Lock Navs
// Keep the Welcome nav enabled so the main Get Started entry remains usable.
Object.keys(navItems).forEach(key => {
if (key !== 'login') navItems[key].classList.add('disabled'); // Login is always enabled technically, but we hide it via switchSection('welcome')
if (key !== 'login' && key !== 'welcome') navItems[key].classList.add('disabled');
});
// Actually, we should probably disable 'login' too until they choose New/Existing User?
// But our nav click logic handles that. If we are at 'welcome', the sidebar is visible but inactive.
Expand Down Expand Up @@ -872,43 +923,13 @@ function initializeFooter() {
// Ko-fi Modal Logic
function initializeKofi() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function now handles the 'Buy me Coffee' button which links to buymemomo.com. The name initializeKofi is misleading and should be updated to reflect its new purpose, for example initializeSponsorButton. This will make the code easier to understand and maintain.

Remember to also update the call to this function within the DOMContentLoaded event listener.

Suggested change
function initializeKofi() {
function initializeSponsorButton() {

const kofiBtn = document.getElementById('kofiBtn');
const modal = document.getElementById('kofi-modal');
const closeBtn = document.getElementById('close-kofi');
const backdrop = document.getElementById('kofi-backdrop');
const iframe = document.getElementById('kofiframe');

// URL for the Ko-fi embed widget
const KOFI_URL = "https://ko-fi.com/timilsinabimal/?hidefeed=true&widget=true&embed=true&preview=true";

function openModal() {
if (!modal) return;
modal.classList.remove('hidden');
// Lazy load the iframe source if not set correctly
if (iframe && (!iframe.src || !iframe.src.includes('ko-fi.com'))) {
iframe.src = KOFI_URL;
}
}

function closeModal() {
if (!modal) return;
modal.classList.add('hidden');
// We don't clear src to keep it loaded for next time, but you could if needed
}
const MEMOMO_URL = 'https://buymemomo.com/timilsinabimal';

if (kofiBtn) kofiBtn.addEventListener('click', (e) => {
e.preventDefault();
openModal();
});

if (closeBtn) closeBtn.addEventListener('click', closeModal);

if (backdrop) backdrop.addEventListener('click', closeModal);

// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal && !modal.classList.contains('hidden')) {
closeModal();
}
// Open BuyMeMoMo in a new tab and remove window.opener for safety
const win = window.open(MEMOMO_URL, '_blank');
try { if (win) win.opener = null; } catch (err) { /* ignore */ }
});
}

Expand Down