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
11 changes: 7 additions & 4 deletions .clinerules
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ If you need to update agent instructions, edit **this file** (`AGENTS.md`) and r

## Project Overview

NullFeed is a self-hosted YouTube media center. This is the Flutter client
targeting iOS only (Apple TV is covered by the separate native tvOS app,
`nullfeed-tvos`).
NullFeed is a self-hosted YouTube media center. This is the Flutter client,
primarily for **iOS**, plus a **Flutter Web build (beta)**. (Apple TV is covered
by the separate native tvOS app, `nullfeed-tvos`.) Web has no on-device storage,
so the offline-save feature and its "Offline" tab are hidden on web (`kIsWeb`
guards); keep new platform-specific code behind `kIsWeb` / conditional imports.

**Stack:** Flutter 3.41+, Dart 3.11+, Riverpod 3.x, Freezed 3.x, GoRouter 17.x,
Hive, Dio, video_player.

## CI Pipeline

This repo has 6 CI checks that run on every PR:
This repo has 7 CI checks that run on every PR:

| Check | What it does |
|-------|-------------|
| **Format Check** | `dart format --set-exit-if-changed .` |
| **Analyze** | `flutter analyze --fatal-infos --fatal-warnings` |
| **Test** | `flutter test` |
| **Build iOS** | `flutter build ios --release --no-codesign` (runs on `macos-latest`) |
| **Build Web** | `flutter build web --release` (runs on `ubuntu-latest`) |
| **Dependency Audit** | Warns on major version drift (non-blocking) |
| **Agent Rules Sync** | Verifies all agent instruction files match `AGENTS.md` |

Expand Down
11 changes: 7 additions & 4 deletions .continuerules
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ If you need to update agent instructions, edit **this file** (`AGENTS.md`) and r

## Project Overview

NullFeed is a self-hosted YouTube media center. This is the Flutter client
targeting iOS only (Apple TV is covered by the separate native tvOS app,
`nullfeed-tvos`).
NullFeed is a self-hosted YouTube media center. This is the Flutter client,
primarily for **iOS**, plus a **Flutter Web build (beta)**. (Apple TV is covered
by the separate native tvOS app, `nullfeed-tvos`.) Web has no on-device storage,
so the offline-save feature and its "Offline" tab are hidden on web (`kIsWeb`
guards); keep new platform-specific code behind `kIsWeb` / conditional imports.

**Stack:** Flutter 3.41+, Dart 3.11+, Riverpod 3.x, Freezed 3.x, GoRouter 17.x,
Hive, Dio, video_player.

## CI Pipeline

This repo has 6 CI checks that run on every PR:
This repo has 7 CI checks that run on every PR:

| Check | What it does |
|-------|-------------|
| **Format Check** | `dart format --set-exit-if-changed .` |
| **Analyze** | `flutter analyze --fatal-infos --fatal-warnings` |
| **Test** | `flutter test` |
| **Build iOS** | `flutter build ios --release --no-codesign` (runs on `macos-latest`) |
| **Build Web** | `flutter build web --release` (runs on `ubuntu-latest`) |
| **Dependency Audit** | Warns on major version drift (non-blocking) |
| **Agent Rules Sync** | Verifies all agent instruction files match `AGENTS.md` |

Expand Down
11 changes: 7 additions & 4 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ If you need to update agent instructions, edit **this file** (`AGENTS.md`) and r

## Project Overview

NullFeed is a self-hosted YouTube media center. This is the Flutter client
targeting iOS only (Apple TV is covered by the separate native tvOS app,
`nullfeed-tvos`).
NullFeed is a self-hosted YouTube media center. This is the Flutter client,
primarily for **iOS**, plus a **Flutter Web build (beta)**. (Apple TV is covered
by the separate native tvOS app, `nullfeed-tvos`.) Web has no on-device storage,
so the offline-save feature and its "Offline" tab are hidden on web (`kIsWeb`
guards); keep new platform-specific code behind `kIsWeb` / conditional imports.

**Stack:** Flutter 3.41+, Dart 3.11+, Riverpod 3.x, Freezed 3.x, GoRouter 17.x,
Hive, Dio, video_player.

## CI Pipeline

This repo has 6 CI checks that run on every PR:
This repo has 7 CI checks that run on every PR:

| Check | What it does |
|-------|-------------|
| **Format Check** | `dart format --set-exit-if-changed .` |
| **Analyze** | `flutter analyze --fatal-infos --fatal-warnings` |
| **Test** | `flutter test` |
| **Build iOS** | `flutter build ios --release --no-codesign` (runs on `macos-latest`) |
| **Build Web** | `flutter build web --release` (runs on `ubuntu-latest`) |
| **Dependency Audit** | Warns on major version drift (non-blocking) |
| **Agent Rules Sync** | Verifies all agent instruction files match `AGENTS.md` |

Expand Down
11 changes: 7 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ If you need to update agent instructions, edit **this file** (`AGENTS.md`) and r

## Project Overview

NullFeed is a self-hosted YouTube media center. This is the Flutter client
targeting iOS only (Apple TV is covered by the separate native tvOS app,
`nullfeed-tvos`).
NullFeed is a self-hosted YouTube media center. This is the Flutter client,
primarily for **iOS**, plus a **Flutter Web build (beta)**. (Apple TV is covered
by the separate native tvOS app, `nullfeed-tvos`.) Web has no on-device storage,
so the offline-save feature and its "Offline" tab are hidden on web (`kIsWeb`
guards); keep new platform-specific code behind `kIsWeb` / conditional imports.

**Stack:** Flutter 3.41+, Dart 3.11+, Riverpod 3.x, Freezed 3.x, GoRouter 17.x,
Hive, Dio, video_player.

## CI Pipeline

This repo has 6 CI checks that run on every PR:
This repo has 7 CI checks that run on every PR:

| Check | What it does |
|-------|-------------|
| **Format Check** | `dart format --set-exit-if-changed .` |
| **Analyze** | `flutter analyze --fatal-infos --fatal-warnings` |
| **Test** | `flutter test` |
| **Build iOS** | `flutter build ios --release --no-codesign` (runs on `macos-latest`) |
| **Build Web** | `flutter build web --release` (runs on `ubuntu-latest`) |
| **Dependency Audit** | Warns on major version drift (non-blocking) |
| **Agent Rules Sync** | Verifies all agent instruction files match `AGENTS.md` |

Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ jobs:
- name: Build iOS (release, no codesign)
run: flutter build ios --release --no-codesign

build-web:
name: Build Web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true

- name: Cache pub dependencies
uses: actions/cache@v4
with:
path: |
~/.pub-cache
.dart_tool
key: pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }}
restore-keys: pub-${{ runner.os }}-

- name: Install dependencies
run: flutter pub get

- name: Run code generation
run: dart run build_runner build --delete-conflicting-outputs

- name: Build web (release)
run: flutter build web --release --no-tree-shake-icons

dependency-audit:
name: Dependency Audit
runs-on: ubuntu-latest
Expand Down
11 changes: 7 additions & 4 deletions .windsurfrules
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ If you need to update agent instructions, edit **this file** (`AGENTS.md`) and r

## Project Overview

NullFeed is a self-hosted YouTube media center. This is the Flutter client
targeting iOS only (Apple TV is covered by the separate native tvOS app,
`nullfeed-tvos`).
NullFeed is a self-hosted YouTube media center. This is the Flutter client,
primarily for **iOS**, plus a **Flutter Web build (beta)**. (Apple TV is covered
by the separate native tvOS app, `nullfeed-tvos`.) Web has no on-device storage,
so the offline-save feature and its "Offline" tab are hidden on web (`kIsWeb`
guards); keep new platform-specific code behind `kIsWeb` / conditional imports.

