Compare commits
11 Commits
92da4423b2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
187b42e465
|
|||
|
227311a39e
|
|||
|
0e46299ee0
|
|||
|
d7ce320702
|
|||
|
4ca90e327f
|
|||
|
4defe266eb
|
|||
|
8f51ac8b24
|
|||
|
4ce471599b
|
|||
|
4587901672
|
|||
|
ce980390df
|
|||
|
a5d9372806
|
42
.gitea/workflows/docker.yml
Normal file
42
.gitea/workflows/docker.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Build and Push Multi-Arch Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Gitea Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.DOMAIN }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Extract Version
|
||||
working-directory: ./server/cinema
|
||||
run: |
|
||||
VERSION=$(grep 'version:' pubspec.yaml | sed 's/version: //')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "MAJOR=$(echo $VERSION | cut -d. -f1)" >> $GITHUB_ENV
|
||||
echo "MAJOR_MINOR=$(echo $VERSION | cut -d. -f1,2)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and Push Multi-Arch Image
|
||||
working-directory: ./server/cinema
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t ${{ secrets.DOMAIN }}/${{ secrets.OWNER }}/${{ secrets.REPO }}/server:latest \
|
||||
-t ${{ secrets.DOMAIN }}/${{ secrets.OWNER }}/${{ secrets.REPO }}/server:${{ env.MAJOR }} \
|
||||
-t ${{ secrets.DOMAIN }}/${{ secrets.OWNER }}/${{ secrets.REPO }}/server:${{ env.MAJOR_MINOR }} \
|
||||
-t ${{ secrets.DOMAIN }}/${{ secrets.OWNER }}/${{ secrets.REPO }}/server:${{ env.VERSION }} \
|
||||
--push .
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
**/.idea/libraries
|
||||
**/.idea/copilot.*
|
||||
*.log
|
||||
|
||||
@@ -20,7 +20,7 @@ body:json {
|
||||
"backgroundColor": "#000",
|
||||
"format": "png",
|
||||
"language": "de-DE",
|
||||
"output": "lvgl_binary"
|
||||
"output": "lvglBinary"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,3 +7,6 @@ build/
|
||||
.gitignore
|
||||
.idea/
|
||||
.packages
|
||||
*.g.dart
|
||||
*.config.dart
|
||||
assets/cache
|
||||
|
||||
5
server/cinema/.gitignore
vendored
5
server/cinema/.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
*.g.dart
|
||||
*.freezed.dart
|
||||
*.config.dart
|
||||
*.log
|
||||
**/cache
|
||||
|
||||
@@ -8,14 +8,18 @@ RUN dart pub get
|
||||
|
||||
# Copy app source code (except anything in .dockerignore) and AOT compile app.
|
||||
COPY . .
|
||||
RUN dart compile exe bin/server.dart -o bin/server
|
||||
RUN dart run build_runner build --delete-conflicting-outputs && \
|
||||
APP_VERSION=$(grep 'version:' pubspec.yaml | sed 's/version: //') && \
|
||||
dart compile exe bin/server.dart -o bin/server \
|
||||
-DAPP_VERSION=$APP_VERSION
|
||||
|
||||
# Build minimal serving image from AOT-compiled `/server`
|
||||
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
|
||||
FROM scratch
|
||||
COPY --from=build /runtime/ /
|
||||
COPY --from=build /app/bin/server /app/bin/
|
||||
COPY assets /assets
|
||||
|
||||
# Start server.
|
||||
EXPOSE 8080
|
||||
EXPOSE 3000
|
||||
CMD ["/app/bin/server"]
|
||||
|
||||
26
server/cinema/Makefile
Normal file
26
server/cinema/Makefile
Normal file
@@ -0,0 +1,26 @@
|
||||
VERSION := $(shell grep 'version:' pubspec.yaml | sed 's/version: //')
|
||||
MAJOR := $(shell echo $(VERSION) | cut -d. -f1)
|
||||
MAJOR_MINOR := $(shell echo $(VERSION) | cut -d. -f1,2)
|
||||
|
||||
build:
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
watch:
|
||||
dart run build_runner watch --delete-conflicting-outputs
|
||||
|
||||
docker: build
|
||||
docker build \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:latest \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:$(MAJOR) \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:$(MAJOR_MINOR) \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:$(VERSION) \
|
||||
.
|
||||
|
||||
multi: build
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:latest \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:$(MAJOR) \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:$(MAJOR_MINOR) \
|
||||
-t cr.mars3142.io/model-railway/cinema-display:$(VERSION) \
|
||||
.
|
||||
5
server/cinema/assets/banner.txt
Normal file
5
server/cinema/assets/banner.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
____ _ ____ _
|
||||
/ ___(_)_ __ ___ _ __ ___ __ _ / ___| ___ _ ____ _(_) ___ ___
|
||||
| | | | '_ \ / _ \ '_ ` _ \ / _` | \___ \ / _ \ '__\ \ / / |/ __/ _ \
|
||||
| |___| | | | | __/ | | | | | (_| | ___) | __/ | \ V /| | (_| __/
|
||||
\____|_|_| |_|\___|_| |_| |_|\__,_| |____/ \___|_| \_/ |_|\___\___|
|
||||
@@ -1,71 +1,62 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cinema/common/env_not_found_exception.dart';
|
||||
import 'package:cinema/feature/middlewares/cors.dart';
|
||||
import 'package:cinema/feature/poster/data/repositories/image_loader.dart';
|
||||
import 'package:cinema/feature/poster/data/services/poster.service.dart';
|
||||
import 'package:cinema/feature/root/data/service/root.service.dart';
|
||||
import 'package:cinema/feature/version/version.dart';
|
||||
import 'package:cinema/injectable.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf/shelf_io.dart' as io;
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
|
||||
final List<dynamic> clients = [];
|
||||
|
||||
// Configure routes.
|
||||
final _router = Router()
|
||||
..get('/', _rootHandler)
|
||||
..get('/echo/<message>', _echoHandler);
|
||||
|
||||
Response _rootHandler(Request req) {
|
||||
return Response.ok('Hello, World!\n');
|
||||
}
|
||||
|
||||
Response _echoHandler(Request request) {
|
||||
final message = request.params['message'];
|
||||
return Response.ok('$message\n');
|
||||
}
|
||||
|
||||
void main(List<String> args) async {
|
||||
// Use any available host or container IP (usually `0.0.0.0`).
|
||||
final ip = InternetAddress.anyIPv4;
|
||||
runZonedGuarded(
|
||||
() async {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
// Configure a pipeline that logs requests.
|
||||
final handler = Pipeline().addMiddleware(logRequests()).addHandler(_router.call);
|
||||
configureDependencies();
|
||||
|
||||
var wsHandler = webSocketHandler((webSocket, _) {
|
||||
clients.add(webSocket);
|
||||
print('Client connected, total: ${clients.length}');
|
||||
final router = Router();
|
||||
router.mount("/poster", getIt<PosterService>().router.call);
|
||||
router.mount("/", getIt<RootService>().router.call);
|
||||
|
||||
webSocket.stream.listen(
|
||||
(message) {
|
||||
webSocket.sink.add('echo $message');
|
||||
},
|
||||
onDone: () {
|
||||
clients.remove(webSocket);
|
||||
print('Client disconnected, total: ${clients.length}');
|
||||
},
|
||||
onError: (error) {
|
||||
clients.remove(webSocket);
|
||||
print('Client error: $error');
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
});
|
||||
|
||||
FutureOr<Response> combinedHandler(Request request) {
|
||||
if (request.url.path == 'ws') {
|
||||
return wsHandler(request);
|
||||
}
|
||||
return handler(request);
|
||||
}
|
||||
/// add middlewares (Logging, CORS)
|
||||
final handler = Pipeline().addMiddleware(logRequests()).addMiddleware(cors()).addHandler(router.call);
|
||||
|
||||
// For running in containers, we respect the PORT environment variable.
|
||||
final port = int.parse(Platform.environment['PORT'] ?? '8080');
|
||||
final server = await shelf_io.serve(combinedHandler, ip, port).then((server) {
|
||||
print('Serving at http|ws://${server.address.host}:${server.port}');
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast to all clients
|
||||
void broadcast(String message) {
|
||||
for (final client in clients) {
|
||||
client.sink.add(message);
|
||||
final port = int.parse(Platform.environment['PORT'] ?? '3000');
|
||||
await io.serve(handler, InternetAddress.anyIPv4, port, poweredByHeader: null).then((server) async {
|
||||
final bannerFile = File('assets/banner.txt');
|
||||
if (await bannerFile.exists()) {
|
||||
final banner = await bannerFile.readAsString();
|
||||
print(banner);
|
||||
}
|
||||
|
||||
print('Caching current trending images...');
|
||||
|
||||
final ImageLoader loader = getIt();
|
||||
final movies = await loader.getPosterURIs();
|
||||
for (var movie in movies) {
|
||||
await loader.downloadImages(movie);
|
||||
}
|
||||
|
||||
getIt<Version>().printVersion();
|
||||
|
||||
print('Serving at ${server.address.host}:${server.port}\n');
|
||||
|
||||
final time = DateTime.now().difference(startTime);
|
||||
print('Server started at: $startTime');
|
||||
print('Startup time: $time\n');
|
||||
});
|
||||
},
|
||||
(error, stackTrace) {
|
||||
stderr.writeln(error);
|
||||
if (error is EnvNotFoundException) {
|
||||
exit(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
38
server/cinema/lib/common/dio_module.dart
Normal file
38
server/cinema/lib/common/dio_module.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cinema/common/env_module.dart';
|
||||
import 'package:cinema/feature/version/version.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
const dioAPI = 'api';
|
||||
const dioIMAGES = 'images';
|
||||
|
||||
@module
|
||||
abstract class DioModule {
|
||||
@Named(dioAPI)
|
||||
@lazySingleton
|
||||
Dio apiDio(@Named(apiKey) String apiKey) => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: 'https://api.themoviedb.org/3',
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $apiKey',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Cinema Service (v${Version().appVersion})',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@Named(dioIMAGES)
|
||||
@lazySingleton
|
||||
Dio get imagesDio => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: 'https://image.tmdb.org/t/p/original',
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:built_value/serializer.dart';
|
||||
|
||||
/// A custom serializer for [DateTime] objects that are represented as
|
||||
/// a simple date string "yyyy-MM-dd" in JSON.
|
||||
class CustomDateTimeSerializer implements PrimitiveSerializer<DateTime> {
|
||||
@override
|
||||
final Iterable<Type> types = const [DateTime];
|
||||
|
||||
@override
|
||||
final String wireName = 'DateTime';
|
||||
|
||||
@override
|
||||
Object serialize(Serializers serializers, DateTime dateTime,
|
||||
{FullType specifiedType = FullType.unspecified}) {
|
||||
// On serialization, convert DateTime to a "yyyy-MM-dd" string.
|
||||
return dateTime.toIso8601String().substring(0, 10);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime deserialize(Serializers serializers, Object serialized,
|
||||
{FullType specifiedType = FullType.unspecified}) {
|
||||
// On deserialization, parse the string to a DateTime object.
|
||||
// This handles formats like "2025-09-23".
|
||||
final parts = (serialized as String).split('-');
|
||||
final dateUtc = DateTime.utc(
|
||||
int.parse(parts[0]),
|
||||
int.parse(parts[1]),
|
||||
int.parse(parts[2]),
|
||||
);
|
||||
return dateUtc;
|
||||
}
|
||||
}
|
||||
20
server/cinema/lib/common/domain/serializers.dart
Normal file
20
server/cinema/lib/common/domain/serializers.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:built_collection/built_collection.dart';
|
||||
import 'package:built_value/serializer.dart';
|
||||
import 'package:built_value/standard_json_plugin.dart';
|
||||
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||
import 'package:cinema/feature/poster/domain/tmdb_trending_response.dart';
|
||||
|
||||
import 'custom_date_time_serializer.dart';
|
||||
|
||||
part 'serializers.g.dart';
|
||||
|
||||
@SerializersFor([TmdbTrendingResponse, TmdbMovieResult])
|
||||
final Serializers serializers =
|
||||
(_$serializers.toBuilder()
|
||||
..add(CustomDateTimeSerializer())
|
||||
..addPlugin(StandardJsonPlugin())
|
||||
..addBuilderFactory(
|
||||
const FullType(BuiltList, [FullType(TmdbMovieResult), FullType(Movie)]),
|
||||
() => ListBuilder<TmdbMovieResult>(),
|
||||
))
|
||||
.build();
|
||||
18
server/cinema/lib/common/env_module.dart
Normal file
18
server/cinema/lib/common/env_module.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'dart:io';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:cinema/common/env_not_found_exception.dart';
|
||||
|
||||
const apiKey = "apiKey";
|
||||
|
||||
@module
|
||||
abstract class EnvModule {
|
||||
@lazySingleton
|
||||
@Named(apiKey)
|
||||
String get api_key {
|
||||
final key = Platform.environment['TMDB_API_KEY'];
|
||||
if (key == null || key.isEmpty) {
|
||||
throw EnvNotFoundException('TMDB_API_KEY environment variable is missing.');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
8
server/cinema/lib/common/env_not_found_exception.dart
Normal file
8
server/cinema/lib/common/env_not_found_exception.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
class EnvNotFoundException implements Exception {
|
||||
final String message;
|
||||
EnvNotFoundException([this.message = '']);
|
||||
|
||||
@override
|
||||
String toString() => 'EnvNotFoundException: $message';
|
||||
}
|
||||
|
||||
24
server/cinema/lib/feature/middlewares/cors.dart
Normal file
24
server/cinema/lib/feature/middlewares/cors.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
||||
Middleware cors() {
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
|
||||
'Access-Control-Allow-Headers': 'Origin, Content-Type',
|
||||
'Access-Control-Max-Age': "600",
|
||||
};
|
||||
|
||||
return createMiddleware(
|
||||
requestHandler: (Request request) {
|
||||
if (request.method == "OPTIONS") {
|
||||
return Response.ok('', headers: corsHeaders);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
responseHandler: (Response response) {
|
||||
final headers = Map<String, String>.from(response.headers);
|
||||
headers.addEntries(corsHeaders.entries.map((e) => MapEntry(e.key, e.value)));
|
||||
return response.change(headers: headers);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||
|
||||
abstract class ImageLoader {
|
||||
Future<List<Movie>> getPosterURIs({String? language = 'de'});
|
||||
|
||||
Future<bool> downloadImages(Movie movie);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cinema/common/dio_module.dart';
|
||||
import 'package:cinema/common/domain/serializers.dart';
|
||||
import 'package:cinema/feature/poster/data/repositories/image_loader.dart';
|
||||
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||
import 'package:cinema/feature/poster/domain/tmdb_trending_response.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@Injectable(as: ImageLoader)
|
||||
class TmDBImageLoader implements ImageLoader {
|
||||
final Dio api;
|
||||
final Dio images;
|
||||
final String imageBaseUrl = 'https://image.tmdb.org/t/p/w500';
|
||||
|
||||
TmDBImageLoader(@Named(dioAPI) this.api, @Named(dioIMAGES) this.images);
|
||||
|
||||
@override
|
||||
Future<List<Movie>> getPosterURIs({String? language = 'de'}) async {
|
||||
final response = await api.get('/trending/movie/week?language=$language');
|
||||
if (response.data != null) {
|
||||
final data = serializers.deserializeWith(TmdbTrendingResponse.serializer, response.data);
|
||||
return data?.results
|
||||
.map(
|
||||
(movie) => Movie(
|
||||
(b) => b
|
||||
..id = movie.id
|
||||
..title = movie.title
|
||||
..poster = movie.posterPath
|
||||
..backdrop = movie.backdropPath
|
||||
..release = movie.releaseDate
|
||||
..video = movie.video,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> downloadImages(Movie movie) async {
|
||||
await _downloadImage(movie.poster, "cache/movie/${movie.id}/poster.png");
|
||||
await _downloadImage(movie.backdrop, "cache/movie/${movie.id}/backdrop.png");
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> _downloadImage(String url, String filename) async {
|
||||
final file = File(filename);
|
||||
if (await file.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await file.parent.create(recursive: true);
|
||||
final response = await images.get(url, options: Options(responseType: ResponseType.bytes));
|
||||
await file.writeAsBytes(response.data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cinema/feature/poster/domain/poster_request.schema.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
part 'poster.service.g.dart';
|
||||
|
||||
@injectable
|
||||
class PosterService {
|
||||
@Route.get('/')
|
||||
Future<Response> getRoot(Request request) async {
|
||||
return Response.ok('deprecated poster endpoint. use POST /poster instead');
|
||||
}
|
||||
|
||||
@Route.post('/')
|
||||
Future<Response> postRoot(Request request) async {
|
||||
final payload = await request.readAsString();
|
||||
final body = jsonDecode(payload);
|
||||
final params = posterSchema.safeParse(body);
|
||||
if (!params.success) {
|
||||
return Response(
|
||||
400,
|
||||
body: jsonEncode({
|
||||
'error': 'Invalid request',
|
||||
'details': params.error?.messages,
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
}
|
||||
return Response.ok(jsonEncode(params.data), headers: {'Content-Type': 'application/json'});
|
||||
}
|
||||
|
||||
Router get router => _$PosterServiceRouter(this);
|
||||
}
|
||||
24
server/cinema/lib/feature/poster/domain/movie.dart
Normal file
24
server/cinema/lib/feature/poster/domain/movie.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:built_value/built_value.dart';
|
||||
import 'package:built_value/serializer.dart';
|
||||
|
||||
part 'movie.g.dart';
|
||||
|
||||
abstract class Movie implements Built<Movie, MovieBuilder> {
|
||||
static Serializer<Movie> get serializer => _$movieSerializer;
|
||||
|
||||
int get id;
|
||||
|
||||
String get title;
|
||||
|
||||
DateTime get release;
|
||||
|
||||
bool get video;
|
||||
|
||||
String get poster;
|
||||
|
||||
String get backdrop;
|
||||
|
||||
Movie._();
|
||||
|
||||
factory Movie([void Function(MovieBuilder) updates]) = _$Movie;
|
||||
}
|
||||
16
server/cinema/lib/feature/poster/domain/poster.enums.dart
Normal file
16
server/cinema/lib/feature/poster/domain/poster.enums.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
enum PosterOrientation {
|
||||
horizontal,
|
||||
vertical,
|
||||
}
|
||||
|
||||
enum PosterFormat {
|
||||
png,
|
||||
jpeg,
|
||||
bmp,
|
||||
}
|
||||
|
||||
enum PosterOutput {
|
||||
image,
|
||||
lvgl,
|
||||
lvglBinary,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:cinema/feature/poster/domain/poster.enums.dart';
|
||||
import 'package:zard/zard.dart';
|
||||
|
||||
final _orientations = PosterOrientation.values.map((e) => e.name);
|
||||
final _formats = PosterFormat.values.map((e) => e.name);
|
||||
final _outputs = PosterOutput.values.map((e) => e.name);
|
||||
|
||||
final posterSchema = z.map({
|
||||
'width': z.int().min(1, message: "'width' must be at least 1").max(4000, message: "'width' must be at most 4000"),
|
||||
'height': z.int().min(1, message: "'height' must be at least 1").max(4000, message: "'height' must be a most 4000"),
|
||||
'count': z.int().min(1, message: "'count' must be at least 1").max(20, message: "'count' must be at most 20"),
|
||||
'orientation': z.string().refine(
|
||||
(value) => _orientations.contains(value),
|
||||
message: "'orientation' must be either ${_orientations.join(', ')}",
|
||||
),
|
||||
'shuffle': z.bool(message: "'shuffle' must be a boolean value"),
|
||||
'language': z.string(message: "'language' must be a string").transform((value) => value.trim()),
|
||||
'backgroundColor': z.string().regex(
|
||||
RegExp(r'^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$'),
|
||||
message: "The 'backgroundColor' must be a valid hexadecimal color code (e.g., #000 or #FF0000)",
|
||||
),
|
||||
'format': z.string().refine(
|
||||
(value) => _formats.contains(value),
|
||||
message: "'format' must be either ${_formats.join(', ')}",
|
||||
),
|
||||
'output': z.string().refine(
|
||||
(value) => _outputs.contains(value),
|
||||
message: "'output' must be either ${_outputs.join(', ')}",
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:built_collection/built_collection.dart';
|
||||
import 'package:built_value/built_value.dart';
|
||||
import 'package:built_value/serializer.dart';
|
||||
|
||||
part 'tmdb_trending_response.g.dart';
|
||||
|
||||
abstract class TmdbTrendingResponse implements Built<TmdbTrendingResponse, TmdbTrendingResponseBuilder> {
|
||||
static Serializer<TmdbTrendingResponse> get serializer => _$tmdbTrendingResponseSerializer;
|
||||
|
||||
int get page;
|
||||
|
||||
BuiltList<TmdbMovieResult> get results;
|
||||
|
||||
@BuiltValueField(wireName: 'total_pages')
|
||||
int get totalPages;
|
||||
|
||||
@BuiltValueField(wireName: 'total_results')
|
||||
int get totalResults;
|
||||
|
||||
TmdbTrendingResponse._();
|
||||
|
||||
factory TmdbTrendingResponse([void Function(TmdbTrendingResponseBuilder) updates]) = _$TmdbTrendingResponse;
|
||||
}
|
||||
|
||||
abstract class TmdbMovieResult implements Built<TmdbMovieResult, TmdbMovieResultBuilder> {
|
||||
static Serializer<TmdbMovieResult> get serializer => _$tmdbMovieResultSerializer;
|
||||
|
||||
bool get adult;
|
||||
|
||||
@BuiltValueField(wireName: 'backdrop_path')
|
||||
String get backdropPath;
|
||||
|
||||
int get id;
|
||||
|
||||
String get title;
|
||||
|
||||
@BuiltValueField(wireName: 'original_title')
|
||||
String get originalTitle;
|
||||
|
||||
String get overview;
|
||||
|
||||
@BuiltValueField(wireName: 'poster_path')
|
||||
String get posterPath;
|
||||
|
||||
@BuiltValueField(wireName: 'media_type')
|
||||
String get mediaType;
|
||||
|
||||
@BuiltValueField(wireName: 'genre_ids')
|
||||
BuiltList<int> get genreIds;
|
||||
|
||||
double get popularity;
|
||||
|
||||
@BuiltValueField(wireName: 'release_date')
|
||||
DateTime get releaseDate;
|
||||
|
||||
bool get video;
|
||||
|
||||
@BuiltValueField(wireName: 'vote_average')
|
||||
double get voteAverage;
|
||||
|
||||
@BuiltValueField(wireName: 'vote_count')
|
||||
int get voteCount;
|
||||
|
||||
TmdbMovieResult._();
|
||||
|
||||
factory TmdbMovieResult([void Function(TmdbMovieResultBuilder) updates]) = _$TmdbMovieResult;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:cinema/feature/websocket/websocket.service.dart';
|
||||
import 'package:cinema/injectable.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
part 'root.service.g.dart';
|
||||
|
||||
@injectable
|
||||
class RootService {
|
||||
@Route.get('/')
|
||||
Future<Response> getRoot(Request request) async {
|
||||
final isWebSocket =
|
||||
request.headers['connection']?.toLowerCase() == 'upgrade' &&
|
||||
request.headers['upgrade']?.toLowerCase() == 'websocket';
|
||||
|
||||
if (isWebSocket) {
|
||||
return getIt<WebSocketService>().handler(request);
|
||||
}
|
||||
|
||||
return Response.ok('REST response');
|
||||
}
|
||||
|
||||
Router get router => _$RootServiceRouter(this);
|
||||
}
|
||||
14
server/cinema/lib/feature/version/version.dart
Normal file
14
server/cinema/lib/feature/version/version.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@injectable
|
||||
class Version {
|
||||
const Version();
|
||||
|
||||
String get appVersion => const String.fromEnvironment('APP_VERSION');
|
||||
|
||||
void printVersion() {
|
||||
if (appVersion.isNotEmpty) {
|
||||
print('App Version: $appVersion');
|
||||
}
|
||||
}
|
||||
}
|
||||
28
server/cinema/lib/feature/websocket/websocket.service.dart
Normal file
28
server/cinema/lib/feature/websocket/websocket.service.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
|
||||
@LazySingleton()
|
||||
class WebSocketService {
|
||||
final List<dynamic> _clients = [];
|
||||
|
||||
Handler get handler => webSocketHandler((webSocket, _) {
|
||||
_clients.add(webSocket);
|
||||
print('Client connected, total: ${_clients.length}');
|
||||
|
||||
webSocket.stream.listen(
|
||||
(message) {
|
||||
webSocket.sink.add('echo $message');
|
||||
},
|
||||
onDone: () {
|
||||
_clients.remove(webSocket);
|
||||
print('Client disconnected, total: ${_clients.length}');
|
||||
},
|
||||
onError: (error) {
|
||||
_clients.remove(webSocket);
|
||||
print('Client error: $error');
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
9
server/cinema/lib/injectable.dart
Normal file
9
server/cinema/lib/injectable.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import 'injectable.config.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
@InjectableInit()
|
||||
void configureDependencies() => getIt.init();
|
||||
@@ -5,18 +5,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e"
|
||||
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "92.0.0"
|
||||
version: "91.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e"
|
||||
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "8.4.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -49,6 +49,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: dfb67ccc9a78c642193e0c2d94cb9e48c2c818b3178a86097d644acdcde6a8d9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "7b5b569f3df370590a85029148d6fc66c7d0201fc6f1847c07dd85d365ae9fcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.3"
|
||||
built_collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: built_value
|
||||
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.0"
|
||||
built_value_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: built_value_generator
|
||||
sha256: "65f5823a2c4158384ebc845218e19286fdf5dd04f8ac2cf607b01a502be40b1b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -57,6 +121,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -89,6 +161,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -105,6 +201,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -113,6 +217,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
get_it:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: get_it
|
||||
sha256: "84792561b731b6463d053e9761a5236da967c369da10b134b8585a5e18429956"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.5"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -121,6 +233,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -161,6 +281,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
injectable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: injectable
|
||||
sha256: "29559f7e3daebf0084597de86a825ae7f149d9e30264b7fbc71d1069ae82697d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
injectable_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: injectable_generator
|
||||
sha256: "309c3f3546160dd00b575f16b341a6a3025479950441bcc7fcb2f8404a40d326"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.1"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -177,6 +313,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -273,6 +417,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
shelf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -297,6 +457,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
shelf_router_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: shelf_router_generator
|
||||
sha256: "310416e0eb5a96c8b27f2586367f07b09dc480af06485c1f7951bbed1e8b8b08"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -313,6 +481,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "9098ab86015c4f1d8af6486b547b11100e73b193e1899015033cb3e14ad20243"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -353,6 +529,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -465,5 +649,13 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
zard:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: zard
|
||||
sha256: "772fc9ef6088123fefaaa88cb986253f0e838aec2af2c3b956a9a1c98ea2b049"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.23"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
|
||||
@@ -7,12 +7,22 @@ environment:
|
||||
sdk: ^3.9.0
|
||||
|
||||
dependencies:
|
||||
built_collection: ^5.1.1
|
||||
built_value: ^8.9.2
|
||||
dio: ^5.9.0
|
||||
get_it: ^9.0.5
|
||||
image: ^4.5.4
|
||||
injectable: ^2.6.0
|
||||
shelf: ^1.4.2
|
||||
shelf_router: ^1.1.2
|
||||
shelf_web_socket: ^3.0.0
|
||||
zard: ^0.0.23
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.10.3
|
||||
built_value_generator: ^8.9.2
|
||||
http: ^1.2.2
|
||||
injectable_generator: ^2.9.1
|
||||
lints: ^6.0.0
|
||||
shelf_router_generator: ^1.1.3
|
||||
test: ^1.25.6
|
||||
|
||||
Reference in New Issue
Block a user