WIP .nomedia 뒤 갤러리 다시 읽기 (관리자 지시 10/20, 판단 ② 바꿈): 넣을 때 · 지울 때 모두 .nomedia 와 그 폴더를 MediaScannerConnection.scanFile 로 (MainActivity "mediaScan", AppShell.mediaScan - Windows 는 할 일 없음, 실패해도 조용히) · 안내 글 "반영되기까지 잠깐 걸릴 수 있습니다" · 시험 (넣고 지울 때 부름) (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_view
RyuJongJin 2026-10-20 22:06:55 +09:00
parent fc0f6316f9
commit 52b3f7e4d1
9 changed files with 43 additions and 5 deletions

View File

@ -4,6 +4,7 @@ import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
@ -165,6 +166,14 @@ class MainActivity : FlutterActivity() {
Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
)
"openFolder" -> result.success(openFolder(call.argument<String>("path") ?: ""))
// X-plore 맞추기 (관리자 지시 10/20): .nomedia 를 넣고 지운 뒤 갤러리 · 음악 앱이 다시 읽게 (실패해도 조용히)
"mediaScan" -> result.success(try {
val paths = call.argument<List<String>>("paths") ?: emptyList()
if (paths.isNotEmpty()) MediaScannerConnection.scanFile(applicationContext, paths.toTypedArray(), null, null)
true
} catch (e: Exception) {
false
})
// 예전 버전으로 되돌리기: Android 는 낮은 버전을 위에 설치하지 못해 앱을 지우는 확인 창을 연다
"uninstallSelf" -> {
startActivity(Intent(Intent.ACTION_DELETE, Uri.parse("package:$packageName")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))

View File

@ -2744,7 +2744,7 @@
"폴더 수정": "Edit folder",
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "Shows the total size of the files inside each folder. Large folders or a NAS can take a while to read. Long-press [Calculate folder size] always works",
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "Syncs the folder contents (no same-named folder is created in the target).",
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 시간이 걸리거나 그 앱을 다시 열어야 할 수 있습니다.": "Files in the folder are not changed. It may take a while to show in the gallery, or you may need to reopen that app.",
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.": "Files in the folder are not changed. It may take a moment to show in the gallery.",
"폴더 열기": "Open folder",
"폴더 이름": "Folder name",
"폴더 이름 바꾸기 · 이동": "Rename · move folder",

View File

@ -2744,7 +2744,7 @@
"폴더 수정": "フォルダーを編集",
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "フォルダー内のファイルサイズの合計を一覧に表示します。大きなフォルダー・NAS は読み込みに時間がかかることがあります。長押しの [フォルダーサイズを計算] はいつでも使えます",
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "フォルダーの中身を合わせます (対象に同じ名前のフォルダーは作りません)。",
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 시간이 걸리거나 그 앱을 다시 열어야 할 수 있습니다.": "フォルダー内のファイルは変更しません。ギャラリーに反映されるまで時間がかかったり、そのアプリを開き直す必要がある場合があります。",
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.": "フォルダー内のファイルは変更しません。ギャラリーに反映されるまで少しかかる場合があります。",
"폴더 열기": "フォルダーを開く",
"폴더 이름": "フォルダー名",
"폴더 이름 바꾸기 · 이동": "フォルダー名の変更・移動",

View File

@ -2744,7 +2744,7 @@
"폴더 수정": "编辑文件夹",
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "在列表中显示文件夹内文件大小的总和。大型文件夹或 NAS 可能需要较长时间读取。长按 [计算文件夹大小] 始终可用",
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "同步文件夹内的内容 (不会在目标中新建同名文件夹)。",
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 시간이 걸리거나 그 앱을 다시 열어야 할 수 있습니다.": "不会更改文件夹中的文件。图库可能需要一段时间才能反映,或需要重新打开该应用。",
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.": "不会更改文件夹中的文件。图库可能需要片刻才能反映。",
"폴더 열기": "打开文件夹",
"폴더 이름": "文件夹名称",
"폴더 이름 바꾸기 · 이동": "重命名 · 移动文件夹",

View File

@ -52,6 +52,15 @@ class AndroidShell extends NoopShell {
} catch (_) {}
}
/// 미디어 목록 다시 읽기 (MainActivity "mediaScan" - MediaScannerConnection.scanFile). 실패해도 조용히
@override
Future<void> mediaScan(List<String> paths) async {
if (paths.isEmpty) return;
try {
await _ch.invokeMethod<bool>('mediaScan', {'paths': paths});
} catch (_) {}
}
/// 다른 앱으로 열기 (MainActivity "openWith"): [choose] 면 늘 앱 고르기 창, 아니면 기본 앱 (없으면 고르기 창)
@override
Future<bool> openWith(String path, {bool choose = false}) async {

View File

@ -376,6 +376,10 @@ class DesktopShell with WindowListener implements AppShell {
@override
Future<bool> shareFiles(List<String> paths) async => false;
/// Windows 는 갤러리 미디어 목록이 없다 (.nomedia 는 Android 만)
@override
Future<void> mediaScan(List<String> paths) async {}
/// 기본 연결 프로그램 (탐색기에서 두 번 누른 것과 같음) 또는 "연결 프로그램" 고르기 창
@override
Future<bool> openWith(String path, {bool choose = false}) async {

View File

@ -74,6 +74,10 @@ abstract class AppShell {
bool get canShare;
Future<bool> shareFiles(List<String> paths);
/// X-plore 맞추기 (관리자 지시 10/20): 바뀐 파일 · 폴더를 기기의 미디어 목록에 다시 읽게 한다 (Android MediaScanner -
/// .nomedia 를 넣고 지운 뒤 갤러리에 반영). 다른 기기는 할 일 없음. 실패해도 조용히 넘긴다
Future<void> mediaScan(List<String> paths);
/// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중)
List<String> installedBrowsers();
@ -147,6 +151,8 @@ class NoopShell implements AppShell {
@override
Future<bool> shareFiles(List<String> paths) async => false;
@override
Future<void> mediaScan(List<String> paths) async {}
@override
List<String> installedBrowsers() => const [];
@override
Future<List<String>> missingTools() async => const [];

View File

@ -4102,7 +4102,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
? tr('이 폴더에 빈 .nomedia 파일을 만듭니다. 갤러리 · 음악 앱이 이 폴더 (안의 폴더 포함) 의 그림 · 동영상 · 음악을 보이지 않게 됩니다.')
: tr('이 폴더의 .nomedia 파일을 지웁니다. 갤러리 · 음악 앱이 이 폴더의 그림 · 동영상 · 음악을 다시 보이게 됩니다.')),
const SizedBox(height: 6),
Text(tr('폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 시간이 걸리거나 그 앱을 다시 열어야 할 수 있습니다.'),
Text(tr('폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
actions: [
@ -4123,6 +4123,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
_snack(trf('바꾸지 못했습니다: {0}', [err]));
return;
}
// 관리자 지시 10/20: 갤러리가 바로 알도록 .nomedia 와 그 폴더를 다시 읽게 (넣을 때 · 지울 때 모두, 실패해도 조용히)
await c.services.shell.mediaScan([f.path, dir]);
await _refreshAll([dir]);
pane.changed();
_snack(on ? tr('갤러리 · 음악 앱에서 숨겼습니다 (.nomedia). 파일은 그대로입니다.') : tr('.nomedia 를 지웠습니다. 갤러리 · 음악 앱에 다시 보입니다.'));

View File

@ -34,6 +34,9 @@ import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
class _Shell extends NoopShell {
final opened = <(String, bool)>[];
final scanned = <List<String>>[];
@override
Future<void> mediaScan(List<String> paths) async => scanned.add(paths);
@override
Future<bool> openWith(String path, {bool choose = false}) async {
opened.add((path, choose));
@ -1767,8 +1770,11 @@ void main() {
await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)')));
await calm(tester);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기')));
await settle(tester, () => mark.existsSync());
await settle(tester, () => mark.existsSync() && shell.scanned.isNotEmpty);
expect(mark.lengthSync(), 0);
expect(shell.scanned, [
[mark.path, p.join(left, 'sub')]
], reason: '갤러리가 다시 읽게 (MediaScanner)');
expect(File(p.join(left, 'sub', 'inner.txt')).readAsStringSync(), 'inner', reason: '안의 파일은 그대로');
// 이제 메뉴는 [다시 보이기]
await idle(tester);
@ -1780,6 +1786,8 @@ void main() {
expect(find.textContaining('.nomedia 파일을 지웁니다'), findsOneWidget);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '다시 보이기')));
await settle(tester, () => !mark.existsSync());
await settle(tester, () => shell.scanned.length == 2);
expect(shell.scanned.last, [mark.path, p.join(left, 'sub')], reason: '지울 때도 다시 읽게');
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue);
// 파일에는 없음
await idle(tester);