Skip to content

Commit

Permalink
Merge pull request #1 from petrichor-hl/lam
Browse files Browse the repository at this point in the history
Lam
  • Loading branch information
petrichor-hl authored Dec 8, 2023
2 parents 5553b89 + 4c9328a commit 1b4ea1b
Show file tree
Hide file tree
Showing 17 changed files with 548 additions and 124 deletions.
1 change: 1 addition & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions:
9 changes: 9 additions & 0 deletions lib/models/poster.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Poster {
final String filmId;
final String posterPath;

const Poster({
required this.filmId,
required this.posterPath,
});
}
4 changes: 3 additions & 1 deletion lib/screens/film_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class _FilmDetailState extends State<FilmDetail> {
appBar: AppBar(
title: Image.asset(
Assets.viovidLogo,
width: 140,
width: 120,
),
centerTitle: true,
foregroundColor: Colors.white,
Expand Down Expand Up @@ -126,6 +126,8 @@ class _FilmDetailState extends State<FilmDetail> {
);
}

// print('film_id = ' + offlineData['film_id']);

final textPainter = TextPainter(
text: TextSpan(
text: _film!['overview'],
Expand Down
20 changes: 12 additions & 8 deletions lib/screens/films_by_genre.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:movie_app/cubits/route_stack/route_stack_cubit.dart';
import 'package:movie_app/main.dart';
import 'package:movie_app/models/poster.dart';
import 'package:movie_app/widgets/grid/grid_films.dart';
import 'package:shimmer/shimmer.dart';

Expand All @@ -21,19 +21,23 @@ class FilmsByGenre extends StatefulWidget {
}

class _FilmsByGenreState extends State<FilmsByGenre> {
late final List<dynamic> _postersData;
final List<Poster> _posters = [];
late final _futureFilms = _fetchFilmsOnDemand();

Future<void> _fetchFilmsOnDemand() async {
_postersData = await supabase
final List<dynamic> postersData = await supabase
.from('film_genre')
.select('film(id, poster_path)')
.eq('genre_id', widget.genreId);
// print(_postersData);

// await Future.delayed(
// const Duration(seconds: 1),
// );
for (var element in postersData) {
_posters.add(
Poster(
filmId: element['film']['id'],
posterPath: element['film']['poster_path'],
),
);
}
}

@override
Expand Down Expand Up @@ -93,7 +97,7 @@ class _FilmsByGenreState extends State<FilmsByGenre> {
return SizedBox.expand(
child: SingleChildScrollView(
child: GridFilms(
posters: _postersData,
posters: _posters,
),
),
);
Expand Down
55 changes: 50 additions & 5 deletions lib/screens/main/new_hot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ class NotificationNewFilm extends StatelessWidget {
String date = uploadDateTime.day.toString().padLeft(2, '0');
String month = 'THG ${uploadDateTime.month}';

double screenWidth = MediaQuery.sizeOf(context).width;

return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Row(
Expand Down Expand Up @@ -269,16 +271,59 @@ class NotificationNewFilm extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
height: 0.5625 * (screenWidth - 20 - 60 - 10),
width: double.infinity,
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
Image.network(
'https://image.tmdb.org/t/p/original/$backdropPath',
// height = 9/16 * width's image
height:
0.5625 * (MediaQuery.sizeOf(context).width - 20 - 60 - 10),
fit: BoxFit.cover,
frameBuilder: (
BuildContext context,
Widget child,
int? frame,
bool wasSynchronouslyLoaded,
) {
if (wasSynchronouslyLoaded) {
return child;
}
return AnimatedOpacity(
opacity: frame == null ? 0 : 1,
duration: const Duration(
milliseconds: 500), // Adjust the duration as needed
curve: Curves.easeInOut,
child: child, // Adjust the curve as needed
);
},
// https://api.flutter.dev/flutter/widgets/Image/loadingBuilder.html
loadingBuilder: (
BuildContext context,
Widget child,
ImageChunkEvent? loadingProgress,
) {
if (loadingProgress == null) {
return child;
}
return Center(
child: SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(
// value: loadingProgress.expectedTotalBytes != null
// ? loadingProgress.cumulativeBytesLoaded /
// loadingProgress.expectedTotalBytes!
// : null,
color: Theme.of(context).colorScheme.primary,
strokeCap: StrokeCap.round,
),
),
);
},
),
Positioned(
top: 10,
Expand Down
222 changes: 222 additions & 0 deletions lib/screens/main/search_film.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:movie_app/main.dart';
import 'package:movie_app/models/poster.dart';
import 'package:movie_app/widgets/grid/grid_films.dart';

class SearchFilmScreen extends StatefulWidget {
const SearchFilmScreen({super.key});

@override
State<SearchFilmScreen> createState() => _SearchFilmScreenState();
}

class _SearchFilmScreenState extends State<SearchFilmScreen> {
final _searchController = TextEditingController();
Timer _timer = Timer(const Duration(microseconds: 0), () {});
final List<Poster> _searchResults = [];

String _searchBarStatus = "empty";
/*
empty: Thanh tìm kiếm đang rỗng
hasText: Thanh tìm kiếm đã được nhập vài ký tự
*/

String _resultStatus = "none";
/*
none: chưa khởi động tìm kiếm
processing: đang tìm
hasResult: đã tìm kiếm xong
*/

void search({required String keyword}) async {
if (_searchController.text == keyword) {
if (keyword.isEmpty) {
return;
}

keyword.trim();
// print("search operation: " + keyword);
setState(() {
_resultStatus = "processing";
});

/*
Ở trên ta có câu lệnh setState(),
Nếu không có lệnh await Future.delayed() bên dưới thì
khi hàm build được gọi _searchResults đã được clear() xong
Vì vậy delay một khoảng để khi hàm build được gọi
_searchResults vẫn còn giá trị cũ chưa bị clear
*/
await Future.delayed(const Duration(milliseconds: 300));
_searchResults.clear();

final List<dynamic> searchResult = await supabase
.from('film')
.select('id, poster_path')
.ilike('search_context', '%$keyword%');

for (var element in searchResult) {
_searchResults.add(
Poster(
filmId: element['id'],
posterPath: element['poster_path'],
),
);
}

setState(() {
_resultStatus = "hasResult";
});
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
body: Column(
children: [
TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
filled: true,
fillColor: const Color(0xFF2B2B2B),
border: InputBorder.none,
prefixIcon: const Icon(
Icons.search_rounded,
),
prefixIconColor: Colors.white.withOpacity(0.5),
suffixIcon: _resultStatus == "processing"
? const SizedBox(
width: 48,
height: 48,
child: Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(
strokeWidth: 3,
),
),
)
: IconButton(
onPressed: _searchBarStatus == "empty"
? () {}
: () {
_searchController.clear();
setState(() {
_searchResults.clear();
_searchBarStatus = "empty";
_resultStatus = "none";
});
},
style: IconButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
icon: Icon(
_searchBarStatus == "empty"
? Icons.mic_rounded
: Icons.close_rounded,
),
),
suffixIconColor: Colors.white.withOpacity(0.5),
hintText: 'Tìm kiếm bộ phim của bạn',
hintStyle: const TextStyle(
color: Color.fromARGB(255, 120, 120, 120),
),
contentPadding: const EdgeInsets.fromLTRB(12, 12, 12, 14),
),
style: const TextStyle(
color: Colors.white,
),
autocorrect: false,
onChanged: (value) async {
// Khi người dùng từng ký tự => Kích hoạt lại bộ đếm
if (_timer.isActive) {
_timer.cancel();
}
// Khi người dùng từng ký tự => Đánh dấu là chưa kích hoạt tìm kiếm
_resultStatus = "none";

/*
Khi người dùng xoá hết tự khoá tìm kiếm
=> Delay 1 khoảng 50ms để tạo độ mượt mà
=> Clear kết quả tìm kiếm trước đó
=> Thiết lập Trạng thái của thanh tìm kiếm là rỗng (chưa nhập bất kỳ ký tự nào)
*/
if (value.isEmpty) {
await Future.delayed(
const Duration(milliseconds: 50),
);
setState(() {
_searchResults.clear();
_searchBarStatus = "empty";
});
} else {
/*
Khi người dùng nhập từ khoá tìm kiếm
=> Thiết lập Trạng thái của thanh tìm kiếm là hasText
=> Tạo bộ điếm ngược, có tác dụng sau 1s sẽ kích hoạt tìm kiếm với từ khoá được nhập
*/
if (_searchBarStatus != "hasText") {
setState(() {
_searchBarStatus = "hasText";
});
}
_timer = Timer(const Duration(seconds: 1), () {
search(keyword: value);
});
}
},
onEditingComplete: () {
FocusManager.instance.primaryFocus?.unfocus();
},
),
const Gap(10),
Expanded(
child: buildSearchResult(),
)
],
),
);
}

Widget buildSearchResult() {
// print(_searchBarStatus);
if (_searchBarStatus == "empty" || _resultStatus == "none") {
return Center(
child: Text(
'Tìm bộ phim yêu thích của bạn',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
),
);
}
if (_resultStatus == "hasResult" && _searchResults.isEmpty) {
return Center(
child: SizedBox(
width: 240,
child: Text(
'Không tìm thấy phim khớp với từ khoá trên',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
),
),
);
}
return SingleChildScrollView(child: GridFilms(posters: _searchResults));
}
}
Loading

0 comments on commit 1b4ea1b

Please sign in to comment.