WIP .nomedia 뒤 갤러리 다시 읽기 (관리자 지시 10/20, 판단 ② 바꿈): 넣을 때 · 지울 때 모두 .nomedia 와 그 폴더를 MediaScannerConnection.scanFile 로 (MainActivity "mediaScan", AppShell.mediaScan - Windows 는 할 일 없음, 실패해도 조용히) · 안내 글 "반영되기까지 잠깐 걸릴 수 있습니다" · 시험 (넣고 지울 때 부름) (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_view
parent
fc0f6316f9
commit
52b3f7e4d1
|
|
@ -4,6 +4,7 @@ import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
|
@ -165,6 +166,14 @@ class MainActivity : FlutterActivity() {
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
|
||||||
)
|
)
|
||||||
"openFolder" -> result.success(openFolder(call.argument<String>("path") ?: ""))
|
"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 는 낮은 버전을 위에 설치하지 못해 앱을 지우는 확인 창을 연다
|
// 예전 버전으로 되돌리기: Android 는 낮은 버전을 위에 설치하지 못해 앱을 지우는 확인 창을 연다
|
||||||
"uninstallSelf" -> {
|
"uninstallSelf" -> {
|
||||||
startActivity(Intent(Intent.ACTION_DELETE, Uri.parse("package:$packageName")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
startActivity(Intent(Intent.ACTION_DELETE, Uri.parse("package:$packageName")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
|
|
||||||
|
|
@ -2744,7 +2744,7 @@
|
||||||
"폴더 수정": "Edit folder",
|
"폴더 수정": "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",
|
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · 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).",
|
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "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",
|
"폴더 열기": "Open folder",
|
||||||
"폴더 이름": "Folder name",
|
"폴더 이름": "Folder name",
|
||||||
"폴더 이름 바꾸기 · 이동": "Rename · move folder",
|
"폴더 이름 바꾸기 · 이동": "Rename · move folder",
|
||||||
|
|
|
||||||
|
|
@ -2744,7 +2744,7 @@
|
||||||
"폴더 수정": "フォルダーを編集",
|
"폴더 수정": "フォルダーを編集",
|
||||||
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "フォルダー内のファイルサイズの合計を一覧に表示します。大きなフォルダー・NAS は読み込みに時間がかかることがあります。長押しの [フォルダーサイズを計算] はいつでも使えます",
|
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "フォルダー内のファイルサイズの合計を一覧に表示します。大きなフォルダー・NAS は読み込みに時間がかかることがあります。長押しの [フォルダーサイズを計算] はいつでも使えます",
|
||||||
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "フォルダーの中身を合わせます (対象に同じ名前のフォルダーは作りません)。",
|
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "フォルダーの中身を合わせます (対象に同じ名前のフォルダーは作りません)。",
|
||||||
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 시간이 걸리거나 그 앱을 다시 열어야 할 수 있습니다.": "フォルダー内のファイルは変更しません。ギャラリーに反映されるまで時間がかかったり、そのアプリを開き直す必要がある場合があります。",
|
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.": "フォルダー内のファイルは変更しません。ギャラリーに反映されるまで少しかかる場合があります。",
|
||||||
"폴더 열기": "フォルダーを開く",
|
"폴더 열기": "フォルダーを開く",
|
||||||
"폴더 이름": "フォルダー名",
|
"폴더 이름": "フォルダー名",
|
||||||
"폴더 이름 바꾸기 · 이동": "フォルダー名の変更・移動",
|
"폴더 이름 바꾸기 · 이동": "フォルダー名の変更・移動",
|
||||||
|
|
|
||||||
|
|
@ -2744,7 +2744,7 @@
|
||||||
"폴더 수정": "编辑文件夹",
|
"폴더 수정": "编辑文件夹",
|
||||||
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "在列表中显示文件夹内文件大小的总和。大型文件夹或 NAS 可能需要较长时间读取。长按 [计算文件夹大小] 始终可用",
|
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "在列表中显示文件夹内文件大小的总和。大型文件夹或 NAS 可能需要较长时间读取。长按 [计算文件夹大小] 始终可用",
|
||||||
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "同步文件夹内的内容 (不会在目标中新建同名文件夹)。",
|
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "同步文件夹内的内容 (不会在目标中新建同名文件夹)。",
|
||||||
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 시간이 걸리거나 그 앱을 다시 열어야 할 수 있습니다.": "不会更改文件夹中的文件。图库可能需要一段时间才能反映,或需要重新打开该应用。",
|
"폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.": "不会更改文件夹中的文件。图库可能需要片刻才能反映。",
|
||||||
"폴더 열기": "打开文件夹",
|
"폴더 열기": "打开文件夹",
|
||||||
"폴더 이름": "文件夹名称",
|
"폴더 이름": "文件夹名称",
|
||||||
"폴더 이름 바꾸기 · 이동": "重命名 · 移动文件夹",
|
"폴더 이름 바꾸기 · 이동": "重命名 · 移动文件夹",
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,15 @@ class AndroidShell extends NoopShell {
|
||||||
} catch (_) {}
|
} 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] 면 늘 앱 고르기 창, 아니면 기본 앱 (없으면 고르기 창)
|
/// 다른 앱으로 열기 (MainActivity "openWith"): [choose] 면 늘 앱 고르기 창, 아니면 기본 앱 (없으면 고르기 창)
|
||||||
@override
|
@override
|
||||||
Future<bool> openWith(String path, {bool choose = false}) async {
|
Future<bool> openWith(String path, {bool choose = false}) async {
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,10 @@ class DesktopShell with WindowListener implements AppShell {
|
||||||
@override
|
@override
|
||||||
Future<bool> shareFiles(List<String> paths) async => false;
|
Future<bool> shareFiles(List<String> paths) async => false;
|
||||||
|
|
||||||
|
/// Windows 는 갤러리 미디어 목록이 없다 (.nomedia 는 Android 만)
|
||||||
|
@override
|
||||||
|
Future<void> mediaScan(List<String> paths) async {}
|
||||||
|
|
||||||
/// 기본 연결 프로그램 (탐색기에서 두 번 누른 것과 같음) 또는 "연결 프로그램" 고르기 창
|
/// 기본 연결 프로그램 (탐색기에서 두 번 누른 것과 같음) 또는 "연결 프로그램" 고르기 창
|
||||||
@override
|
@override
|
||||||
Future<bool> openWith(String path, {bool choose = false}) async {
|
Future<bool> openWith(String path, {bool choose = false}) async {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,10 @@ abstract class AppShell {
|
||||||
bool get canShare;
|
bool get canShare;
|
||||||
Future<bool> shareFiles(List<String> paths);
|
Future<bool> shareFiles(List<String> paths);
|
||||||
|
|
||||||
|
/// X-plore 맞추기 (관리자 지시 10/20): 바뀐 파일 · 폴더를 기기의 미디어 목록에 다시 읽게 한다 (Android MediaScanner -
|
||||||
|
/// .nomedia 를 넣고 지운 뒤 갤러리에 반영). 다른 기기는 할 일 없음. 실패해도 조용히 넘긴다
|
||||||
|
Future<void> mediaScan(List<String> paths);
|
||||||
|
|
||||||
/// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중)
|
/// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중)
|
||||||
List<String> installedBrowsers();
|
List<String> installedBrowsers();
|
||||||
|
|
||||||
|
|
@ -147,6 +151,8 @@ class NoopShell implements AppShell {
|
||||||
@override
|
@override
|
||||||
Future<bool> shareFiles(List<String> paths) async => false;
|
Future<bool> shareFiles(List<String> paths) async => false;
|
||||||
@override
|
@override
|
||||||
|
Future<void> mediaScan(List<String> paths) async {}
|
||||||
|
@override
|
||||||
List<String> installedBrowsers() => const [];
|
List<String> installedBrowsers() => const [];
|
||||||
@override
|
@override
|
||||||
Future<List<String>> missingTools() async => const [];
|
Future<List<String>> missingTools() async => const [];
|
||||||
|
|
|
||||||
|
|
@ -4102,7 +4102,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
? tr('이 폴더에 빈 .nomedia 파일을 만듭니다. 갤러리 · 음악 앱이 이 폴더 (안의 폴더 포함) 의 그림 · 동영상 · 음악을 보이지 않게 됩니다.')
|
? tr('이 폴더에 빈 .nomedia 파일을 만듭니다. 갤러리 · 음악 앱이 이 폴더 (안의 폴더 포함) 의 그림 · 동영상 · 음악을 보이지 않게 됩니다.')
|
||||||
: tr('이 폴더의 .nomedia 파일을 지웁니다. 갤러리 · 음악 앱이 이 폴더의 그림 · 동영상 · 음악을 다시 보이게 됩니다.')),
|
: tr('이 폴더의 .nomedia 파일을 지웁니다. 갤러리 · 음악 앱이 이 폴더의 그림 · 동영상 · 음악을 다시 보이게 됩니다.')),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(tr('폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 시간이 걸리거나 그 앱을 다시 열어야 할 수 있습니다.'),
|
Text(tr('폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.'),
|
||||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -4123,6 +4123,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
_snack(trf('바꾸지 못했습니다: {0}', [err]));
|
_snack(trf('바꾸지 못했습니다: {0}', [err]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 관리자 지시 10/20: 갤러리가 바로 알도록 .nomedia 와 그 폴더를 다시 읽게 (넣을 때 · 지울 때 모두, 실패해도 조용히)
|
||||||
|
await c.services.shell.mediaScan([f.path, dir]);
|
||||||
await _refreshAll([dir]);
|
await _refreshAll([dir]);
|
||||||
pane.changed();
|
pane.changed();
|
||||||
_snack(on ? tr('갤러리 · 음악 앱에서 숨겼습니다 (.nomedia). 파일은 그대로입니다.') : tr('.nomedia 를 지웠습니다. 갤러리 · 음악 앱에 다시 보입니다.'));
|
_snack(on ? tr('갤러리 · 음악 앱에서 숨겼습니다 (.nomedia). 파일은 그대로입니다.') : tr('.nomedia 를 지웠습니다. 갤러리 · 음악 앱에 다시 보입니다.'));
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||||
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
|
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
|
||||||
class _Shell extends NoopShell {
|
class _Shell extends NoopShell {
|
||||||
final opened = <(String, bool)>[];
|
final opened = <(String, bool)>[];
|
||||||
|
final scanned = <List<String>>[];
|
||||||
|
@override
|
||||||
|
Future<void> mediaScan(List<String> paths) async => scanned.add(paths);
|
||||||
@override
|
@override
|
||||||
Future<bool> openWith(String path, {bool choose = false}) async {
|
Future<bool> openWith(String path, {bool choose = false}) async {
|
||||||
opened.add((path, choose));
|
opened.add((path, choose));
|
||||||
|
|
@ -1767,8 +1770,11 @@ void main() {
|
||||||
await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)')));
|
await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)')));
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기')));
|
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(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: '안의 파일은 그대로');
|
expect(File(p.join(left, 'sub', 'inner.txt')).readAsStringSync(), 'inner', reason: '안의 파일은 그대로');
|
||||||
// 이제 메뉴는 [다시 보이기]
|
// 이제 메뉴는 [다시 보이기]
|
||||||
await idle(tester);
|
await idle(tester);
|
||||||
|
|
@ -1780,6 +1786,8 @@ void main() {
|
||||||
expect(find.textContaining('.nomedia 파일을 지웁니다'), findsOneWidget);
|
expect(find.textContaining('.nomedia 파일을 지웁니다'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '다시 보이기')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '다시 보이기')));
|
||||||
await settle(tester, () => !mark.existsSync());
|
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);
|
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue);
|
||||||
// 파일에는 없음
|
// 파일에는 없음
|
||||||
await idle(tester);
|
await idle(tester);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue