Skip to content

Commit dd26a63

Browse files
committed
Initial commit
0 parents  commit dd26a63

10 files changed

Lines changed: 602 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
build/
2+
.dart_tool/
3+
.flutter-plugins
4+
.flutter-plugins-dependencies
5+
.packages
6+
.pub-cache/
7+
.pub/
8+
*.iml
9+
.idea/

‎README.md‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# CipherStream — Flutter Scaffold
2+
3+
This is a starting structure, not a finished app — enough real code to
4+
learn Dart/Flutter against, wired to your actual live backend.
5+
6+
## What's here
7+
8+
```
9+
lib/
10+
main.dart — app entry point, theme, routes to Auth or Home
11+
models/
12+
channel.dart — mirrors your backend's Channel schema
13+
user.dart — mirrors the auth response shape
14+
services/
15+
api_service.dart — talks to https://cipherstream-backend.vercel.app/api
16+
(same backend the web test-harness used)
17+
screens/
18+
auth_screen.dart — sign up / log in
19+
home_screen.dart — channel catalog, grouped by category
20+
player_screen.dart — HLS playback via video_player + chewie
21+
```
22+
23+
## What this does NOT include yet (on purpose, so it's not overwhelming)
24+
25+
- No unlock/payment UI (matches your backend's current free-access mode)
26+
- No EPG/schedule display
27+
- No resume-watching, favorites, or search
28+
- No proper state management library (just basic StatefulWidget) — add
29+
Provider/Riverpod once the app has enough moving parts to need it
30+
31+
## How to actually turn this into a runnable project
32+
33+
I can't run `flutter create` for you from here (no Flutter SDK in this
34+
environment), so you'll do that part locally — it's one command:
35+
36+
```bash
37+
flutter create cipherstream
38+
```
39+
40+
Then:
41+
1. Delete the generated `lib/main.dart` and the generated `pubspec.yaml`
42+
2. Copy everything from this scaffold's `lib/` folder into your new
43+
project's `lib/` folder
44+
3. Copy this scaffold's `pubspec.yaml` over the generated one
45+
4. Run:
46+
```bash
47+
flutter pub get
48+
flutter run
49+
```
50+
51+
## Where to actually learn while reading this code
52+
53+
- `lib/services/api_service.dart` is the best file to study first — it's
54+
the direct Dart translation of what `app.js` did in the web version.
55+
Compare them side by side; that's the fastest way to map JS concepts
56+
you already know onto Dart syntax.
57+
- `main.dart`'s `FutureBuilder` pattern (checking `isSignedIn` before
58+
deciding which screen to show) is a very common Flutter idiom — worth
59+
understanding deeply since you'll reuse it everywhere.
60+
- The `_ChannelTile` and `_PlayerScreenState` classes show the two core
61+
widget types: stateless (just displays props) vs stateful (holds and
62+
reacts to changing data, like the video controller).
63+
64+
## Known rough edges (be aware, don't panic)
65+
66+
- `video_player`'s HLS support varies by platform — works well on
67+
iOS/Android, web HLS playback can be pickier and sometimes needs an
68+
extra package. Test on device early, not just web, once you have this
69+
running.
70+
- No error boundary/retry logic yet on network failures.

‎lib/main.dart‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'package:flutter/material.dart';
2+
import 'services/api_service.dart';
3+
import 'screens/auth_screen.dart';
4+
import 'screens/home_screen.dart';
5+
6+
void main() {
7+
runApp(const CipherStreamApp());
8+
}
9+
10+
class CipherStreamApp extends StatelessWidget {
11+
const CipherStreamApp({super.key});
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
final api = ApiService();
16+
17+
return MaterialApp(
18+
title: 'CipherStream',
19+
debugShowCheckedModeBanner: false,
20+
theme: ThemeData(
21+
scaffoldBackgroundColor: const Color(0xFF0B0F1A),
22+
colorScheme: ColorScheme.fromSeed(
23+
seedColor: const Color(0xFF2F6FED),
24+
brightness: Brightness.dark,
25+
),
26+
elevatedButtonTheme: ElevatedButtonThemeData(
27+
style: ElevatedButton.styleFrom(
28+
backgroundColor: const Color(0xFF2F6FED),
29+
foregroundColor: Colors.white,
30+
),
31+
),
32+
),
33+
home: FutureBuilder<bool>(
34+
future: api.isSignedIn,
35+
builder: (context, snapshot) {
36+
if (!snapshot.hasData) {
37+
return const Scaffold(
38+
backgroundColor: Color(0xFF0B0F1A),
39+
body: Center(child: CircularProgressIndicator()),
40+
);
41+
}
42+
return snapshot.data!
43+
? HomeScreen(api: api)
44+
: AuthScreen(api: api);
45+
},
46+
),
47+
);
48+
}
49+
}

