flet-media-scanner 0.86.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: flet-media-scanner
3
+ Version: 0.86.0
4
+ Summary: Flet extension for Android MediaStore — scan, save, list, and delete videos in the Gallery
5
+ Author-email: Fazi-Gondal <nextinpk@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fazi-gondal/Vidsaver
8
+ Project-URL: Repository, https://github.com/fazi-gondal/Vidsaver
9
+ Project-URL: Bug Tracker, https://github.com/fazi-gondal/Vidsaver/issues
10
+ Keywords: flet,android,media,mediascanner,gallery,flutter
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: flet>=0.86.0
21
+
22
+ # flet-media-scanner
23
+
24
+ A [Flet](https://flet.dev) extension for Android **MediaStore** integration — save, list, and delete videos in the device Gallery without requiring broad storage permissions.
25
+
26
+ ## Requirements
27
+
28
+ - **Platform**: Android only (no-op on other platforms)
29
+ - **Android**: API 23+ (minSdk 23)
30
+ - **Flet**: >= 0.86.0
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install flet-media-scanner
36
+ ```
37
+
38
+ ## Setup
39
+
40
+ Register the extension in your `pyproject.toml`:
41
+
42
+ ```toml
43
+ [tool.flet.extensions]
44
+ flet_media_scanner = "flet_media_scanner.Extension"
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ import flet as ft
51
+ from flet_media_scanner import MediaScanner, SaveResult
52
+
53
+ async def main(page: ft.Page):
54
+ scanner = MediaScanner()
55
+ page.services.append(scanner)
56
+ page.update()
57
+
58
+ # Save a video to the Gallery (Movies/MyApp folder)
59
+ result: SaveResult = await scanner.save_video(
60
+ "/data/user/0/com.example.app/cache/video.mp4",
61
+ file_name="my_video.mp4",
62
+ album="MyApp",
63
+ )
64
+ if result.success:
65
+ print(f"Saved: {result.content_uri}")
66
+ else:
67
+ print(f"Error: {result.error}")
68
+
69
+ # List videos in the album
70
+ videos = await scanner.list_videos(album="MyApp")
71
+ for v in videos:
72
+ print(v["display_name"], v["content_uri"])
73
+
74
+ # Delete a video by content URI
75
+ deleted = await scanner.delete_video("content://media/external/video/media/123")
76
+ print("Deleted:", deleted)
77
+
78
+ ft.run(main)
79
+ ```
80
+
81
+ ## API
82
+
83
+ ### `MediaScanner` (Service)
84
+
85
+ Add to `page.services` before calling any methods.
86
+
87
+ #### `await save_video(file_path, file_name=None, album="MyApp") → SaveResult`
88
+
89
+ Copies a private app file into Android MediaStore (`Movies/<album>`). Works on Android 10+ without broad storage permissions.
90
+
91
+ | Parameter | Type | Description |
92
+ |---|---|---|
93
+ | `file_path` | `str` | Absolute path to the source video file |
94
+ | `file_name` | `str \| None` | Display name in Gallery (defaults to basename) |
95
+ | `album` | `str` | Subfolder inside `Movies/` |
96
+
97
+ #### `await list_videos(album="MyApp") → list[dict]`
98
+
99
+ Returns all videos previously saved to the album. Each dict contains: `display_name`, `content_uri`, `mime_type`, `relative_path`, `size`, `date_added`.
100
+
101
+ #### `await delete_video(content_uri) → bool`
102
+
103
+ Deletes a MediaStore item by its `content://` URI.
104
+
105
+ #### `await scan_media(file_path) → bool`
106
+
107
+ *(Legacy)* Triggers a media scan for a file that already exists in public storage. Use `save_video()` for new downloads instead.
108
+
109
+ ---
110
+
111
+ ### `SaveResult` (dataclass)
112
+
113
+ | Field | Type | Description |
114
+ |---|---|---|
115
+ | `success` | `bool` | Whether the operation succeeded |
116
+ | `content_uri` | `str` | MediaStore `content://` URI |
117
+ | `display_name` | `str` | File name as shown in Gallery |
118
+ | `mime_type` | `str` | e.g. `video/mp4` |
119
+ | `relative_path` | `str` | e.g. `Movies/MyApp/` |
120
+ | `size` | `int` | File size in bytes |
121
+ | `error` | `str` | Error message if `success=False` |
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,104 @@
1
+ # flet-media-scanner
2
+
3
+ A [Flet](https://flet.dev) extension for Android **MediaStore** integration — save, list, and delete videos in the device Gallery without requiring broad storage permissions.
4
+
5
+ ## Requirements
6
+
7
+ - **Platform**: Android only (no-op on other platforms)
8
+ - **Android**: API 23+ (minSdk 23)
9
+ - **Flet**: >= 0.86.0
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install flet-media-scanner
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ Register the extension in your `pyproject.toml`:
20
+
21
+ ```toml
22
+ [tool.flet.extensions]
23
+ flet_media_scanner = "flet_media_scanner.Extension"
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ import flet as ft
30
+ from flet_media_scanner import MediaScanner, SaveResult
31
+
32
+ async def main(page: ft.Page):
33
+ scanner = MediaScanner()
34
+ page.services.append(scanner)
35
+ page.update()
36
+
37
+ # Save a video to the Gallery (Movies/MyApp folder)
38
+ result: SaveResult = await scanner.save_video(
39
+ "/data/user/0/com.example.app/cache/video.mp4",
40
+ file_name="my_video.mp4",
41
+ album="MyApp",
42
+ )
43
+ if result.success:
44
+ print(f"Saved: {result.content_uri}")
45
+ else:
46
+ print(f"Error: {result.error}")
47
+
48
+ # List videos in the album
49
+ videos = await scanner.list_videos(album="MyApp")
50
+ for v in videos:
51
+ print(v["display_name"], v["content_uri"])
52
+
53
+ # Delete a video by content URI
54
+ deleted = await scanner.delete_video("content://media/external/video/media/123")
55
+ print("Deleted:", deleted)
56
+
57
+ ft.run(main)
58
+ ```
59
+
60
+ ## API
61
+
62
+ ### `MediaScanner` (Service)
63
+
64
+ Add to `page.services` before calling any methods.
65
+
66
+ #### `await save_video(file_path, file_name=None, album="MyApp") → SaveResult`
67
+
68
+ Copies a private app file into Android MediaStore (`Movies/<album>`). Works on Android 10+ without broad storage permissions.
69
+
70
+ | Parameter | Type | Description |
71
+ |---|---|---|
72
+ | `file_path` | `str` | Absolute path to the source video file |
73
+ | `file_name` | `str \| None` | Display name in Gallery (defaults to basename) |
74
+ | `album` | `str` | Subfolder inside `Movies/` |
75
+
76
+ #### `await list_videos(album="MyApp") → list[dict]`
77
+
78
+ Returns all videos previously saved to the album. Each dict contains: `display_name`, `content_uri`, `mime_type`, `relative_path`, `size`, `date_added`.
79
+
80
+ #### `await delete_video(content_uri) → bool`
81
+
82
+ Deletes a MediaStore item by its `content://` URI.
83
+
84
+ #### `await scan_media(file_path) → bool`
85
+
86
+ *(Legacy)* Triggers a media scan for a file that already exists in public storage. Use `save_video()` for new downloads instead.
87
+
88
+ ---
89
+
90
+ ### `SaveResult` (dataclass)
91
+
92
+ | Field | Type | Description |
93
+ |---|---|---|
94
+ | `success` | `bool` | Whether the operation succeeded |
95
+ | `content_uri` | `str` | MediaStore `content://` URI |
96
+ | `display_name` | `str` | File name as shown in Gallery |
97
+ | `mime_type` | `str` | e.g. `video/mp4` |
98
+ | `relative_path` | `str` | e.g. `Movies/MyApp/` |
99
+ | `size` | `int` | File size in bytes |
100
+ | `error` | `str` | Error message if `success=False` |
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "flet-media-scanner"
3
+ version = "0.86.0"
4
+ description = "Flet extension for Android MediaStore — scan, save, list, and delete videos in the Gallery"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.12"
8
+ authors = [
9
+ { name = "Fazi-Gondal", email = "nextinpk@gmail.com" }
10
+ ]
11
+ keywords = ["flet", "android", "media", "mediascanner", "gallery", "flutter"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ dependencies = [
22
+ "flet>=0.86.0"
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/fazi-gondal/Vidsaver"
27
+ Repository = "https://github.com/fazi-gondal/Vidsaver"
28
+ "Bug Tracker" = "https://github.com/fazi-gondal/Vidsaver/issues"
29
+
30
+ [build-system]
31
+ requires = ["setuptools"]
32
+ build-backend = "setuptools.build_meta"
33
+
34
+ [tool.setuptools]
35
+ package-dir = {"" = "src"}
36
+ packages = ["flet_media_scanner", "flutter.flet_media_scanner"]
37
+
38
+ [tool.setuptools.package-data]
39
+ "flutter.flet_media_scanner" = ["**/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from flet_media_scanner.media_scanner import MediaScanner, SaveResult
2
+
3
+ __all__ = ["MediaScanner", "SaveResult"]
@@ -0,0 +1,124 @@
1
+ from dataclasses import dataclass
2
+ import json
3
+ import os
4
+ from typing import Any, Optional
5
+
6
+ import flet as ft
7
+
8
+
9
+ @dataclass
10
+ class SaveResult:
11
+ success: bool = False
12
+ content_uri: str = ""
13
+ display_name: str = ""
14
+ mime_type: str = ""
15
+ relative_path: str = ""
16
+ source_path: str = ""
17
+ size: int = 0
18
+ error: str = ""
19
+
20
+
21
+ @ft.control("MediaScanner")
22
+ class MediaScanner(ft.Service):
23
+ """
24
+ Android media service for publishing videos through MediaStore.
25
+
26
+ New downloads should use save_video(). scan_media() is kept only for
27
+ legacy files that already exist in public storage and need Gallery indexing.
28
+ """
29
+
30
+ on_saved: Optional[ft.EventHandler[Any]] = None
31
+ on_scanned: Optional[ft.EventHandler[Any]] = None
32
+
33
+ async def save_video(
34
+ self,
35
+ file_path: str,
36
+ file_name: str | None = None,
37
+ album: str = "Vidsaver",
38
+ ) -> SaveResult:
39
+ """
40
+ Copy an app-private video into Android MediaStore.Video.
41
+
42
+ On Android 10+ this publishes to Movies/<album> without requiring
43
+ broad storage permissions or a media scan.
44
+ """
45
+ if not file_path or not os.path.exists(file_path):
46
+ return SaveResult(error=f"file does not exist: {file_path}")
47
+
48
+ try:
49
+ result = await self._invoke_method(
50
+ "save_video",
51
+ {
52
+ "path": file_path,
53
+ "file_name": file_name or os.path.basename(file_path),
54
+ "album": album,
55
+ },
56
+ timeout=60.0,
57
+ )
58
+ payload = json.loads(result) if result else {}
59
+ return SaveResult(
60
+ success=bool(payload.get("success")),
61
+ content_uri=str(payload.get("content_uri") or ""),
62
+ display_name=str(payload.get("display_name") or ""),
63
+ mime_type=str(payload.get("mime_type") or ""),
64
+ relative_path=str(payload.get("relative_path") or ""),
65
+ source_path=str(payload.get("source_path") or file_path),
66
+ size=int(payload.get("size") or 0),
67
+ error=str(payload.get("error") or ""),
68
+ )
69
+ except Exception as e:
70
+ return SaveResult(error=str(e), source_path=file_path)
71
+
72
+ async def scan_media(self, file_path: str) -> bool:
73
+ """
74
+ Scan a legacy public media file so it appears in Gallery/Photos.
75
+
76
+ New downloads should use save_video() instead; MediaStore inserts do
77
+ not need this scan.
78
+ """
79
+ if not file_path or not os.path.exists(file_path):
80
+ print(f"[MediaScanner] scan_media: file does not exist: {file_path}")
81
+ return False
82
+ try:
83
+ result = await self._invoke_method(
84
+ "scan_media",
85
+ {"path": file_path},
86
+ timeout=15.0,
87
+ )
88
+ success = result == "true"
89
+ print(f"[MediaScanner] scan_media: {file_path} -> result={result} success={success}")
90
+ return success
91
+ except Exception as e:
92
+ print(f"[MediaScanner] scan_media error: {file_path}: {e}")
93
+ return False
94
+
95
+ async def delete_video(self, content_uri: str) -> bool:
96
+ """Delete a MediaStore item by its content URI."""
97
+ if not content_uri:
98
+ return False
99
+ try:
100
+ result = await self._invoke_method(
101
+ "delete_video",
102
+ {"content_uri": content_uri},
103
+ timeout=15.0,
104
+ )
105
+ payload = json.loads(result) if result else {}
106
+ return bool(payload.get("success"))
107
+ except Exception as e:
108
+ print(f"[MediaScanner] delete_video error: {content_uri}: {e}")
109
+ return False
110
+
111
+ async def list_videos(self, album: str = "Vidsaver") -> list[dict]:
112
+ """List videos previously saved to the MediaStore album."""
113
+ try:
114
+ result = await self._invoke_method(
115
+ "list_videos",
116
+ {"album": album},
117
+ timeout=15.0,
118
+ )
119
+ payload = json.loads(result) if result else {}
120
+ videos = payload.get("videos") or []
121
+ return videos if isinstance(videos, list) else []
122
+ except Exception as e:
123
+ print(f"[MediaScanner] list_videos error: {e}")
124
+ return []
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: flet-media-scanner
3
+ Version: 0.86.0
4
+ Summary: Flet extension for Android MediaStore — scan, save, list, and delete videos in the Gallery
5
+ Author-email: Fazi-Gondal <nextinpk@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fazi-gondal/Vidsaver
8
+ Project-URL: Repository, https://github.com/fazi-gondal/Vidsaver
9
+ Project-URL: Bug Tracker, https://github.com/fazi-gondal/Vidsaver/issues
10
+ Keywords: flet,android,media,mediascanner,gallery,flutter
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: flet>=0.86.0
21
+
22
+ # flet-media-scanner
23
+
24
+ A [Flet](https://flet.dev) extension for Android **MediaStore** integration — save, list, and delete videos in the device Gallery without requiring broad storage permissions.
25
+
26
+ ## Requirements
27
+
28
+ - **Platform**: Android only (no-op on other platforms)
29
+ - **Android**: API 23+ (minSdk 23)
30
+ - **Flet**: >= 0.86.0
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install flet-media-scanner
36
+ ```
37
+
38
+ ## Setup
39
+
40
+ Register the extension in your `pyproject.toml`:
41
+
42
+ ```toml
43
+ [tool.flet.extensions]
44
+ flet_media_scanner = "flet_media_scanner.Extension"
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ import flet as ft
51
+ from flet_media_scanner import MediaScanner, SaveResult
52
+
53
+ async def main(page: ft.Page):
54
+ scanner = MediaScanner()
55
+ page.services.append(scanner)
56
+ page.update()
57
+
58
+ # Save a video to the Gallery (Movies/MyApp folder)
59
+ result: SaveResult = await scanner.save_video(
60
+ "/data/user/0/com.example.app/cache/video.mp4",
61
+ file_name="my_video.mp4",
62
+ album="MyApp",
63
+ )
64
+ if result.success:
65
+ print(f"Saved: {result.content_uri}")
66
+ else:
67
+ print(f"Error: {result.error}")
68
+
69
+ # List videos in the album
70
+ videos = await scanner.list_videos(album="MyApp")
71
+ for v in videos:
72
+ print(v["display_name"], v["content_uri"])
73
+
74
+ # Delete a video by content URI
75
+ deleted = await scanner.delete_video("content://media/external/video/media/123")
76
+ print("Deleted:", deleted)
77
+
78
+ ft.run(main)
79
+ ```
80
+
81
+ ## API
82
+
83
+ ### `MediaScanner` (Service)
84
+
85
+ Add to `page.services` before calling any methods.
86
+
87
+ #### `await save_video(file_path, file_name=None, album="MyApp") → SaveResult`
88
+
89
+ Copies a private app file into Android MediaStore (`Movies/<album>`). Works on Android 10+ without broad storage permissions.
90
+
91
+ | Parameter | Type | Description |
92
+ |---|---|---|
93
+ | `file_path` | `str` | Absolute path to the source video file |
94
+ | `file_name` | `str \| None` | Display name in Gallery (defaults to basename) |
95
+ | `album` | `str` | Subfolder inside `Movies/` |
96
+
97
+ #### `await list_videos(album="MyApp") → list[dict]`
98
+
99
+ Returns all videos previously saved to the album. Each dict contains: `display_name`, `content_uri`, `mime_type`, `relative_path`, `size`, `date_added`.
100
+
101
+ #### `await delete_video(content_uri) → bool`
102
+
103
+ Deletes a MediaStore item by its `content://` URI.
104
+
105
+ #### `await scan_media(file_path) → bool`
106
+
107
+ *(Legacy)* Triggers a media scan for a file that already exists in public storage. Use `save_video()` for new downloads instead.
108
+
109
+ ---
110
+
111
+ ### `SaveResult` (dataclass)
112
+
113
+ | Field | Type | Description |
114
+ |---|---|---|
115
+ | `success` | `bool` | Whether the operation succeeded |
116
+ | `content_uri` | `str` | MediaStore `content://` URI |
117
+ | `display_name` | `str` | File name as shown in Gallery |
118
+ | `mime_type` | `str` | e.g. `video/mp4` |
119
+ | `relative_path` | `str` | e.g. `Movies/MyApp/` |
120
+ | `size` | `int` | File size in bytes |
121
+ | `error` | `str` | Error message if `success=False` |
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/flet_media_scanner/__init__.py
4
+ src/flet_media_scanner/media_scanner.py
5
+ src/flet_media_scanner.egg-info/PKG-INFO
6
+ src/flet_media_scanner.egg-info/SOURCES.txt
7
+ src/flet_media_scanner.egg-info/dependency_links.txt
8
+ src/flet_media_scanner.egg-info/requires.txt
9
+ src/flet_media_scanner.egg-info/top_level.txt
10
+ src/flutter/flet_media_scanner/pubspec.yaml
11
+ src/flutter/flet_media_scanner/android/build.gradle
12
+ src/flutter/flet_media_scanner/android/src/main/AndroidManifest.xml
13
+ src/flutter/flet_media_scanner/android/src/main/kotlin/com/example/flet_media_scanner/FletMediaScannerPlugin.kt
14
+ src/flutter/flet_media_scanner/lib/flet_media_scanner.dart
15
+ src/flutter/flet_media_scanner/lib/src/extension.dart
16
+ src/flutter/flet_media_scanner/lib/src/media_scanner_service.dart
@@ -0,0 +1,2 @@
1
+ flet_media_scanner
2
+ flutter
@@ -0,0 +1,46 @@
1
+ group 'com.example.flet_media_scanner'
2
+ version '1.0'
3
+
4
+ buildscript {
5
+ ext.kotlin_version = '1.8.22'
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+ dependencies {
11
+ classpath 'com.android.tools.build:gradle:7.3.0'
12
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13
+ }
14
+ }
15
+
16
+ allprojects {
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+ }
22
+
23
+ apply plugin: 'com.android.library'
24
+ apply plugin: 'kotlin-android'
25
+
26
+ android {
27
+ namespace "com.example.flet_media_scanner"
28
+ compileSdkVersion 33
29
+
30
+ compileOptions {
31
+ sourceCompatibility JavaVersion.VERSION_1_8
32
+ targetCompatibility JavaVersion.VERSION_1_8
33
+ }
34
+
35
+ kotlinOptions {
36
+ jvmTarget = '1.8'
37
+ }
38
+
39
+ defaultConfig {
40
+ minSdkVersion 21
41
+ }
42
+ }
43
+
44
+ dependencies {
45
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
46
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,252 @@
1
+ package com.example.flet_media_scanner
2
+
3
+ import android.content.ContentUris
4
+ import android.content.ContentValues
5
+ import android.content.Context
6
+ import android.net.Uri
7
+ import android.os.Build
8
+ import android.os.Environment
9
+ import android.provider.MediaStore
10
+ import android.util.Log
11
+ import io.flutter.embedding.engine.plugins.FlutterPlugin
12
+ import io.flutter.plugin.common.MethodCall
13
+ import io.flutter.plugin.common.MethodChannel
14
+ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
15
+ import io.flutter.plugin.common.MethodChannel.Result
16
+ import java.io.File
17
+ import java.io.FileInputStream
18
+ import java.net.URLConnection
19
+
20
+ class FletMediaScannerPlugin : FlutterPlugin, MethodCallHandler {
21
+ private lateinit var channel: MethodChannel
22
+ private lateinit var context: Context
23
+
24
+ companion object {
25
+ private const val TAG = "FletMediaScanner"
26
+ private const val CHANNEL = "flet_media_scanner/scan"
27
+ }
28
+
29
+ override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
30
+ context = binding.applicationContext
31
+ channel = MethodChannel(binding.binaryMessenger, CHANNEL)
32
+ channel.setMethodCallHandler(this)
33
+ Log.d(TAG, "onAttachedToEngine: channel registered")
34
+ }
35
+
36
+ override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
37
+ channel.setMethodCallHandler(null)
38
+ Log.d(TAG, "onDetachedFromEngine")
39
+ }
40
+
41
+ override fun onMethodCall(call: MethodCall, result: Result) {
42
+ when (call.method) {
43
+ "saveVideo" -> saveVideo(call, result)
44
+ "deleteVideo" -> deleteVideo(call, result)
45
+ "listVideos" -> listVideos(call, result)
46
+ else -> result.notImplemented()
47
+ }
48
+ }
49
+
50
+ private fun saveVideo(call: MethodCall, result: Result) {
51
+ val path = call.argument<String>("path")
52
+ val requestedFileName = call.argument<String>("fileName")
53
+ val album = call.argument<String>("album")
54
+ ?.trim()
55
+ ?.trim('/')
56
+ ?.takeIf { it.isNotBlank() }
57
+ ?: "Vidsaver"
58
+
59
+ if (path.isNullOrBlank()) {
60
+ result.error("INVALID_ARGUMENT", "path must not be null or empty", null)
61
+ return
62
+ }
63
+
64
+ val source = File(path)
65
+ if (!source.isFile) {
66
+ result.error("FILE_NOT_FOUND", "source file does not exist: $path", null)
67
+ return
68
+ }
69
+
70
+ val displayName = requestedFileName
71
+ ?.trim()
72
+ ?.takeIf { it.isNotBlank() }
73
+ ?: source.name
74
+ val mimeType = URLConnection.guessContentTypeFromName(displayName) ?: "video/mp4"
75
+ val relativePath = "${Environment.DIRECTORY_MOVIES}/$album"
76
+ val resolver = context.contentResolver
77
+ val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
78
+ MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
79
+ } else {
80
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
81
+ }
82
+
83
+ var uri: Uri? = null
84
+ try {
85
+ val values = ContentValues().apply {
86
+ put(MediaStore.Video.Media.DISPLAY_NAME, displayName)
87
+ put(MediaStore.Video.Media.MIME_TYPE, mimeType)
88
+ put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
89
+ put(MediaStore.Video.Media.DATE_MODIFIED, source.lastModified() / 1000)
90
+ put(MediaStore.Video.Media.SIZE, source.length())
91
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
92
+ put(MediaStore.Video.Media.RELATIVE_PATH, relativePath)
93
+ put(MediaStore.Video.Media.IS_PENDING, 1)
94
+ }
95
+ }
96
+
97
+ uri = resolver.insert(collection, values)
98
+ ?: throw IllegalStateException("MediaStore insert returned null")
99
+
100
+ resolver.openOutputStream(uri)?.use { output ->
101
+ FileInputStream(source).use { input ->
102
+ input.copyTo(output)
103
+ }
104
+ } ?: throw IllegalStateException("Unable to open MediaStore output stream")
105
+
106
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
107
+ val publishedValues = ContentValues().apply {
108
+ put(MediaStore.Video.Media.IS_PENDING, 0)
109
+ put(MediaStore.Video.Media.SIZE, source.length())
110
+ }
111
+ resolver.update(uri, publishedValues, null, null)
112
+ }
113
+
114
+ result.success(
115
+ mapOf(
116
+ "success" to true,
117
+ "content_uri" to uri.toString(),
118
+ "display_name" to displayName,
119
+ "mime_type" to mimeType,
120
+ "relative_path" to relativePath,
121
+ "source_path" to path,
122
+ "size" to source.length(),
123
+ )
124
+ )
125
+ } catch (e: Exception) {
126
+ Log.e(TAG, "saveVideo: exception: ${e.message}", e)
127
+ uri?.let {
128
+ try {
129
+ resolver.delete(it, null, null)
130
+ } catch (deleteError: Exception) {
131
+ Log.w(TAG, "saveVideo: failed to delete incomplete item: $deleteError")
132
+ }
133
+ }
134
+ result.error("SAVE_ERROR", e.message, e.toString())
135
+ }
136
+ }
137
+
138
+ private fun deleteVideo(call: MethodCall, result: Result) {
139
+ val contentUri = call.argument<String>("contentUri")
140
+ if (contentUri.isNullOrBlank()) {
141
+ result.error("INVALID_ARGUMENT", "contentUri must not be null or empty", null)
142
+ return
143
+ }
144
+
145
+ try {
146
+ val uri = Uri.parse(contentUri)
147
+ val deletedRows = context.contentResolver.delete(uri, null, null)
148
+ result.success(
149
+ mapOf(
150
+ "success" to (deletedRows > 0),
151
+ "content_uri" to contentUri,
152
+ "deleted_rows" to deletedRows,
153
+ )
154
+ )
155
+ } catch (e: Exception) {
156
+ Log.e(TAG, "deleteVideo: exception: ${e.message}", e)
157
+ result.error("DELETE_ERROR", e.message, e.toString())
158
+ }
159
+ }
160
+
161
+ private fun listVideos(call: MethodCall, result: Result) {
162
+ val album = call.argument<String>("album")
163
+ ?.trim()
164
+ ?.trim('/')
165
+ ?.takeIf { it.isNotBlank() }
166
+ ?: "Vidsaver"
167
+ val resolver = context.contentResolver
168
+ val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
169
+ MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
170
+ } else {
171
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
172
+ }
173
+
174
+ val projection = mutableListOf(
175
+ MediaStore.Video.Media._ID,
176
+ MediaStore.Video.Media.DISPLAY_NAME,
177
+ MediaStore.Video.Media.MIME_TYPE,
178
+ MediaStore.Video.Media.SIZE,
179
+ MediaStore.Video.Media.DATE_ADDED,
180
+ MediaStore.Video.Media.DATE_MODIFIED,
181
+ )
182
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
183
+ projection.add(MediaStore.Video.Media.RELATIVE_PATH)
184
+ }
185
+
186
+ val selection: String?
187
+ val selectionArgs: Array<String>?
188
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
189
+ selection = "${MediaStore.Video.Media.RELATIVE_PATH} LIKE ?"
190
+ selectionArgs = arrayOf("${Environment.DIRECTORY_MOVIES}/$album%")
191
+ } else {
192
+ selection = null
193
+ selectionArgs = null
194
+ }
195
+
196
+ val videos = mutableListOf<Map<String, Any?>>()
197
+ try {
198
+ resolver.query(
199
+ collection,
200
+ projection.toTypedArray(),
201
+ selection,
202
+ selectionArgs,
203
+ "${MediaStore.Video.Media.DATE_ADDED} DESC"
204
+ )?.use { cursor ->
205
+ val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
206
+ val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
207
+ val mimeIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE)
208
+ val sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
209
+ val addedIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)
210
+ val modifiedIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED)
211
+ val relativePathIndex = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
212
+ cursor.getColumnIndex(MediaStore.Video.Media.RELATIVE_PATH)
213
+ } else {
214
+ -1
215
+ }
216
+
217
+ while (cursor.moveToNext()) {
218
+ val id = cursor.getLong(idIndex)
219
+ val uri = ContentUris.withAppendedId(collection, id)
220
+ val displayName = cursor.getString(nameIndex) ?: continue
221
+ val relativePath = if (relativePathIndex >= 0) {
222
+ cursor.getString(relativePathIndex) ?: ""
223
+ } else {
224
+ "${Environment.DIRECTORY_MOVIES}/$album"
225
+ }
226
+
227
+ videos.add(
228
+ mapOf(
229
+ "content_uri" to uri.toString(),
230
+ "display_name" to displayName,
231
+ "mime_type" to (cursor.getString(mimeIndex) ?: "video/mp4"),
232
+ "relative_path" to relativePath.trimEnd('/'),
233
+ "size" to cursor.getLong(sizeIndex),
234
+ "date_added" to cursor.getLong(addedIndex),
235
+ "date_modified" to cursor.getLong(modifiedIndex),
236
+ )
237
+ )
238
+ }
239
+ }
240
+
241
+ result.success(
242
+ mapOf(
243
+ "success" to true,
244
+ "videos" to videos,
245
+ )
246
+ )
247
+ } catch (e: Exception) {
248
+ Log.e(TAG, "listVideos: exception: ${e.message}", e)
249
+ result.error("LIST_ERROR", e.message, e.toString())
250
+ }
251
+ }
252
+ }
@@ -0,0 +1,2 @@
1
+ library flet_media_scanner;
2
+ export 'src/extension.dart' show Extension;
@@ -0,0 +1,17 @@
1
+ import 'package:flet/flet.dart';
2
+ import 'media_scanner_service.dart';
3
+
4
+ class Extension extends FletExtension {
5
+ @override
6
+ void ensureInitialized() {}
7
+
8
+ @override
9
+ FletService? createService(Control control) {
10
+ switch (control.type) {
11
+ case "MediaScanner":
12
+ return MediaScannerService(control: control);
13
+ default:
14
+ return null;
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,186 @@
1
+ import 'dart:convert';
2
+
3
+ import 'package:flet/flet.dart';
4
+ import 'package:flutter/foundation.dart';
5
+ import 'package:flutter/services.dart';
6
+ import 'package:media_scanner/media_scanner.dart';
7
+
8
+ class MediaScannerService extends FletService {
9
+ static const MethodChannel _channel =
10
+ MethodChannel('flet_media_scanner/scan');
11
+
12
+ MediaScannerService({required super.control});
13
+
14
+ @override
15
+ void init() {
16
+ super.init();
17
+ control.addInvokeMethodListener(_onInvokeMethod);
18
+ debugPrint("MediaScannerService: initialized");
19
+ }
20
+
21
+ Future<dynamic> _onInvokeMethod(String methodName, dynamic args) async {
22
+ Map<String, dynamic> arguments = {};
23
+ if (args != null && args is Map) {
24
+ arguments = Map<String, dynamic>.from(args);
25
+ }
26
+
27
+ debugPrint(
28
+ "MediaScannerService._onInvokeMethod: $methodName args=$arguments");
29
+
30
+ return switch (methodName) {
31
+ "save_video" => await _saveVideo(arguments),
32
+ "delete_video" => await _deleteVideo(arguments),
33
+ "list_videos" => await _listVideos(arguments),
34
+ "scan_media" => await _scanMedia(arguments),
35
+ _ => throw Exception("MediaScannerService: unknown method '$methodName'"),
36
+ };
37
+ }
38
+
39
+ Future<String> _saveVideo(Map<String, dynamic> args) async {
40
+ final String? path = args["path"] as String?;
41
+ final String? fileName = args["file_name"] as String?;
42
+ final String album = args["album"] as String? ?? "Vidsaver";
43
+ if (path == null || path.isEmpty) {
44
+ return _encodeResult({
45
+ "success": false,
46
+ "error": "path is required",
47
+ });
48
+ }
49
+
50
+ try {
51
+ final result = await _channel.invokeMethod<Map<dynamic, dynamic>>(
52
+ 'saveVideo',
53
+ {
54
+ 'path': path,
55
+ 'fileName': fileName,
56
+ 'album': album,
57
+ },
58
+ );
59
+ final payload = Map<String, dynamic>.from(result ?? {});
60
+ control.triggerEvent("saved", payload);
61
+ return _encodeResult(payload);
62
+ } on PlatformException catch (e) {
63
+ final payload = {
64
+ "success": false,
65
+ "path": path,
66
+ "error": "${e.code}: ${e.message}",
67
+ };
68
+ control.triggerEvent("saved", payload);
69
+ return _encodeResult(payload);
70
+ } catch (error, stack) {
71
+ debugPrint("MediaScannerService._saveVideo: ERROR '$path': $error\n$stack");
72
+ final payload = {
73
+ "success": false,
74
+ "path": path,
75
+ "error": error.toString(),
76
+ };
77
+ control.triggerEvent("saved", payload);
78
+ return _encodeResult(payload);
79
+ }
80
+ }
81
+
82
+ Future<String> _scanMedia(Map<String, dynamic> args) async {
83
+ final String? path = args["path"] as String?;
84
+ if (path == null || path.isEmpty) {
85
+ debugPrint("MediaScannerService._scanMedia: path is null or empty");
86
+ control.triggerEvent(
87
+ "scanned", {"path": path ?? "", "success": "false", "error": "path is required"});
88
+ return "false";
89
+ }
90
+
91
+ debugPrint("MediaScannerService._scanMedia: scanning '$path'");
92
+
93
+ try {
94
+ await MediaScanner.loadMedia(path: path);
95
+ debugPrint(
96
+ "MediaScannerService._scanMedia: media_scanner completed path='$path'");
97
+ control.triggerEvent("scanned", {
98
+ "path": path,
99
+ "success": "true",
100
+ "result": "scanned",
101
+ });
102
+ return "true";
103
+ } catch (error, stack) {
104
+ debugPrint(
105
+ "MediaScannerService._scanMedia: ERROR '$path': $error\n$stack");
106
+ control.triggerEvent("scanned", {
107
+ "path": path,
108
+ "success": "false",
109
+ "error": error.toString(),
110
+ });
111
+ return "false";
112
+ }
113
+ }
114
+
115
+ Future<String> _deleteVideo(Map<String, dynamic> args) async {
116
+ final String? contentUri = args["content_uri"] as String?;
117
+ if (contentUri == null || contentUri.isEmpty) {
118
+ return _encodeResult({
119
+ "success": false,
120
+ "error": "content_uri is required",
121
+ });
122
+ }
123
+
124
+ try {
125
+ final result = await _channel.invokeMethod<Map<dynamic, dynamic>>(
126
+ 'deleteVideo',
127
+ {'contentUri': contentUri},
128
+ );
129
+ final payload = Map<String, dynamic>.from(result ?? {});
130
+ control.triggerEvent("deleted", payload);
131
+ return _encodeResult(payload);
132
+ } on PlatformException catch (e) {
133
+ final payload = {
134
+ "success": false,
135
+ "content_uri": contentUri,
136
+ "error": "${e.code}: ${e.message}",
137
+ };
138
+ control.triggerEvent("deleted", payload);
139
+ return _encodeResult(payload);
140
+ } catch (error, stack) {
141
+ debugPrint("MediaScannerService._deleteVideo: ERROR '$contentUri': $error\n$stack");
142
+ final payload = {
143
+ "success": false,
144
+ "content_uri": contentUri,
145
+ "error": error.toString(),
146
+ };
147
+ control.triggerEvent("deleted", payload);
148
+ return _encodeResult(payload);
149
+ }
150
+ }
151
+
152
+ Future<String> _listVideos(Map<String, dynamic> args) async {
153
+ final String album = args["album"] as String? ?? "Vidsaver";
154
+
155
+ try {
156
+ final result = await _channel.invokeMethod<Map<dynamic, dynamic>>(
157
+ 'listVideos',
158
+ {'album': album},
159
+ );
160
+ return _encodeResult(Map<String, dynamic>.from(result ?? {}));
161
+ } on PlatformException catch (e) {
162
+ return _encodeResult({
163
+ "success": false,
164
+ "videos": [],
165
+ "error": "${e.code}: ${e.message}",
166
+ });
167
+ } catch (error, stack) {
168
+ debugPrint("MediaScannerService._listVideos: ERROR '$album': $error\n$stack");
169
+ return _encodeResult({
170
+ "success": false,
171
+ "videos": [],
172
+ "error": error.toString(),
173
+ });
174
+ }
175
+ }
176
+
177
+ String _encodeResult(Map<String, dynamic> payload) {
178
+ return jsonEncode(payload);
179
+ }
180
+
181
+ @override
182
+ void dispose() {
183
+ debugPrint("MediaScannerService: disposed");
184
+ super.dispose();
185
+ }
186
+ }
@@ -0,0 +1,21 @@
1
+ name: flet_media_scanner
2
+ description: Flet extension for Android MediaStore saving with media scanner fallback.
3
+ version: 0.85.3
4
+ publish_to: none
5
+
6
+ environment:
7
+ sdk: ">=3.0.0 <4.0.0"
8
+ flutter: ">=3.0.0"
9
+
10
+ dependencies:
11
+ flutter:
12
+ sdk: flutter
13
+ flet: ^0.85.0
14
+ media_scanner: ^2.2.1
15
+
16
+ flutter:
17
+ plugin:
18
+ platforms:
19
+ android:
20
+ package: com.example.flet_media_scanner
21
+ pluginClass: FletMediaScannerPlugin