Compare commits
3 Commits
main
...
jellyflood
Author | SHA1 | Date |
---|---|---|
|
67e8e46b8b | |
|
4f27067db2 | |
|
ed91d39ad9 |
|
@ -1,44 +0,0 @@
|
||||||
---
|
|
||||||
description: Build jellypig tvOS (debug or release)
|
|
||||||
---
|
|
||||||
|
|
||||||
Build jellypig tvOS for the simulator. Takes an optional configuration argument:
|
|
||||||
- `debug` (default) - Fast build with debugging symbols
|
|
||||||
- `release` - Optimized build for distribution
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
- `/build` - Build in Debug configuration (default)
|
|
||||||
- `/build debug` - Build in Debug configuration (explicit)
|
|
||||||
- `/build release` - Build in Release configuration
|
|
||||||
|
|
||||||
Steps to execute:
|
|
||||||
1. Parse the configuration argument (default to "debug" if not provided or invalid)
|
|
||||||
2. Validate the configuration is either "debug" or "release" (case-insensitive)
|
|
||||||
3. Run xcodebuild with the specified configuration:
|
|
||||||
```bash
|
|
||||||
cd /Users/ashikkizhakkepallathu/Documents/claude/jellypig/jellypig
|
|
||||||
|
|
||||||
# For debug:
|
|
||||||
xcodebuild -project jellypig.xcodeproj \
|
|
||||||
-scheme "jellypig tvOS" \
|
|
||||||
-sdk appletvsimulator \
|
|
||||||
-configuration Debug \
|
|
||||||
-derivedDataPath ./DerivedData \
|
|
||||||
clean build \
|
|
||||||
CODE_SIGNING_ALLOWED=NO
|
|
||||||
|
|
||||||
# For release:
|
|
||||||
xcodebuild -project jellypig.xcodeproj \
|
|
||||||
-scheme "jellypig tvOS" \
|
|
||||||
-sdk appletvsimulator \
|
|
||||||
-configuration Release \
|
|
||||||
-derivedDataPath ./DerivedData \
|
|
||||||
clean build \
|
|
||||||
CODE_SIGNING_ALLOWED=NO
|
|
||||||
```
|
|
||||||
4. Report build status (success or failure)
|
|
||||||
5. Display the output path of the built app
|
|
||||||
|
|
||||||
Expected output location:
|
|
||||||
- Debug: `./DerivedData/Build/Products/Debug-appletvsimulator/jellypig tvOS.app`
|
|
||||||
- Release: `./DerivedData/Build/Products/Release-appletvsimulator/jellypig tvOS.app`
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
description: Initialize development session by reading project context and displaying available commands
|
|
||||||
---
|
|
||||||
|
|
||||||
Read the chats-summary.txt file from the parent directory (/Users/ashikkizhakkepallathu/Documents/claude/jellypig/chats-summary.txt) to understand the project context, then display a quick summary of what you can help with.
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
1. Read /Users/ashikkizhakkepallathu/Documents/claude/jellypig/chats-summary.txt
|
|
||||||
2. Display a concise summary including:
|
|
||||||
- Project name and description
|
|
||||||
- Available custom slash commands (/build, /sim, etc.)
|
|
||||||
- Recent features implemented
|
|
||||||
- Key configuration details (Bundle ID, Simulator UUID, etc.)
|
|
||||||
- Build method: **Command-line builds work** via xcodebuild (no Xcode GUI required)
|
|
||||||
- Common tasks you can help with
|
|
||||||
|
|
||||||
Make the output brief and actionable - focus on what's immediately useful for the developer.
|
|
|
@ -1,41 +0,0 @@
|
||||||
---
|
|
||||||
description: Build and launch jellypig tvOS in simulator
|
|
||||||
---
|
|
||||||
|
|
||||||
Build the latest version of jellypig tvOS in Debug configuration, install it on the Apple TV simulator, and launch it.
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
1. First, build the project using the same approach as `/build debug`:
|
|
||||||
```bash
|
|
||||||
cd /Users/ashikkizhakkepallathu/Documents/claude/jellypig/jellypig
|
|
||||||
xcodebuild -project jellypig.xcodeproj \
|
|
||||||
-scheme "jellypig tvOS" \
|
|
||||||
-sdk appletvsimulator \
|
|
||||||
-configuration Debug \
|
|
||||||
-derivedDataPath ./DerivedData \
|
|
||||||
clean build \
|
|
||||||
CODE_SIGNING_ALLOWED=NO
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Boot the Apple TV simulator:
|
|
||||||
```bash
|
|
||||||
xcrun simctl boot 16A71179-729D-4F1B-8698-8371F137025B 2>/dev/null || true
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Open Simulator.app:
|
|
||||||
```bash
|
|
||||||
open -a Simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Install the built app on the simulator:
|
|
||||||
```bash
|
|
||||||
xcrun simctl install 16A71179-729D-4F1B-8698-8371F137025B \
|
|
||||||
"./DerivedData/Build/Products/Debug-appletvsimulator/jellypig tvOS.app"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Launch the app:
|
|
||||||
```bash
|
|
||||||
xcrun simctl launch 16A71179-729D-4F1B-8698-8371F137025B org.ashik.jellypig
|
|
||||||
```
|
|
||||||
|
|
||||||
Report build and launch status. If any step fails, provide clear error message.
|
|
|
@ -101,3 +101,9 @@ Shared/Generated/Strings.swift
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
Carthage/Build/
|
Carthage/Build/
|
||||||
|
|
||||||
|
# Claude Code local configuration
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Local session notes (stored in parent directory)
|
||||||
|
chats-summary.txt
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
# jellyflood Development Practices
|
||||||
|
|
||||||
|
## Issue-Driven Development
|
||||||
|
|
||||||
|
All work on jellyflood follows an **issue-driven workflow**. Every feature, bug fix, or improvement must have a corresponding issue on Gitea before work begins.
|
||||||
|
|
||||||
|
### Creating Issues
|
||||||
|
|
||||||
|
Use the provided script to create issues:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/create-issue.sh "jellyflood-X: Title" "Description" "feature|bug"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/create-issue.sh \
|
||||||
|
"jellyflood-8: Add EPG support for Xtream Live TV" \
|
||||||
|
"Implement EPG (Electronic Program Guide) for Xtream Live TV channels.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- [ ] Fetch EPG data from Xtream API
|
||||||
|
- [ ] Display EPG in channel list
|
||||||
|
- [ ] Show current/next program info
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- Requires jellyflood-3 (XtreamAPIClient)" \
|
||||||
|
"feature"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue Naming Convention:**
|
||||||
|
- Format: `jellyflood-X: Title`
|
||||||
|
- Where `X` is the next sequential number
|
||||||
|
- Use descriptive titles
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `feature` - New functionality
|
||||||
|
- `bug` - Bug fixes
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
The `create-issue.sh` script requires:
|
||||||
|
- `GITEA_API_TOKEN` environment variable set in `~/.zshrc`
|
||||||
|
- Get your token at: https://git.ashik.se/user/settings/applications
|
||||||
|
|
||||||
|
## Branching Strategy
|
||||||
|
|
||||||
|
### Branch Naming
|
||||||
|
|
||||||
|
All work-in-progress code uses the branch naming pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
jellyflood-<ticket_num>/wip<iteration>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `jellyflood-1/wip0` - First WIP for issue #1
|
||||||
|
- `jellyflood-1/wip1` - Second iteration/WIP for issue #1
|
||||||
|
- `jellyflood-8/wip0` - First WIP for issue #8
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. **Create Issue** (using `create-issue.sh`)
|
||||||
|
2. **Create WIP Branch**
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git pull mine main
|
||||||
|
git checkout -b jellyflood-X/wip0
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Work on Changes**
|
||||||
|
- Make commits with descriptive messages
|
||||||
|
- Reference issue number in commits: `Closes: jellyflood-X` or `Related: jellyflood-X`
|
||||||
|
|
||||||
|
4. **Build and Test Locally**
|
||||||
|
```bash
|
||||||
|
# Always build before pushing
|
||||||
|
xcodebuild -project jellyflood.xcodeproj \
|
||||||
|
-scheme "jellyflood tvOS" \
|
||||||
|
-destination "platform=tvOS Simulator,id=16A71179-729D-4F1B-8698-8371F137025B" \
|
||||||
|
-configuration Debug \
|
||||||
|
build
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Push WIP Branch**
|
||||||
|
```bash
|
||||||
|
git push mine jellyflood-X/wip0 -u
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Do NOT Push to Main Directly**
|
||||||
|
- Never push directly to `main` branch
|
||||||
|
- Never use `git add -A` (review files individually)
|
||||||
|
- All integration happens via CI (see below)
|
||||||
|
|
||||||
|
## Continuous Integration (Pending)
|
||||||
|
|
||||||
|
> **Status:** CI/CD toolchain pending setup
|
||||||
|
|
||||||
|
Once CI is configured with a dedicated Mac runner (using Tailscale):
|
||||||
|
|
||||||
|
1. **Create Pull Request** on Gitea
|
||||||
|
- From: `jellyflood-X/wip0`
|
||||||
|
- To: `main`
|
||||||
|
|
||||||
|
2. **CI Pipeline Runs**
|
||||||
|
- Build verification
|
||||||
|
- Tests execution
|
||||||
|
- Code quality checks
|
||||||
|
|
||||||
|
3. **Merge After CI Passes**
|
||||||
|
- Only merge if all CI checks pass
|
||||||
|
- Squash or merge commits as appropriate
|
||||||
|
|
||||||
|
## Git Commit Guidelines
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
```
|
||||||
|
Short summary (50 chars or less)
|
||||||
|
|
||||||
|
Detailed description if needed:
|
||||||
|
- What changed
|
||||||
|
- Why it changed
|
||||||
|
- Any breaking changes
|
||||||
|
|
||||||
|
Closes: jellyflood-X
|
||||||
|
Related: jellyflood-Y, jellyflood-Z
|
||||||
|
|
||||||
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||||
|
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Staging
|
||||||
|
|
||||||
|
**NEVER use `git add -A` or `git add .`**
|
||||||
|
|
||||||
|
Always add files individually after review:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what changed
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Review each file
|
||||||
|
git diff path/to/file
|
||||||
|
|
||||||
|
# Add individually
|
||||||
|
git add path/to/file1
|
||||||
|
git add path/to/file2
|
||||||
|
|
||||||
|
# Verify staged changes
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Review Checklist
|
||||||
|
|
||||||
|
Before pushing to WIP branch:
|
||||||
|
|
||||||
|
- [ ] Issue created on Gitea
|
||||||
|
- [ ] Branch follows naming: `jellyflood-X/wip0`
|
||||||
|
- [ ] Code builds without errors
|
||||||
|
- [ ] Files added individually (no `git add -A`)
|
||||||
|
- [ ] Commit message references issue
|
||||||
|
- [ ] No API tokens or secrets in code
|
||||||
|
- [ ] No build artifacts committed
|
||||||
|
|
||||||
|
## Issue Tracking
|
||||||
|
|
||||||
|
View all issues at: https://git.ashik.se/ashikk/jellyflood/issues
|
||||||
|
|
||||||
|
**Current Issues:**
|
||||||
|
- jellyflood-1: Xtream Player
|
||||||
|
- jellyflood-2: Dual Provider Architecture
|
||||||
|
- jellyflood-3: XtreamAPIClient VOD and Series support
|
||||||
|
- jellyflood-4: Multi-provider account switching
|
||||||
|
- jellyflood-5: Xtream channels in Media tab (bug)
|
||||||
|
- jellyflood-6: Xtream tab UI sections
|
||||||
|
- jellyflood-7: Provider selection in login flow
|
||||||
|
|
||||||
|
## Self-Hosted Infrastructure
|
||||||
|
|
||||||
|
jellyflood follows a **fully self-hosted ideology**:
|
||||||
|
|
||||||
|
- **Git Repository:** Self-hosted Gitea at git.ashik.se
|
||||||
|
- **Issue Tracking:** Gitea Issues
|
||||||
|
- **CI/CD (Pending):** Dedicated Mac runner via Tailscale
|
||||||
|
- **Media Server:** Self-hosted Jellyfin
|
||||||
|
|
||||||
|
All development infrastructure is under your control.
|
|
@ -15,32 +15,16 @@ import SwiftUI
|
||||||
// TODO: fix weird tvOS icon rendering
|
// TODO: fix weird tvOS icon rendering
|
||||||
struct MainTabView: View {
|
struct MainTabView: View {
|
||||||
|
|
||||||
#if os(iOS)
|
@Injected(\.sessionManager)
|
||||||
|
private var sessionManager
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var tabCoordinator = TabCoordinator {
|
private var tabCoordinator: TabCoordinator
|
||||||
TabItem.home
|
|
||||||
TabItem.search
|
init() {
|
||||||
TabItem.media
|
let sessionManager = Container.shared.sessionManager()
|
||||||
|
_tabCoordinator = StateObject(wrappedValue: TabCoordinator(sessionManager: sessionManager))
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
@StateObject
|
|
||||||
private var tabCoordinator = TabCoordinator {
|
|
||||||
TabItem.home
|
|
||||||
TabItem.library(
|
|
||||||
title: L10n.tvShows,
|
|
||||||
systemName: "tv",
|
|
||||||
filters: .init(itemTypes: [.series])
|
|
||||||
)
|
|
||||||
TabItem.library(
|
|
||||||
title: L10n.movies,
|
|
||||||
systemName: "film",
|
|
||||||
filters: .init(itemTypes: [.movie])
|
|
||||||
)
|
|
||||||
TabItem.search
|
|
||||||
TabItem.media
|
|
||||||
TabItem.settings
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -65,5 +49,11 @@ struct MainTabView: View {
|
||||||
.tag(tab.item.id)
|
.tag(tab.item.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(sessionManager.$jellyfinSession) { _ in
|
||||||
|
tabCoordinator.refreshTabs()
|
||||||
|
}
|
||||||
|
.onReceive(sessionManager.$xtreamSession) { _ in
|
||||||
|
tabCoordinator.refreshTabs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,12 +39,73 @@ final class TabCoordinator: ObservableObject {
|
||||||
@Published
|
@Published
|
||||||
var tabs: [TabData] = []
|
var tabs: [TabData] = []
|
||||||
|
|
||||||
init(@ArrayBuilder<TabItem> tabs: () -> [TabItem]) {
|
private let sessionManager: SessionManager
|
||||||
let tabs = tabs()
|
|
||||||
self.tabs = tabs.map { tab in
|
init(sessionManager: SessionManager) {
|
||||||
let coordinator = NavigationCoordinator()
|
self.sessionManager = sessionManager
|
||||||
let event = TabItemSelectedPublisher()
|
refreshTabs()
|
||||||
return (tab, coordinator, event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshTabs() {
|
||||||
|
let newTabs = buildTabs()
|
||||||
|
|
||||||
|
// Preserve navigation coordinators for existing tabs
|
||||||
|
let preservedTabs = newTabs.map { newTab in
|
||||||
|
if let existingTab = tabs.first(where: { $0.item.id == newTab.id }) {
|
||||||
|
return (newTab, existingTab.coordinator, existingTab.publisher)
|
||||||
|
} else {
|
||||||
|
let coordinator = NavigationCoordinator()
|
||||||
|
let publisher = TabItemSelectedPublisher()
|
||||||
|
return (newTab, coordinator, publisher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs = preservedTabs
|
||||||
|
|
||||||
|
// Reset selected tab if it no longer exists
|
||||||
|
if let selectedID = selectedTabID, !tabs.contains(where: { $0.item.id == selectedID }) {
|
||||||
|
selectedTabID = tabs.first?.item.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildTabs() -> [TabItem] {
|
||||||
|
var tabItems: [TabItem] = []
|
||||||
|
|
||||||
|
// Add Jellyfin tabs if JF session is active
|
||||||
|
if sessionManager.hasJellyfinProvider {
|
||||||
|
#if os(iOS)
|
||||||
|
tabItems.append(TabItem.home)
|
||||||
|
tabItems.append(TabItem.search)
|
||||||
|
tabItems.append(TabItem.media)
|
||||||
|
#else
|
||||||
|
tabItems.append(TabItem.home)
|
||||||
|
tabItems.append(TabItem.library(
|
||||||
|
title: L10n.tvShows,
|
||||||
|
systemName: "tv",
|
||||||
|
filters: .init(itemTypes: [.series])
|
||||||
|
))
|
||||||
|
tabItems.append(TabItem.library(
|
||||||
|
title: L10n.movies,
|
||||||
|
systemName: "film",
|
||||||
|
filters: .init(itemTypes: [.movie])
|
||||||
|
))
|
||||||
|
tabItems.append(TabItem.search)
|
||||||
|
tabItems.append(TabItem.media)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Xtream tab if XC session is active
|
||||||
|
if sessionManager.hasXtreamProvider {
|
||||||
|
tabItems.append(TabItem.xtream)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always add settings on tvOS (if JF is active)
|
||||||
|
#if !os(iOS)
|
||||||
|
if sessionManager.hasJellyfinProvider {
|
||||||
|
tabItems.append(TabItem.settings)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return tabItems
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,14 @@ extension TabItem {
|
||||||
MediaView()
|
MediaView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static let xtream = TabItem(
|
||||||
|
id: "xtream",
|
||||||
|
title: "Xtream",
|
||||||
|
systemImage: "antenna.radiowaves.left.and.right"
|
||||||
|
) {
|
||||||
|
XtreamView()
|
||||||
|
}
|
||||||
|
|
||||||
static let search = TabItem(
|
static let search = TabItem(
|
||||||
id: "search",
|
id: "search",
|
||||||
title: L10n.search,
|
title: L10n.search,
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Defaults
|
||||||
|
import Factory
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Manages active provider sessions (Jellyfin and/or Xtream)
|
||||||
|
final class SessionManager: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var jellyfinSession: UserSession?
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var xtreamSession: XtreamSession?
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Initialize with current sessions
|
||||||
|
self.jellyfinSession = Container.shared.currentUserSession()
|
||||||
|
self.xtreamSession = Container.shared.currentXtreamSession()
|
||||||
|
|
||||||
|
// Listen for session changes
|
||||||
|
NotificationCenter.default.publisher(for: .didSignIn)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.refreshJellyfinSession()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
NotificationCenter.default.publisher(for: .didSignOut)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.refreshJellyfinSession()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Listen for Xtream session changes
|
||||||
|
Defaults.publisher(.currentXtreamServerID)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.refreshXtreamSession()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasActiveProvider: Bool {
|
||||||
|
jellyfinSession != nil || xtreamSession != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasJellyfinProvider: Bool {
|
||||||
|
jellyfinSession != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasXtreamProvider: Bool {
|
||||||
|
xtreamSession != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshJellyfinSession() {
|
||||||
|
jellyfinSession = Container.shared.currentUserSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshXtreamSession() {
|
||||||
|
xtreamSession = Container.shared.currentXtreamSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signOutJellyfin() {
|
||||||
|
NotificationCenter.default.post(name: .didSignOut, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func signOutXtream() {
|
||||||
|
Defaults[.currentXtreamServerID] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Container {
|
||||||
|
|
||||||
|
var sessionManager: Factory<SessionManager> {
|
||||||
|
self {
|
||||||
|
SessionManager()
|
||||||
|
}.singleton
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import Factory
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Session for an active Xtream Codes provider
|
||||||
|
final class XtreamSession {
|
||||||
|
|
||||||
|
let server: XtreamServer
|
||||||
|
let client: XtreamAPIClient
|
||||||
|
|
||||||
|
init(server: XtreamServer) {
|
||||||
|
self.server = server
|
||||||
|
self.client = XtreamAPIClient(server: server)
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
server.id
|
||||||
|
}
|
||||||
|
|
||||||
|
var name: String {
|
||||||
|
server.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Container {
|
||||||
|
|
||||||
|
/// Current active Xtream session (if any)
|
||||||
|
var currentXtreamSession: Factory<XtreamSession?> {
|
||||||
|
self {
|
||||||
|
guard let currentServerID = Defaults[.currentXtreamServerID] else { return nil }
|
||||||
|
|
||||||
|
let servers = Defaults[.xtreamServers]
|
||||||
|
guard let server = servers.first(where: { $0.id == currentServerID }) else {
|
||||||
|
// Had last server ID but no saved server
|
||||||
|
Defaults[.currentXtreamServerID] = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return XtreamSession(server: server)
|
||||||
|
}.cached
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,9 +97,9 @@ final class MediaViewModel: ViewModel, Stateful {
|
||||||
// force it to `folders` for better view handling
|
// force it to `folders` for better view handling
|
||||||
let supportedUserViews = try await (userViews.value.items ?? [])
|
let supportedUserViews = try await (userViews.value.items ?? [])
|
||||||
.filter { item in
|
.filter { item in
|
||||||
// Include channels (which don't have collectionType)
|
// EXCLUDE channels - they are now in the Xtream tab
|
||||||
if item.type == .channel {
|
if item.type == .channel {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
// Include items with supported collectionTypes
|
// Include items with supported collectionTypes
|
||||||
if let collectionType = item.collectionType {
|
if let collectionType = item.collectionType {
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import Factory
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
import OrderedCollections
|
||||||
|
|
||||||
|
final class XtreamViewModel: ViewModel, Stateful {
|
||||||
|
|
||||||
|
@Injected(\.sessionManager)
|
||||||
|
private var sessionManager
|
||||||
|
|
||||||
|
var xtreamSession: XtreamSession? {
|
||||||
|
sessionManager.xtreamSession
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Action
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
case refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: State
|
||||||
|
|
||||||
|
enum State: Hashable {
|
||||||
|
case content
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
case initial
|
||||||
|
case refreshing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var xtreamChannels: OrderedSet<BaseItemDto> = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var backgroundStates: Set<BackgroundState> = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var state: State = .initial
|
||||||
|
|
||||||
|
func respond(to action: Action) -> State {
|
||||||
|
switch action {
|
||||||
|
case let .error(error):
|
||||||
|
return .error(error)
|
||||||
|
case .refresh:
|
||||||
|
cancellables.removeAll()
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await refresh()
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .content
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .error(.init(error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
return .refreshing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refresh() async throws {
|
||||||
|
|
||||||
|
guard let session = xtreamSession else {
|
||||||
|
throw XtreamAPIError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
xtreamChannels.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
let categories = try await session.client.getLiveCategories()
|
||||||
|
|
||||||
|
// Convert Xtream categories to BaseItemDto for now
|
||||||
|
// TODO: Create proper Xtream data models
|
||||||
|
let items = categories.map { category -> BaseItemDto in
|
||||||
|
var item = BaseItemDto()
|
||||||
|
item.id = category.categoryId
|
||||||
|
item.name = category.categoryName
|
||||||
|
item.type = .channel
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
xtreamChannels.elements = items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomItemImageSources(for channel: BaseItemDto) async throws -> [ImageSource] {
|
||||||
|
// TODO: Implement Xtream-specific image fetching
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import CollectionVGrid
|
||||||
|
import Defaults
|
||||||
|
import Factory
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct XtreamView: View {
|
||||||
|
|
||||||
|
@Router
|
||||||
|
private var router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel = XtreamViewModel()
|
||||||
|
|
||||||
|
private var layout: CollectionVGridLayout {
|
||||||
|
if UIDevice.isTV {
|
||||||
|
.columns(4, insets: .init(50), itemSpacing: 50, lineSpacing: 50)
|
||||||
|
} else if UIDevice.isPad {
|
||||||
|
.minWidth(200)
|
||||||
|
} else {
|
||||||
|
.columns(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var content: some View {
|
||||||
|
CollectionVGrid(
|
||||||
|
uniqueElements: viewModel.xtreamChannels,
|
||||||
|
layout: layout
|
||||||
|
) { channel in
|
||||||
|
MediaItem(viewModel: viewModel, type: .collectionFolder(channel)) { namespace in
|
||||||
|
let channelViewModel = ItemLibraryViewModel(
|
||||||
|
parent: channel,
|
||||||
|
filters: .default
|
||||||
|
)
|
||||||
|
router.route(to: .library(viewModel: channelViewModel), in: namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func errorView(with error: some Error) -> some View {
|
||||||
|
ErrorView(error: error)
|
||||||
|
.onRetry {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
|
||||||
|
switch viewModel.state {
|
||||||
|
case .content:
|
||||||
|
content
|
||||||
|
case let .error(error):
|
||||||
|
errorView(with: error)
|
||||||
|
case .initial:
|
||||||
|
content
|
||||||
|
case .refreshing:
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.linear(duration: 0.1), value: viewModel.state)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.navigationTitle("Xtream")
|
||||||
|
.onFirstAppear {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
.if(UIDevice.isTV) { view in
|
||||||
|
view.toolbar(.hidden, for: .navigationBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Create a Gitea issue for jellyflood
|
||||||
|
# Usage: ./scripts/create-issue.sh "Issue Title" "Issue Body" "feature|bug"
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
source ~/.zshrc
|
||||||
|
|
||||||
|
# Gitea configuration
|
||||||
|
GITEA_URL="https://git.ashik.se"
|
||||||
|
REPO_OWNER="ashikk"
|
||||||
|
REPO_NAME="jellyflood"
|
||||||
|
|
||||||
|
# Check for API token in environment
|
||||||
|
if [ -z "$GITEA_API_TOKEN" ]; then
|
||||||
|
echo "Error: GITEA_API_TOKEN environment variable not set"
|
||||||
|
echo "Please set it in your ~/.zshrc"
|
||||||
|
echo "Get a token at: $GITEA_URL/user/settings/applications"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check arguments
|
||||||
|
if [ $# -lt 2 ]; then
|
||||||
|
echo "Usage: $0 \"Issue Title\" \"Issue Body\" [feature|bug]"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 \"jellyflood-8: New Feature\" \"Description here\" \"feature\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TITLE="$1"
|
||||||
|
BODY="$2"
|
||||||
|
LABEL="${3:-feature}"
|
||||||
|
|
||||||
|
# Add label to body
|
||||||
|
FULL_BODY="**Label:** $LABEL
|
||||||
|
|
||||||
|
$BODY"
|
||||||
|
|
||||||
|
# Create issue
|
||||||
|
echo "Creating issue: $TITLE"
|
||||||
|
RESPONSE=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n --arg title "$TITLE" --arg body "$FULL_BODY" '{title: $title, body: $body}')" \
|
||||||
|
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues")
|
||||||
|
|
||||||
|
ISSUE_NUMBER=$(echo "$RESPONSE" | jq -r '.number // "error"')
|
||||||
|
|
||||||
|
if [ "$ISSUE_NUMBER" = "error" ]; then
|
||||||
|
echo "Error creating issue:"
|
||||||
|
echo "$RESPONSE" | jq -r '.message // .'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Issue #$ISSUE_NUMBER created successfully!"
|
||||||
|
echo "View at: $GITEA_URL/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_NUMBER"
|
Loading…
Reference in New Issue