Flutter 앱: 다운로드 개선 및 불필요한 패키지 제거
- 다운로드 서비스 개선 - 저장 경로를 Pictures/fromis_9 폴더로 변경 (갤러리 표시) - URL에서 파일 확장자 자동 추출 (동영상/이미지) - saveInPublicStorage 옵션으로 미디어 스캐너 트리거 - 티저 라이트박스에서 동영상 다운로드 지원 - video_thumbnail 패키지 제거 (백엔드에서 썸네일 처리) - 티저 썸네일 위젯 간소화 (StatefulWidget → StatelessWidget) - 홈 화면 하단 여백 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b512f943e
commit
3b5f8a93ca
5 changed files with 65 additions and 151 deletions
|
|
@ -14,7 +14,32 @@ Future<void> initDownloadService() async {
|
|||
);
|
||||
}
|
||||
|
||||
/// 이미지 다운로드
|
||||
/// URL에서 파일 확장자 추출
|
||||
String _getExtensionFromUrl(String url) {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
final path = uri.path.toLowerCase();
|
||||
|
||||
// 동영상 확장자
|
||||
if (path.endsWith('.mp4')) return '.mp4';
|
||||
if (path.endsWith('.mov')) return '.mov';
|
||||
if (path.endsWith('.avi')) return '.avi';
|
||||
if (path.endsWith('.webm')) return '.webm';
|
||||
|
||||
// 이미지 확장자
|
||||
if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return '.jpg';
|
||||
if (path.endsWith('.png')) return '.png';
|
||||
if (path.endsWith('.gif')) return '.gif';
|
||||
if (path.endsWith('.webp')) return '.webp';
|
||||
|
||||
// 기본값
|
||||
return '.jpg';
|
||||
} catch (_) {
|
||||
return '.jpg';
|
||||
}
|
||||
}
|
||||
|
||||
/// 파일 다운로드 (이미지/동영상)
|
||||
Future<String?> downloadImage(String url, {String? fileName}) async {
|
||||
// 권한 요청
|
||||
if (Platform.isAndroid) {
|
||||
|
|
@ -24,21 +49,20 @@ Future<String?> downloadImage(String url, {String? fileName}) async {
|
|||
}
|
||||
}
|
||||
|
||||
// 다운로드 경로 설정
|
||||
Directory? directory;
|
||||
// 다운로드 경로 설정 (Pictures 폴더)
|
||||
final Directory directory;
|
||||
if (Platform.isAndroid) {
|
||||
directory = Directory('/storage/emulated/0/Download');
|
||||
directory = Directory('/storage/emulated/0/Pictures/fromis_9');
|
||||
if (!await directory.exists()) {
|
||||
directory = await getExternalStorageDirectory();
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
} else {
|
||||
directory = await getApplicationDocumentsDirectory();
|
||||
}
|
||||
|
||||
if (directory == null) return null;
|
||||
|
||||
// 파일명 생성
|
||||
final name = fileName ?? 'fromis9_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
// 파일명 생성 (URL에서 확장자 추출)
|
||||
final extension = _getExtensionFromUrl(url);
|
||||
final name = fileName ?? 'fromis9_${DateTime.now().millisecondsSinceEpoch}$extension';
|
||||
|
||||
// 다운로드 시작
|
||||
final taskId = await FlutterDownloader.enqueue(
|
||||
|
|
@ -47,6 +71,7 @@ Future<String?> downloadImage(String url, {String? fileName}) async {
|
|||
fileName: name,
|
||||
showNotification: true,
|
||||
openFileFromNotification: true,
|
||||
saveInPublicStorage: true, // 갤러리에 표시
|
||||
);
|
||||
|
||||
return taskId;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/// 앨범 상세 화면
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -13,8 +12,6 @@ import 'package:photo_view/photo_view_gallery.dart';
|
|||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../../core/constants.dart';
|
||||
import '../../models/album.dart';
|
||||
import '../../services/albums_service.dart';
|
||||
|
|
@ -481,70 +478,16 @@ class _TeaserSection extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// 티저 썸네일 (동영상의 경우 video_thumbnail으로 1초 프레임 추출)
|
||||
class _TeaserThumbnail extends StatefulWidget {
|
||||
/// 티저 썸네일
|
||||
class _TeaserThumbnail extends StatelessWidget {
|
||||
final Teaser teaser;
|
||||
|
||||
const _TeaserThumbnail({required this.teaser});
|
||||
|
||||
@override
|
||||
State<_TeaserThumbnail> createState() => _TeaserThumbnailState();
|
||||
}
|
||||
|
||||
class _TeaserThumbnailState extends State<_TeaserThumbnail> {
|
||||
String? _thumbnailPath;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.teaser.mediaType == 'video' && widget.teaser.thumbUrl == null) {
|
||||
_extractThumbnail();
|
||||
} else {
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _extractThumbnail() async {
|
||||
if (widget.teaser.originalUrl == null) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 임시 디렉토리 경로 획득
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
|
||||
// 썸네일 파일 생성
|
||||
final thumbnailPath = await VideoThumbnail.thumbnailFile(
|
||||
video: widget.teaser.originalUrl!,
|
||||
thumbnailPath: tempDir.path,
|
||||
imageFormat: ImageFormat.JPEG,
|
||||
maxHeight: 200,
|
||||
quality: 75,
|
||||
timeMs: 1000, // 1초 위치
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_thumbnailPath = thumbnailPath;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 썸네일 추출 실패 - 플레이스홀더 표시
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final teaser = widget.teaser;
|
||||
final isVideo = teaser.mediaType == 'video';
|
||||
final imageUrl = teaser.thumbUrl ?? teaser.originalUrl;
|
||||
|
||||
return Container(
|
||||
width: 96,
|
||||
|
|
@ -558,8 +501,16 @@ class _TeaserThumbnailState extends State<_TeaserThumbnail> {
|
|||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 이미지 또는 동영상 썸네일
|
||||
_buildThumbnail(),
|
||||
// 썸네일 이미지
|
||||
if (imageUrl != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(color: AppColors.divider),
|
||||
errorWidget: (context, url, error) => _buildPlaceholder(),
|
||||
)
|
||||
else
|
||||
_buildPlaceholder(),
|
||||
// 동영상 재생 버튼 오버레이
|
||||
if (isVideo)
|
||||
Container(
|
||||
|
|
@ -582,54 +533,6 @@ class _TeaserThumbnailState extends State<_TeaserThumbnail> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildThumbnail() {
|
||||
final teaser = widget.teaser;
|
||||
final isVideo = teaser.mediaType == 'video';
|
||||
|
||||
// 이미지이거나 thumbUrl이 있는 경우
|
||||
if (!isVideo || teaser.thumbUrl != null) {
|
||||
final imageUrl = teaser.thumbUrl ?? teaser.originalUrl;
|
||||
if (imageUrl != null) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(color: AppColors.divider),
|
||||
errorWidget: (context, url, error) => _buildPlaceholder(),
|
||||
);
|
||||
}
|
||||
return _buildPlaceholder();
|
||||
}
|
||||
|
||||
// 동영상 썸네일 로딩 중
|
||||
if (_isLoading) {
|
||||
return Container(
|
||||
color: AppColors.divider,
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 동영상 썸네일 추출 성공
|
||||
if (_thumbnailPath != null) {
|
||||
return Image.file(
|
||||
File(_thumbnailPath!),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => _buildPlaceholder(),
|
||||
);
|
||||
}
|
||||
|
||||
// 에러 발생 시 플레이스홀더
|
||||
return _buildPlaceholder();
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder() {
|
||||
return Container(
|
||||
color: AppColors.divider,
|
||||
|
|
@ -959,14 +862,14 @@ class _TeaserViewerState extends State<_TeaserViewer> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
/// 이미지 다운로드 (시스템 다운로드 매니저 사용)
|
||||
Future<void> _downloadImage() async {
|
||||
/// 다운로드 (이미지 또는 동영상)
|
||||
Future<void> _download() async {
|
||||
final teaser = widget.teasers[_currentIndex];
|
||||
if (teaser.mediaType == 'video') return; // 동영상은 다운로드 안함
|
||||
final imageUrl = teaser.originalUrl;
|
||||
if (imageUrl == null || imageUrl.isEmpty) return;
|
||||
final isVideo = teaser.mediaType == 'video';
|
||||
final url = isVideo ? (teaser.videoUrl ?? teaser.originalUrl) : teaser.originalUrl;
|
||||
if (url == null || url.isEmpty) return;
|
||||
|
||||
final taskId = await downloadImage(imageUrl);
|
||||
final taskId = await downloadImage(url);
|
||||
if (taskId != null && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
|
|
@ -981,8 +884,6 @@ class _TeaserViewerState extends State<_TeaserViewer> {
|
|||
Widget build(BuildContext context) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
final currentTeaser = widget.teasers[_currentIndex];
|
||||
final isVideo = currentTeaser.mediaType == 'video';
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light,
|
||||
|
|
@ -1072,14 +973,12 @@ class _TeaserViewerState extends State<_TeaserViewer> {
|
|||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
// 오른쪽: 다운로드 버튼 (이미지만)
|
||||
// 오른쪽: 다운로드 버튼
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: isVideo
|
||||
? const SizedBox(width: 30)
|
||||
: GestureDetector(
|
||||
onTap: _downloadImage,
|
||||
child: GestureDetector(
|
||||
onTap: _download,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
|
|
|
|||
|
|
@ -205,7 +205,6 @@ class _HomeViewState extends State<HomeView> with TickerProviderStateMixin {
|
|||
_buildMembersSection(),
|
||||
_buildAlbumsSection(),
|
||||
_buildSchedulesSection(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -981,14 +981,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
video_thumbnail:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_thumbnail
|
||||
sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.6"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ dependencies:
|
|||
flutter_downloader: ^1.11.8
|
||||
permission_handler: ^11.3.1
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
video_thumbnail: ^0.5.3
|
||||
video_player: ^2.9.2
|
||||
chewie: ^1.8.5
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue