포스트

chatgpt로 simple flutter code 만들기(13) - 업데이트 진행 및 에로사항

  1. 유투브 썸네일, 블로그 썸네일 등을 자동으로 완성해주는 기능은 완성함.

loadImage 위젯의 loadImageByType()함수를 수정해서

기존의 file디렉토리 내 이미지 로딩과 asset이미지 로딩 외에도 network이미지 로딩 로직을 추가했다.

그래서 이미지를 reformat, resize, save하는 단계를 거치지 않고도

network의 image url을 그대로 사용하여 이미지를 화면 상에 (원하는 사이즈로) 로드할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Widget loadImage(WidgetRef ref, String imagePath,
    {double? width,
    double? height,
    bool fullScreen = false,
    Function? onFail}) {
  BoxFit fit = fullScreen ? BoxFit.fitWidth : BoxFit.cover;

  // Function to load the default image
  Image loadDefaultImage() {
    // Call the callback function if provided
    onFail?.call();
    printDebug("Resetting to default image");
    // Return the default image if the file does not exist
    return Image.asset('assets/images/default.ico',
        width: width, height: height, fit: fit);
  }

  Image loadImageByType() {
    // Check if the imagePath is a network URL
    if (Uri.parse(imagePath).isAbsolute && !imagePath.startsWith('file://')) {
      // Load image from the network
      return Image.network(
        imagePath,
        width: width,
        height: height,
        fit: fit,
        errorBuilder: (context, error, stackTrace) {
          printDebug("Failed to load network image: $error");
          return loadDefaultImage();
        },
      );
    } else if (imagePath.startsWith('assets/')) {
      // Load image from assets
      return Image.asset(imagePath, width: width, height: height, fit: fit);
    } else {
      // Load image from file system
      File imageFile = File(imagePath);
      if (imageFile.existsSync() && imageFile.lengthSync() > 0) {
        return Image.file(imageFile, width: width, height: height, fit: fit,
            errorBuilder: (context, error, stackTrace) {
              printDebug("Failed to load file image: $error");
              return loadDefaultImage();
            });
      } else {
        printDebug("File does not exist: $imagePath");
        return loadDefaultImage();
      }
    }
  }


  // Get the state
  final imageProcessingState = ref.watch(imageProcessingProvider);

  if (imageProcessingState.imagePath == imagePath) {
    // If the image is being processed, show a progress indicator
    return Center(child: CircularProgressIndicator());
  } else {
    // Otherwise, load the image
    return loadImageByType();
  }
}

그리고 사이트 url을 공유받을 때 콜하는 _handleSharedText()함수에서,

기존에는 유투브 영상인지만 판단해서 특별하게 처리했다면

이제는 보편적인 홈페이지의 썸네일을 뽑아내는 함수인 fetchThumbnailUrl()함수를 추가했다.

이 함수를 거치면

홈페이지의 썸네일이 존재한다면 곧잘 이미지를 뽑아낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Improved function to handle shared URLs and YouTube videos
  void _handleSharedText(String sharedUrl) async {
    if (_instance != null) {
      String title = await fetchPageTitle(sharedUrl);
      String? thumbnailUrl;

      if (sharedUrl.contains("youtube.com") || sharedUrl.contains("youtu.be")) {
        thumbnailUrl = await fetchYouTubeThumbnail(sharedUrl);
        _instance!._showSharedYouTubeContentDialog(sharedUrl, title, thumbnailUrl ?? '');
      } else {
        thumbnailUrl = await fetchThumbnailUrl(sharedUrl);
        _instance!._showSharedContentDialog(sharedUrl, title, thumbnailUrl ?? '');
      }
    }
  }

// Function to fetch YouTube video thumbnail with fallback
  Future fetchYouTubeThumbnail(String url) async {
    try {
      String videoId = extractYouTubeId(url);
      if (videoId.isEmpty) return null;
      String thumbnailUrl = 'https://img.youtube.com/vi/$videoId/maxresdefault.jpg';
      // Check if the URL is valid
      final response = await http.get(Uri.parse(thumbnailUrl));
      if (response.statusCode == 200) {
        return thumbnailUrl;
      } else {
        return 'https://img.youtube.com/vi/$videoId/default.jpg'; // Fallback thumbnail
      }
    } catch (e) {
      printDebug("Error fetching YouTube thumbnail: $e");
      return null;
    }
  }

// Function to fetch page thumbnail from URL
  Future fetchThumbnailUrl(String blogUrl) async {
    try {
      final response = await http.get(Uri.parse(blogUrl));

      if (response.statusCode == 200) {
        var document = parse(response.body);
        var metaTag = document.querySelector('meta[property="og:image"]');
        return metaTag?.attributes['content'];
      }
    } catch (e) {
      printDebug("Error fetching thumbnail: $e");
    }
    return null;
  }
// Function to extract YouTube video ID from URL
  String extractYouTubeId(String url) {
    RegExp regExp = RegExp(
      r"((?<&#x3D;(v|V)/)|(?<&#x3D;be/)|(?<&#x3D;(\?|\&)v&#x3D;)|(?<&#x3D;embed/))([\w-]+)",
      caseSensitive: false,
      multiLine: false,
    );
    Match? match &#x3D; regExp.firstMatch(url);
    return match?.group(0) ?? &#x27;&#x27;;
  }

  1. 이미지 공유를 할 때에 siteName에 관련한 자잘한 기능을 보완했고, pixel overflow도 해결하였음.

해결 방법은,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DropdownButtonFormField(
  isExpanded: true, // This ensures the dropdown matches the parent width
  value: selectedSiteInfo,
  decoration: InputDecoration(labelText: &#x27;Site Name&#x27;),
  items: drawerItems
      .firstWhere((drawer) &#x3D;> drawer.name &#x3D;&#x3D; selectedDrawerName)
      .sites
      .map((SiteInfo site) {
    return DropdownMenuItem(
      value: site,
      // Wrap the text with Tooltip and constrain the width
      child: Tooltip(
        message: site.siteName,
        child: Text(
          site.siteName,
          overflow: TextOverflow.ellipsis, // Use ellipsis for text overflow
        ),
      ),
    );
  }).toList(),
  onChanged: (SiteInfo? newValue) {
    setState(() {
      selectedSiteInfo &#x3D; newValue;
    });
  },
),

DropdownButtonFormField 위젯에서 “isExpanded: true” 를 설정해주면 됨.

그리고 text는 “overflow: TextOverflow.ellipsis” 를 설정하는 방법도 있지만,

나의 경우는

marquee라는 텍스트애니메이션을 사용했다. chatgpt가 알려주었다.

me: In siteName, make sure that letters that escape the box are hidden, and as the letters flow, you can see the last part of the long name if you wait.
chatgpt: To ensure that long site names in a DropdownButtonFormField don't overflow and instead scroll horizontally, allowing users to see the end of a long string, you can make use of the Ticker class and an AnimationController to create a marquee effect.

1
2
3
4
                          return DropdownMenuItem(
                            value: site,
                            child: MarqueeWidget(text: site.siteName), // Wrap text with MarqueeWidget
                          );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class MarqueeWidget extends StatefulWidget {
  final String text;
  final TextStyle? style;
  final Axis direction;
  final Duration animationDuration, backDuration, pauseDuration;

  MarqueeWidget({
    required this.text,
    this.style,
    this.direction &#x3D; Axis.horizontal,
    this.animationDuration &#x3D; const Duration(seconds: 2),
    this.backDuration &#x3D; const Duration(seconds: 3),
    this.pauseDuration &#x3D; const Duration(seconds: 1),
  });

  @override
  _MarqueeWidgetState createState() &#x3D;> _MarqueeWidgetState();
}

class _MarqueeWidgetState extends State {
  ScrollController scrollController &#x3D; ScrollController(initialScrollOffset: 0.0);

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (scrollController.hasClients) {
        scroll();
      }
    });
  }

  Future scroll() async {
    while (true) {
      await Future.delayed(widget.pauseDuration);
      if (scrollController.hasClients) {
        await scrollController.animateTo(
          scrollController.position.maxScrollExtent,
          duration: widget.animationDuration,
          curve: Curves.easeInOut,
        );
      }
      await Future.delayed(widget.pauseDuration);
      if (scrollController.hasClients) {
        await scrollController.animateTo(
          0.0,
          duration: widget.backDuration,
          curve: Curves.easeInOut,
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: widget.direction,
      controller: scrollController,
      child: Text(widget.text, style: widget.style),
    );
  }
}

[영상]


해결할 수 없는 문제.

몇 가지 사이트를 테스트해보던 중, 해결할 수 없는 부분을 발견했다.

바로 어느 특정 사이트에서는 크롬브라우저에서 공유하기를 통해 url을 뽑아낼 때, ** ** baseUrl만 내보내기가 가능했다. (삼성브라우저에서는 정상적으로 내보낸다)

이 부분은 나의 플러터 앱과는 전혀 상관이 없는 부분이고,

해당 홈페이지의 개발자가 홈페이지를 그런 방식으로 만들었기 때문에 이를 해결하기 위해서는 그 개발자한테 문의해서 고쳐야 한다고 한다.

참 곤란하다.

이런 경우까지 커버하려면 나의 즐겨찾기 앱도, '스크린 녹화'앱처럼 타 앱의 위에서 백그라운드로 돌아가는 플로팅 공유 버튼을 만들던가 해야할 것 같다.

하지만 거기까지 개발하는 것은 매우 지나친 오버인 것 같기 때문에 하지 않을 계획이다.

+추가 (20240112): 그런데 웹뷰로 그 플로팅 공유버튼을 만들고야 말았다. 웹뷰 패키지에 딱 좋응 버튼이 예제에 있길래 그 버튼에 공유기능 넣어서 구현했다..ㅎㅎ😅 단, 이 기능을 쓰려면 크롬같은 외부 브라우저가 아닌 인앱 웹뷰 브라우저로 브라우징을 해야한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.