**Stack:** Flutter 3.41+, Dart 3.11+, Riverpod 3.x, Freezed 3.x, GoRouter 17.x,
Hive, Dio, video_player.

## CI Pipeline

This repo has 6 CI checks that run on every PR:
This repo has 7 CI checks that run on every PR:

| Check | What it does |
|-------|-------------|
| **Format Check** | `dart format --set-exit-if-changed .` |
| **Analyze** | `flutter analyze --fatal-infos --fatal-warnings` |
| **Test** | `flutter test` |
| **Build iOS** | `flutter build ios --release --no-codesign` (runs on `macos-latest`) |
| **Build Web** | `flutter build web --release` (runs on `ubuntu-latest`) |
| **Dependency Audit** | Warns on major version drift (non-blocking) |
| **Agent Rules Sync** | Verifies all agent instruction files match `AGENTS.md` |

Expand Down
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ If you need to update agent instructions, edit **this file** (`AGENTS.md`) and r

## Project Overview

NullFeed is a self-hosted YouTube media center. This is the Flutter client
targeting iOS only (Apple TV is covered by the separate native tvOS app,
`nullfeed-tvos`).
NullFeed is a self-hosted YouTube media center. This is the Flutter client,
primarily for **iOS**, plus a **Flutter Web build (beta)**. (Apple TV is covered
by the separate native tvOS app, `nullfeed-tvos`.) Web has no on-device storage,
so the offline-save feature and its "Offline" tab are hidden on web (`kIsWeb`
guards); keep new platform-specific code behind `kIsWeb` / conditional imports.

**Stack:** Flutter 3.41+, Dart 3.11+, Riverpod 3.x, Freezed 3.x, GoRouter 17.x,
Hive, Dio, video_player.

## CI Pipeline

This repo has 6 CI checks that run on every PR:
This repo has 7 CI checks that run on every PR:

| Check | What it does |
|-------|-------------|
| **Format Check** | `dart format --set-exit-if-changed .` |
| **Analyze** | `flutter analyze --fatal-infos --fatal-warnings` |
| **Test** | `flutter test` |
| **Build iOS** | `flutter build ios --release --no-codesign` (runs on `macos-latest`) |
| **Build Web** | `flutter build web --release` (runs on `ubuntu-latest`) |
| **Dependency Audit** | Warns on major version drift (non-blocking) |
| **Agent Rules Sync** | Verifies all agent instruction files match `AGENTS.md` |

Expand Down
11 changes: 7 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ If you need to update agent instructions, edit **this file** (`AGENTS.md`) and r

## Project Overview

NullFeed is a self-hosted YouTube media center. This is the Flutter client
targeting iOS only (Apple TV is covered by the separate native tvOS app,
`nullfeed-tvos`).
NullFeed is a self-hosted YouTube media center. This is the Flutter client,
primarily for **iOS**, plus a **Flutter Web build (beta)**. (Apple TV is covered
by the separate native tvOS app, `nullfeed-tvos`.) Web has no on-device storage,
so the offline-save feature and its "Offline" tab are hidden on web (`kIsWeb`
guards); keep new platform-specific code behind `kIsWeb` / conditional imports.

**Stack:** Flutter 3.41+, Dart 3.11+, Riverpod 3.x, Freezed 3.x, GoRouter 17.x,
Hive, Dio, video_player.

## CI Pipeline

This repo has 6 CI checks that run on every PR:
This repo has 7 CI checks that run on every PR:

| Check | What it does |
|-------|-------------|
| **Format Check** | `dart format --set-exit-if-changed .` |
| **Analyze** | `flutter analyze --fatal-infos --fatal-warnings` |
| **Test** | `flutter test` |
| **Build iOS** | `flutter build ios --release --no-codesign` (runs on `macos-latest`) |
| **Build Web** | `flutter build web --release` (runs on `ubuntu-latest`) |
| **Dependency Audit** | Warns on major version drift (non-blocking) |
| **Agent Rules Sync** | Verifies all agent instruction files match `AGENTS.md` |

Expand Down
92 changes: 44 additions & 48 deletions lib/config/routes.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
Expand Down Expand Up @@ -121,29 +122,34 @@ class _ScaffoldWithNav extends ConsumerWidget {
final Widget child;
const _ScaffoldWithNav({required this.child});

static const List<_NavDest> _allDestinations = [
_NavDest('/home', Icons.home_outlined, Icons.home, 'Home'),
_NavDest(
'/library',
Icons.video_library_outlined,
Icons.video_library,
'Library',
),
_NavDest('/discover', Icons.explore_outlined, Icons.explore, 'Discover'),
_NavDest(
'/downloads',
Icons.offline_pin_outlined,
Icons.offline_pin,
'Offline',
),
_NavDest('/settings', Icons.settings_outlined, Icons.settings, 'Settings'),
];

/// The Offline tab is on-device storage, which the web build doesn't have, so
/// it's dropped on web.
static List<_NavDest> get _destinations => kIsWeb
? _allDestinations.where((d) => d.route != '/downloads').toList()
: _allDestinations;

static int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
if (location.startsWith('/home')) return 0;
if (location.startsWith('/library')) return 1;
if (location.startsWith('/discover')) return 2;
if (location.startsWith('/downloads')) return 3;
if (location.startsWith('/settings')) return 4;
return 0;
}

void _onTap(BuildContext context, int index) {
switch (index) {
case 0:
context.go('/home');
case 1:
context.go('/library');
case 2:
context.go('/discover');
case 3:
context.go('/downloads');
case 4:
context.go('/settings');
}
final index = _destinations.indexWhere((d) => location.startsWith(d.route));
return index < 0 ? 0 : index;
}

@override
Expand All @@ -152,42 +158,32 @@ class _ScaffoldWithNav extends ConsumerWidget {
// events keep flowing and a Settings-triggered invalidate reconnects
// immediately instead of waiting for the Home tab to be visited.
ref.watch(webSocketConnectionProvider);
final destinations = _destinations;
final selectedIndex = _calculateSelectedIndex(context);

return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: selectedIndex,
onTap: (index) => _onTap(context, index),
onTap: (index) => context.go(destinations[index].route),
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.video_library_outlined),
activeIcon: Icon(Icons.video_library),
label: 'Library',
),
BottomNavigationBarItem(
icon: Icon(Icons.explore_outlined),
activeIcon: Icon(Icons.explore),
label: 'Discover',
),
BottomNavigationBarItem(
icon: Icon(Icons.offline_pin_outlined),
activeIcon: Icon(Icons.offline_pin),
label: 'Offline',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings',
),
items: [
for (final d in destinations)
BottomNavigationBarItem(
icon: Icon(d.icon),
activeIcon: Icon(d.activeIcon),
label: d.label,
),
],
),
);
}
}

class _NavDest {
final String route;
final IconData icon;
final IconData activeIcon;
final String label;
const _NavDest(this.route, this.icon, this.activeIcon, this.label);
}
6 changes: 4 additions & 2 deletions lib/providers/websocket_provider.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/websocket_service.dart';
import '../services/storage_service.dart';
Expand Down Expand Up @@ -57,8 +58,9 @@ final webSocketConnectionProvider = Provider<void>((ref) {
ref.invalidate(newEpisodesProvider);
ref.invalidate(recentlyAddedProvider);
ref.invalidate(homeFeedProvider);
// Auto-offline: download to device if enabled for this channel
if (videoId != null && channelId != null) {
// Auto-offline: download to device if enabled for this channel.
// Web has no on-device storage, so it never auto-saves.
if (!kIsWeb && videoId != null && channelId != null) {
final storageService = ref.read(storageServiceProvider);
if (storageService.isAutoOfflineEnabled(channelId)) {
final offlineService = ref.read(offlineServiceProvider);
Expand Down
Loading
Loading