‎lib/models/channel.dart‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Mirrors the Channel schema from the backend's src/models/Channel.js
2+
class Channel {
3+
final String id;
4+
final String displayName;
5+
final String category;
6+
final String tier; // 'free' | 'individual' | 'business'
7+
final String bundleId;
8+
final String description;
9+
final String logo;
10+
11+
Channel({
12+
required this.id,
13+
required this.displayName,
14+
required this.category,
15+
required this.tier,
16+
required this.bundleId,
17+
required this.description,
18+
required this.logo,
19+
});
20+
21+
// The backend never sends streamUrl in the catalog list — only via
22+
// GET /api/channels/:id/stream once gating is checked. Same rule here.
23+
factory Channel.fromJson(Map<String, dynamic> json) {
24+
return Channel(
25+
id: json['_id'] as String,
26+
displayName: json['displayName'] as String,
27+
category: json['category'] as String,
28+
tier: json['tier'] as String,
29+
bundleId: json['bundleId'] as String,
30+
description: (json['description'] ?? '') as String,
31+
logo: (json['logo'] ?? '') as String,
32+
);
33+
}
34+
}

‎lib/models/user.dart‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class CipherUser {
2+
final String id;
3+
final String name;
4+
final String email;
5+
6+
CipherUser({required this.id, required this.name, required this.email});
7+
8+
factory CipherUser.fromJson(Map<String, dynamic> json) {
9+
return CipherUser(
10+
id: json['id'].toString(),
11+
name: json['name'] as String,
12+
email: json['email'] as String,
13+
);
14+
}
15+
}

‎lib/screens/auth_screen.dart‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import 'package:flutter/material.dart';
2+
import '../services/api_service.dart';
3+
import 'home_screen.dart';
4+
5+
class AuthScreen extends StatefulWidget {
6+
final ApiService api;
7+
const AuthScreen({super.key, required this.api});
8+
9+
@override
10+
State<AuthScreen> createState() => _AuthScreenState();
11+
}
12+
13+
class _AuthScreenState extends State<AuthScreen> {
14+
final _nameController = TextEditingController();
15+
final _emailController = TextEditingController();
16+
final _passwordController = TextEditingController();
17+
String _message = '';
18+
bool _loading = false;
19+
20+
Future<void> _submit(bool isSignUp) async {
21+
setState(() {
22+
_loading = true;
23+
_message = '';
24+
});
25+
try {
26+
if (isSignUp) {
27+
await widget.api.signUp(
28+
_nameController.text,
29+
_emailController.text,
30+
_passwordController.text,
31+
);
32+
} else {
33+
await widget.api.logIn(_emailController.text, _passwordController.text);
34+
}
35+
if (!mounted) return;
36+
Navigator.of(context).pushReplacement(
37+
MaterialPageRoute(builder: (_) => HomeScreen(api: widget.api)),
38+
);
39+
} catch (e) {
40+
setState(() => _message = e.toString().replaceFirst('Exception: ', ''));
41+
} finally {
42+
setState(() => _loading = false);
43+
}
44+
}
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
return Scaffold(
49+
backgroundColor: const Color(0xFF0B0F1A),
50+
body: SafeArea(
51+
child: Padding(
52+
padding: const EdgeInsets.all(24),
53+
child: Column(
54+
crossAxisAlignment: CrossAxisAlignment.start,
55+
children: [
56+
const Text(
57+
'CipherStream',
58+
style: TextStyle(
59+
color: Colors.white,
60+
fontSize: 28,
61+
fontWeight: FontWeight.bold,
62+
),
63+
),
64+
const SizedBox(height: 4),
65+
const Text(
66+
'free access — no unlocks needed during launch',
67+
style: TextStyle(color: Color(0xFF3ECF8E), fontSize: 13),
68+
),
69+
const SizedBox(height: 32),
70+
_buildField(_nameController, 'Name'),
71+
_buildField(_emailController, 'Email'),
72+
_buildField(_passwordController, 'Password', obscure: true),
73+
const SizedBox(height: 16),
74+
Row(
75+
children: [
76+
Expanded(
77+
child: ElevatedButton(
78+
onPressed: _loading ? null : () => _submit(true),
79+
child: const Text('Sign up'),
80+
),
81+
),
82+
const SizedBox(width: 12),
83+
Expanded(
84+
child: OutlinedButton(
85+
onPressed: _loading ? null : () => _submit(false),
86+
child: const Text('Log in'),
87+
),
88+
),
89+
],
90+
),
91+
const SizedBox(height: 12),
92+
if (_message.isNotEmpty)
93+
Text(_message, style: const TextStyle(color: Colors.redAccent)),
94+
],
95+
),
96+
),
97+
),
98+
);
99+
}
100+
101+
Widget _buildField(TextEditingController controller, String label,
102+
{bool obscure = false}) {
103+
return Padding(
104+
padding: const EdgeInsets.only(bottom: 12),
105+
child: TextField(
106+
controller: controller,
107+
obscureText: obscure,
108+
style: const TextStyle(color: Colors.white),
109+
decoration: InputDecoration(
110+
labelText: label,
111+
labelStyle: const TextStyle(color: Colors.grey),
112+
enabledBorder: OutlineInputBorder(
113+
borderRadius: BorderRadius.circular(8),
114+
borderSide: const BorderSide(color: Color(0xFF232C42)),
115+
),
116+
),
117+
),
118+
);
119+
}
120+
}

0 commit comments

Comments
 (0)